UNPKG

polen

Version:

A framework for delightful GraphQL developer portals

166 lines 7.59 kB
import { FileRouter } from '#lib/file-router/index'; import { Str } from '@wollybeard/kit'; /** * Builds sidebars for all top-level directories that contain both an index page and nested content. * * This function analyzes a scan result to identify which directories should have sidebars. * A directory gets a sidebar if it meets these criteria: * 1. It's a top-level directory (e.g., /guide, /api, /docs) * 2. It has an index page (e.g., /guide/index.md) * 3. It has nested pages (e.g., /guide/getting-started.md, /guide/configuration.md) * * @param scanResult - The result of scanning pages, containing both a flat list and tree structure * @returns A mapping of route paths to sidebar structures * * @example * ```ts * const scanResult = await Content.scan({ dir: './pages' }) * const sidebars = buildSidebarIndex(scanResult) * // Returns: { * // '/guide': { items: [...] }, * // '/api': { items: [...] } * // } * ``` */ export const buildSidebarIndex = (scanResult) => { const sidebarIndex = {}; // Group pages by their top-level directory const pagesByTopLevelDir = new Map(); for (const page of scanResult.list) { const topLevelDir = page.route.logical.path[0]; // Skip pages that are not in a directory or are hidden if (!topLevelDir || page.metadata.hidden) continue; if (!pagesByTopLevelDir.has(topLevelDir)) { pagesByTopLevelDir.set(topLevelDir, []); } pagesByTopLevelDir.get(topLevelDir).push(page); } // Build sidebar for each directory that has an index page for (const [topLevelDir, pages] of pagesByTopLevelDir) { const hasIndexPage = pages.some(page => page.route.logical.path.length === 1 && FileRouter.routeIsFromIndexFile(page.route)); // Skip directories without index pages if (!hasIndexPage) continue; const pathExp = `/${topLevelDir}`; const sidebar = buildSidebarForDirectory(topLevelDir, pages); if (sidebar.items.length > 0) { sidebarIndex[pathExp] = sidebar; } } return sidebarIndex; }; /** * Builds a sidebar for a specific directory from its pages */ const buildSidebarForDirectory = (topLevelDir, pages) => { const items = []; // Group pages by their immediate parent path const pagesByParent = new Map(); for (const page of pages) { // Skip the index page at the top level directory if (page.route.logical.path.length === 1 && FileRouter.routeIsFromIndexFile(page.route)) { continue; } // Get the immediate parent path (e.g., for ['guide', 'advanced', 'tips'], parent is ['guide', 'advanced']) const parentPath = page.route.logical.path.slice(0, -1).join(`/`); if (!pagesByParent.has(parentPath)) { pagesByParent.set(parentPath, []); } pagesByParent.get(parentPath).push(page); } // Process top-level pages (direct children of the directory) const topLevelPages = pagesByParent.get(topLevelDir) ?? []; // Sort pages by their directory order (extracted from file path) const sortedTopLevelPages = [...topLevelPages].sort((a, b) => { // For sections, we need to look at the directory name in the file path const dirA = a.route.file.path.relative.dir.split(`/`).pop() ?? ``; const dirB = b.route.file.path.relative.dir.split(`/`).pop() ?? ``; // Extract order from directory names like "10_b", "20_c" const orderMatchA = /^(\d+)[_-]/.exec(dirA); const orderMatchB = /^(\d+)[_-]/.exec(dirB); const orderA = orderMatchA ? parseInt(orderMatchA[1], 10) : Number.MAX_SAFE_INTEGER; const orderB = orderMatchB ? parseInt(orderMatchB[1], 10) : Number.MAX_SAFE_INTEGER; if (orderA !== orderB) return orderA - orderB; // Fall back to alphabetical order return dirA.localeCompare(dirB); }); for (const page of sortedTopLevelPages) { const pageName = page.route.logical.path[page.route.logical.path.length - 1]; const childPath = page.route.logical.path.join(`/`); const childPages = pagesByParent.get(childPath) ?? []; if (childPages.length > 0 || FileRouter.routeIsFromIndexFile(page.route)) { // This is a section (has children or is an index page for a subdirectory) const hasIndex = FileRouter.routeIsFromIndexFile(page.route); const section = { type: `ItemSection`, title: Str.titlizeSlug(pageName), pathExp: childPath, isLinkToo: hasIndex, links: [], }; // Add direct children as links (sorted by order) const sortedChildPages = [...childPages].sort((a, b) => { const orderA = a.route.logical.order ?? Number.MAX_SAFE_INTEGER; const orderB = b.route.logical.order ?? Number.MAX_SAFE_INTEGER; return orderA - orderB; }); for (const childPage of sortedChildPages) { if (!FileRouter.routeIsFromIndexFile(childPage.route)) { section.links.push({ type: `ItemLink`, title: Str.titlizeSlug(childPage.route.logical.path[childPage.route.logical.path.length - 1]), pathExp: childPage.route.logical.path.join(`/`), }); } } // Also add any deeper descendants as flat links const allDescendants = []; for (const [parentPath, pagesInParent] of pagesByParent) { // Check if this path is a descendant (but not direct child) if (parentPath.startsWith(childPath + `/`)) { for (const descendantPage of pagesInParent) { if (!FileRouter.routeIsFromIndexFile(descendantPage.route)) { allDescendants.push(descendantPage); } } } } // Sort all descendants by their full path order allDescendants.sort((a, b) => { // Compare paths segment by segment, considering order at each level const pathA = a.route.logical.path; const pathB = b.route.logical.path; const minLength = Math.min(pathA.length, pathB.length); for (let i = 0; i < minLength; i++) { const segmentCompare = pathA[i].localeCompare(pathB[i]); if (segmentCompare !== 0) return segmentCompare; } return pathA.length - pathB.length; }); for (const descendantPage of allDescendants) { section.links.push({ type: `ItemLink`, title: Str.titlizeSlug(descendantPage.route.logical.path[descendantPage.route.logical.path.length - 1]), pathExp: descendantPage.route.logical.path.join(`/`), }); } if (section.links.length > 0 || section.isLinkToo) { items.push(section); } } else { // This is a simple link items.push({ type: `ItemLink`, title: Str.titlizeSlug(pageName), pathExp: page.route.logical.path.join(`/`), }); } } return { items }; }; //# sourceMappingURL=sidebar.js.map