mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 02:37:59 +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() {
|
||||
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 &&
|
||||
this.props.config.facebookComments && (
|
||||
|
@ -93,7 +93,7 @@ class BlogPostLayout extends React.Component {
|
|||
render() {
|
||||
const hasOnPageNav = this.props.config.onPageNav === 'separate';
|
||||
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 || {};
|
||||
return (
|
||||
<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 globby from 'globby';
|
||||
import {getDocsDirPaths} from './versions';
|
||||
import {stripNumberPrefix} from './numberPrefix';
|
||||
|
||||
type LastUpdateOptions = Pick<
|
||||
PluginOptions,
|
||||
|
@ -115,9 +116,15 @@ export function processDocMetadata({
|
|||
const {homePageId} = options;
|
||||
const {siteDir, i18n} = context;
|
||||
|
||||
// ex: api/myDoc -> api
|
||||
// ex: api/plugins/myDoc -> api/plugins
|
||||
// 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 {
|
||||
|
@ -126,7 +133,7 @@ export function processDocMetadata({
|
|||
} = frontMatter;
|
||||
|
||||
const baseID: string =
|
||||
frontMatter.id || path.basename(source, path.extname(source));
|
||||
frontMatter.id || stripNumberPrefix(sourceFileNameWithoutExtension);
|
||||
if (baseID.includes('/')) {
|
||||
throw new Error(`Document id [${baseID}] cannot include "/".`);
|
||||
}
|
||||
|
@ -141,7 +148,7 @@ export function processDocMetadata({
|
|||
|
||||
// TODO legacy retrocompatibility
|
||||
// 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
|
||||
const id = `${versionIdPart}${dirNameIdPart}${baseID}`;
|
||||
|
@ -160,7 +167,7 @@ export function processDocMetadata({
|
|||
? '/'
|
||||
: getSlug({
|
||||
baseID,
|
||||
dirName: docsFileDirName,
|
||||
dirName: sourceDirName,
|
||||
frontmatterSlug: frontMatter.slug,
|
||||
});
|
||||
|
||||
|
@ -207,6 +214,7 @@ export function processDocMetadata({
|
|||
title,
|
||||
description,
|
||||
source: aliasedSitePath(filePath, siteDir),
|
||||
sourceDirName,
|
||||
slug: docSlug,
|
||||
permalink,
|
||||
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,
|
||||
SidebarItemAutogenerated,
|
||||
} from './types';
|
||||
import {mapValues, flatten, flatMap, difference} from 'lodash';
|
||||
import {getElementsAround} from '@docusaurus/utils';
|
||||
import {
|
||||
mapValues,
|
||||
flatten,
|
||||
flatMap,
|
||||
difference,
|
||||
sortBy,
|
||||
take,
|
||||
last,
|
||||
} from 'lodash';
|
||||
import {addTrailingSlash, getElementsAround} from '@docusaurus/utils';
|
||||
import combinePromises from 'combine-promises';
|
||||
import {stripNumberPrefix} from './numberPrefix';
|
||||
|
||||
type SidebarItemCategoryJSON = SidebarItemBase & {
|
||||
type: 'category';
|
||||
|
@ -35,7 +44,7 @@ type SidebarItemCategoryJSON = SidebarItemBase & {
|
|||
|
||||
type SidebarItemAutogeneratedJSON = {
|
||||
type: 'autogenerated';
|
||||
path: string;
|
||||
dirPath: string;
|
||||
};
|
||||
|
||||
type SidebarItemJSON =
|
||||
|
@ -130,10 +139,17 @@ function assertIsCategory(
|
|||
function assertIsAutogenerated(
|
||||
item: Record<string, unknown>,
|
||||
): asserts item is SidebarItemAutogeneratedJSON {
|
||||
assertItem(item, ['path']);
|
||||
if (typeof item.path !== 'string') {
|
||||
assertItem(item, ['dirPath']);
|
||||
if (typeof item.dirPath !== 'string') {
|
||||
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);
|
||||
}
|
||||
|
||||
export async function processSidebar(
|
||||
unprocessedSidebar: UnprocessedSidebar,
|
||||
_docs: DocMetadataBase[],
|
||||
): Promise<Sidebar> {
|
||||
async function transformAutogeneratedItem(
|
||||
_item: SidebarItemAutogenerated,
|
||||
): Promise<SidebarItem[]> {
|
||||
// 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',
|
||||
},
|
||||
];
|
||||
async function transformAutogeneratedSidebarItem(
|
||||
autogeneratedItem: SidebarItemAutogenerated,
|
||||
allDocs: DocMetadataBase[],
|
||||
): Promise<SidebarItem[]> {
|
||||
// Doc at the root of the autogenerated sidebar slice
|
||||
function isRootDoc(doc: DocMetadataBase) {
|
||||
return doc.sourceDirName === autogeneratedItem.dirPath;
|
||||
}
|
||||
|
||||
// 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(
|
||||
item: UnprocessedSidebarItem,
|
||||
): Promise<SidebarItem[]> {
|
||||
|
@ -285,13 +413,14 @@ export async function processSidebar(
|
|||
];
|
||||
}
|
||||
if (item.type === 'autogenerated') {
|
||||
return transformAutogeneratedItem(item);
|
||||
return transformAutogeneratedSidebarItem(item, allDocs);
|
||||
}
|
||||
return [item];
|
||||
}
|
||||
|
||||
return (await Promise.all(unprocessedSidebar.map(processRecursive))).flat();
|
||||
}
|
||||
|
||||
export async function processSidebars(
|
||||
unprocessedSidebars: UnprocessedSidebars,
|
||||
docs: DocMetadataBase[],
|
||||
|
|
|
@ -110,7 +110,7 @@ export type SidebarItemCategory = SidebarItemBase & {
|
|||
|
||||
export type SidebarItemAutogenerated = {
|
||||
type: 'autogenerated';
|
||||
path: string;
|
||||
dirPath: string;
|
||||
};
|
||||
|
||||
export type UnprocessedSidebarItemCategory = SidebarItemBase & {
|
||||
|
@ -162,6 +162,7 @@ export type DocMetadataBase = LastUpdateData & {
|
|||
title: string;
|
||||
description: string;
|
||||
source: string;
|
||||
sourceDirName: string; // relative to the docs folder (can be ".")
|
||||
slug: string;
|
||||
permalink: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
This doc is in a subfolder
|
|
@ -9,7 +9,7 @@ module.exports = {
|
|||
docs: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
path: 'autogenerated-folder',
|
||||
dirPath: 'autogenerated-folder',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
|
|
Loading…
Add table
Reference in a new issue