feat(content-docs): sidebar category linking to document or auto-generated index page (#5830)

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
Co-authored-by: Armano <armano2@users.noreply.github.com>
Co-authored-by: Alexey Pyltsyn <lex61rus@gmail.com>
This commit is contained in:
Sébastien Lorber 2021-12-03 14:44:59 +01:00 committed by GitHub
parent 95f911efef
commit cfae5d0933
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
105 changed files with 3904 additions and 816 deletions

View file

@ -11,19 +11,27 @@ import type {
SidebarItemCategory,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
SidebarItemCategoryLink,
SidebarItemCategoryLinkConfig,
} from './types';
import {keyBy, sortBy} from 'lodash';
import {sortBy, last} from 'lodash';
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
import {Joi} from '@docusaurus/utils-validation';
import chalk from 'chalk';
import path from 'path';
import fs from 'fs-extra';
import Yaml from 'js-yaml';
import {validateCategoryMetadataFile} from './validation';
import {createDocsByIdIndex, isConventionalDocIndex} from '../docs';
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
function getLocalDocId(docId: string): string {
return last(docId.split('/'))!;
}
export const CategoryMetadataFilenameBase = '_category_';
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
@ -33,6 +41,7 @@ export type CategoryMetadataFile = {
collapsed?: boolean;
collapsible?: boolean;
className?: string;
link?: SidebarItemCategoryLinkConfig;
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
@ -50,17 +59,9 @@ type Dir = {
[item: string]: Dir | null;
};
const CategoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
label: Joi.string(),
position: Joi.number(),
collapsed: Joi.boolean(),
collapsible: Joi.boolean(),
className: Joi.string(),
});
// TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata
// Example use-case being able to disable number prefix parsing at the folder level, or customize the default route path segment for an intermediate directory...
// TODO later if there is `CategoryFolder/index.md`, we may want to read the metadata as yaml on it
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
async function readCategoryMetadataFile(
categoryDirPath: string,
@ -69,7 +70,7 @@ async function readCategoryMetadataFile(
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
const unsafeContent = Yaml.load(contentString);
try {
return Joi.attempt(unsafeContent, CategoryMetadataFileSchema);
return validateCategoryMetadataFile(unsafeContent);
} catch (e) {
console.error(
chalk.red(
@ -100,6 +101,21 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
item: {dirName: autogenDir},
version,
}) => {
const docsById = createDocsByIdIndex(allDocs);
const findDoc = (docId: string): SidebarItemsGeneratorDoc | undefined =>
docsById[docId];
const getDoc = (docId: string): SidebarItemsGeneratorDoc => {
const doc = findDoc(docId);
if (!doc) {
throw new Error(
`Can't find any doc with id=${docId}.\nAvailable doc ids:\n- ${Object.keys(
docsById,
).join('\n- ')}`,
);
}
return doc;
};
/**
* Step 1. Extract the docs that are in the autogen dir.
*/
@ -163,12 +179,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
* (From a record to an array of items, akin to normalizing shorthand)
*/
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
const docsById = keyBy(allDocs, (doc) => doc.id);
function createDocItem(id: string): WithPosition<SidebarItemDoc> {
const {
sidebarPosition: position,
frontMatter: {sidebar_label: label, sidebar_class_name: className},
} = docsById[id];
} = getDoc(id);
return {
type: 'doc',
id,
@ -187,6 +202,57 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
const categoryMetadata = await readCategoryMetadataFile(categoryPath);
const className = categoryMetadata?.className;
const {filename, numberPrefix} = numberPrefixParser(folderName);
const allItems = await Promise.all(
Object.entries(dir).map(([key, content]) =>
dirToItem(content, key, `${fullPath}/${key}`),
),
);
// Try to match a doc inside the category folder,
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
return allItems.find(
(item) => item.type === 'doc' && getLocalDocId(item.id) === localId,
) as SidebarItemDoc | undefined;
}
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
return allItems.find(
(item) =>
item.type === 'doc' && isConventionalDocIndex(getDoc(item.id)),
) as SidebarItemDoc | undefined;
}
function getCategoryLinkedDocId(): string | undefined {
const link = categoryMetadata?.link;
if (link) {
if (link.type === 'doc') {
return findDocByLocalId(link.id)?.id || getDoc(link.id).id;
} else {
// We don't continue for other link types on purpose!
// IE if user decide to use type "generated-index", we should not pick a README.md file as the linked doc
return undefined;
}
}
// Apply default convention to pick index.md, README.md or <categoryName>.md as the category doc
return findConventionalCategoryDocLink()?.id;
}
const categoryLinkedDocId = getCategoryLinkedDocId();
const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId
? {
type: 'doc',
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
}
: // TODO typing issue
(categoryMetadata?.link as SidebarItemCategoryLink | undefined);
// If a doc is linked, remove it from the category subItems
const items = allItems.filter(
(item) => !(item.type === 'doc' && item.id === categoryLinkedDocId),
);
return {
type: 'category',
label: categoryMetadata?.label ?? filename,
@ -195,11 +261,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
position: categoryMetadata?.position ?? numberPrefix,
...(className !== undefined && {className}),
items: await Promise.all(
Object.entries(dir).map(([key, content]) =>
dirToItem(content, key, `${fullPath}/${key}`),
),
),
items,
...(link && {link}),
};
}
async function dirToItem(