Skip to content

Commit

Permalink
fix: tree generation for autogenerated groups (#1151)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
  • Loading branch information
kevinzunigacuellar and delucis committed Jan 18, 2024
1 parent 8398432 commit 134292d
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 36 deletions.
7 changes: 7 additions & 0 deletions .changeset/dry-pandas-yawn.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 23 additions & 5 deletions packages/starlight/__tests__/sidebar/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
],
Expand Down Expand Up @@ -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",
Expand Down
67 changes: 36 additions & 31 deletions packages/starlight/utils/navigation.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
});
}

Expand Down

0 comments on commit 134292d

Please sign in to comment.