From a2bf37f059b2f7c0dfca88eefb3861b2ceb0df7e Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 29 Oct 2021 19:48:42 +0200 Subject: [PATCH 01/92] initial poc of DocCategory page --- .../package.json | 1 + .../src/index.ts | 41 +++++---- .../src/plugin-content-docs.d.ts | 9 ++ .../src/routes.ts | 83 +++++++++++++++++++ .../src/theme/DocCategory/index.tsx | 19 +++++ .../src/theme/DocCategory/styles.module.css | 6 ++ 6 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 packages/docusaurus-plugin-content-docs/src/routes.ts create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCategory/styles.module.css diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index ea54fd4b8581..c33661afe3e1 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -36,6 +36,7 @@ "escape-string-regexp": "^4.0.0", "execa": "^5.0.0", "fs-extra": "^10.0.0", + "github-slugger": "^1.4.0", "globby": "^11.0.2", "import-fresh": "^3.2.2", "js-yaml": "^4.0.0", diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 74970360ecad..22c427d31de3 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -52,6 +52,7 @@ import { import chalk from 'chalk'; import {getVersionTags} from './tags'; import {PropTagsListPage} from '@docusaurus/plugin-content-docs-types'; +import {createSidebarsRoutes} from './routes'; export default function pluginContentDocs( context: LoadContext, @@ -242,11 +243,11 @@ export default function pluginContentDocs( }), ); - return routes.sort((a, b) => a.path.localeCompare(b.path)); + return routes; }; - async function createVersionTagsRoutes(loadedVersion: LoadedVersion) { - const versionTags = getVersionTags(loadedVersion.docs); + async function createVersionTagsRoutes(version: LoadedVersion) { + const versionTags = getVersionTags(version.docs); async function createTagsListPage() { const tagsProp: PropTagsListPage['tags'] = Object.values( @@ -260,11 +261,11 @@ export default function pluginContentDocs( // Only create /tags page if there are tags. if (Object.keys(tagsProp).length > 0) { const tagsPropPath = await createData( - `${docuHash(`tags-list-${loadedVersion.versionName}-prop`)}.json`, + `${docuHash(`tags-list-${version.versionName}-prop`)}.json`, JSON.stringify(tagsProp, null, 2), ); addRoute({ - path: loadedVersion.tagsPath, + path: version.tagsPath, exact: true, component: options.docTagsListComponent, modules: { @@ -276,9 +277,9 @@ export default function pluginContentDocs( async function createTagDocListPage(tag: VersionTag) { const tagProps = toTagDocListProp({ - allTagsPath: loadedVersion.tagsPath, + allTagsPath: version.tagsPath, tag, - docs: loadedVersion.docs, + docs: version.docs, }); const tagPropPath = await createData( `${docuHash(`tag-${tag.permalink}`)}.json`, @@ -299,30 +300,38 @@ export default function pluginContentDocs( } async function doCreateVersionRoutes( - loadedVersion: LoadedVersion, + version: LoadedVersion, ): Promise { - await createVersionTagsRoutes(loadedVersion); + await createVersionTagsRoutes(version); - const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion); + const versionMetadata = toVersionMetadataProp(pluginId, version); const versionMetadataPropPath = await createData( - `${docuHash( - `version-${loadedVersion.versionName}-metadata-prop`, - )}.json`, + `${docuHash(`version-${version.versionName}-metadata-prop`)}.json`, JSON.stringify(versionMetadata, null, 2), ); + async function createVersionSubRoutes() { + const [docRoutes, sidebarsRoutes] = await Promise.all([ + createDocRoutes(version.docs), + createSidebarsRoutes({version, actions}), + ]); + + const routes = [...docRoutes, ...sidebarsRoutes]; + return routes.sort((a, b) => a.path.localeCompare(b.path)); + } + addRoute({ - path: loadedVersion.versionPath, + path: version.versionPath, // allow matching /docs/* as well exact: false, // main docs component (DocPage) component: docLayoutComponent, // sub-routes for each doc - routes: await createDocRoutes(loadedVersion.docs), + routes: await createVersionSubRoutes(), modules: { versionMetadata: aliasedSource(versionMetadataPropPath), }, - priority: loadedVersion.routePriority, + priority: version.routePriority, }); } diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index d856c18da013..4b469441d360 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -115,6 +115,15 @@ declare module '@theme/DocItem' { export default DocItem; } +declare module '@theme/DocCategory' { + // TODO + export interface Props { + readonly category: any; + } + + export default function DocCategory(props: Props): JSX.Element; +} + declare module '@theme/DocItemFooter' { import type {Props} from '@theme/DocItem'; diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts new file mode 100644 index 000000000000..2c1705c167f4 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -0,0 +1,83 @@ +/** + * 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 {Sidebar, SidebarItemCategory} from './sidebars/types'; +import {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types'; +import {collectSidebarCategories} from './sidebars/utils'; +import {docuHash, normalizeUrl} from '@docusaurus/utils'; +import {LoadedVersion} from './types'; +import Slugger from 'github-slugger'; + +const createSidebarRoutes = async ({ + sidebarName, + sidebar, + versionPath, + actions, +}: { + sidebarName: string; + sidebar: Sidebar; + versionPath: string; + actions: PluginContentLoadedActions; +}): Promise => { + const slugs = new Slugger(); + + async function createCategoryRoute( + category: SidebarItemCategory, + ): Promise { + // TODO temporary + + const categorySlug = slugs.slug(category.label); + + const slug = normalizeUrl([ + versionPath, + slugs.slug(`${sidebarName}/category/${categorySlug}`), + ]); + const propFileName = `${versionPath}-${sidebarName}-category-${categorySlug}`; + + const prop: any = category; + + const categoryProp = await actions.createData( + `${docuHash(`category/${propFileName}`)}.json`, + JSON.stringify(prop, null, 2), + ); + return { + path: slug, + component: '@theme/DocCategory', + exact: true, + modules: { + category: categoryProp, + }, + }; + } + + const routes = await Promise.all( + collectSidebarCategories(sidebar).map(createCategoryRoute), + ); + + return routes.filter(Boolean) as RouteConfig[]; +}; + +export async function createSidebarsRoutes({ + version, + actions, +}: { + version: LoadedVersion; + actions: PluginContentLoadedActions; +}): Promise { + return ( + await Promise.all( + Object.entries(version.sidebars).map(([sidebarName, sidebar]) => + createSidebarRoutes({ + sidebarName, + sidebar, + versionPath: version.versionPath, + actions, + }), + ), + ) + ).flat(); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx new file mode 100644 index 000000000000..eec195c572b5 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx @@ -0,0 +1,19 @@ +/** + * 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 React from 'react'; + +import type {Props} from '@theme/DocCategory'; + +export default function DocCategory(props: Props): JSX.Element { + return ( + <> +

Category!

+
{JSON.stringify(props.category)}
+ + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCategory/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCategory/styles.module.css new file mode 100644 index 000000000000..b5c0e33b4a5b --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCategory/styles.module.css @@ -0,0 +1,6 @@ +/** + * 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. + */ From 76ade80d9481b76246d548363cae1320e0b29ffb Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 4 Nov 2021 18:34:02 +0100 Subject: [PATCH 02/92] refactor => extract slugification logic to @docusaurus/utils --- packages/docusaurus-mdx-loader/package.json | 1 - .../src/remark/headings/index.ts | 7 ++-- .../package.json | 1 - .../src/routes.ts | 5 ++- packages/docusaurus-utils/package.json | 1 + packages/docusaurus-utils/src/index.ts | 1 + packages/docusaurus-utils/src/slugger.ts | 26 +++++++++++++++ packages/docusaurus/package.json | 1 - .../__tests__/writeHeadingIds.test.ts | 32 +++++++++---------- .../src/commands/writeHeadingIds.ts | 17 ++++++---- 10 files changed, 58 insertions(+), 34 deletions(-) create mode 100644 packages/docusaurus-utils/src/slugger.ts diff --git a/packages/docusaurus-mdx-loader/package.json b/packages/docusaurus-mdx-loader/package.json index 25ee47fa79b6..54da8a9f393a 100644 --- a/packages/docusaurus-mdx-loader/package.json +++ b/packages/docusaurus-mdx-loader/package.json @@ -28,7 +28,6 @@ "escape-html": "^1.0.3", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", - "github-slugger": "^1.4.0", "gray-matter": "^4.0.3", "mdast-util-to-string": "^2.0.0", "remark-emoji": "^2.1.0", diff --git a/packages/docusaurus-mdx-loader/src/remark/headings/index.ts b/packages/docusaurus-mdx-loader/src/remark/headings/index.ts index 957ecdd727c1..99b8afb70c24 100644 --- a/packages/docusaurus-mdx-loader/src/remark/headings/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/headings/index.ts @@ -7,19 +7,16 @@ /* Based on remark-slug (https://github.com/remarkjs/remark-slug) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */ -import {parseMarkdownHeadingId} from '@docusaurus/utils'; +import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils'; import visit, {Visitor} from 'unist-util-visit'; import toString from 'mdast-util-to-string'; -import Slugger from 'github-slugger'; import type {Transformer} from 'unified'; import type {Parent} from 'unist'; import type {Heading, Text} from 'mdast'; -const slugs = new Slugger(); - function headings(): Transformer { const transformer: Transformer = (ast) => { - slugs.reset(); + const slugs = createSlugger(); const visitor: Visitor = (headingNode) => { const data = headingNode.data || (headingNode.data = {}); diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index 862e48a5e44e..61849c6a1ca0 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -36,7 +36,6 @@ "escape-string-regexp": "^4.0.0", "execa": "^5.0.0", "fs-extra": "^10.0.0", - "github-slugger": "^1.4.0", "globby": "^11.0.2", "import-fresh": "^3.2.2", "js-yaml": "^4.0.0", diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index 2c1705c167f4..cb781f860802 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -8,9 +8,8 @@ import {Sidebar, SidebarItemCategory} from './sidebars/types'; import {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types'; import {collectSidebarCategories} from './sidebars/utils'; -import {docuHash, normalizeUrl} from '@docusaurus/utils'; +import {docuHash, normalizeUrl, createSlugger} from '@docusaurus/utils'; import {LoadedVersion} from './types'; -import Slugger from 'github-slugger'; const createSidebarRoutes = async ({ sidebarName, @@ -23,7 +22,7 @@ const createSidebarRoutes = async ({ versionPath: string; actions: PluginContentLoadedActions; }): Promise => { - const slugs = new Slugger(); + const slugs = createSlugger(); async function createCategoryRoute( category: SidebarItemCategory, diff --git a/packages/docusaurus-utils/package.json b/packages/docusaurus-utils/package.json index 8f3febb9b88e..ea67e31c14b0 100644 --- a/packages/docusaurus-utils/package.json +++ b/packages/docusaurus-utils/package.json @@ -24,6 +24,7 @@ "chalk": "^4.1.2", "escape-string-regexp": "^4.0.0", "fs-extra": "^10.0.0", + "github-slugger": "^1.4.0", "globby": "^11.0.4", "gray-matter": "^4.0.3", "lodash": "^4.17.20", diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index e7b8fdf9ab32..478f4aa07b51 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -34,6 +34,7 @@ export * from './codeTranslationsUtils'; export * from './markdownParser'; export * from './markdownLinks'; export * from './escapePath'; +export * from './slugger'; export {md5Hash, simpleHash, docuHash} from './hashUtils'; export { Globby, diff --git a/packages/docusaurus-utils/src/slugger.ts b/packages/docusaurus-utils/src/slugger.ts new file mode 100644 index 000000000000..5cf8d9793b3e --- /dev/null +++ b/packages/docusaurus-utils/src/slugger.ts @@ -0,0 +1,26 @@ +/** + * 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 GithubSlugger from 'github-slugger'; + +// We create our own abstraction on top of the lib: +// - unify usage everywhere in the codebase +// - ability to add extra options +export type SluggerOptions = {maintainCase?: boolean}; + +export type Slugger = { + slug: (value: string, options?: SluggerOptions) => string; +}; + +export function createSlugger(): Slugger { + const githubSlugger = new GithubSlugger(); + return { + slug: (value, options) => { + return githubSlugger.slug(value, options?.maintainCase); + }, + }; +} diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index 4000e8996bd0..6cae49f58533 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -81,7 +81,6 @@ "eta": "^1.12.3", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", - "github-slugger": "^1.4.0", "globby": "^11.0.2", "html-minifier-terser": "^6.0.2", "html-tags": "^3.1.0", diff --git a/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts b/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts index e965c5ea32f9..360a64ecc1e6 100644 --- a/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts +++ b/packages/docusaurus/src/commands/__tests__/writeHeadingIds.test.ts @@ -9,51 +9,51 @@ import { transformMarkdownHeadingLine, transformMarkdownContent, } from '../writeHeadingIds'; -import GithubSlugger from 'github-slugger'; +import {createSlugger} from '@docusaurus/utils'; describe('transformMarkdownHeadingLine', () => { test('throws when not a heading', () => { expect(() => - transformMarkdownHeadingLine('ABC', new GithubSlugger()), + transformMarkdownHeadingLine('ABC', createSlugger()), ).toThrowErrorMatchingInlineSnapshot( `"Line is not a Markdown heading: ABC."`, ); }); test('works for simple level-2 heading', () => { - expect(transformMarkdownHeadingLine('## ABC', new GithubSlugger())).toEqual( + expect(transformMarkdownHeadingLine('## ABC', createSlugger())).toEqual( '## ABC {#abc}', ); }); test('works for simple level-3 heading', () => { - expect( - transformMarkdownHeadingLine('### ABC', new GithubSlugger()), - ).toEqual('### ABC {#abc}'); + expect(transformMarkdownHeadingLine('### ABC', createSlugger())).toEqual( + '### ABC {#abc}', + ); }); test('works for simple level-4 heading', () => { - expect( - transformMarkdownHeadingLine('#### ABC', new GithubSlugger()), - ).toEqual('#### ABC {#abc}'); + expect(transformMarkdownHeadingLine('#### ABC', createSlugger())).toEqual( + '#### ABC {#abc}', + ); }); test('works for simple level-2 heading', () => { - expect(transformMarkdownHeadingLine('## ABC', new GithubSlugger())).toEqual( + expect(transformMarkdownHeadingLine('## ABC', createSlugger())).toEqual( '## ABC {#abc}', ); }); test('unwraps markdown links', () => { const input = `## hello [facebook](https://facebook.com) [crowdin](https://crowdin.com/translate/docusaurus-v2/126/en-fr?filter=basic&value=0)`; - expect(transformMarkdownHeadingLine(input, new GithubSlugger())).toEqual( + expect(transformMarkdownHeadingLine(input, createSlugger())).toEqual( `${input} {#hello-facebook-crowdin}`, ); }); test('can slugify complex headings', () => { const input = '## abc [Hello] How are you %Sébastien_-_$)( ## -56756'; - expect(transformMarkdownHeadingLine(input, new GithubSlugger())).toEqual( + expect(transformMarkdownHeadingLine(input, createSlugger())).toEqual( `${input} {#abc-hello-how-are-you-sébastien_-_---56756}`, ); }); @@ -62,7 +62,7 @@ describe('transformMarkdownHeadingLine', () => { expect( transformMarkdownHeadingLine( '## hello world {#hello-world}', - new GithubSlugger(), + createSlugger(), ), ).toEqual('## hello world {#hello-world}'); }); @@ -71,7 +71,7 @@ describe('transformMarkdownHeadingLine', () => { expect( transformMarkdownHeadingLine( '## New heading {#old-heading}', - new GithubSlugger(), + createSlugger(), ), ).toEqual('## New heading {#old-heading}'); }); @@ -80,7 +80,7 @@ describe('transformMarkdownHeadingLine', () => { expect( transformMarkdownHeadingLine( '## New heading {#old-heading}', - new GithubSlugger(), + createSlugger(), {overwrite: true}, ), ).toEqual('## New heading {#new-heading}'); @@ -88,7 +88,7 @@ describe('transformMarkdownHeadingLine', () => { test('maintains casing when asked to', () => { expect( - transformMarkdownHeadingLine('## getDataFromAPI()', new GithubSlugger(), { + transformMarkdownHeadingLine('## getDataFromAPI()', createSlugger(), { maintainCase: true, }), ).toEqual('## getDataFromAPI() {#getDataFromAPI}'); diff --git a/packages/docusaurus/src/commands/writeHeadingIds.ts b/packages/docusaurus/src/commands/writeHeadingIds.ts index dddc9ee870d4..ff7b6d2557ac 100644 --- a/packages/docusaurus/src/commands/writeHeadingIds.ts +++ b/packages/docusaurus/src/commands/writeHeadingIds.ts @@ -6,12 +6,15 @@ */ import fs from 'fs-extra'; -import GithubSlugger from 'github-slugger'; import chalk from 'chalk'; import {loadContext, loadPluginConfigs} from '../server'; import initPlugins from '../server/plugins/init'; -import {parseMarkdownHeadingId} from '@docusaurus/utils'; +import { + parseMarkdownHeadingId, + createSlugger, + Slugger, +} from '@docusaurus/utils'; import {safeGlobby} from '../server/utils'; type Options = { @@ -25,7 +28,7 @@ function unwrapMarkdownLinks(line: string): string { function addHeadingId( line: string, - slugger: GithubSlugger, + slugger: Slugger, maintainCase: boolean, ): string { let headingLevel = 0; @@ -36,7 +39,7 @@ function addHeadingId( const headingText = line.slice(headingLevel).trimEnd(); const headingHashes = line.slice(0, headingLevel); const slug = slugger - .slug(unwrapMarkdownLinks(headingText).trim(), maintainCase) + .slug(unwrapMarkdownLinks(headingText).trim(), {maintainCase}) .replace(/^-+/, '') .replace(/-+$/, ''); @@ -45,7 +48,7 @@ function addHeadingId( export function transformMarkdownHeadingLine( line: string, - slugger: GithubSlugger, + slugger: Slugger, options: Options = {maintainCase: false, overwrite: false}, ): string { const {maintainCase = false, overwrite = false} = options; @@ -64,7 +67,7 @@ export function transformMarkdownHeadingLine( function transformMarkdownLine( line: string, - slugger: GithubSlugger, + slugger: Slugger, options?: Options, ): string { // Ignore h1 headings on purpose, as we don't create anchor links for those @@ -77,7 +80,7 @@ function transformMarkdownLine( function transformMarkdownLines(lines: string[], options?: Options): string[] { let inCode = false; - const slugger = new GithubSlugger(); + const slugger = createSlugger(); return lines.map((line) => { if (line.startsWith('```')) { From ef2d1c3ba0a5d6810e23c229affba84f86fa98b9 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 4 Nov 2021 19:16:56 +0100 Subject: [PATCH 03/92] generateHugeSidebarItems => reduce size generated --- website/_dogfooding/docs-tests-sidebars.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/_dogfooding/docs-tests-sidebars.js b/website/_dogfooding/docs-tests-sidebars.js index 02682bbdd8d3..a0be9b8a3693 100644 --- a/website/_dogfooding/docs-tests-sidebars.js +++ b/website/_dogfooding/docs-tests-sidebars.js @@ -40,7 +40,7 @@ module.exports = { label: 'Test Test test test test test test', }, ], - ...generateHugeSidebarItems(4), + ...generateHugeSidebarItems(), ], }, { @@ -64,7 +64,7 @@ module.exports = { }; function generateHugeSidebarItems() { - const maxLevel = 4; + const maxLevel = 3; const linksCount = 8; const categoriesCount = 8; From c1c2e1ad4a7da480cd8c944d63b69805a966efa8 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 4 Nov 2021 20:14:40 +0100 Subject: [PATCH 04/92] progress on implementing category link system --- .../src/props.ts | 39 +++++++++++--- .../src/routes.ts | 4 ++ .../src/sidebars/processor.ts | 54 ++++++++++++------- .../src/sidebars/types.ts | 12 +++++ .../src/sidebars/utils.ts | 9 +++- .../src/sidebars/validation.ts | 32 +++++++++++ .../src/theme/DocSidebarItem/index.tsx | 16 +++--- website/sidebars.js | 5 +- 8 files changed, 137 insertions(+), 34 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/props.ts b/packages/docusaurus-plugin-content-docs/src/props.ts index d02342d44f2a..bb8981330d0d 100644 --- a/packages/docusaurus-plugin-content-docs/src/props.ts +++ b/packages/docusaurus-plugin-content-docs/src/props.ts @@ -10,11 +10,14 @@ import type { SidebarItemDoc, SidebarItemLink, SidebarItem, + SidebarItemCategory, + SidebarItemCategoryLink, } from './sidebars/types'; import type { PropSidebars, PropVersionMetadata, PropSidebarItem, + PropSidebarItemCategory, PropTagDocList, PropTagDocListDoc, } from '@docusaurus/plugin-content-docs-types'; @@ -23,8 +26,7 @@ import {compact, keyBy, mapValues} from 'lodash'; export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars { const docsById = keyBy(loadedVersion.docs, (doc) => doc.id); - const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => { - const docId = item.id; + function getDocById(docId: string): DocMetadata { const docMetadata = docsById[docId]; if (!docMetadata) { @@ -34,13 +36,16 @@ Available document ids are: - ${Object.keys(docsById).sort().join('\n- ')}`, ); } + return docMetadata; + } + const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => { + const docMetadata = getDocById(item.id); const { title, permalink, frontMatter: {sidebar_label: sidebarLabel}, } = docMetadata; - return { type: 'link', label: sidebarLabel || item.label || title, @@ -50,10 +55,32 @@ Available document ids are: }; }; - const normalizeItem = (item: SidebarItem): PropSidebarItem => { + function getCategoryLinkHref( + link: SidebarItemCategoryLink | undefined, + ): string | undefined { + switch (link?.type) { + case 'doc': { + const doc = getDocById(link.id); + return doc.permalink; + } + case 'index': { + throw new Error('TODO'); // TODO !!! + } + default: + return undefined; + } + } + + function convertCategory(item: SidebarItemCategory): PropSidebarItemCategory { + const {link, ...rest} = item; + const href = getCategoryLinkHref(link); + return {...rest, items: item.items.map(normalizeItem), ...(href && {href})}; + } + + function normalizeItem(item: SidebarItem): PropSidebarItem { switch (item.type) { case 'category': - return {...item, items: item.items.map(normalizeItem)}; + return convertCategory(item); case 'ref': case 'doc': return convertDocLink(item); @@ -61,7 +88,7 @@ Available document ids are: default: return item; } - }; + } // Transform the sidebar so that all sidebar item will be in the // form of 'link' or 'category' only. diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index cb781f860802..746bcb6b9cc6 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -27,6 +27,10 @@ const createSidebarRoutes = async ({ async function createCategoryRoute( category: SidebarItemCategory, ): Promise { + if (true) { + return undefined; + } + // TODO temporary const categorySlug = slugs.slug(category.label); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts index 3f229f177284..cf6402a45372 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts @@ -21,6 +21,9 @@ import type { SidebarItemsGeneratorOption, SidebarItemsGeneratorDoc, SidebarItemsGeneratorVersion, + NormalizedSidebarItemCategory, + SidebarItemCategory, + SidebarItemAutogenerated, } from './types'; import {transformSidebarItems} from './utils'; import {DefaultSidebarItemsGenerator} from './generator'; @@ -70,34 +73,47 @@ async function processSidebar( version: toSidebarItemsGeneratorVersion(version), })); - async function handleAutoGeneratedItems( + async function processCategoryItem( + item: NormalizedSidebarItemCategory, + ): Promise { + return { + ...item, + items: (await Promise.all(item.items.map(processItem))).flat(), + }; + } + + async function processAutoGeneratedItem( + item: SidebarItemAutogenerated, + ): Promise { + const generatedItems = await sidebarItemsGenerator({ + item, + numberPrefixParser, + defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, + ...getSidebarItemsGeneratorDocsAndVersion(), + options, + }); + return processItems(generatedItems); + } + + async function processItem( item: NormalizedSidebarItem, ): Promise { if (item.type === 'category') { - return [ - { - ...item, - items: ( - await Promise.all(item.items.map(handleAutoGeneratedItems)) - ).flat(), - }, - ]; + return [await processCategoryItem(item)]; } if (item.type === 'autogenerated') { - return sidebarItemsGenerator({ - item, - numberPrefixParser, - defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, - ...getSidebarItemsGeneratorDocsAndVersion(), - options, - }); + return processAutoGeneratedItem(item); } return [item]; } - const processedSidebar = ( - await Promise.all(unprocessedSidebar.map(handleAutoGeneratedItems)) - ).flat(); + async function processItems( + items: NormalizedSidebarItem[], + ): Promise { + return (await Promise.all(items.map(processItem))).flat(); + } + + const processedSidebar = await processItems(unprocessedSidebar); const fixSidebarItemInconsistencies = (item: SidebarItem): SidebarItem => { // A non-collapsible category can't be collapsed! diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts index 59e4c73b3a00..dd28bc2c7724 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts @@ -45,10 +45,19 @@ type SidebarItemCategoryBase = SidebarItemBase & { collapsible: boolean; }; +export type SidebarItemCategoryLinkDoc = {type: 'doc'; id: string}; + +export type SidebarItemCategoryLinkIndex = {type: 'index'; slug?: string}; + +export type SidebarItemCategoryLink = + | SidebarItemCategoryLinkDoc + | SidebarItemCategoryLinkIndex; + // The user-given configuration in sidebars.js, before normalization export type SidebarItemCategoryConfig = Expand< Optional & { items: SidebarItemConfig[]; + link?: SidebarItemCategoryLink; } >; @@ -79,6 +88,7 @@ export type SidebarsConfig = { export type NormalizedSidebarItemCategory = Expand< SidebarItemCategoryBase & { items: NormalizedSidebarItem[]; + link?: SidebarItemCategoryLink; } >; @@ -96,6 +106,7 @@ export type NormalizedSidebars = { export type SidebarItemCategory = Expand< SidebarItemCategoryBase & { items: SidebarItem[]; + link?: SidebarItemCategoryLink; } >; @@ -114,6 +125,7 @@ export type Sidebars = { export type PropSidebarItemCategory = Expand< SidebarItemCategoryBase & { items: PropSidebarItem[]; + href?: string; } >; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts index 465af0f9aa17..fd1bfeee69f2 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts @@ -62,11 +62,18 @@ export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] { return collectSidebarItemsOfType('link', sidebar); } +// /!\ docId order matters! export function collectSidebarsDocIds( sidebars: Sidebars, ): Record { return mapValues(sidebars, (sidebar) => { - return collectSidebarDocItems(sidebar).map((docItem) => docItem.id); + // TODO does not collect ids in correct order! + return [ + ...collectSidebarDocItems(sidebar).map((docItem) => docItem.id), + ...collectSidebarCategories(sidebar).flatMap((category) => + category?.link?.type === 'doc' ? [category.link.id] : [], + ), + ]; }); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts index db2e93fbe189..7dececb015cc 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts @@ -14,8 +14,11 @@ import { SidebarItemDoc, SidebarItemLink, SidebarItemCategoryConfig, + SidebarItemCategoryLink, SidebarsConfig, isCategoriesShorthand, + SidebarItemCategoryLinkDoc, + SidebarItemCategoryLinkIndex, } from './types'; const sidebarItemBaseSchema = Joi.object({ @@ -48,6 +51,33 @@ const sidebarItemLinkSchema = sidebarItemBaseSchema.append({ .messages({'any.unknown': '"label" must be a string'}), }); +const sidebarItemCategoryLinkSchema = Joi.object() + .when('.type', { + switch: [ + { + is: 'doc', + then: Joi.object({ + type: 'doc', + id: Joi.string().required(), + }), + }, + { + is: 'index', + then: Joi.object({ + type: 'index', + slug: Joi.string().optional(), + }), + }, + { + is: Joi.string().required(), + then: Joi.forbidden().messages({ + 'any.unknown': 'Unknown sidebar category link type "{.type}".', + }), + }, + ], + }) + .id('sidebarCategoryLinkSchema'); + const sidebarItemCategorySchema = sidebarItemBaseSchema.append({ type: 'category', @@ -58,6 +88,7 @@ const sidebarItemCategorySchema = items: Joi.array() .required() .messages({'any.unknown': '"items" must be an array'}), // .items(Joi.link('#sidebarItemSchema')), + link: sidebarItemCategoryLinkSchema, collapsed: Joi.boolean().messages({ 'any.unknown': '"collapsed" must be a boolean', }), @@ -76,6 +107,7 @@ const sidebarItemSchema: Joi.Schema = Joi.object() }, {is: 'autogenerated', then: sidebarItemAutogeneratedSchema}, {is: 'category', then: sidebarItemCategorySchema}, + // TODO looks deprecated for a very long time? =>remove? { is: 'subcategory', then: Joi.forbidden().messages({ diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx index 59a2d2a330c4..5d88f4071799 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx @@ -103,7 +103,7 @@ function DocSidebarItemCategory({ level, ...props }: Props & {item: PropSidebarItemCategory}) { - const {items, label, collapsible, className} = item; + const {items, label, collapsible, className, href} = item; const isActive = isActiveSidebarItem(item, activePath); @@ -132,7 +132,7 @@ function DocSidebarItemCategory({ className, )}> {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - { - e.preventDefault(); - toggleCollapsed(); + if (href) { + setCollapsed(false); + } else { + e.preventDefault(); + toggleCollapsed(); + } } : undefined } - href={collapsible ? '#' : undefined} + href={collapsible ? href ?? '#' : href} {...props}> {label} - + Date: Fri, 5 Nov 2021 10:44:08 +0100 Subject: [PATCH 05/92] isActiveSidebarItem => support for category link --- .../src/theme/DocSidebarItem/index.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx index 5d88f4071799..ce733bc6f0e3 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx @@ -20,26 +20,39 @@ import IconExternalLink from '@theme/IconExternalLink'; import type {Props, DocSidebarItemsProps} from '@theme/DocSidebarItem'; import type { + PropSidebarItem, PropSidebarItemCategory, PropSidebarItemLink, } from '@docusaurus/plugin-content-docs-types'; import styles from './styles.module.css'; -const isActiveSidebarItem = ( - item: Props['item'], +function containsActiveSidebarItem( + items: PropSidebarItem[], activePath: string, -): boolean => { +): boolean { + return items.some((subItem) => isActiveSidebarItem(subItem, activePath)); +} + +function isActiveSidebarItem( + item: PropSidebarItem, + activePath: string, +): boolean { + const isActive = (testedPath: string | undefined) => + typeof testedPath !== 'undefined' && isSamePath(testedPath, activePath); + if (item.type === 'link') { - return isSamePath(item.href, activePath); + return isActive(item.href); } + if (item.type === 'category') { - return item.items.some((subItem) => - isActiveSidebarItem(subItem, activePath), + return ( + isActive(item.href) || containsActiveSidebarItem(item.items, activePath) ); } + return false; -}; +} // Optimize sidebar at each "level" // TODO this item should probably not receive the "activePath" props From e9643a3ed4a9834707defa89a2034c526db79a3d Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 11:06:15 +0100 Subject: [PATCH 06/92] fix navigation ordering --- .../src/sidebars/utils.ts | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts index fd1bfeee69f2..3eb8a00bf3eb 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts @@ -33,21 +33,24 @@ export function transformSidebarItems( return sidebar.map(transformRecursive); } +// Flatten sidebar items into a single flat array (containing categories/docs on the same level) +// /!\ order matters (useful for next/prev nav), top categories appear before their child elements +function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] { + function flattenRecursive(item: SidebarItem): SidebarItem[] { + return item.type === 'category' + ? [item, ...item.items.flatMap(flattenRecursive)] + : [item]; + } + return items.flatMap(flattenRecursive); +} + function collectSidebarItemsOfType< Type extends SidebarItemType, Item extends SidebarItem & {type: SidebarItemType}, >(type: Type, sidebar: Sidebar): Item[] { - function collectRecursive(item: SidebarItem): Item[] { - const currentItemsCollected: Item[] = - item.type === type ? [item as Item] : []; - - const childItemsCollected: Item[] = - item.type === 'category' ? item.items.flatMap(collectRecursive) : []; - - return [...currentItemsCollected, ...childItemsCollected]; - } - - return sidebar.flatMap(collectRecursive); + return flattenSidebarItems(sidebar).filter( + (item) => item.type === type, + ) as Item[]; } export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] { @@ -62,19 +65,23 @@ export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] { return collectSidebarItemsOfType('link', sidebar); } -// /!\ docId order matters! +// /!\ docId order matters for navigation! +export function collectSidebarDocIds(sidebar: Sidebar): string[] { + return flattenSidebarItems(sidebar).flatMap((item) => { + if (item.type === 'category') { + return item.link?.type === 'doc' ? [item.link.id] : []; + } + if (item.type === 'doc') { + return [item.id]; + } + return []; + }); +} + export function collectSidebarsDocIds( sidebars: Sidebars, ): Record { - return mapValues(sidebars, (sidebar) => { - // TODO does not collect ids in correct order! - return [ - ...collectSidebarDocItems(sidebar).map((docItem) => docItem.id), - ...collectSidebarCategories(sidebar).flatMap((category) => - category?.link?.type === 'doc' ? [category.link.id] : [], - ), - ]; - }); + return mapValues(sidebars, collectSidebarDocIds); } export function createSidebarsUtils(sidebars: Sidebars): { From f0474a3408bcbba138bbbf0ea0b13af17bc29d0d Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 14:42:19 +0100 Subject: [PATCH 07/92] implementation POC for autogenerated sidebars --- .../src/sidebars/generator.ts | 48 ++++++++++++++++--- .../regular-category/sample-doc.md | 3 ++ .../tests/category-links/sample-doc.md | 3 ++ .../with-category-name-doc/sample-doc.md | 3 ++ .../with-category-name-doc.md | 3 ++ .../category-links/with-index-doc/index.md | 3 ++ .../with-index-doc/sample-doc.md | 3 ++ .../category-links/with-readme-doc/readme.md | 3 ++ .../with-readme-doc/sample-doc.md | 3 ++ 9 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 website/_dogfooding/_docs tests/tests/category-links/regular-category/sample-doc.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/sample-doc.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/sample-doc.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/with-category-name-doc.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/with-index-doc/index.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/with-index-doc/sample-doc.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/readme.md create mode 100644 website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/sample-doc.md diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index d5dde0d7f1ea..6fa27299fd3f 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -11,6 +11,7 @@ import type { SidebarItemCategory, SidebarItemsGenerator, SidebarItemsGeneratorDoc, + SidebarItemCategoryLinkDoc, } from './types'; import {keyBy, sortBy} from 'lodash'; import {addTrailingSlash, posixPath} from '@docusaurus/utils'; @@ -24,6 +25,23 @@ const BreadcrumbSeparator = '/'; // To avoid possible name clashes with a folder of the same name as the ID const docIdPrefix = '$doc$/'; +// TODO make this function configurable? +function isCategoryIndexDoc({ + folderName, + item, +}: { + folderName: string; + item: SidebarItemDoc; +}): boolean { + // TODO using the id is not 100% accurate, but good enough for now? + const parts = item.id.split('/'); + const docName = parts[parts.length - 1]!; + + const eligibleDocIndexNames = ['index', 'readme', folderName]; + + return eligibleDocIndexNames.includes(docName); +} + export const CategoryMetadataFilenameBase = '_category_'; export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}'; @@ -60,7 +78,7 @@ const CategoryMetadatasFileSchema = Joi.object({ // 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 readCategoryMetadatasFile( categoryDirPath: string, @@ -187,6 +205,27 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ const categoryMetadatas = await readCategoryMetadatasFile(categoryPath); const className = categoryMetadatas?.className; const {filename, numberPrefix} = numberPrefixParser(folderName); + const allItems = await Promise.all( + Object.entries(dir).map(([key, content]) => + dirToItem(content, key, `${fullPath}/${key}`), + ), + ); + + const categoryIndexDoc = allItems.find( + (item) => item.type === 'doc' && isCategoryIndexDoc({folderName, item}), + ) as SidebarItemDoc | undefined; + + const link: SidebarItemCategoryLinkDoc | undefined = categoryIndexDoc + ? { + type: 'doc', + id: categoryIndexDoc.id, + } + : undefined; + + const items = categoryIndexDoc + ? allItems.filter((item) => item !== categoryIndexDoc) + : allItems; + return { type: 'category', label: categoryMetadatas?.label ?? filename, @@ -195,11 +234,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ collapsed: categoryMetadatas?.collapsed ?? options.sidebarCollapsed, position: categoryMetadatas?.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( diff --git a/website/_dogfooding/_docs tests/tests/category-links/regular-category/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/regular-category/sample-doc.md new file mode 100644 index 000000000000..15fb7937c350 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/regular-category/sample-doc.md @@ -0,0 +1,3 @@ +# Sample doc + +Lorem Ipsum diff --git a/website/_dogfooding/_docs tests/tests/category-links/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/sample-doc.md new file mode 100644 index 000000000000..15fb7937c350 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/sample-doc.md @@ -0,0 +1,3 @@ +# Sample doc + +Lorem Ipsum diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/sample-doc.md new file mode 100644 index 000000000000..15fb7937c350 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/sample-doc.md @@ -0,0 +1,3 @@ +# Sample doc + +Lorem Ipsum diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/with-category-name-doc.md b/website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/with-category-name-doc.md new file mode 100644 index 000000000000..762746bebf33 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/with-category-name-doc/with-category-name-doc.md @@ -0,0 +1,3 @@ +# Category with a doc of category's name + +You should be able to click on the category and browse this `/.md` doc diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-index-doc/index.md b/website/_dogfooding/_docs tests/tests/category-links/with-index-doc/index.md new file mode 100644 index 000000000000..38215175ad84 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/with-index-doc/index.md @@ -0,0 +1,3 @@ +# Category with index.md doc + +You should be able to click on the category and browse this `index.md` doc diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-index-doc/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/with-index-doc/sample-doc.md new file mode 100644 index 000000000000..15fb7937c350 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/with-index-doc/sample-doc.md @@ -0,0 +1,3 @@ +# Sample doc + +Lorem Ipsum diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/readme.md b/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/readme.md new file mode 100644 index 000000000000..a47c08568323 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/readme.md @@ -0,0 +1,3 @@ +# Category with readme.md doc + +You should be able to click on the category and browse this `readme.md` doc diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/sample-doc.md new file mode 100644 index 000000000000..15fb7937c350 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/sample-doc.md @@ -0,0 +1,3 @@ +# Sample doc + +Lorem Ipsum From 15bb221810cda0e397841dacaa7de3d828af921d Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 15:12:05 +0100 Subject: [PATCH 08/92] do not use conventional category doc link when an explicit "generated-index" category link is used --- .../src/sidebars/generator.ts | 32 +++++++++-- .../src/sidebars/validation.ts | 53 ++++++++++--------- .../tests/category-links/_category_.json | 7 +++ .../tests/category-links/readme.md | 3 ++ .../tests/category-links/sample-doc.md | 3 -- 5 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 website/_dogfooding/_docs tests/tests/category-links/_category_.json create mode 100644 website/_dogfooding/_docs tests/tests/category-links/readme.md delete mode 100644 website/_dogfooding/_docs tests/tests/category-links/sample-doc.md diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index 6fa27299fd3f..87e25ffe18a6 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -12,6 +12,7 @@ import type { SidebarItemsGenerator, SidebarItemsGeneratorDoc, SidebarItemCategoryLinkDoc, + SidebarItemCategoryLink, } from './types'; import {keyBy, sortBy} from 'lodash'; import {addTrailingSlash, posixPath} from '@docusaurus/utils'; @@ -20,13 +21,15 @@ import chalk from 'chalk'; import path from 'path'; import fs from 'fs-extra'; import Yaml from 'js-yaml'; +import {sidebarItemCategoryLinkSchema} from './validation'; const BreadcrumbSeparator = '/'; // To avoid possible name clashes with a folder of the same name as the ID const docIdPrefix = '$doc$/'; +// By convention, Docusaurus turns certain doc filenames as category doc links // TODO make this function configurable? -function isCategoryIndexDoc({ +function isConventionalCategoryDocLink({ folderName, item, }: { @@ -51,6 +54,7 @@ export type CategoryMetadatasFile = { collapsed?: boolean; collapsible?: boolean; className?: string; + link?: SidebarItemCategoryLink; // 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/ @@ -74,6 +78,7 @@ const CategoryMetadatasFileSchema = Joi.object({ collapsed: Joi.boolean(), collapsible: Joi.boolean(), className: Joi.string(), + link: sidebarItemCategoryLinkSchema, }); // TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata @@ -211,9 +216,28 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ ), ); - const categoryIndexDoc = allItems.find( - (item) => item.type === 'doc' && isCategoryIndexDoc({folderName, item}), - ) as SidebarItemDoc | undefined; + function getCategoryDoc(): SidebarItemDoc | undefined { + const link = categoryMetadatas?.link; + if (link) { + if (link.type === 'doc') { + return allItems.find( + (item) => item.type === 'doc' && item.id === link.id, + ) as SidebarItemDoc | undefined; + } else { + // We don't continue for other link types on purpose! + // IE if user decide to use type "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 .md as the category doc + return allItems.find( + (item) => + item.type === 'doc' && + isConventionalCategoryDocLink({folderName, item}), + ) as SidebarItemDoc | undefined; + } + + const categoryIndexDoc = getCategoryDoc(); const link: SidebarItemCategoryLinkDoc | undefined = categoryIndexDoc ? { diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts index 7dececb015cc..a16edbc5ec55 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts @@ -51,32 +51,33 @@ const sidebarItemLinkSchema = sidebarItemBaseSchema.append({ .messages({'any.unknown': '"label" must be a string'}), }); -const sidebarItemCategoryLinkSchema = Joi.object() - .when('.type', { - switch: [ - { - is: 'doc', - then: Joi.object({ - type: 'doc', - id: Joi.string().required(), - }), - }, - { - is: 'index', - then: Joi.object({ - type: 'index', - slug: Joi.string().optional(), - }), - }, - { - is: Joi.string().required(), - then: Joi.forbidden().messages({ - 'any.unknown': 'Unknown sidebar category link type "{.type}".', - }), - }, - ], - }) - .id('sidebarCategoryLinkSchema'); +export const sidebarItemCategoryLinkSchema = + Joi.object() + .when('.type', { + switch: [ + { + is: 'doc', + then: Joi.object({ + type: 'doc', + id: Joi.string().required(), + }), + }, + { + is: 'generated-index', + then: Joi.object({ + type: 'generated-index', + slug: Joi.string().optional(), + }), + }, + { + is: Joi.string().required(), + then: Joi.forbidden().messages({ + 'any.unknown': 'Unknown sidebar category link type "{.type}".', + }), + }, + ], + }) + .id('sidebarCategoryLinkSchema'); const sidebarItemCategorySchema = sidebarItemBaseSchema.append({ diff --git a/website/_dogfooding/_docs tests/tests/category-links/_category_.json b/website/_dogfooding/_docs tests/tests/category-links/_category_.json new file mode 100644 index 000000000000..d2b06a536458 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Category Links", + "link": { + "type": "generated-index", + "slug": "/category-links-slug" + } +} diff --git a/website/_dogfooding/_docs tests/tests/category-links/readme.md b/website/_dogfooding/_docs tests/tests/category-links/readme.md new file mode 100644 index 000000000000..7989bc9ea445 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/category-links/readme.md @@ -0,0 +1,3 @@ +# Readme + +This `readme.md` should not be used as the category index due to the `_category_.json` link diff --git a/website/_dogfooding/_docs tests/tests/category-links/sample-doc.md b/website/_dogfooding/_docs tests/tests/category-links/sample-doc.md deleted file mode 100644 index 15fb7937c350..000000000000 --- a/website/_dogfooding/_docs tests/tests/category-links/sample-doc.md +++ /dev/null @@ -1,3 +0,0 @@ -# Sample doc - -Lorem Ipsum From 9e95fe0a0c4da5a6a8a506fd57c881347a2ecf33 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 17:56:42 +0100 Subject: [PATCH 09/92] make category index page work --- .eslintrc.js | 1 + .../docusaurus-plugin-content-docs/src/cli.ts | 8 +- .../src/plugin-content-docs.d.ts | 17 ++-- .../src/props.ts | 11 +-- .../src/routes.ts | 60 +++++++++------ .../__snapshots__/index.test.ts.snap | 12 +-- .../src/sidebars/__tests__/index.test.ts | 50 ++++++------ .../src/sidebars/index.ts | 30 ++++---- .../src/sidebars/normalization.ts | 77 ++++++++++++++----- .../src/sidebars/types.ts | 18 ++++- .../src/sidebars/validation.ts | 4 +- .../src/types.ts | 11 ++- .../index.tsx | 8 +- .../styles.module.css | 0 website/_dogfooding/docs-tests-sidebars.js | 7 +- website/sidebars.js | 3 + 16 files changed, 199 insertions(+), 118 deletions(-) rename packages/docusaurus-theme-classic/src/theme/{DocCategory => DocCategoryGeneratedIndex}/index.tsx (51%) rename packages/docusaurus-theme-classic/src/theme/{DocCategory => DocCategoryGeneratedIndex}/styles.module.css (100%) diff --git a/.eslintrc.js b/.eslintrc.js index d379a078aa32..31b3a4291ed5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,6 +52,7 @@ module.exports = { {ignore: ['^@theme', '^@docusaurus', '^@generated', 'unist', 'mdast']}, ], 'import/extensions': OFF, + 'no-inner-declarations': OFF, 'header/header': [ ERROR, 'block', diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts index 1e5c87257f44..7719cdd9bd68 100644 --- a/packages/docusaurus-plugin-content-docs/src/cli.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -15,7 +15,7 @@ import path from 'path'; import type {PathOptions, SidebarOptions} from './types'; import {transformSidebarItems} from './sidebars/utils'; import type {SidebarItem, NormalizedSidebars, Sidebar} from './sidebars/types'; -import {loadUnprocessedSidebars, resolveSidebarPathOption} from './sidebars'; +import {loadSidebarsFile, resolveSidebarPathOption} from './sidebars'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; function createVersionedSidebarFile({ @@ -23,16 +23,15 @@ function createVersionedSidebarFile({ pluginId, sidebarPath, version, - options, }: { siteDir: string; pluginId: string; sidebarPath: string | false | undefined; version: string; - options: SidebarOptions; }) { // Load current sidebar and create a new versioned sidebars file (if needed). - const loadedSidebars = loadUnprocessedSidebars(sidebarPath, options); + // Note: we don't need the sidebars file to be normalized: it's ok to let plugin option changes to impact versioned sidebars + const loadedSidebars = loadSidebarsFile(sidebarPath); // Do not create a useless versioned sidebars file if sidebars file is empty or sidebars are disabled/false) const shouldCreateVersionedSidebarFile = @@ -155,7 +154,6 @@ export function cliDocsVersionCommand( pluginId, version, sidebarPath: resolveSidebarPathOption(siteDir, sidebarPath), - options, }); // Update versions.json file. diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 4b469441d360..e5c162299eb0 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -30,6 +30,12 @@ declare module '@docusaurus/plugin-content-docs-types' { docsSidebars: PropSidebars; }; + export type PropCategoryGeneratedIndex = { + label: string; + slug: string; + permalink: string; + }; + export type PropSidebarItemLink = import('./sidebars/types').SidebarItemLink; export type PropSidebarItemCategory = import('./sidebars/types').PropSidebarItemCategory; @@ -115,13 +121,12 @@ declare module '@theme/DocItem' { export default DocItem; } -declare module '@theme/DocCategory' { - // TODO - export interface Props { - readonly category: any; - } +declare module '@theme/DocCategoryGeneratedIndex' { + import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs-types'; + + export interface Props extends PropCategoryGeneratedIndex {} - export default function DocCategory(props: Props): JSX.Element; + export default function DocCategoryGeneratedIndex(props: Props): JSX.Element; } declare module '@theme/DocItemFooter' { diff --git a/packages/docusaurus-plugin-content-docs/src/props.ts b/packages/docusaurus-plugin-content-docs/src/props.ts index bb8981330d0d..2dbafeabecf8 100644 --- a/packages/docusaurus-plugin-content-docs/src/props.ts +++ b/packages/docusaurus-plugin-content-docs/src/props.ts @@ -59,13 +59,10 @@ Available document ids are: link: SidebarItemCategoryLink | undefined, ): string | undefined { switch (link?.type) { - case 'doc': { - const doc = getDocById(link.id); - return doc.permalink; - } - case 'index': { - throw new Error('TODO'); // TODO !!! - } + case 'doc': + return getDocById(link.id).permalink; + case 'generated-index': + return link.permalink; default: return undefined; } diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index 746bcb6b9cc6..21beb63a286a 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -5,13 +5,18 @@ * LICENSE file in the root directory of this source tree. */ -import {Sidebar, SidebarItemCategory} from './sidebars/types'; +import { + Sidebar, + SidebarItemCategory, + SidebarItemCategoryLinkGeneratedIndex, +} from './sidebars/types'; import {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types'; import {collectSidebarCategories} from './sidebars/utils'; -import {docuHash, normalizeUrl, createSlugger} from '@docusaurus/utils'; +import {docuHash, createSlugger} from '@docusaurus/utils'; import {LoadedVersion} from './types'; +import {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs-types'; -const createSidebarRoutes = async ({ +async function createSidebarRoutes({ sidebarName, sidebar, versionPath, @@ -21,48 +26,53 @@ const createSidebarRoutes = async ({ sidebar: Sidebar; versionPath: string; actions: PluginContentLoadedActions; -}): Promise => { +}): Promise { const slugs = createSlugger(); - async function createCategoryRoute( + async function createCategoryGeneratedIndexRoute( category: SidebarItemCategory, - ): Promise { - if (true) { - return undefined; - } - - // TODO temporary - - const categorySlug = slugs.slug(category.label); - - const slug = normalizeUrl([ - versionPath, - slugs.slug(`${sidebarName}/category/${categorySlug}`), - ]); - const propFileName = `${versionPath}-${sidebarName}-category-${categorySlug}`; + link: SidebarItemCategoryLinkGeneratedIndex, + ): Promise { + const propFileName = slugs.slug( + `${versionPath}-${sidebarName}-category-${category.label}`, + ); - const prop: any = category; + const prop: PropCategoryGeneratedIndex = { + label: category.label, + slug: link.slug, + permalink: link.permalink, + }; - const categoryProp = await actions.createData( + const propData = await actions.createData( `${docuHash(`category/${propFileName}`)}.json`, JSON.stringify(prop, null, 2), ); + return { - path: slug, - component: '@theme/DocCategory', + path: link.permalink, + component: '@theme/DocCategoryGeneratedIndex', exact: true, modules: { - category: categoryProp, + categoryIndex: propData, }, }; } + async function createCategoryRoute( + category: SidebarItemCategory, + ): Promise { + if (category.link?.type === 'generated-index') { + return createCategoryGeneratedIndexRoute(category, category.link); + } + return undefined; + } + const routes = await Promise.all( collectSidebarCategories(sidebar).map(createCategoryRoute), ); return routes.filter(Boolean) as RouteConfig[]; -}; +} export async function createSidebarsRoutes({ version, diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap index a4457c5bc788..61d897258e9b 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`loadUnprocessedSidebars sidebars link 1`] = ` +exports[`loadNormalizedSidebars sidebars link 1`] = ` Object { "docs": Array [ Object { @@ -20,7 +20,7 @@ Object { } `; -exports[`loadUnprocessedSidebars sidebars with category.collapsed property 1`] = ` +exports[`loadNormalizedSidebars sidebars with category.collapsed property 1`] = ` Object { "docs": Array [ Object { @@ -67,7 +67,7 @@ Object { } `; -exports[`loadUnprocessedSidebars sidebars with category.collapsed property at first level 1`] = ` +exports[`loadNormalizedSidebars sidebars with category.collapsed property at first level 1`] = ` Object { "docs": Array [ Object { @@ -98,7 +98,7 @@ Object { } `; -exports[`loadUnprocessedSidebars sidebars with deep level of category 1`] = ` +exports[`loadNormalizedSidebars sidebars with deep level of category 1`] = ` Object { "docs": Array [ Object { @@ -165,7 +165,7 @@ Object { } `; -exports[`loadUnprocessedSidebars sidebars with first level not a category 1`] = ` +exports[`loadNormalizedSidebars sidebars with first level not a category 1`] = ` Object { "docs": Array [ Object { @@ -188,7 +188,7 @@ Object { } `; -exports[`loadUnprocessedSidebars sidebars with known sidebar item type 1`] = ` +exports[`loadNormalizedSidebars sidebars with known sidebar item type 1`] = ` Object { "docs": Array [ Object { diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts index 8d6f389202f0..13a41c213ec6 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/index.test.ts @@ -7,27 +7,31 @@ import path from 'path'; import { - loadUnprocessedSidebars, + loadNormalizedSidebars, DefaultSidebars, DisabledSidebars, } from '../index'; -import type {SidebarOptions} from '../../types'; +import type {NormalizeSidebarsParams, VersionMetadata} from '../../types'; -describe('loadUnprocessedSidebars', () => { +describe('loadNormalizedSidebars', () => { const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars'); - const options: SidebarOptions = { + const options: NormalizeSidebarsParams = { sidebarCollapsed: true, sidebarCollapsible: true, + version: { + versionName: 'version', + versionPath: 'versionPath', + } as VersionMetadata, }; test('sidebars with known sidebar item type', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars.json'); - const result = loadUnprocessedSidebars(sidebarPath, options); + const result = loadNormalizedSidebars(sidebarPath, options); expect(result).toMatchSnapshot(); }); test('sidebars with deep level of category', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-category.js'); - const result = loadUnprocessedSidebars(sidebarPath, options); + const result = loadNormalizedSidebars(sidebarPath, options); expect(result).toMatchSnapshot(); }); @@ -37,8 +41,8 @@ describe('loadUnprocessedSidebars', () => { fixtureDir, 'sidebars-category-shorthand.js', ); - const sidebar1 = loadUnprocessedSidebars(sidebarPath1, options); - const sidebar2 = loadUnprocessedSidebars(sidebarPath2, options); + const sidebar1 = loadNormalizedSidebars(sidebarPath1, options); + const sidebar2 = loadNormalizedSidebars(sidebarPath2, options); expect(sidebar1).toEqual(sidebar2); }); @@ -47,7 +51,7 @@ describe('loadUnprocessedSidebars', () => { fixtureDir, 'sidebars-category-wrong-items.json', ); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"category\\", @@ -64,7 +68,7 @@ describe('loadUnprocessedSidebars', () => { fixtureDir, 'sidebars-category-wrong-label.json', ); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"category\\", @@ -83,7 +87,7 @@ describe('loadUnprocessedSidebars', () => { fixtureDir, 'sidebars-doc-id-not-string.json', ); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"doc\\", @@ -101,19 +105,19 @@ describe('loadUnprocessedSidebars', () => { fixtureDir, 'sidebars-first-level-not-category.js', ); - const result = loadUnprocessedSidebars(sidebarPath, options); + const result = loadNormalizedSidebars(sidebarPath, options); expect(result).toMatchSnapshot(); }); test('sidebars link', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link.json'); - const result = loadUnprocessedSidebars(sidebarPath, options); + const result = loadNormalizedSidebars(sidebarPath, options); expect(result).toMatchSnapshot(); }); test('sidebars link wrong label', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json'); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"link\\", @@ -127,7 +131,7 @@ describe('loadUnprocessedSidebars', () => { test('sidebars link wrong href', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json'); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"link\\", @@ -143,7 +147,7 @@ describe('loadUnprocessedSidebars', () => { test('sidebars with unknown sidebar item type', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json'); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"superman\\", @@ -156,7 +160,7 @@ describe('loadUnprocessedSidebars', () => { test('sidebars with known sidebar item type but wrong field', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json'); - expect(() => loadUnprocessedSidebars(sidebarPath, options)) + expect(() => loadNormalizedSidebars(sidebarPath, options)) .toThrowErrorMatchingInlineSnapshot(` "{ \\"type\\": \\"category\\", @@ -170,24 +174,22 @@ describe('loadUnprocessedSidebars', () => { }); test('unexisting path', () => { - expect(loadUnprocessedSidebars('badpath', options)).toEqual( + expect(loadNormalizedSidebars('badpath', options)).toEqual( DisabledSidebars, ); }); test('undefined path', () => { - expect(loadUnprocessedSidebars(undefined, options)).toEqual( - DefaultSidebars, - ); + expect(loadNormalizedSidebars(undefined, options)).toEqual(DefaultSidebars); }); test('literal false path', () => { - expect(loadUnprocessedSidebars(false, options)).toEqual(DisabledSidebars); + expect(loadNormalizedSidebars(false, options)).toEqual(DisabledSidebars); }); test('sidebars with category.collapsed property', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json'); - const result = loadUnprocessedSidebars(sidebarPath, options); + const result = loadNormalizedSidebars(sidebarPath, options); expect(result).toMatchSnapshot(); }); @@ -196,7 +198,7 @@ describe('loadUnprocessedSidebars', () => { fixtureDir, 'sidebars-collapsed-first-level.json', ); - const result = loadUnprocessedSidebars(sidebarPath, options); + const result = loadNormalizedSidebars(sidebarPath, options); expect(result).toMatchSnapshot(); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts index 9a86b664ec52..414f5fe247ed 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts @@ -8,7 +8,7 @@ import fs from 'fs-extra'; import importFresh from 'import-fresh'; import type {SidebarsConfig, Sidebars, NormalizedSidebars} from './types'; -import type {PluginOptions} from '../types'; +import type {NormalizeSidebarsParams, PluginOptions} from '../types'; import {validateSidebars} from './validation'; import {normalizeSidebars} from './normalization'; import {processSidebars, SidebarProcessorProps} from './processor'; @@ -36,7 +36,7 @@ export function resolveSidebarPathOption( : sidebarPathOption; } -function loadSidebarFile( +function loadSidebarsFileUnsafe( sidebarFilePath: string | false | undefined, ): SidebarsConfig { // false => no sidebars @@ -60,15 +60,19 @@ function loadSidebarFile( return importFresh(sidebarFilePath); } -export function loadUnprocessedSidebars( +export function loadSidebarsFile( sidebarFilePath: string | false | undefined, - options: SidebarProcessorProps['options'], -): NormalizedSidebars { - const sidebarsConfig = loadSidebarFile(sidebarFilePath); +): SidebarsConfig { + const sidebarsConfig = loadSidebarsFileUnsafe(sidebarFilePath); validateSidebars(sidebarsConfig); + return sidebarsConfig; +} - const normalizedSidebars = normalizeSidebars(sidebarsConfig, options); - return normalizedSidebars; +export function loadNormalizedSidebars( + sidebarFilePath: string | false | undefined, + params: NormalizeSidebarsParams, +): NormalizedSidebars { + return normalizeSidebars(loadSidebarsFile(sidebarFilePath), params); } // Note: sidebarFilePath must be absolute, use resolveSidebarPathOption @@ -76,9 +80,9 @@ export async function loadSidebars( sidebarFilePath: string | false | undefined, options: SidebarProcessorProps, ): Promise { - const unprocessedSidebars = loadUnprocessedSidebars( - sidebarFilePath, - options.options, - ); - return processSidebars(unprocessedSidebars, options); + const normalizedSidebars = loadNormalizedSidebars(sidebarFilePath, { + ...options.options, + version: options.version, + }); + return processSidebars(normalizedSidebars, options); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts index 7e333d57d9de..74fc2d780f13 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {SidebarOptions} from '../types'; +import type {NormalizeSidebarsParams, SidebarOptions} from '../types'; import { NormalizedSidebarItem, NormalizedSidebar, @@ -16,8 +16,34 @@ import { SidebarConfig, SidebarsConfig, isCategoriesShorthand, + SidebarItemCategoryLink, + NormalizedSidebarItemCategory, } from './types'; import {mapValues} from 'lodash'; +import {NormalizeSidebarParams} from '../types'; +import {createSlugger, normalizeUrl} from '@docusaurus/utils'; + +function normalizeCategoryLink( + category: SidebarItemCategoryConfig, + params: NormalizeSidebarParams, +): SidebarItemCategoryLink | undefined { + if (category.link?.type === 'generated-index') { + // default slug logic can be improved + function getDefaultSlug() { + return params.slugger.slug( + `${params.sidebarName}/category/${category.label}`, + ); + } + const slug = category.link.slug ?? getDefaultSlug(); + const permalink = normalizeUrl([params.version.versionPath, slug]); + return { + ...category.link, + slug, + permalink, + }; + } + return category.link; +} function normalizeCategoriesShorthand( sidebar: SidebarCategoriesShorthand, @@ -38,7 +64,7 @@ function normalizeCategoriesShorthand( */ function normalizeItem( item: SidebarItemConfig, - options: SidebarOptions, + options: NormalizeSidebarParams, ): NormalizedSidebarItem[] { if (typeof item === 'string') { return [ @@ -49,40 +75,49 @@ function normalizeItem( ]; } if (isCategoriesShorthand(item)) { - return normalizeCategoriesShorthand(item, options).flatMap((subitem) => - normalizeItem(subitem, options), + return normalizeCategoriesShorthand(item, options).flatMap((subItem) => + normalizeItem(subItem, options), ); } - return item.type === 'category' - ? [ - { - ...item, - items: item.items.flatMap((subItem) => - normalizeItem(subItem, options), - ), - collapsible: item.collapsible ?? options.sidebarCollapsible, - collapsed: item.collapsed ?? options.sidebarCollapsed, - }, - ] - : [item]; + if (item.type === 'category') { + const link = normalizeCategoryLink(item, options); + link && console.log({link}); + const normalizedCategory: NormalizedSidebarItemCategory = { + ...item, + link, + items: item.items.flatMap((subItem) => normalizeItem(subItem, options)), + collapsible: item.collapsible ?? options.sidebarCollapsible, + collapsed: item.collapsed ?? options.sidebarCollapsed, + }; + return [normalizedCategory]; + } + return [item]; } function normalizeSidebar( sidebar: SidebarConfig, - options: SidebarOptions, + options: NormalizeSidebarParams, ): NormalizedSidebar { const normalizedSidebar = Array.isArray(sidebar) ? sidebar : normalizeCategoriesShorthand(sidebar, options); - return normalizedSidebar.flatMap((subitem) => - normalizeItem(subitem, options), + return normalizedSidebar.flatMap((subItem) => + normalizeItem(subItem, options), ); } export function normalizeSidebars( sidebars: SidebarsConfig, - options: SidebarOptions, + params: NormalizeSidebarsParams, ): NormalizedSidebars { - return mapValues(sidebars, (subitem) => normalizeSidebar(subitem, options)); + const slugger = createSlugger(); + + return mapValues(sidebars, (items, sidebarName) => { + return normalizeSidebar(items, { + ...params, + sidebarName, + slugger, + }); + }); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts index dd28bc2c7724..0bcc42b82ee5 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts @@ -47,17 +47,29 @@ type SidebarItemCategoryBase = SidebarItemBase & { export type SidebarItemCategoryLinkDoc = {type: 'doc'; id: string}; -export type SidebarItemCategoryLinkIndex = {type: 'index'; slug?: string}; +export type SidebarItemCategoryLinkGeneratedIndexConfig = { + type: 'generated-index'; + slug?: string; +}; +export type SidebarItemCategoryLinkGeneratedIndex = { + type: 'generated-index'; + slug: string; + permalink: string; +}; + +export type SidebarItemCategoryLinkConfig = + | SidebarItemCategoryLinkDoc + | SidebarItemCategoryLinkGeneratedIndexConfig; export type SidebarItemCategoryLink = | SidebarItemCategoryLinkDoc - | SidebarItemCategoryLinkIndex; + | SidebarItemCategoryLinkGeneratedIndex; // The user-given configuration in sidebars.js, before normalization export type SidebarItemCategoryConfig = Expand< Optional & { items: SidebarItemConfig[]; - link?: SidebarItemCategoryLink; + link?: SidebarItemCategoryLinkConfig; } >; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts index a16edbc5ec55..4399e7dd01d3 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts @@ -18,7 +18,7 @@ import { SidebarsConfig, isCategoriesShorthand, SidebarItemCategoryLinkDoc, - SidebarItemCategoryLinkIndex, + SidebarItemCategoryLinkGeneratedIndex, } from './types'; const sidebarItemBaseSchema = Joi.object({ @@ -64,7 +64,7 @@ export const sidebarItemCategoryLinkSchema = }, { is: 'generated-index', - then: Joi.object({ + then: Joi.object({ type: 'generated-index', slug: Joi.string().optional(), }), diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index eebe226722d0..40a4220b132a 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -8,7 +8,7 @@ /// import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader'; -import type {Tag, FrontMatterTag} from '@docusaurus/utils'; +import type {Tag, FrontMatterTag, Slugger} from '@docusaurus/utils'; import type { BrokenMarkdownLink as IBrokenMarkdownLink, ContentPaths, @@ -86,6 +86,15 @@ export type SidebarOptions = { sidebarCollapsed: boolean; }; +export type NormalizeSidebarsParams = SidebarOptions & { + version: VersionMetadata; +}; + +export type NormalizeSidebarParams = NormalizeSidebarsParams & { + sidebarName: string; + slugger: Slugger; +}; + export type PluginOptions = MetadataOptions & PathOptions & VersionsOptions & diff --git a/packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx similarity index 51% rename from packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx rename to packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx index eec195c572b5..7d2eb5af20c8 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCategory/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx @@ -7,13 +7,13 @@ import React from 'react'; -import type {Props} from '@theme/DocCategory'; +import type {Props} from '@theme/DocCategoryGeneratedIndex'; -export default function DocCategory(props: Props): JSX.Element { +export default function DocCategoryGeneratedIndex(props: Props): JSX.Element { return ( <> -

Category!

-
{JSON.stringify(props.category)}
+

{props.categoryIndex.label}

+
{JSON.stringify(props.categoryIndex)}
); } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCategory/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/styles.module.css similarity index 100% rename from packages/docusaurus-theme-classic/src/theme/DocCategory/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/styles.module.css diff --git a/website/_dogfooding/docs-tests-sidebars.js b/website/_dogfooding/docs-tests-sidebars.js index a0be9b8a3693..ebbf15f6b407 100644 --- a/website/_dogfooding/docs-tests-sidebars.js +++ b/website/_dogfooding/docs-tests-sidebars.js @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -module.exports = { +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +const sidebars = { sidebar: [ { type: 'doc', @@ -16,6 +17,9 @@ module.exports = { { type: 'category', label: 'Tests', + link: { + type: 'generated-index', + }, items: [ { type: 'autogenerated', @@ -62,6 +66,7 @@ module.exports = { }, ], }; +module.exports = sidebars; function generateHugeSidebarItems() { const maxLevel = 3; diff --git a/website/sidebars.js b/website/sidebars.js index bf1e69ff12be..dfb13654b33d 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -14,6 +14,9 @@ const sidebars = { { type: 'category', label: 'Getting Started', + link: { + type: 'generated-index', + }, collapsed: false, items: [ 'installation', From 24e56831bfb7b9d85ea29d830fe001316186f624 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 18:11:37 +0100 Subject: [PATCH 10/92] refactor routes --- .../src/index.ts | 107 ++++-------------- .../src/routes.ts | 102 ++++++++++++++++- 2 files changed, 120 insertions(+), 89 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 22c427d31de3..258b0e6019ed 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -20,7 +20,7 @@ import { addTrailingPathSeparator, createAbsoluteFilePathMatcher, } from '@docusaurus/utils'; -import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types'; +import {LoadContext, Plugin} from '@docusaurus/types'; import {loadSidebars} from './sidebars'; import {CategoryMetadataFilenamePattern} from './sidebars/generator'; import {readVersionDocs, processDocMetadata, handleNavigation} from './docs'; @@ -31,7 +31,6 @@ import { LoadedContent, SourceToPermalink, DocMetadataBase, - DocMetadata, GlobalPluginData, VersionMetadata, LoadedVersion, @@ -44,7 +43,7 @@ import {cliDocsVersionCommand} from './cli'; import {VERSIONS_JSON_FILE} from './constants'; import {keyBy, mapValues} from 'lodash'; import {toGlobalDataVersion} from './globalData'; -import {toTagDocListProp, toVersionMetadataProp} from './props'; +import {toTagDocListProp} from './props'; import { translateLoadedContent, getLoadedContentTranslationFiles, @@ -52,7 +51,7 @@ import { import chalk from 'chalk'; import {getVersionTags} from './tags'; import {PropTagsListPage} from '@docusaurus/plugin-content-docs-types'; -import {createSidebarsRoutes} from './routes'; +import {createVersionRoutes} from './routes'; export default function pluginContentDocs( context: LoadContext, @@ -213,42 +212,10 @@ export default function pluginContentDocs( const {docLayoutComponent, docItemComponent} = options; const {addRoute, createData, setGlobalData} = actions; - const createDocRoutes = async ( - docs: DocMetadata[], - ): Promise => { - const routes = await Promise.all( - docs.map(async (metadataItem) => { - await createData( - // Note that this created data path must be in sync with - // metadataPath provided to mdx-loader. - `${docuHash(metadataItem.source)}.json`, - JSON.stringify(metadataItem, null, 2), - ); - - const docRoute: RouteConfig = { - path: metadataItem.permalink, - component: docItemComponent, - exact: true, - modules: { - content: metadataItem.source, - }, - // Because the parent (DocPage) comp need to access it easily - // This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state) - ...(metadataItem.sidebar && { - sidebar: metadataItem.sidebar, - }), - }; - - return docRoute; - }), - ); - - return routes; - }; - async function createVersionTagsRoutes(version: LoadedVersion) { const versionTags = getVersionTags(version.docs); + // TODO tags should be a sub route of the version route async function createTagsListPage() { const tagsProp: PropTagsListPage['tags'] = Object.values( versionTags, @@ -275,6 +242,7 @@ export default function pluginContentDocs( } } + // TODO tags should be a sub route of the version route async function createTagDocListPage(tag: VersionTag) { const tagProps = toTagDocListProp({ allTagsPath: version.tagsPath, @@ -299,58 +267,21 @@ export default function pluginContentDocs( await Promise.all(Object.values(versionTags).map(createTagDocListPage)); } - async function doCreateVersionRoutes( - version: LoadedVersion, - ): Promise { - await createVersionTagsRoutes(version); - - const versionMetadata = toVersionMetadataProp(pluginId, version); - const versionMetadataPropPath = await createData( - `${docuHash(`version-${version.versionName}-metadata-prop`)}.json`, - JSON.stringify(versionMetadata, null, 2), - ); - - async function createVersionSubRoutes() { - const [docRoutes, sidebarsRoutes] = await Promise.all([ - createDocRoutes(version.docs), - createSidebarsRoutes({version, actions}), - ]); - - const routes = [...docRoutes, ...sidebarsRoutes]; - return routes.sort((a, b) => a.path.localeCompare(b.path)); - } - - addRoute({ - path: version.versionPath, - // allow matching /docs/* as well - exact: false, - // main docs component (DocPage) - component: docLayoutComponent, - // sub-routes for each doc - routes: await createVersionSubRoutes(), - modules: { - versionMetadata: aliasedSource(versionMetadataPropPath), - }, - priority: version.routePriority, - }); - } - - async function createVersionRoutes( - loadedVersion: LoadedVersion, - ): Promise { - try { - return await doCreateVersionRoutes(loadedVersion); - } catch (e) { - console.error( - chalk.red( - `Can't create version routes for version "${loadedVersion.versionName}"`, - ), - ); - throw e; - } - } + await Promise.all( + loadedVersions.map((loadedVersion) => + createVersionRoutes({ + loadedVersion, + docItemComponent, + docLayoutComponent, + pluginId, + aliasedSource, + actions, + }), + ), + ); - await Promise.all(loadedVersions.map(createVersionRoutes)); + // TODO tags should be a sub route of the version route + await Promise.all(loadedVersions.map(createVersionTagsRoutes)); setGlobalData({ path: normalizeUrl([baseUrl, options.routeBasePath]), diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index 21beb63a286a..90b8edc34aa2 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -13,8 +13,10 @@ import { import {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types'; import {collectSidebarCategories} from './sidebars/utils'; import {docuHash, createSlugger} from '@docusaurus/utils'; -import {LoadedVersion} from './types'; +import {DocMetadata, LoadedVersion} from './types'; import {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs-types'; +import {toVersionMetadataProp} from './props'; +import chalk from 'chalk'; async function createSidebarRoutes({ sidebarName, @@ -55,6 +57,8 @@ async function createSidebarRoutes({ modules: { categoryIndex: propData, }, + // Same as doc, this sidebar route attribute permits to associate this subpage to the given sidebar + ...(sidebarName && {sidebar: sidebarName}), }; } @@ -94,3 +98,99 @@ export async function createSidebarsRoutes({ ) ).flat(); } + +export async function createDocRoutes({ + docs, + actions, + docItemComponent, +}: { + docs: DocMetadata[]; + actions: PluginContentLoadedActions; + docItemComponent: string; +}): Promise { + return Promise.all( + docs.map(async (metadataItem) => { + await actions.createData( + // Note that this created data path must be in sync with + // metadataPath provided to mdx-loader. + `${docuHash(metadataItem.source)}.json`, + JSON.stringify(metadataItem, null, 2), + ); + + const docRoute: RouteConfig = { + path: metadataItem.permalink, + component: docItemComponent, + exact: true, + modules: { + content: metadataItem.source, + }, + // Because the parent (DocPage) comp need to access it easily + // This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state) + ...(metadataItem.sidebar && { + sidebar: metadataItem.sidebar, + }), + }; + + return docRoute; + }), + ); +} + +export async function createVersionRoutes({ + loadedVersion, + actions, + docItemComponent, + docLayoutComponent, + pluginId, + aliasedSource, +}: { + loadedVersion: LoadedVersion; + actions: PluginContentLoadedActions; + docLayoutComponent: string; + docItemComponent: string; + pluginId: string; + aliasedSource: (str: string) => string; +}): Promise { + async function doCreateVersionRoutes(version: LoadedVersion): Promise { + const versionMetadata = toVersionMetadataProp(pluginId, version); + const versionMetadataPropPath = await actions.createData( + `${docuHash(`version-${version.versionName}-metadata-prop`)}.json`, + JSON.stringify(versionMetadata, null, 2), + ); + + async function createVersionSubRoutes() { + const [docRoutes, sidebarsRoutes] = await Promise.all([ + createDocRoutes({docs: version.docs, actions, docItemComponent}), + createSidebarsRoutes({version, actions}), + ]); + + const routes = [...docRoutes, ...sidebarsRoutes]; + return routes.sort((a, b) => a.path.localeCompare(b.path)); + } + + actions.addRoute({ + path: version.versionPath, + // allow matching /docs/* as well + exact: false, + // main docs component (DocPage) + component: docLayoutComponent, + // sub-routes for each doc + routes: await createVersionSubRoutes(), + modules: { + versionMetadata: aliasedSource(versionMetadataPropPath), + }, + priority: version.routePriority, + }); + } + + try { + return await doCreateVersionRoutes(loadedVersion); + } catch (e) { + console.error( + chalk.red( + `Can't create version routes for version "${loadedVersion.versionName}"`, + ), + ); + throw e; + } +} From 895eee321b553284d5d89df4941bb11be8213aff Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 19:13:18 +0100 Subject: [PATCH 11/92] working POC of category index --- .../src/plugin-content-docs.d.ts | 5 +- .../src/sidebars/normalization.ts | 6 +- .../theme/DocCategoryGeneratedIndex/index.tsx | 105 +++++++++++++++++- .../src/theme/DocPage/index.tsx | 37 ++++-- packages/docusaurus-theme-common/src/index.ts | 6 +- .../src/utils/docsUtils.ts | 11 -- .../src/utils/docsUtils.tsx | 43 +++++++ website/sidebars.js | 3 + 8 files changed, 185 insertions(+), 31 deletions(-) delete mode 100644 packages/docusaurus-theme-common/src/utils/docsUtils.ts create mode 100644 packages/docusaurus-theme-common/src/utils/docsUtils.tsx diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index e5c162299eb0..b4fe8e7331c5 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -40,6 +40,7 @@ declare module '@docusaurus/plugin-content-docs-types' { export type PropSidebarItemCategory = import('./sidebars/types').PropSidebarItemCategory; export type PropSidebarItem = import('./sidebars/types').PropSidebarItem; + export type PropSidebar = import('./sidebars/types').PropSidebar; export type PropSidebars = import('./sidebars/types').PropSidebars; export type PropTagDocListDoc = { @@ -124,7 +125,9 @@ declare module '@theme/DocItem' { declare module '@theme/DocCategoryGeneratedIndex' { import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs-types'; - export interface Props extends PropCategoryGeneratedIndex {} + export interface Props { + categoryIndex: PropCategoryGeneratedIndex; + } export default function DocCategoryGeneratedIndex(props: Props): JSX.Element; } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts index 74fc2d780f13..140e5400372f 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts @@ -30,9 +30,8 @@ function normalizeCategoryLink( if (category.link?.type === 'generated-index') { // default slug logic can be improved function getDefaultSlug() { - return params.slugger.slug( - `${params.sidebarName}/category/${category.label}`, - ); + const categoryLabelSlug = params.slugger.slug(category.label); + return `${params.sidebarName}/category/${categoryLabelSlug}`; } const slug = category.link.slug ?? getDefaultSlug(); const permalink = normalizeUrl([params.version.versionPath, slug]); @@ -81,7 +80,6 @@ function normalizeItem( } if (item.type === 'category') { const link = normalizeCategoryLink(item, options); - link && console.log({link}); const normalizedCategory: NormalizedSidebarItemCategory = { ...item, link, diff --git a/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx index 7d2eb5af20c8..ba6de3f92008 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx @@ -8,12 +8,113 @@ import React from 'react'; import type {Props} from '@theme/DocCategoryGeneratedIndex'; +import {useDocsSidebar} from '@docusaurus/theme-common'; +import { + PropSidebarItem, + PropSidebarItemCategory, +} from '@docusaurus/plugin-content-docs/lib/sidebars/types'; +import Link from '@docusaurus/Link'; +import {PropSidebarItemLink} from '@docusaurus/plugin-content-docs-types'; + +// Use the components props and the sidebar in context +// to get back the related sidebar category that we want to render +function useSidebarCategory(categoryIndex: Props['categoryIndex']) { + const sidebar = useDocsSidebar(); + if (!sidebar) { + throw new Error( + `unexpected: a category index should have a sidebar: ${JSON.stringify( + categoryIndex, + )}`, + ); + } + + // TODO more performant algo returning single element + function collectCategoriesMatch( + items: PropSidebarItem[], + ): PropSidebarItemCategory[] { + return items.flatMap((item) => { + if (item.type === 'category') { + if (item.href === categoryIndex.permalink) { + return [item]; + } + return collectCategoriesMatch(item.items); + } + return []; + }); + } + + const [sidebarCategory] = collectCategoriesMatch(sidebar); + + if (!sidebarCategory) { + throw new Error( + `Unexpected: sidebar category could not be found for categoryIndex=${JSON.stringify( + categoryIndex, + )}`, + ); + } + + return sidebarCategory; +} + +function SidebarItemCategory({ + item, +}: { + item: PropSidebarItemCategory; +}): JSX.Element { + const label = item.href ? ( + {item.label} + ) : ( + {item.label} + ); + + return ( +
+
+ {'=> '} {label} +
+
+ +
+
+ ); +} + +function SidebarItemLink({item}: {item: PropSidebarItemLink}): JSX.Element { + return {item.label}; +} + +function SidebarItem({item}: {item: PropSidebarItem}): JSX.Element { + switch (item.type) { + case 'link': + return ; + case 'category': + return ; + default: + throw new Error(`unknown item type ${JSON.stringify(item)}`); + } +} + +function SidebarItemsList({items}: {items: PropSidebarItem[]}): JSX.Element { + return ( +
    + {items.map((item, index) => ( +
  • + +
  • + ))} +
+ ); +} export default function DocCategoryGeneratedIndex(props: Props): JSX.Element { + const {categoryIndex} = props; + const category = useSidebarCategory(categoryIndex); return ( <> -

{props.categoryIndex.label}

-
{JSON.stringify(props.categoryIndex)}
+

{category.label}

+
+ +
); } diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index df3a2a29d206..f20454b61237 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -23,27 +23,29 @@ import {translate} from '@docusaurus/Translate'; import clsx from 'clsx'; import styles from './styles.module.css'; -import {ThemeClassNames, docVersionSearchTag} from '@docusaurus/theme-common'; +import { + ThemeClassNames, + docVersionSearchTag, + DocsSidebarProvider, + useDocsSidebar, +} from '@docusaurus/theme-common'; import Head from '@docusaurus/Head'; type DocPageContentProps = { readonly currentDocRoute: DocumentRoute; readonly versionMetadata: PropVersionMetadata; readonly children: ReactNode; + readonly sidebarName: string | undefined; }; function DocPageContent({ currentDocRoute, versionMetadata, children, + sidebarName, }: DocPageContentProps): JSX.Element { + const sidebar = useDocsSidebar(); const {pluginId, version} = versionMetadata; - - const sidebarName = currentDocRoute.sidebar; - const sidebar = sidebarName - ? versionMetadata.docsSidebars[sidebarName] - : undefined; - const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false); const [hiddenSidebar, setHiddenSidebar] = useState(false); const toggleSidebar = useCallback(() => { @@ -150,17 +152,28 @@ function DocPage(props: Props): JSX.Element { if (!currentDocRoute) { return ; } + + // For now, the sidebarName is added as route config: not ideal! + const sidebarName = currentDocRoute.sidebar; + + const sidebar = sidebarName + ? versionMetadata.docsSidebars[sidebarName] + : null; + return ( <> {/* TODO we should add a core addRoute({htmlClassName}) generic plugin option */} - - {renderRoutes(docRoutes, {versionMetadata})} - + + + {renderRoutes(docRoutes, {versionMetadata})} + + ); } diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index c8e66543a17f..92e06fccd2a1 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -26,7 +26,11 @@ export {parseCodeBlockTitle} from './utils/codeBlockUtils'; export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils'; -export {isDocsPluginEnabled} from './utils/docsUtils'; +export { + isDocsPluginEnabled, + DocsSidebarProvider, + useDocsSidebar, +} from './utils/docsUtils'; export {isSamePath} from './utils/pathUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.ts b/packages/docusaurus-theme-common/src/utils/docsUtils.ts deleted file mode 100644 index eba2c7cd2f6e..000000000000 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 {useAllDocsData} from '@theme/hooks/useDocs'; - -// TODO not ideal, see also "useDocs" -export const isDocsPluginEnabled: boolean = !!useAllDocsData; diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx new file mode 100644 index 000000000000..2f86977ed9c2 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -0,0 +1,43 @@ +/** + * 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 React, {createContext, ReactNode, useContext} from 'react'; +import {useAllDocsData} from '@theme/hooks/useDocs'; +import {PropSidebar} from '@docusaurus/plugin-content-docs-types'; + +// TODO not ideal, see also "useDocs" +export const isDocsPluginEnabled: boolean = !!useAllDocsData; + +// Using a Symbol because null is a valid context value (a doc can have no sidebar) +// Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx +const EmptyContextValue: unique symbol = Symbol('EmptyContext'); + +const DocsSidebarContext = createContext< + PropSidebar | null | typeof EmptyContextValue +>(EmptyContextValue); + +export function DocsSidebarProvider({ + children, + sidebar, +}: { + children: ReactNode; + sidebar: PropSidebar | null; +}): JSX.Element { + return ( + + {children} + + ); +} + +export function useDocsSidebar(): PropSidebar | null { + const sidebar = useContext(DocsSidebarContext); + if (sidebar === EmptyContextValue) { + throw new Error('This hook requires usage of '); + } + return sidebar; +} diff --git a/website/sidebars.js b/website/sidebars.js index dfb13654b33d..807cc0f009d1 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -28,6 +28,9 @@ const sidebars = { { type: 'category', label: 'Guides', + link: { + type: 'generated-index', + }, items: [ 'guides/creating-pages', { From 9cfebb826e70652b355e9f425c5da59acf1aed67 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 5 Nov 2021 19:27:45 +0100 Subject: [PATCH 12/92] working POC of category index --- packages/docusaurus-mdx-loader/src/remark/headings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/headings/index.ts b/packages/docusaurus-mdx-loader/src/remark/headings/index.ts index 99b8afb70c24..5626a4bd7d77 100644 --- a/packages/docusaurus-mdx-loader/src/remark/headings/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/headings/index.ts @@ -26,7 +26,7 @@ function headings(): Transformer { let {id} = properties; if (id) { - id = slugs.slug(id, true); + id = slugs.slug(id, {maintainCase: true}); } else { const headingTextNodes = headingNode.children.filter( ({type}) => !['html', 'jsx'].includes(type), From d385737abc4dd4e857649b4dd344516c22efe825 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:28:57 +0100 Subject: [PATCH 13/92] allow lodash last() --- .eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5cafd2375fab..306ad212139a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -163,7 +163,6 @@ module.exports = { 'head', 'tail', 'initial', - 'last', ], message: 'These APIs have their ES counterparts.', }, From b8ff50eccdce8ee2a308f53c8819c547c5eeaa1a Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:30:06 +0100 Subject: [PATCH 14/92] category config should support local or qualified ids --- .../src/sidebars/generator.ts | 64 +++++++++++++++---- website/docs/api/plugins/_category_.yml | 3 + website/docs/api/themes/_category_.yml | 3 + .../docs/api/themes/theme-configuration.md | 1 + 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index be0b22c0fc22..d90728722f89 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -14,7 +14,7 @@ import type { SidebarItemCategoryLinkDoc, SidebarItemCategoryLink, } from './types'; -import {keyBy, sortBy} from 'lodash'; +import {keyBy, sortBy, last} from 'lodash'; import {addTrailingSlash, posixPath} from '@docusaurus/utils'; import {Joi} from '@docusaurus/utils-validation'; import chalk from 'chalk'; @@ -37,8 +37,7 @@ function isConventionalCategoryDocLink({ item: SidebarItemDoc; }): boolean { // TODO using the id is not 100% accurate, but good enough for now? - const parts = item.id.split('/'); - const docName = parts[parts.length - 1]!; + const docName = last(item.id.split('/'))!; const eligibleDocIndexNames = ['index', 'readme', folderName]; @@ -123,6 +122,21 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ item: {dirName: autogenDir}, version, }) => { + const docsById = keyBy(allDocs, (doc) => doc.id); + 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. */ @@ -186,12 +200,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ * (From a record to an array of items, akin to normalizing shorthand) */ function generateSidebar(fsModel: Dir): Promise[]> { - const docsById = keyBy(allDocs, (doc) => doc.id); function createDocItem(id: string): WithPosition { const { sidebarPosition: position, frontMatter: {sidebar_label: label, sidebar_class_name: className}, - } = docsById[id]; + } = getDoc(id); return { type: 'doc', id, @@ -216,25 +229,48 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ ), ); + function findOrCreateDocLinkItem( + link: SidebarItemCategoryLinkDoc, + ): SidebarItemDoc | undefined { + function findExistingDocItem() { + return allItems.find( + (item) => + item.type === 'doc' && + (item.id === link.id || last(item.id.split('/')) === link.id), + ) as SidebarItemDoc | undefined; + } + return ( + // Try to match a doc inside the category folder, + // using the "local id" (myDoc) or "qualified id" (dirName/myDoc) + findExistingDocItem() || + // Or try to match a doc anywhere with a "qualified id" (otherDirName/myDoc) + // (even outside of the current folder) + // and create a new doc sidebar item + createDocItem(link.id) + ); + } + + function findConventionalCategoryDocLink(): SidebarItemDoc | undefined { + return allItems.find( + (item) => + item.type === 'doc' && + isConventionalCategoryDocLink({folderName, item}), + ) as SidebarItemDoc | undefined; + } + function getCategoryDoc(): SidebarItemDoc | undefined { const link = categoryMetadata?.link; if (link) { if (link.type === 'doc') { - return allItems.find( - (item) => item.type === 'doc' && item.id === link.id, - ) as SidebarItemDoc | undefined; + return findOrCreateDocLinkItem(link); } else { // We don't continue for other link types on purpose! - // IE if user decide to use type "index", we should not pick a README.md file as the linked doc + // 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 .md as the category doc - return allItems.find( - (item) => - item.type === 'doc' && - isConventionalCategoryDocLink({folderName, item}), - ) as SidebarItemDoc | undefined; + return findConventionalCategoryDocLink(); } const categoryIndexDoc = getCategoryDoc(); diff --git a/website/docs/api/plugins/_category_.yml b/website/docs/api/plugins/_category_.yml index 3339c70225d9..cffabddbd5db 100644 --- a/website/docs/api/plugins/_category_.yml +++ b/website/docs/api/plugins/_category_.yml @@ -1,2 +1,5 @@ label: Plugins position: 2 +link: + type: doc + id: api/plugins/plugins-overview # Dogfood using a "qualified id" diff --git a/website/docs/api/themes/_category_.yml b/website/docs/api/themes/_category_.yml index a016263a4b9e..a0ceda5d5956 100644 --- a/website/docs/api/themes/_category_.yml +++ b/website/docs/api/themes/_category_.yml @@ -1,2 +1,5 @@ label: Themes position: 3 +link: + type: doc + id: themes-overview # Dogfood using a "local id" diff --git a/website/docs/api/themes/theme-configuration.md b/website/docs/api/themes/theme-configuration.md index efde10ef935b..4023f10ab1c0 100644 --- a/website/docs/api/themes/theme-configuration.md +++ b/website/docs/api/themes/theme-configuration.md @@ -2,6 +2,7 @@ sidebar_position: 1 id: theme-configuration title: 'Theme configuration' +sidebar_label: 'Configuration' slug: '/api/themes/configuration' toc_max_heading_level: 4 --- From f9d593f99ece6a52c9af2b0b5069c1245754041a Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:31:14 +0100 Subject: [PATCH 15/92] better comment --- .../docusaurus-plugin-content-docs/src/sidebars/generator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index d90728722f89..515a092ccbca 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -236,7 +236,10 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ return allItems.find( (item) => item.type === 'doc' && - (item.id === link.id || last(item.id.split('/')) === link.id), + // Search by "local id" + (item.id === link.id || + // Search by "qualified id" + last(item.id.split('/')) === link.id), ) as SidebarItemDoc | undefined; } return ( From 9ee14c1dcd97623e151190658f3475ea31938b74 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:38:37 +0100 Subject: [PATCH 16/92] move categoryMetadataFileSchema to validation.ts --- .../src/sidebars/generator.ts | 14 ++------------ .../src/sidebars/validation.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index 515a092ccbca..370a919c4f04 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -16,12 +16,11 @@ import type { } from './types'; import {keyBy, 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 {sidebarItemCategoryLinkSchema} from './validation'; +import {validateCategoryMetadata} from './validation'; const BreadcrumbSeparator = '/'; // To avoid possible name clashes with a folder of the same name as the ID @@ -71,15 +70,6 @@ type Dir = { [item: string]: Dir | null; }; -const CategoryMetadataFileSchema = Joi.object({ - label: Joi.string(), - position: Joi.number(), - collapsed: Joi.boolean(), - collapsible: Joi.boolean(), - className: Joi.string(), - link: sidebarItemCategoryLinkSchema, -}); - // 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/with-category-name-doc.md`, we may want to read the metadata as yaml on it @@ -91,7 +81,7 @@ async function readCategoryMetadataFile( const contentString = await fs.readFile(filePath, {encoding: 'utf8'}); const unsafeContent = Yaml.load(contentString); try { - return Joi.attempt(unsafeContent, CategoryMetadataFileSchema); + return validateCategoryMetadata(unsafeContent); } catch (e) { console.error( chalk.red( diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts index 4399e7dd01d3..04ad80a5c7e3 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts @@ -20,6 +20,7 @@ import { SidebarItemCategoryLinkDoc, SidebarItemCategoryLinkGeneratedIndex, } from './types'; +import {CategoryMetadataFile} from './generator'; const sidebarItemBaseSchema = Joi.object({ className: Joi.string(), @@ -155,3 +156,18 @@ export function validateSidebars( } }); } + +const categoryMetadataFileSchema = Joi.object({ + label: Joi.string(), + position: Joi.number(), + collapsed: Joi.boolean(), + collapsible: Joi.boolean(), + className: Joi.string(), + link: sidebarItemCategoryLinkSchema, +}); + +export function validateCategoryMetadata( + unsafeContent: unknown, +): CategoryMetadataFile { + return Joi.attempt(unsafeContent, categoryMetadataFileSchema); +} From 418210e2af6b540333fa2c00900edfcdaac334d6 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:45:22 +0100 Subject: [PATCH 17/92] add external link for generated-index test --- website/_dogfooding/docs-tests-sidebars.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/_dogfooding/docs-tests-sidebars.js b/website/_dogfooding/docs-tests-sidebars.js index 98171f5061e6..f1004bab4348 100644 --- a/website/_dogfooding/docs-tests-sidebars.js +++ b/website/_dogfooding/docs-tests-sidebars.js @@ -25,6 +25,11 @@ const sidebars = { type: 'autogenerated', dirName: 'tests', }, + { + type: 'link', + label: 'External Link test', + href: 'https://docusaurus.io', + }, ], }, { From 9bcc798993c5afeca2c9977e07268c82680eca4a Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:45:35 +0100 Subject: [PATCH 18/92] comment --- .../docusaurus-plugin-content-docs/src/sidebars/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index 370a919c4f04..d02505eecc27 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -262,7 +262,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ return undefined; } } - // Apply default convention to pick index.md, readme.md or .md as the category doc + // Apply default convention to pick index.md, README.md or .md as the category doc return findConventionalCategoryDocLink(); } From 6edf6f7ee22a0d3f1c55fe0203676347fd1912fb Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:45:56 +0100 Subject: [PATCH 19/92] change readme case --- .../tests/category-links/with-readme-doc/{readme.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/{readme.md => README.md} (100%) diff --git a/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/readme.md b/website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/README.md similarity index 100% rename from website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/readme.md rename to website/_dogfooding/_docs tests/tests/category-links/with-readme-doc/README.md From fe6ffe168feedb2f3e4a53d6e9ff8bd7f03222ee Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:47:11 +0100 Subject: [PATCH 20/92] update category slug --- .../_docs tests/tests/category-links/_category_.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/_dogfooding/_docs tests/tests/category-links/_category_.json b/website/_dogfooding/_docs tests/tests/category-links/_category_.json index d2b06a536458..8d0c8da76ae0 100644 --- a/website/_dogfooding/_docs tests/tests/category-links/_category_.json +++ b/website/_dogfooding/_docs tests/tests/category-links/_category_.json @@ -2,6 +2,6 @@ "label": "Category Links", "link": { "type": "generated-index", - "slug": "/category-links-slug" + "slug": "/category-links-generated-index-slug" } } From cf9eef1621858ab4c3056d52e69020663dafcc36 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 17:49:41 +0100 Subject: [PATCH 21/92] isConventionalCategoryDocLink => case insensitive --- .../docusaurus-plugin-content-docs/src/sidebars/generator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index d02505eecc27..e5e6f169ec2b 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -38,9 +38,9 @@ function isConventionalCategoryDocLink({ // TODO using the id is not 100% accurate, but good enough for now? const docName = last(item.id.split('/'))!; - const eligibleDocIndexNames = ['index', 'readme', folderName]; + const eligibleDocIndexNames = ['index', 'readme', folderName.toLowerCase()]; - return eligibleDocIndexNames.includes(docName); + return eligibleDocIndexNames.includes(docName.toLowerCase()); } export const CategoryMetadataFilenameBase = '_category_'; From 7907d074b6a52348a2b2512b69245970db14caf5 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 19:07:27 +0100 Subject: [PATCH 22/92] generated sidebar items should be normalized too! --- .../src/sidebars/index.ts | 12 +++++++--- .../src/sidebars/normalization.ts | 24 +++++++------------ .../src/sidebars/processor.ts | 15 ++++++++++-- .../src/types.ts | 6 +---- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts index 414f5fe247ed..e25f4a932459 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/index.ts @@ -13,6 +13,7 @@ import {validateSidebars} from './validation'; import {normalizeSidebars} from './normalization'; import {processSidebars, SidebarProcessorProps} from './processor'; import path from 'path'; +import {createSlugger} from '@docusaurus/utils'; export const DefaultSidebars: SidebarsConfig = { defaultSidebar: [ @@ -80,9 +81,14 @@ export async function loadSidebars( sidebarFilePath: string | false | undefined, options: SidebarProcessorProps, ): Promise { - const normalizedSidebars = loadNormalizedSidebars(sidebarFilePath, { + const normalizeSidebarsParams: NormalizeSidebarsParams = { ...options.options, version: options.version, - }); - return processSidebars(normalizedSidebars, options); + categoryLabelSlugger: createSlugger(), + }; + const normalizedSidebars = loadNormalizedSidebars( + sidebarFilePath, + normalizeSidebarsParams, + ); + return processSidebars(normalizedSidebars, options, normalizeSidebarsParams); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts index 140e5400372f..9677f034a22b 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/normalization.ts @@ -20,18 +20,16 @@ import { NormalizedSidebarItemCategory, } from './types'; import {mapValues} from 'lodash'; -import {NormalizeSidebarParams} from '../types'; -import {createSlugger, normalizeUrl} from '@docusaurus/utils'; +import {normalizeUrl} from '@docusaurus/utils'; function normalizeCategoryLink( category: SidebarItemCategoryConfig, - params: NormalizeSidebarParams, + params: NormalizeSidebarsParams, ): SidebarItemCategoryLink | undefined { if (category.link?.type === 'generated-index') { // default slug logic can be improved function getDefaultSlug() { - const categoryLabelSlug = params.slugger.slug(category.label); - return `${params.sidebarName}/category/${categoryLabelSlug}`; + return `/category/${params.categoryLabelSlugger.slug(category.label)}`; } const slug = category.link.slug ?? getDefaultSlug(); const permalink = normalizeUrl([params.version.versionPath, slug]); @@ -61,9 +59,9 @@ function normalizeCategoriesShorthand( * Normalizes recursively item and all its children. Ensures that at the end * each item will be an object with the corresponding type. */ -function normalizeItem( +export function normalizeItem( item: SidebarItemConfig, - options: NormalizeSidebarParams, + options: NormalizeSidebarsParams, ): NormalizedSidebarItem[] { if (typeof item === 'string') { return [ @@ -94,7 +92,7 @@ function normalizeItem( function normalizeSidebar( sidebar: SidebarConfig, - options: NormalizeSidebarParams, + options: NormalizeSidebarsParams, ): NormalizedSidebar { const normalizedSidebar = Array.isArray(sidebar) ? sidebar @@ -109,13 +107,7 @@ export function normalizeSidebars( sidebars: SidebarsConfig, params: NormalizeSidebarsParams, ): NormalizedSidebars { - const slugger = createSlugger(); - - return mapValues(sidebars, (items, sidebarName) => { - return normalizeSidebar(items, { - ...params, - sidebarName, - slugger, - }); + return mapValues(sidebars, (items) => { + return normalizeSidebar(items, params); }); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts index cf6402a45372..7580eb03a008 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/processor.ts @@ -10,6 +10,7 @@ import type { DocMetadataBase, VersionMetadata, SidebarOptions, + NormalizeSidebarsParams, } from '../types'; import type { Sidebars, @@ -29,6 +30,7 @@ import {transformSidebarItems} from './utils'; import {DefaultSidebarItemsGenerator} from './generator'; import {mapValues, memoize, pick} from 'lodash'; import combinePromises from 'combine-promises'; +import {normalizeItem} from './normalization'; export type SidebarProcessorProps = { sidebarItemsGenerator: SidebarItemsGeneratorOption; @@ -66,6 +68,7 @@ async function processSidebar( version, options, }: SidebarProcessorProps, + normalizeSidebarParams: NormalizeSidebarsParams, ): Promise { // Just a minor lazy transformation optimization const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({ @@ -85,6 +88,7 @@ async function processSidebar( async function processAutoGeneratedItem( item: SidebarItemAutogenerated, ): Promise { + // TODO the returned type can't be trusted in practice (generator can be user-provided) const generatedItems = await sidebarItemsGenerator({ item, numberPrefixParser, @@ -92,7 +96,13 @@ async function processSidebar( ...getSidebarItemsGeneratorDocsAndVersion(), options, }); - return processItems(generatedItems); + // TODO validate generated items: user can generate bad items + + const generatedItemsNormalized = generatedItems.flatMap((generatedItem) => + normalizeItem(generatedItem, normalizeSidebarParams), + ); + + return processItems(generatedItemsNormalized); } async function processItem( @@ -131,10 +141,11 @@ async function processSidebar( export async function processSidebars( unprocessedSidebars: NormalizedSidebars, props: SidebarProcessorProps, + normalizeSidebarParams: NormalizeSidebarsParams, ): Promise { return combinePromises( mapValues(unprocessedSidebars, (unprocessedSidebar) => - processSidebar(unprocessedSidebar, props), + processSidebar(unprocessedSidebar, props, normalizeSidebarParams), ), ); } diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 40a4220b132a..9526eea13c28 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -88,11 +88,7 @@ export type SidebarOptions = { export type NormalizeSidebarsParams = SidebarOptions & { version: VersionMetadata; -}; - -export type NormalizeSidebarParams = NormalizeSidebarsParams & { - sidebarName: string; - slugger: Slugger; + categoryLabelSlugger: Slugger; }; export type PluginOptions = MetadataOptions & From 3920e1c3b97597419fe90b665e17360bb5828c37 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 19:28:02 +0100 Subject: [PATCH 23/92] fix generator category linking --- .../src/sidebars/generator.ts | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index e5e6f169ec2b..9688c8bcc765 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -11,7 +11,6 @@ import type { SidebarItemCategory, SidebarItemsGenerator, SidebarItemsGeneratorDoc, - SidebarItemCategoryLinkDoc, SidebarItemCategoryLink, } from './types'; import {keyBy, sortBy, last} from 'lodash'; @@ -26,6 +25,10 @@ const BreadcrumbSeparator = '/'; // To avoid possible name clashes with a folder of the same name as the ID const docIdPrefix = '$doc$/'; +function getLocalDocId(docId: string): string { + return last(docId.split('/'))!; +} + // By convention, Docusaurus turns certain doc filenames as category doc links // TODO make this function configurable? function isConventionalCategoryDocLink({ @@ -36,7 +39,7 @@ function isConventionalCategoryDocLink({ item: SidebarItemDoc; }): boolean { // TODO using the id is not 100% accurate, but good enough for now? - const docName = last(item.id.split('/'))!; + const docName = getLocalDocId(item.id)!; const eligibleDocIndexNames = ['index', 'readme', folderName.toLowerCase()]; @@ -219,28 +222,12 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ ), ); - function findOrCreateDocLinkItem( - link: SidebarItemCategoryLinkDoc, - ): SidebarItemDoc | undefined { - function findExistingDocItem() { - return allItems.find( - (item) => - item.type === 'doc' && - // Search by "local id" - (item.id === link.id || - // Search by "qualified id" - last(item.id.split('/')) === link.id), - ) as SidebarItemDoc | undefined; - } - return ( - // Try to match a doc inside the category folder, - // using the "local id" (myDoc) or "qualified id" (dirName/myDoc) - findExistingDocItem() || - // Or try to match a doc anywhere with a "qualified id" (otherDirName/myDoc) - // (even outside of the current folder) - // and create a new doc sidebar item - createDocItem(link.id) - ); + // 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 { @@ -251,11 +238,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ ) as SidebarItemDoc | undefined; } - function getCategoryDoc(): SidebarItemDoc | undefined { + function getCategoryLinkedDocId(): string | undefined { const link = categoryMetadata?.link; if (link) { if (link.type === 'doc') { - return findOrCreateDocLinkItem(link); + 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 @@ -263,21 +250,22 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ } } // Apply default convention to pick index.md, README.md or .md as the category doc - return findConventionalCategoryDocLink(); + return findConventionalCategoryDocLink()?.id; } - const categoryIndexDoc = getCategoryDoc(); + const categoryLinkedDocId = getCategoryLinkedDocId(); - const link: SidebarItemCategoryLinkDoc | undefined = categoryIndexDoc + const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId ? { type: 'doc', - id: categoryIndexDoc.id, + id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id" } - : undefined; + : categoryMetadata?.link; - const items = categoryIndexDoc - ? allItems.filter((item) => item !== categoryIndexDoc) - : allItems; + // 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', From 52a150f46b296c98f61ceaba4406f7516d3e6e74 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Wed, 10 Nov 2021 20:13:20 +0100 Subject: [PATCH 24/92] style generated index page --- .../theme/DocCategoryGeneratedIndex/index.tsx | 85 ++++++++++++------- .../styles.module.css | 24 ++++++ 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx index ba6de3f92008..6a953dec5946 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCategoryGeneratedIndex/index.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import React, {ReactNode} from 'react'; import type {Props} from '@theme/DocCategoryGeneratedIndex'; import {useDocsSidebar} from '@docusaurus/theme-common'; @@ -15,6 +15,9 @@ import { } from '@docusaurus/plugin-content-docs/lib/sidebars/types'; import Link from '@docusaurus/Link'; import {PropSidebarItemLink} from '@docusaurus/plugin-content-docs-types'; +import clsx from 'clsx'; +import styles from './styles.module.css'; +import isInternalUrl from '@docusaurus/isInternalUrl'; // Use the components props and the sidebar in context // to get back the related sidebar category that we want to render @@ -56,50 +59,70 @@ function useSidebarCategory(categoryIndex: Props['categoryIndex']) { return sidebarCategory; } -function SidebarItemCategory({ - item, +function Card({ + icon, + href, + title, + description, }: { - item: PropSidebarItemCategory; + icon: ReactNode; + title: string; + description?: string; + href?: string; }): JSX.Element { - const label = item.href ? ( - {item.label} + const className = clsx('card margin--md padding--md', styles.card); + + const content = ( +
+

+ {icon} {title} +

+
{description}
+
+ ); + + return href ? ( + + {content} + ) : ( - {item.label} +
{content}
); +} +function CardCategory({item}: {item: PropSidebarItemCategory}): JSX.Element { return ( -
-
- {'=> '} {label} -
-
- -
-
+ ); } -function SidebarItemLink({item}: {item: PropSidebarItemLink}): JSX.Element { - return {item.label}; +function CardLink({item}: {item: PropSidebarItemLink}): JSX.Element { + const icon = isInternalUrl(item.href) ? '📄️' : '🔗'; + return ; } -function SidebarItem({item}: {item: PropSidebarItem}): JSX.Element { +function CardListItem({item}: {item: PropSidebarItem}): JSX.Element { switch (item.type) { case 'link': - return ; + return ; case 'category': - return ; + return ; default: throw new Error(`unknown item type ${JSON.stringify(item)}`); } } -function SidebarItemsList({items}: {items: PropSidebarItem[]}): JSX.Element { +function CardList({items}: {items: PropSidebarItem[]}): JSX.Element { return ( - + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCardList/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCardList/styles.module.css deleted file mode 100644 index 59a860ebc9af..000000000000 --- a/packages/docusaurus-theme-classic/src/theme/DocCardList/styles.module.css +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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. - */ - -.docCardList { - --ifm-list-left-padding: 0; - list-style: none; -} From d96e9ac655cc1edccd13ab122adcc2a56af692cd Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 2 Dec 2021 19:37:58 +0100 Subject: [PATCH 86/92] integrate new infima --- .../docusaurus-theme-classic/package.json | 2 +- .../src/theme/DocSidebarItem/index.tsx | 82 ++++++++++--------- yarn.lock | 8 +- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index fa782d9956c7..3c371d2ed66c 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -37,7 +37,7 @@ "clsx": "^1.1.1", "copy-text-to-clipboard": "^3.0.1", "globby": "^11.0.2", - "infima": "0.2.0-alpha.35", + "infima": "0.2.0-alpha.36", "lodash": "^4.17.20", "postcss": "^8.3.7", "prism-react-renderer": "^1.2.1", diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx index 3584e6f231d7..0694164469c2 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx @@ -139,47 +139,49 @@ function DocSidebarItemCategory({ }, className, )}> - { - if (href) { - setCollapsed(false); - } else { - e.preventDefault(); - toggleCollapsed(); +
+ { + if (href) { + setCollapsed(false); + } else { + e.preventDefault(); + toggleCollapsed(); + } } - } - : undefined - } - href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback} - {...props}> - {label} - - {href && ( -
Date: Thu, 2 Dec 2021 19:57:36 +0100 Subject: [PATCH 87/92] minor fixes for non-collapsible sidebar items --- .../src/theme/DocSidebarItem/index.tsx | 5 +++-- .../src/theme/DocSidebarItem/styles.module.css | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx index 0694164469c2..6de4be931d31 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx @@ -143,8 +143,9 @@ function DocSidebarItemCategory({ {label} - {href && ( + {href && collapsible && (