fix(content-docs): always sort autogenerated sidebar items by file/folder name by default (#6700)

This commit is contained in:
Joshua Chen 2022-03-03 19:39:54 +08:00 committed by GitHub
parent f04cb7abb9
commit c90d25ca2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 66 deletions

View file

@ -6,8 +6,8 @@ Object {
Object { Object {
"id": "doc with space", "id": "doc with space",
"next": Object { "next": Object {
"permalink": "/docs/headingAsTitle", "permalink": "/docs/foo/bar",
"title": "My heading as title", "title": "Bar",
}, },
"prev": undefined, "prev": undefined,
}, },
@ -19,8 +19,8 @@ Object {
Object { Object {
"id": "foo/baz", "id": "foo/baz",
"next": Object { "next": Object {
"permalink": "/docs/absoluteSlug", "permalink": "/docs/headingAsTitle",
"title": "absoluteSlug", "title": "My heading as title",
}, },
"prev": Object { "prev": Object {
"permalink": "/docs/foo/bar", "permalink": "/docs/foo/bar",
@ -34,8 +34,8 @@ Object {
"title": "Hello sidebar_label", "title": "Hello sidebar_label",
}, },
"prev": Object { "prev": Object {
"permalink": "/docs/doc with space", "permalink": "/docs/foo/bazSlug.html",
"title": "Hoo hoo, if this path tricks you...", "title": "baz pagination_label",
}, },
}, },
Object { Object {
@ -122,8 +122,8 @@ Object {
"title": "relativeSlug", "title": "relativeSlug",
}, },
"prev": Object { "prev": Object {
"permalink": "/docs/foo/bazSlug.html", "permalink": "/docs/rootTryToEscapeSlug",
"title": "baz pagination_label", "title": "rootTryToEscapeSlug",
}, },
}, },
Object { Object {
@ -163,6 +163,23 @@ Object {
"id": "doc with space", "id": "doc with space",
"type": "doc", "type": "doc",
}, },
Object {
"collapsed": false,
"collapsible": true,
"items": Array [
Object {
"id": "foo/bar",
"type": "doc",
},
Object {
"id": "foo/baz",
"type": "doc",
},
],
"label": "foo",
"link": undefined,
"type": "category",
},
Object { Object {
"id": "headingAsTitle", "id": "headingAsTitle",
"type": "doc", "type": "doc",
@ -196,23 +213,6 @@ Object {
"id": "rootTryToEscapeSlug", "id": "rootTryToEscapeSlug",
"type": "doc", "type": "doc",
}, },
Object {
"collapsed": false,
"collapsible": true,
"items": Array [
Object {
"id": "foo/bar",
"type": "doc",
},
Object {
"id": "foo/baz",
"type": "doc",
},
],
"label": "foo",
"link": undefined,
"type": "category",
},
Object { Object {
"collapsed": false, "collapsed": false,
"collapsible": true, "collapsible": true,

View file

@ -142,7 +142,7 @@ describe('DefaultSidebarItemsGenerator', () => {
id: 'intro', id: 'intro',
source: '@site/docs/intro.md', source: '@site/docs/intro.md',
sourceDirName: '.', sourceDirName: '.',
sidebarPosition: 1, sidebarPosition: 0,
frontMatter: {}, frontMatter: {},
}, },
{ {
@ -183,7 +183,7 @@ describe('DefaultSidebarItemsGenerator', () => {
id: 'guide1', id: 'guide1',
source: '@site/docs/02-Guides/guide1.md', source: '@site/docs/02-Guides/guide1.md',
sourceDirName: '02-Guides', sourceDirName: '02-Guides',
sidebarPosition: 1, sidebarPosition: 0,
frontMatter: { frontMatter: {
sidebar_class_name: 'foo', sidebar_class_name: 'foo',
}, },
@ -406,7 +406,7 @@ describe('DefaultSidebarItemsGenerator', () => {
}, },
{ {
id: 'parent/doc2', id: 'parent/doc2',
source: '@site/docs/Category/index.md', source: '@site/docs/Category/doc2.md',
sourceDirName: 'Category', sourceDirName: 'Category',
frontMatter: {}, frontMatter: {},
}, },
@ -451,11 +451,12 @@ describe('DefaultSidebarItemsGenerator', () => {
}, },
items: [ items: [
{ {
id: 'parent/doc1', id: 'parent/doc2',
type: 'doc', type: 'doc',
}, },
// doc1 is below doc2, because its file name is index.md
{ {
id: 'parent/doc2', id: 'parent/doc1',
type: 'doc', type: 'doc',
}, },
], ],
@ -469,11 +470,11 @@ describe('DefaultSidebarItemsGenerator', () => {
type: 'doc', type: 'doc',
}, },
{ {
id: 'parent/doc5', id: 'parent/doc6',
type: 'doc', type: 'doc',
}, },
{ {
id: 'parent/doc6', id: 'parent/doc5',
type: 'doc', type: 'doc',
}, },
], ],
@ -508,7 +509,7 @@ describe('DefaultSidebarItemsGenerator', () => {
id: 'intro', id: 'intro',
source: '@site/docs/intro.md', source: '@site/docs/intro.md',
sourceDirName: '.', sourceDirName: '.',
sidebarPosition: 1, sidebarPosition: 0,
frontMatter: {}, frontMatter: {},
}, },
{ {

View file

@ -20,8 +20,6 @@ import path from 'path';
import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs'; import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs';
const BreadcrumbSeparator = '/'; const BreadcrumbSeparator = '/';
// To avoid possible name clashes with a folder of the same name as the ID
const docIdPrefix = '$doc$/';
// Just an alias to the make code more explicit // Just an alias to the make code more explicit
function getLocalDocId(docId: string): string { function getLocalDocId(docId: string): string {
@ -31,16 +29,20 @@ function getLocalDocId(docId: string): string {
export const CategoryMetadataFilenameBase = '_category_'; export const CategoryMetadataFilenameBase = '_category_';
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}'; export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
type WithPosition<T> = T & {position?: number}; type WithPosition<T> = T & {
position?: number;
/** The source is the file/folder name */
source?: string;
};
/** /**
* A representation of the fs structure. For each object entry: * A representation of the fs structure. For each object entry:
* If it's a folder, the key is the directory name, and value is the directory * If it's a folder, the key is the directory name, and value is the directory
* content; If it's a doc file, the key is the doc id prefixed with '$doc$/', * content; If it's a doc file, the key is the doc's source file name, and value
* and value is null * is the doc ID
*/ */
type Dir = { type Dir = {
[item: string]: Dir | null; [item: string]: Dir | string;
}; };
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449 // Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
@ -108,14 +110,16 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
const treeRoot: Dir = {}; const treeRoot: Dir = {};
docs.forEach((doc) => { docs.forEach((doc) => {
const breadcrumb = getRelativeBreadcrumb(doc); const breadcrumb = getRelativeBreadcrumb(doc);
let currentDir = treeRoot; // We walk down the file's path to generate the fs structure // We walk down the file's path to generate the fs structure
let currentDir = treeRoot;
breadcrumb.forEach((dir) => { breadcrumb.forEach((dir) => {
if (typeof currentDir[dir] === 'undefined') { if (typeof currentDir[dir] === 'undefined') {
currentDir[dir] = {}; // Create new folder. currentDir[dir] = {}; // Create new folder.
} }
currentDir = currentDir[dir]!; // Go into the subdirectory. currentDir = currentDir[dir] as Dir; // Go into the subdirectory.
}); });
currentDir[`${docIdPrefix}${doc.id}`] = null; // We've walked through the file path. Register the file in this directory. // We've walked through the path. Register the file in this directory.
currentDir[path.basename(doc.source)] = doc.id;
}); });
return treeRoot; return treeRoot;
} }
@ -126,8 +130,12 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
*/ */
function generateSidebar( function generateSidebar(
fsModel: Dir, fsModel: Dir,
): Promise<WithPosition<NormalizedSidebarItem>[]> { ): WithPosition<NormalizedSidebarItem>[] {
function createDocItem(id: string): WithPosition<SidebarItemDoc> { function createDocItem(
id: string,
fullPath: string,
fileName: string,
): WithPosition<SidebarItemDoc> {
const { const {
sidebarPosition: position, sidebarPosition: position,
frontMatter: {sidebar_label: label, sidebar_class_name: className}, frontMatter: {sidebar_label: label, sidebar_class_name: className},
@ -136,25 +144,24 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
type: 'doc', type: 'doc',
id, id,
position, position,
source: fileName,
// We don't want these fields to magically appear in the generated // We don't want these fields to magically appear in the generated
// sidebar // sidebar
...(label !== undefined && {label}), ...(label !== undefined && {label}),
...(className !== undefined && {className}), ...(className !== undefined && {className}),
}; };
} }
async function createCategoryItem( function createCategoryItem(
dir: Dir, dir: Dir,
fullPath: string, fullPath: string,
folderName: string, folderName: string,
): Promise<WithPosition<NormalizedSidebarItemCategory>> { ): WithPosition<NormalizedSidebarItemCategory> {
const categoryMetadata = const categoryMetadata =
categoriesMetadata[posixPath(path.join(autogenDir, fullPath))]; categoriesMetadata[posixPath(path.join(autogenDir, fullPath))];
const className = categoryMetadata?.className; const className = categoryMetadata?.className;
const {filename, numberPrefix} = numberPrefixParser(folderName); const {filename, numberPrefix} = numberPrefixParser(folderName);
const allItems = await Promise.all( const allItems = Object.entries(dir).map(([key, content]) =>
Object.entries(dir).map(([key, content]) => dirToItem(content, key, `${fullPath}/${key}`),
dirToItem(content, key, `${fullPath}/${key}`),
),
); );
// Try to match a doc inside the category folder, // Try to match a doc inside the category folder,
@ -211,24 +218,23 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
collapsible: categoryMetadata?.collapsible, collapsible: categoryMetadata?.collapsible,
collapsed: categoryMetadata?.collapsed, collapsed: categoryMetadata?.collapsed,
position: categoryMetadata?.position ?? numberPrefix, position: categoryMetadata?.position ?? numberPrefix,
source: folderName,
...(className !== undefined && {className}), ...(className !== undefined && {className}),
items, items,
...(link && {link}), ...(link && {link}),
}; };
} }
async function dirToItem( function dirToItem(
dir: Dir | null, // The directory item to be transformed. dir: Dir | string, // The directory item to be transformed.
itemKey: string, // For docs, it's the doc ID; for categories, it's used to generate the next `relativePath`. itemKey: string, // File/folder name; for categories, it's used to generate the next `relativePath`.
fullPath: string, // `dir`'s full path relative to the autogen dir. fullPath: string, // `dir`'s full path relative to the autogen dir.
): Promise<WithPosition<NormalizedSidebarItem>> { ): WithPosition<NormalizedSidebarItem> {
return dir return typeof dir === 'object'
? createCategoryItem(dir, fullPath, itemKey) ? createCategoryItem(dir, fullPath, itemKey)
: createDocItem(itemKey.substring(docIdPrefix.length)); : createDocItem(dir, fullPath, itemKey);
} }
return Promise.all( return Object.entries(fsModel).map(([key, content]) =>
Object.entries(fsModel).map(([key, content]) => dirToItem(content, key, key),
dirToItem(content, key, key),
),
); );
} }
@ -248,16 +254,16 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
} }
return item; return item;
}); });
const sortedSidebarItems = _.sortBy( const sortedSidebarItems = _.sortBy(processedSidebarItems, [
processedSidebarItems, 'position',
(item) => item.position, 'source',
); ]);
return sortedSidebarItems.map(({position, ...item}) => item); return sortedSidebarItems.map(({position, source, ...item}) => item);
} }
// TODO: the whole code is designed for pipeline operator // TODO: the whole code is designed for pipeline operator
const docs = getAutogenDocs(); const docs = getAutogenDocs();
const fsModel = treeify(docs); const fsModel = treeify(docs);
const sidebarWithPosition = await generateSidebar(fsModel); const sidebarWithPosition = generateSidebar(fsModel);
const sortedSidebar = sortItems(sidebarWithPosition); const sortedSidebar = sortItems(sidebarWithPosition);
return sortedSidebar; return sortedSidebar;
}; };