mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-06 02:08:55 +02:00
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:
parent
95f911efef
commit
cfae5d0933
105 changed files with 3904 additions and 816 deletions
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue