Skip to content

Commit

Permalink
autogenerated sidebar poc working
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber committed Apr 7, 2021
1 parent 749a251 commit c81da98
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 35 deletions.
4 changes: 2 additions & 2 deletions packages/docusaurus-1.x/lib/core/BlogPostLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
18 changes: 13 additions & 5 deletions packages/docusaurus-plugin-content-docs/src/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 "/".`);
}
Expand All @@ -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}`;
Expand All @@ -160,7 +167,7 @@ export function processDocMetadata({
? '/'
: getSlug({
baseID,
dirName: docsFileDirName,
dirName: sourceDirName,
frontmatterSlug: frontMatter.slug,
});

Expand Down Expand Up @@ -207,6 +214,7 @@ export function processDocMetadata({
title,
description,
source: aliasedSitePath(filePath, siteDir),
sourceDirName,
slug: docSlug,
permalink,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
Expand Down
12 changes: 12 additions & 0 deletions packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
Original file line number Diff line number Diff line change
@@ -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;
}
181 changes: 155 additions & 26 deletions packages/docusaurus-plugin-content-docs/src/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,7 +44,7 @@ type SidebarItemCategoryJSON = SidebarItemBase & {

type SidebarItemAutogeneratedJSON = {
type: 'autogenerated';
path: string;
dirPath: string;
};

type SidebarItemJSON =
Expand Down Expand Up @@ -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)}. "dirPath" must be a string.`,
);
}
if (item.dirPath.startsWith('/') || item.dirPath.endsWith('/')) {
throw new Error(
`Error loading ${JSON.stringify(item)}. "path" must be a string.`,
`Error loading ${JSON.stringify(
item,
)}. "dirPath" must be a dir path relative to the docs folder root, and should not start or end with /`,
);
}
}
Expand Down Expand Up @@ -251,28 +267,140 @@ export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars {
return normalizeSidebars(sidebarJson);
}

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,
_docs: DocMetadataBase[],
allDocs: 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 processRecursive(
item: UnprocessedSidebarItem,
): Promise<SidebarItem[]> {
Expand All @@ -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[],
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus-plugin-content-docs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export type SidebarItemCategory = SidebarItemBase & {

export type SidebarItemAutogenerated = {
type: 'autogenerated';
path: string;
dirPath: string;
};

export type UnprocessedSidebarItemCategory = SidebarItemBase & {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This doc is in a subfolder
Loading

0 comments on commit c81da98

Please sign in to comment.