autogenerated sidebar poc working

This commit is contained in:
slorber 2021-04-07 18:50:46 +02:00
parent 749a251e3d
commit c81da9808e
9 changed files with 241 additions and 36 deletions

View file

@ -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

View file

@ -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);
});
});
});

View file

@ -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(),

View 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;
}

View file

@ -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[],

View file

@ -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

View file

@ -0,0 +1 @@
This doc is in a subfolder

View file

@ -9,7 +9,7 @@ module.exports = {
docs: [
{
type: 'autogenerated',
path: 'autogenerated-folder',
dirPath: 'autogenerated-folder',
},
{
type: 'category',