summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Zuniga Cuellar2024-01-18 12:27:55 -0500
committerGitHub2024-01-18 18:27:55 +0100
commit134292ddd89683007d7de25545d39738a82c626c (patch)
treebfb858d383a26e454e17a32fe1d0a4a23c95a9fc
parent8398432aa4a0f38e2dd4452dfcdf7033c5713334 (diff)
downloadIT.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.md7
-rw-r--r--packages/starlight/__tests__/sidebar/navigation.test.ts28
-rw-r--r--packages/starlight/utils/navigation.ts67
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);
});
}