UNPKG

astro-accelerator-utils

Version:
447 lines (380 loc) 12.9 kB
import * as PostFiltering from '../postFiltering.mjs'; /** * @typedef { import("./posts.mjs").Posts } Posts * @typedef { import("./taxonomy.mjs").Taxonomy } Taxonomy * @typedef { import("./urls.mjs").UrlFormatter } UrlFormatter * @typedef { import("../../types/Astro").MarkdownInstance } MarkdownInstance * @typedef { import("../../types/NavPage").NavPage } NavPage * @typedef { import("../../types/NavPage").MenuItem } MenuItem */ const defaults = { url: '', ariaCurrent: false, isOpen: false, section: '', children: [] } export class Navigation { /** * Constructor * @param {Posts} posts * @param {UrlFormatter} urlFormatter * @param {Taxonomy} taxonomy */ constructor(posts, urlFormatter, taxonomy) { this.posts = posts; this.urlFormatter = urlFormatter; this.taxonomy = taxonomy; } /** * Returns a list of breadcrumbs * @param {URL} currentUrl * @param {string} subfolder * @param {number} customCount * @returns {NavPage[]} */ breadcrumbs(currentUrl, subfolder, customCount) { const allPages = this.posts.all(); const pathParts = currentUrl.pathname.split('/'); if (subfolder.length > 0) { // Running in a subfolder pathParts.shift(); } /** @type {NavPage[]} */ const navPages = []; let path = ''; pathParts.forEach((part) => { path += part.length > 0 ? '/' + part : ''; const match = this.popMatchingPage(allPages, path); if (match) { navPages.push(this.mapNavPage(match)); } }); if (customCount === 0) { navPages[navPages.length -1].url = currentUrl.pathname; } this.setCurrentPage(navPages, currentUrl); return navPages; } /** * * @param {URL} currentUrl * @param {string} subfolder * @param {(MenuItem | 'auto')[]} menu * @returns {NavPage[]} */ menu(currentUrl, subfolder, menu) { const pages = []; for (let i = 0; i < menu.length; i++) { const item = menu[i]; this.addMenuItem(pages, item, subfolder); } this.setCurrentPage(pages, currentUrl); return pages; } addMenuItem(pages, item, subfolder) { if (this.isNavPage(item)) { // Expand defaults with custom values const result = { ...defaults, ...item }; // Recursively add children const children = []; for (const child of result.children) { this.addMenuItem(children, child, subfolder); } result.children = children; pages.push(result); } else { const p = this.autoMenu(subfolder); for (let j = 0; j < p.length; j++) { pages.push(p[j]); } } } /** * * @param {NavPage} page * @param {NavPage[]} pageList */ getChildren(page, pageList) { const children = pageList .filter((mp) => page.url != '/' && mp.url != page.url && mp.url.startsWith(page.url) && mp.url.split('/').length == (page.url.split('/').length + 1) ) .sort((mp) => mp.order); for (let child of children) { child.children = this.getChildren(child, pageList); } if (children.length > 0) { // Add the item to itself as the first item const ownChild = structuredClone(page); ownChild.order = -1; ownChild.children = []; children.push(ownChild); } return children; } /** * * @param {string} subfolder * @returns {NavPage[]} */ autoMenu(subfolder) { const allPages = this.posts .all() .filter(PostFiltering.showInMenu); const topLevelPages = this.posts .root(subfolder) .filter(PostFiltering.showInMenu); const pageHierarchy = topLevelPages .map(p => this.mapNavPage(p)) .sort((a, b) => parseInt(a.order, 10) - parseInt(b.order, 10)); /** @type {NavPage[]} */ const pageList = allPages.map(p => this.mapNavPage(p)); for (let i = 0; i < pageHierarchy.length; i++) { const page = pageHierarchy[i]; if (i > 0) { // Don't add children to first link (Home) page.children = this.getChildren(page, pageList); } } return pageHierarchy; } /** * * @param {URL} currentUrl * @param {TranslationProvider} _ * @param {any} translations * @param {string} subfolder * @param {(MenuItem | 'categories' | 'tags' | 'toptags')[]} menu * @returns {NavPage[]} */ footer(currentUrl, _, translations, subfolder, menu) { // const cache = new Cache(site.cacheMaxAge); // const posts = new Posts(cache); // const urlFormatter = new UrlFormatter(site.url); // const taxonomy = new Taxonomy(cache, posts, urlFormatter); // const navigation = new Navigation(posts, urlFormatter); const links = this.taxonomy.links(translations, _, subfolder); const entries = this.taxonomy.getTaxonomy(); /** @type {NavPage[]} */ let pages = []; for (let i = 0; i < menu.length; i++) { const item = menu[i]; this.addFooterItem(pages, item, links, _, translations, subfolder, entries); } this.setCurrentPage(pages, currentUrl); return pages; } addFooterItem(pages, item, links, _, translations, subfolder, entries) { if (this.isNavPage(item)) { const result = {...defaults, ...item}; pages.push(result) } else { switch (item) { case 'tags': const tags = this.getTags(links, _, translations, subfolder, entries); for (let j = 0; j < tags.length; j++) { pages.push(tags[j]); } break; case 'toptags': const toptags = this.getTopTags(links, _, translations, subfolder, entries); for (let j = 0; j < toptags.length; j++) { pages.push(toptags[j]); } break; case 'categories': const categories = this.getCategories(links, _, translations, subfolder, entries); for (let j = 0; j < categories.length; j++) { pages.push(categories[j]); } break; } } } /** * * @param {TaxonomyLinks} links * @param {TranslationProvider} _ * @param {any} translations * @param {string} subfolder * @param {TaxonomyList} entries * @returns {NavPage[]} */ getCategories(links, _, translations, subfolder, entries) { const category = _(translations.articles.category) ?? 'category'; const categoryTitle = _(translations.articles.category_title) ?? 'Categories'; const categoryLink = `${subfolder}/${category}/`; let order = 0; /** @type {NavPage[]} */ const pageHierarchy = [{ title: categoryTitle, url: categoryLink, ariaCurrent: false, isOpen: false, order: 1, children: entries.categories.map(item => { return { title: item.title, url: links.getCategoryLink(item.title), ariaCurrent: false, isOpen: false, order: ++order, children: [] }; }) }]; return pageHierarchy; } /** * * @param {TaxonomyLinks} links * @param {TranslationProvider} _ * @param {any} translations * @param {string} subfolder * @param {TaxonomyList} entries * @returns {NavPage[]} */ getTags(links, _, translations, subfolder, entries) { const tag = _(translations.articles.tag) ?? 'tag'; const tagTitle = _(translations.articles.tag_title) ?? 'Tags'; const tagLink = `${subfolder}/${tag}/`; let order = 0; /** @type {NavPage[]} */ const pageHierarchy = [{ title: tagTitle, url: tagLink, ariaCurrent: false, isOpen: false, order: 1, children: entries.tags.map(item => { return { title: item.title, url: links.getTagLink(item.title), ariaCurrent: false, isOpen: false, order: ++order, children: [] }; }) }]; return pageHierarchy; } /** * * @param {TaxonomyLinks} links * @param {TranslationProvider} _ * @param {any} translations * @param {string} subfolder * @param {TaxonomyList} entries * @returns {NavPage[]} */ getTopTags(links, _, translations, subfolder, entries) { const tag = _(translations.articles.tag) ?? 'tag'; const tagTitle = _(translations.articles.tag_title) ?? 'Tags'; const tagLink = `${subfolder}/${tag}/`; let order = 0; /** @type {NavPage[]} */ const pageHierarchy = [{ title: tagTitle, url: tagLink, ariaCurrent: false, isOpen: false, order: 1, children: entries.topTags.map(item => { return { title: item.title, url: links.getTagLink(item.title), ariaCurrent: false, isOpen: false, order: ++order, children: [] }; }) }]; return pageHierarchy; } /** * Walks a NavPage tree to set current page * @param {NavPage[]} pages * @param {URL} currentUrl */ setCurrentPage(pages, currentUrl) { pages.forEach(p => { p.isOpen = currentUrl.pathname.startsWith(p.url); p.ariaCurrent = p.url == currentUrl.pathname ? 'page' : false; if (p.children) { this.setCurrentPage(p.children, currentUrl); } }); } /** * Converts a MarkdownInstance into a NavPage * @param {MarkdownInstance} page * @returns {NavPage} */ mapNavPage(page) { let url = page.url == null || (page.url ?? '').length == 0 ? '/' : page.url; // Send visitors straight to the first page if (page.frontmatter.paged) { url += '/1/'; } url = this.urlFormatter.addSlashToAddress(url); if (page.frontmatter.layout == 'src/layouts/Redirect.astro') { // Skips past the redirect url = page.frontmatter.redirect; } /** @type {NavPage} */ const entry = { fullTitle: page.frontmatter.title, section: page.frontmatter.navSection ?? page.frontmatter.navTitle ?? page.frontmatter.title, title: page.frontmatter.navTitle ?? page.frontmatter.title, url: url, order: page.frontmatter.navOrder ?? Number.MAX_SAFE_INTEGER, children: [], // These are later set to the correct value, but not now as we want to cache isOpen: false, ariaCurrent: false } return entry; } /** * Checks whether the item is a NavPage * @param {NavPage | 'auto' | 'tags' | 'toptags' | 'categories'} item * @returns {item is NavPage} */ isNavPage(item) { if (typeof item === 'string' && ['auto', 'tags', 'toptags', 'categories'].includes(item)) { return false; } return true; } /** * Pops matching page from array * @param {MarkdownInstance[]} allPages * @param {string} search * @returns */ popMatchingPage(allPages, search) { const numberToRemove = 1; let indexToRemove = -1; let match = null; for (let i = 0; i < allPages.length; i++) { if (allPages[i].url == search) { indexToRemove = i; match = allPages[i]; } } if (match) { allPages.splice(indexToRemove, numberToRemove); } return match; } }