mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-17 02:02:38 +02:00
autogenerated sidebar poc working
This commit is contained in:
parent
749a251e3d
commit
c81da9808e
9 changed files with 241 additions and 36 deletions
|
@ -30,7 +30,7 @@ class BlogPostLayout extends React.Component {
|
||||||
|
|
||||||
renderSocialButtons() {
|
renderSocialButtons() {
|
||||||
const post = this.props.metadata;
|
const post = this.props.metadata;
|
||||||
post.path = utils.getPath(post.path, this.props.config.cleanUrl);
|
post.dirPath = utils.getPath(post.path, this.props.config.cleanUrl);
|
||||||
|
|
||||||
const fbComment = this.props.config.facebookAppId &&
|
const fbComment = this.props.config.facebookAppId &&
|
||||||
this.props.config.facebookComments && (
|
this.props.config.facebookComments && (
|
||||||
|
@ -93,7 +93,7 @@ class BlogPostLayout extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const hasOnPageNav = this.props.config.onPageNav === 'separate';
|
const hasOnPageNav = this.props.config.onPageNav === 'separate';
|
||||||
const post = this.props.metadata;
|
const post = this.props.metadata;
|
||||||
post.path = utils.getPath(post.path, this.props.config.cleanUrl);
|
post.dirPath = utils.getPath(post.path, this.props.config.cleanUrl);
|
||||||
const blogSidebarTitleConfig = this.props.config.blogSidebarTitle || {};
|
const blogSidebarTitleConfig = this.props.config.blogSidebarTitle || {};
|
||||||
return (
|
return (
|
||||||
<Site
|
<Site
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {stripNumberPrefix} from '../numberPrefix';
|
||||||
|
|
||||||
|
describe('stripNumberPrefix', () => {
|
||||||
|
test('should strip number prefix if present', () => {
|
||||||
|
expect(stripNumberPrefix('1-My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('01-My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001-My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('999 - My Doc')).toEqual('My Doc');
|
||||||
|
//
|
||||||
|
expect(stripNumberPrefix('1---My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('01---My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001---My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('999 --- My Doc')).toEqual('My Doc');
|
||||||
|
//
|
||||||
|
expect(stripNumberPrefix('1___My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('01___My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001___My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('999 ___ My Doc')).toEqual('My Doc');
|
||||||
|
//
|
||||||
|
expect(stripNumberPrefix('1.My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('01.My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001.My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc');
|
||||||
|
expect(stripNumberPrefix('999 . My Doc')).toEqual('My Doc');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not strip number prefix if pattern does not match', () => {
|
||||||
|
const badPatterns = [
|
||||||
|
'a1-My Doc',
|
||||||
|
'My Doc-000',
|
||||||
|
'00abc01-My Doc',
|
||||||
|
'My 001- Doc',
|
||||||
|
'My -001 Doc',
|
||||||
|
];
|
||||||
|
|
||||||
|
badPatterns.forEach((badPattern) => {
|
||||||
|
expect(stripNumberPrefix(badPattern)).toEqual(badPattern);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -30,6 +30,7 @@ import getSlug from './slug';
|
||||||
import {CURRENT_VERSION_NAME} from './constants';
|
import {CURRENT_VERSION_NAME} from './constants';
|
||||||
import globby from 'globby';
|
import globby from 'globby';
|
||||||
import {getDocsDirPaths} from './versions';
|
import {getDocsDirPaths} from './versions';
|
||||||
|
import {stripNumberPrefix} from './numberPrefix';
|
||||||
|
|
||||||
type LastUpdateOptions = Pick<
|
type LastUpdateOptions = Pick<
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
|
@ -115,9 +116,15 @@ export function processDocMetadata({
|
||||||
const {homePageId} = options;
|
const {homePageId} = options;
|
||||||
const {siteDir, i18n} = context;
|
const {siteDir, i18n} = context;
|
||||||
|
|
||||||
// ex: api/myDoc -> api
|
// ex: api/plugins/myDoc -> api/plugins
|
||||||
// ex: myDoc -> .
|
// ex: myDoc -> .
|
||||||
const docsFileDirName = path.dirname(source);
|
const sourceDirName = path.dirname(source);
|
||||||
|
// ex: api/plugins/myDoc -> myDoc
|
||||||
|
// ex: myDoc -> myDoc
|
||||||
|
const sourceFileNameWithoutExtension = path.basename(
|
||||||
|
source,
|
||||||
|
path.extname(source),
|
||||||
|
);
|
||||||
|
|
||||||
const {frontMatter = {}, excerpt} = parseMarkdownString(content, source);
|
const {frontMatter = {}, excerpt} = parseMarkdownString(content, source);
|
||||||
const {
|
const {
|
||||||
|
@ -126,7 +133,7 @@ export function processDocMetadata({
|
||||||
} = frontMatter;
|
} = frontMatter;
|
||||||
|
|
||||||
const baseID: string =
|
const baseID: string =
|
||||||
frontMatter.id || path.basename(source, path.extname(source));
|
frontMatter.id || stripNumberPrefix(sourceFileNameWithoutExtension);
|
||||||
if (baseID.includes('/')) {
|
if (baseID.includes('/')) {
|
||||||
throw new Error(`Document id [${baseID}] cannot include "/".`);
|
throw new Error(`Document id [${baseID}] cannot include "/".`);
|
||||||
}
|
}
|
||||||
|
@ -141,7 +148,7 @@ export function processDocMetadata({
|
||||||
|
|
||||||
// TODO legacy retrocompatibility
|
// TODO legacy retrocompatibility
|
||||||
// I think it's bad to affect the frontmatter id with the dirname
|
// I think it's bad to affect the frontmatter id with the dirname
|
||||||
const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`;
|
const dirNameIdPart = sourceDirName === '.' ? '' : `${sourceDirName}/`;
|
||||||
|
|
||||||
// TODO legacy composite id, requires a breaking change to modify this
|
// TODO legacy composite id, requires a breaking change to modify this
|
||||||
const id = `${versionIdPart}${dirNameIdPart}${baseID}`;
|
const id = `${versionIdPart}${dirNameIdPart}${baseID}`;
|
||||||
|
@ -160,7 +167,7 @@ export function processDocMetadata({
|
||||||
? '/'
|
? '/'
|
||||||
: getSlug({
|
: getSlug({
|
||||||
baseID,
|
baseID,
|
||||||
dirName: docsFileDirName,
|
dirName: sourceDirName,
|
||||||
frontmatterSlug: frontMatter.slug,
|
frontmatterSlug: frontMatter.slug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -207,6 +214,7 @@ export function processDocMetadata({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
source: aliasedSitePath(filePath, siteDir),
|
source: aliasedSitePath(filePath, siteDir),
|
||||||
|
sourceDirName,
|
||||||
slug: docSlug,
|
slug: docSlug,
|
||||||
permalink,
|
permalink,
|
||||||
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
|
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
|
||||||
|
|
12
packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
Normal file
12
packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function stripNumberPrefix(str: string) {
|
||||||
|
const numberPrefixPattern = /(?:^(\d)+(\s)*([-_.])+(\s)*)(?<suffix>.*)/;
|
||||||
|
const result = numberPrefixPattern.exec(str);
|
||||||
|
return result?.groups?.suffix ?? str;
|
||||||
|
}
|
|
@ -22,9 +22,18 @@ import {
|
||||||
DocMetadataBase,
|
DocMetadataBase,
|
||||||
SidebarItemAutogenerated,
|
SidebarItemAutogenerated,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {mapValues, flatten, flatMap, difference} from 'lodash';
|
import {
|
||||||
import {getElementsAround} from '@docusaurus/utils';
|
mapValues,
|
||||||
|
flatten,
|
||||||
|
flatMap,
|
||||||
|
difference,
|
||||||
|
sortBy,
|
||||||
|
take,
|
||||||
|
last,
|
||||||
|
} from 'lodash';
|
||||||
|
import {addTrailingSlash, getElementsAround} from '@docusaurus/utils';
|
||||||
import combinePromises from 'combine-promises';
|
import combinePromises from 'combine-promises';
|
||||||
|
import {stripNumberPrefix} from './numberPrefix';
|
||||||
|
|
||||||
type SidebarItemCategoryJSON = SidebarItemBase & {
|
type SidebarItemCategoryJSON = SidebarItemBase & {
|
||||||
type: 'category';
|
type: 'category';
|
||||||
|
@ -35,7 +44,7 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
|
||||||
|
|
||||||
type SidebarItemAutogeneratedJSON = {
|
type SidebarItemAutogeneratedJSON = {
|
||||||
type: 'autogenerated';
|
type: 'autogenerated';
|
||||||
path: string;
|
dirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SidebarItemJSON =
|
type SidebarItemJSON =
|
||||||
|
@ -130,10 +139,17 @@ function assertIsCategory(
|
||||||
function assertIsAutogenerated(
|
function assertIsAutogenerated(
|
||||||
item: Record<string, unknown>,
|
item: Record<string, unknown>,
|
||||||
): asserts item is SidebarItemAutogeneratedJSON {
|
): asserts item is SidebarItemAutogeneratedJSON {
|
||||||
assertItem(item, ['path']);
|
assertItem(item, ['dirPath']);
|
||||||
if (typeof item.path !== 'string') {
|
if (typeof item.dirPath !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error loading ${JSON.stringify(item)}. "path" must be a string.`,
|
`Error loading ${JSON.stringify(item)}. "dirPath" must be a string.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (item.dirPath.startsWith('/') || item.dirPath.endsWith('/')) {
|
||||||
|
throw new Error(
|
||||||
|
`Error loading ${JSON.stringify(
|
||||||
|
item,
|
||||||
|
)}. "dirPath" must be a dir path relative to the docs folder root, and should not start or end with /`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,28 +267,140 @@ export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars {
|
||||||
return normalizeSidebars(sidebarJson);
|
return normalizeSidebars(sidebarJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processSidebar(
|
async function transformAutogeneratedSidebarItem(
|
||||||
unprocessedSidebar: UnprocessedSidebar,
|
autogeneratedItem: SidebarItemAutogenerated,
|
||||||
_docs: DocMetadataBase[],
|
allDocs: DocMetadataBase[],
|
||||||
): Promise<Sidebar> {
|
): Promise<SidebarItem[]> {
|
||||||
async function transformAutogeneratedItem(
|
// Doc at the root of the autogenerated sidebar slice
|
||||||
_item: SidebarItemAutogenerated,
|
function isRootDoc(doc: DocMetadataBase) {
|
||||||
): Promise<SidebarItem[]> {
|
return doc.sourceDirName === autogeneratedItem.dirPath;
|
||||||
// TODO temp: perform real sidebars processing here!
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
href: 'https://docusaurus.io',
|
|
||||||
label: 'DOCUSAURUS_TEST 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
href: 'https://docusaurus.io',
|
|
||||||
label: 'DOCUSAURUS_TEST 2',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Doc inside a subfolder of the autogenerated sidebar slice
|
||||||
|
const categoryDirNameSuffix = addTrailingSlash(autogeneratedItem.dirPath);
|
||||||
|
function isCategoryDoc(doc: DocMetadataBase) {
|
||||||
|
// "api/plugins" startsWith "api/" (but "api2/" docs are excluded)
|
||||||
|
return doc.sourceDirName.startsWith(categoryDirNameSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsUnsorted: DocMetadataBase[] = allDocs.filter(
|
||||||
|
(doc) => isRootDoc(doc) || isCategoryDoc(doc),
|
||||||
|
);
|
||||||
|
// Sort by folder+filename at once
|
||||||
|
const docs = sortBy(docsUnsorted, (d) => d.source);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'autogenDocsSorted',
|
||||||
|
docs.map((d) => ({
|
||||||
|
source: d.source,
|
||||||
|
dir: d.sourceDirName,
|
||||||
|
permalin: d.permalink,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
function createDocSidebarItem(doc: DocMetadataBase): SidebarItemDoc {
|
||||||
|
return {
|
||||||
|
type: 'doc',
|
||||||
|
id: doc.id,
|
||||||
|
...(doc.frontMatter.sidebar_label && {
|
||||||
|
label: doc.frontMatter.sidebar_label,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCategorySidebarItem({
|
||||||
|
dirName,
|
||||||
|
}: {
|
||||||
|
dirName: string;
|
||||||
|
}): Promise<SidebarItemCategory> {
|
||||||
|
// TODO read metadata file from the directory for additional config?
|
||||||
|
return {
|
||||||
|
type: 'category',
|
||||||
|
label: stripNumberPrefix(dirName),
|
||||||
|
items: [],
|
||||||
|
collapsed: true, // TODO use default value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure how to simplify this algorithm :/
|
||||||
|
async function autogenerateSidebarItems(): Promise<SidebarItem[]> {
|
||||||
|
const BreadcrumbSeparator = '/';
|
||||||
|
|
||||||
|
const sidebarItems: SidebarItem[] = []; // mutable result
|
||||||
|
|
||||||
|
const categoriesByBreadcrumb: Record<string, SidebarItemCategory> = {}; // mutable cache of categories already created
|
||||||
|
|
||||||
|
async function getOrCreateCategoriesForBreadcrumb(
|
||||||
|
breadcrumb: string[],
|
||||||
|
): Promise<SidebarItemCategory | null> {
|
||||||
|
if (breadcrumb.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parentBreadcrumb = take(breadcrumb, breadcrumb.length - 1);
|
||||||
|
const lastBreadcrumbElement = last(breadcrumb)!;
|
||||||
|
const parentCategory = await getOrCreateCategoriesForBreadcrumb(
|
||||||
|
parentBreadcrumb,
|
||||||
|
);
|
||||||
|
const existingCategory =
|
||||||
|
categoriesByBreadcrumb[breadcrumb.join(BreadcrumbSeparator)];
|
||||||
|
|
||||||
|
if (existingCategory) {
|
||||||
|
return existingCategory;
|
||||||
|
} else {
|
||||||
|
const newCategory = await createCategorySidebarItem({
|
||||||
|
dirName: lastBreadcrumbElement,
|
||||||
|
});
|
||||||
|
if (parentCategory) {
|
||||||
|
parentCategory.items.push(newCategory);
|
||||||
|
} else {
|
||||||
|
sidebarItems.push(newCategory);
|
||||||
|
}
|
||||||
|
categoriesByBreadcrumb[
|
||||||
|
breadcrumb.join(BreadcrumbSeparator)
|
||||||
|
] = newCategory;
|
||||||
|
return newCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item)
|
||||||
|
function getBreadcrumb(doc: DocMetadataBase): string[] {
|
||||||
|
return isCategoryDoc(doc)
|
||||||
|
? doc.sourceDirName
|
||||||
|
.replace(categoryDirNameSuffix, '')
|
||||||
|
.split(BreadcrumbSeparator)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDocItem(doc: DocMetadataBase): Promise<void> {
|
||||||
|
const breadcrumb = getBreadcrumb(doc);
|
||||||
|
const category = await getOrCreateCategoriesForBreadcrumb(breadcrumb);
|
||||||
|
|
||||||
|
const docSidebarItem = createDocSidebarItem(doc);
|
||||||
|
if (category) {
|
||||||
|
category.items.push(docSidebarItem);
|
||||||
|
} else {
|
||||||
|
sidebarItems.push(docSidebarItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async process made sequential on purpose! order matters
|
||||||
|
for (const doc of docs) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await handleDocItem(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({sidebarItems});
|
||||||
|
|
||||||
|
return sidebarItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
return autogenerateSidebarItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processSidebar(
|
||||||
|
unprocessedSidebar: UnprocessedSidebar,
|
||||||
|
allDocs: DocMetadataBase[],
|
||||||
|
): Promise<Sidebar> {
|
||||||
async function processRecursive(
|
async function processRecursive(
|
||||||
item: UnprocessedSidebarItem,
|
item: UnprocessedSidebarItem,
|
||||||
): Promise<SidebarItem[]> {
|
): Promise<SidebarItem[]> {
|
||||||
|
@ -285,13 +413,14 @@ export async function processSidebar(
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (item.type === 'autogenerated') {
|
if (item.type === 'autogenerated') {
|
||||||
return transformAutogeneratedItem(item);
|
return transformAutogeneratedSidebarItem(item, allDocs);
|
||||||
}
|
}
|
||||||
return [item];
|
return [item];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await Promise.all(unprocessedSidebar.map(processRecursive))).flat();
|
return (await Promise.all(unprocessedSidebar.map(processRecursive))).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processSidebars(
|
export async function processSidebars(
|
||||||
unprocessedSidebars: UnprocessedSidebars,
|
unprocessedSidebars: UnprocessedSidebars,
|
||||||
docs: DocMetadataBase[],
|
docs: DocMetadataBase[],
|
||||||
|
|
|
@ -110,7 +110,7 @@ export type SidebarItemCategory = SidebarItemBase & {
|
||||||
|
|
||||||
export type SidebarItemAutogenerated = {
|
export type SidebarItemAutogenerated = {
|
||||||
type: 'autogenerated';
|
type: 'autogenerated';
|
||||||
path: string;
|
dirPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UnprocessedSidebarItemCategory = SidebarItemBase & {
|
export type UnprocessedSidebarItemCategory = SidebarItemBase & {
|
||||||
|
@ -162,6 +162,7 @@ export type DocMetadataBase = LastUpdateData & {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
sourceDirName: string; // relative to the docs folder (can be ".")
|
||||||
slug: string;
|
slug: string;
|
||||||
permalink: string;
|
permalink: string;
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
This doc is in a subfolder
|
|
@ -9,7 +9,7 @@ module.exports = {
|
||||||
docs: [
|
docs: [
|
||||||
{
|
{
|
||||||
type: 'autogenerated',
|
type: 'autogenerated',
|
||||||
path: 'autogenerated-folder',
|
dirPath: 'autogenerated-folder',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue