diff options
author | Kevin Zuniga Cuellar | 2024-01-18 12:27:55 -0500 |
---|---|---|
committer | GitHub | 2024-01-18 18:27:55 +0100 |
commit | 134292ddd89683007d7de25545d39738a82c626c (patch) | |
tree | bfb858d383a26e454e17a32fe1d0a4a23c95a9fc | |
parent | 8398432aa4a0f38e2dd4452dfcdf7033c5713334 (diff) | |
download | IT.starlight-134292ddd89683007d7de25545d39738a82c626c.tar.gz IT.starlight-134292ddd89683007d7de25545d39738a82c626c.tar.bz2 IT.starlight-134292ddd89683007d7de25545d39738a82c626c.zip |
fix: tree generation for autogenerated groups (#1151)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r-- | .changeset/dry-pandas-yawn.md | 7 | ||||
-rw-r--r-- | packages/starlight/__tests__/sidebar/navigation.test.ts | 28 | ||||
-rw-r--r-- | packages/starlight/utils/navigation.ts | 67 |
3 files changed, 66 insertions, 36 deletions
diff --git a/.changeset/dry-pandas-yawn.md b/.changeset/dry-pandas-yawn.md new file mode 100644 index 00000000..dee67082 --- /dev/null +++ b/.changeset/dry-pandas-yawn.md @@ -0,0 +1,7 @@ +--- +'@astrojs/starlight': minor +--- + +Fixes sidebar auto-generation issue when a file and a directory, located at the same level, have identical names. + +For example, `src/content/docs/guides.md` and `src/content/docs/guides/example.md` will now both be included and `src/content/docs/guides.md` is treated in the same way a `src/content/docs/guides/index.md` file would be. diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts index 7930d087..321f00fa 100644 --- a/packages/starlight/__tests__/sidebar/navigation.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation.test.ts @@ -9,6 +9,8 @@ vi.mock('astro:content', async () => ['reference/configuration.mdx', { title: 'Config Reference' }], ['reference/frontmatter.md', { title: 'Frontmatter Reference' }], // @ts-expect-error — Using a slug not present in Starlight docs site + ['reference/frontmatter/foo.mdx', { title: 'Foo' }], + // @ts-expect-error — Using a slug not present in Starlight docs site ['api/v1/users.md', { title: 'Users API' }], ['guides/components.mdx', { title: 'Components' }], ], @@ -84,12 +86,28 @@ describe('getSidebar', () => { "type": "link", }, { - "attrs": {}, "badge": undefined, - "href": "/reference/frontmatter/", - "isCurrent": false, - "label": "Frontmatter Reference", - "type": "link", + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", }, ], "label": "Reference", diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index 73626a7c..7be5253d 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -1,4 +1,3 @@ -import { basename, dirname } from 'node:path'; import config from 'virtual:starlight/user-config'; import type { Badge } from '../schemas/badge'; import type { PrevNextLinkConfig } from '../schemas/prevNextLink'; @@ -11,11 +10,12 @@ import type { import { createPathFormatter } from './createPathFormatter'; import { formatPath } from './format-path'; import { pickLang } from './i18n'; -import { ensureLeadingSlash } from './path'; +import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path'; import { getLocaleRoutes, type Route } from './routing'; import { localeToLang, slugToPathname } from './slugs'; const DirKey = Symbol('DirKey'); +const SlugKey = Symbol('SlugKey'); export interface Link { type: 'link'; @@ -44,14 +44,16 @@ export type SidebarEntry = Link | Group; */ interface Dir { [DirKey]: undefined; + [SlugKey]: string; [item: string]: Dir | Route; } /** Create a new directory object. */ -function makeDir(): Dir { +function makeDir(slug: string): Dir { const dir = {} as Dir; - // Add DirKey as a non-enumerable property so that `Object.entries(dir)` ignores it. + // Add DirKey and SlugKey as non-enumerable properties so that `Object.entries(dir)` ignores them. Object.defineProperty(dir, DirKey, { enumerable: false }); + Object.defineProperty(dir, SlugKey, { value: slug, enumerable: false }); return dir; } @@ -157,37 +159,48 @@ function getBreadcrumbs(path: string, baseDir: string): string[] { // Index paths will match `baseDir` and don’t include breadcrumbs. if (pathWithoutExt === baseDir) return []; // Ensure base directory ends in a trailing slash. - if (!baseDir.endsWith('/')) baseDir += '/'; + baseDir = ensureTrailingSlash(baseDir); // Strip base directory from path if present. const relativePath = pathWithoutExt.startsWith(baseDir) ? pathWithoutExt.replace(baseDir, '') : pathWithoutExt; - let dir = dirname(relativePath); - // Return no breadcrumbs for items in the root directory. - if (dir === '.') return []; - return dir.split('/'); + + return relativePath.split('/'); } /** Turn a flat array of routes into a tree structure. */ function treeify(routes: Route[], baseDir: string): Dir { - const treeRoot: Dir = makeDir(); + const treeRoot: Dir = makeDir(baseDir); routes // Remove any entries that should be hidden .filter((doc) => !doc.entry.data.sidebar.hidden) + // Sort by depth, to build the tree depth first. + .sort((a, b) => b.id.split('/').length - a.id.split('/').length) + // Build the tree .forEach((doc) => { - const breadcrumbs = getBreadcrumbs(doc.id, baseDir); - - // Walk down the route’s path to generate the tree. - let currentDir = treeRoot; - breadcrumbs.forEach((dir) => { - // Create new folder if needed. - if (typeof currentDir[dir] === 'undefined') currentDir[dir] = makeDir(); - // Go into the subdirectory. - currentDir = currentDir[dir] as Dir; + const parts = getBreadcrumbs(doc.id, baseDir); + let currentNode = treeRoot; + + parts.forEach((part, index) => { + const isLeaf = index === parts.length - 1; + + // Handle directory index pages by renaming them to `index` + if (isLeaf && currentNode.hasOwnProperty(part)) { + currentNode = currentNode[part] as Dir; + part = 'index'; + } + + // Recurse down the tree if this isn’t the leaf node. + if (!isLeaf) { + const path = currentNode[SlugKey]; + currentNode[part] ||= makeDir(stripLeadingAndTrailingSlashes(path + '/' + part)); + currentNode = currentNode[part] as Dir; + } else { + currentNode[part] = doc; + } }); - // We’ve walked through the path. Register the route in this directory. - currentDir[basename(doc.slug)] = doc; }); + return treeRoot; } @@ -212,24 +225,16 @@ function getOrder(routeOrDir: Route | Dir): number { : // If no order value is found, set it to the largest number possible. routeOrDir.entry.data.sidebar.order ?? Number.MAX_VALUE; } -/** Get the comparison ID for a given route to sort them alphabetically. */ -function getComparisonId(id: string) { - const filename = stripExtension(basename(id)); - return filename === 'index' ? '' : filename; -} /** Sort a directory’s entries by user-specified order or alphabetically if no order specified. */ function sortDirEntries(dir: [string, Dir | Route][]): [string, Dir | Route][] { const collator = new Intl.Collator(localeToLang(undefined)); - return dir.sort(([keyA, a], [keyB, b]) => { + return dir.sort(([_keyA, a], [_keyB, b]) => { const [aOrder, bOrder] = [getOrder(a), getOrder(b)]; // Pages are sorted by order in ascending order. if (aOrder !== bOrder) return aOrder < bOrder ? -1 : 1; // If two pages have the same order value they will be sorted by their slug. - return collator.compare( - isDir(a) ? keyA : getComparisonId(a.id), - isDir(b) ? keyB : getComparisonId(b.id) - ); + return collator.compare(isDir(a) ? a[SlugKey] : a.slug, isDir(b) ? b[SlugKey] : b.slug); }); } |