From db79d462abd0fb04423a8a43b2e04a1dc643ad6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Thu, 15 Apr 2021 16:20:11 +0200 Subject: [PATCH] feat(v2): auto-generated sidebars, frontmatter-less sites (#4582) * POC of autogenerated sidebars * use combine-promises utility lib * autogenerated sidebar poc working * Revert "autogenerated sidebar poc working" This reverts commit c81da980 * POC of auto-generated sidebars for community docs * update tests * add initial test suite for autogenerated sidebars + fix some edge cases * Improve autogen sidebars: strip more number prefixes in folder breadcrumb + slugs * fix typo! * Add tests for partially generated sidebars + fix edge cases + extract sidebar generation code * Ability to read category metadatas file from a file in the category * fix tests * change position of API * ability to extract number prefix * stable system to enable position frontmatter * fix tests for autogen sidebar position * renamings * restore community sidebars * rename frontmatter position -> sidebar_position * make sidebarItemsGenerator fn configurable * minor changes * rename dirPath => dirName * Make the init template use autogenerated sidebars * fix options * fix docusaurus site: remove test docs * add _category_ file to docs pathsToWatch * add _category_ file to docs pathsToWatch * tutorial: use sidebar_position instead of file number prefixes * Adapt Docusaurus tutorial for autogenerated sidebars * remove slug: / * polish the homepage template * rename _category_ sidebar_position to just "position" * test for custom sidebarItemsGenerator fn * fix category metadata + add link to report tutorial issues * fix absolute path breaking tests * fix absolute path breaking tests * Add test for floating number sidebar_position * add sidebarItemsGenerator unit tests * add processSidebars unit tests * Fix init template broken links * windows test * increase code translations test timeout * cleanup mockCategoryMetadataFiles after windows test fixed * update init template positions * fix windows tests * fix comment * Add autogenerated sidebar items documentation + rewrite the full sidebars page doc * add useful comment * fix code block title --- .../classic/docs/create-a-document.md | 38 - .../docs/{getting-started.md => intro.md} | 13 +- .../docs/tutorial-basics/_category_.json | 4 + .../{ => tutorial-basics}/congratulations.md | 10 +- .../create-a-blog-post.md | 6 +- .../docs/tutorial-basics/create-a-document.md | 56 ++ .../{ => tutorial-basics}/create-a-page.md | 14 +- .../{ => tutorial-basics}/deploy-your-site.md | 10 +- .../markdown-features.mdx | 14 +- .../docs/tutorial-extras/_category_.json | 4 + .../manage-docs-versions.md | 6 +- .../translate-your-site.md | 8 +- .../templates/classic/docusaurus.config.js | 6 +- .../templates/classic/sidebars.js | 36 +- .../src/components/HomepageFeatures.js | 6 +- .../templates/classic/src/pages/index.js | 6 +- .../package.json | 3 + .../docs/0-getting-started.md | 3 + .../docs/1-installation.md | 3 + .../docs/3-API/00_api-overview.md | 3 + .../3-API/01_Core APIs/0 --- Client API.md | 1 + .../3-API/01_Core APIs/1 --- Server API.md | 1 + .../3-API/02_Extension APIs/0. Plugin API.md | 1 + .../3-API/02_Extension APIs/1. Theme API.md | 1 + .../3-API/02_Extension APIs/_category_.yml | 1 + .../docs/3-API/03_api-end.md | 3 + .../docs/3-API/_category_.json | 3 + .../docs/Guides/0-guide2.5.md | 8 + .../docs/Guides/02-guide2.md | 7 + .../docs/Guides/_category_.json | 3 + .../docs/Guides/a-guide4.md | 7 + .../docs/Guides/b-guide5.md | 7 + .../docs/Guides/guide3.md | 8 + .../docs/Guides/z-guide1.md | 8 + .../docusaurus.config.js | 14 + .../partialAutogeneratedSidebars.js | 23 + .../__snapshots__/index.test.ts.snap | 163 ++++ .../src/__tests__/docs.test.ts | 19 + .../src/__tests__/index.test.ts | 851 +++++++++++++++++- .../src/__tests__/numberPrefix.test.ts | 115 +++ .../src/__tests__/options.test.ts | 2 + .../__tests__/sidebarItemsGenerator.test.ts | 268 ++++++ .../src/__tests__/sidebars.test.ts | 137 ++- .../src/__tests__/slug.test.ts | 17 + .../docusaurus-plugin-content-docs/src/cli.ts | 29 +- .../src/docFrontMatter.ts | 4 + .../src/docs.ts | 63 +- .../src/index.ts | 19 +- .../src/numberPrefix.ts | 35 + .../src/options.ts | 7 +- .../src/sidebarItemsGenerator.ts | 305 +++++++ .../src/sidebars.ts | 151 +++- .../src/slug.ts | 10 +- .../src/types.ts | 42 +- .../src/versions.ts | 1 + .../src/theme/CodeBlock/index.tsx | 7 +- .../docusaurus-theme-classic/src/types.d.ts | 1 + .../update-code-translations.test.js | 3 + .../community/{support.md => 0-support.md} | 6 +- website/community/{team.mdx => 1-team.mdx} | 6 +- .../{resources.md => 2-resources.md} | 6 +- .../docs/api/plugins/plugin-content-docs.md | 17 +- website/docs/guides/creating-pages.md | 10 +- website/docs/guides/docs/sidebar.md | 521 ++++++++--- website/docs/i18n/i18n-crowdin.mdx | 4 +- website/sidebarsCommunity.js | 7 +- yarn.lock | 22 + 67 files changed, 2887 insertions(+), 306 deletions(-) delete mode 100644 packages/docusaurus-init/templates/classic/docs/create-a-document.md rename packages/docusaurus-init/templates/classic/docs/{getting-started.md => intro.md} (73%) create mode 100644 packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-basics}/congratulations.md (50%) rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-basics}/create-a-blog-post.md (90%) create mode 100644 packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-basics}/create-a-page.md (80%) rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-basics}/deploy-your-site.md (61%) rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-basics}/markdown-features.mdx (91%) create mode 100644 packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-extras}/manage-docs-versions.md (88%) rename packages/docusaurus-init/templates/classic/docs/{ => tutorial-extras}/translate-your-site.md (92%) create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts create mode 100644 packages/docusaurus-plugin-content-docs/src/numberPrefix.ts create mode 100644 packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts rename website/community/{support.md => 0-support.md} (97%) rename website/community/{team.mdx => 1-team.mdx} (97%) rename website/community/{resources.md => 2-resources.md} (97%) diff --git a/packages/docusaurus-init/templates/classic/docs/create-a-document.md b/packages/docusaurus-init/templates/classic/docs/create-a-document.md deleted file mode 100644 index fbcc2782a9..0000000000 --- a/packages/docusaurus-init/templates/classic/docs/create-a-document.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Create a Document ---- - -Documents are a **group of pages** connected through a **sidebar**, a **previous/next navigation** and **versioning**. - -## Create a Document - -Create a markdown file at `docs/my-doc.md`: - -```mdx title="docs/hello.md" ---- -title: Hello, World! ---- - -## Hello, World! - -This is your first document in **Docusaurus**, Congratulations! -``` - -A new document is now available at `http://localhost:3000/docs/hello`. - -## Add your document to the sidebar - -Add `hello` to the `sidebars.js` file: - -```diff title="sidebars.js" -module.exports = { - docs: [ - { - type: 'category', - label: 'Docusaurus Tutorial', -- items: ['getting-started', 'create-a-doc', ...], -+ items: ['getting-started', 'create-a-doc', 'hello', ...], - }, - ], -}; -``` diff --git a/packages/docusaurus-init/templates/classic/docs/getting-started.md b/packages/docusaurus-init/templates/classic/docs/intro.md similarity index 73% rename from packages/docusaurus-init/templates/classic/docs/getting-started.md rename to packages/docusaurus-init/templates/classic/docs/intro.md index 34a553c907..cb9ab0b030 100644 --- a/packages/docusaurus-init/templates/classic/docs/getting-started.md +++ b/packages/docusaurus-init/templates/classic/docs/intro.md @@ -1,11 +1,16 @@ --- -title: Getting Started -slug: / +sidebar_position: 1 --- -Get started by **creating a new site** +# Tutorial Intro -Or **try Docusaurus immediately** with **[new.docusaurus.io](https://new.docusaurus.io)** (CodeSandbox). +Let's discover **Docusaurus in less than 5 minutes**. + +## Getting Started + +Get started by **creating a new site**. + +Or **try Docusaurus immediately** with **[new.docusaurus.io](https://new.docusaurus.io)**. ## Generate a new site diff --git a/packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json new file mode 100644 index 0000000000..135e4a6858 --- /dev/null +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tutorial - Basics", + "position": 2 +} diff --git a/packages/docusaurus-init/templates/classic/docs/congratulations.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/congratulations.md similarity index 50% rename from packages/docusaurus-init/templates/classic/docs/congratulations.md rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/congratulations.md index 71010450e8..9ef99bbadd 100644 --- a/packages/docusaurus-init/templates/classic/docs/congratulations.md +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/congratulations.md @@ -1,14 +1,16 @@ --- -title: Congratulations! +sidebar_position: 6 --- -Congratulations on making it this far! +# Congratulations! -You have learned the **basics of Docusaurus** and made some changes to the **initial template**. +You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. Docusaurus has **much more to offer**! -Have 5 more minutes? Take a look at **[versioning](./manage-docs-versions.md)** and **[i18n](./translate-your-site.md)**. +Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. + +Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) ## What's next? diff --git a/packages/docusaurus-init/templates/classic/docs/create-a-blog-post.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-blog-post.md similarity index 90% rename from packages/docusaurus-init/templates/classic/docs/create-a-blog-post.md rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-blog-post.md index a50ccb337f..d893e1c74f 100644 --- a/packages/docusaurus-init/templates/classic/docs/create-a-blog-post.md +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-blog-post.md @@ -1,10 +1,12 @@ --- -title: Create a Blog Post +sidebar_position: 3 --- +# Create a Blog Post + Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... -## Create a Blog Post +## Create your first Post Create a file at `blog/2021-02-28-greetings.md`: diff --git a/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md new file mode 100644 index 0000000000..66b58543d4 --- /dev/null +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-document.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 2 +--- + +# Create a Document + +Documents are **groups of pages** connected through: + +- a **sidebar** +- **previous/next navigation** +- **versioning** + +## Create your first Doc + +Create a markdown file at `docs/hello.md`: + +```md title="docs/hello.md" +# Hello + +This is my **first Docusaurus document**! +``` + +A new document is now available at `http://localhost:3000/docs/hello`. + +## Configure the Sidebar + +Docusaurus automatically **creates a sidebar** from the `docs` folder. + +Add metadatas to customize the sidebar label and position: + +```diff title="docs/hello.md" ++ --- ++ sidebar_label: "Hi!" ++ sidebar_position: 3 ++ --- + + +# Hello + +This is my **first Docusaurus document**! +``` + +It is also possible to create your sidebar explicitly in `sidebars.js`: + +```diff title="sidebars.js" +module.exports = { + tutorialSidebar: [ + { + type: 'category', + label: 'Tutorial', +- items: [...], ++ items: ['hello'], + }, + ], +}; +``` diff --git a/packages/docusaurus-init/templates/classic/docs/create-a-page.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-page.md similarity index 80% rename from packages/docusaurus-init/templates/classic/docs/create-a-page.md rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-page.md index 314bd87e9d..e112b0059c 100644 --- a/packages/docusaurus-init/templates/classic/docs/create-a-page.md +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/create-a-page.md @@ -1,14 +1,16 @@ --- -title: Create a Page +sidebar_position: 1 --- -Add **Markdown or React** files to `src/pages` to create **standalone pages**: +# Create a Page + +Add **Markdown or React** files to `src/pages` to create a **standalone page**: - `src/pages/index.js` -> `localhost:3000/` - `src/pages/foo.md` -> `localhost:3000/foo` - `src/pages/foo/bar.js` -> `localhost:3000/foo/bar` -## Create a React Page +## Create your first React Page Create a file at `src/pages/my-react-page.js`: @@ -28,15 +30,11 @@ export default function MyReactPage() { A new page is now available at `http://localhost:3000/my-react-page`. -## Create a Markdown Page +## Create your first Markdown Page Create a file at `src/pages/my-markdown-page.md`: ```mdx title="src/pages/my-markdown-page.md" ---- -title: My Markdown page ---- - # My Markdown page This is a Markdown page diff --git a/packages/docusaurus-init/templates/classic/docs/deploy-your-site.md b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/deploy-your-site.md similarity index 61% rename from packages/docusaurus-init/templates/classic/docs/deploy-your-site.md rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/deploy-your-site.md index cf0bff5277..492eae0276 100644 --- a/packages/docusaurus-init/templates/classic/docs/deploy-your-site.md +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/deploy-your-site.md @@ -1,8 +1,12 @@ --- -title: Deploy your site +sidebar_position: 5 --- -Docusaurus is a **static-site-generator** (also called [Jamstack](https://jamstack.org/)), and builds your site as **static HTML, JavaScript and CSS files**. +# Deploy your site + +Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). + +It builds your site as simple **static HTML, JavaScript and CSS files**. ## Build your site @@ -12,7 +16,7 @@ Build your site **for production**: npm run build ``` -The static files are generated in the `build` directory. +The static files are generated in the `build` folder. ## Deploy your site diff --git a/packages/docusaurus-init/templates/classic/docs/markdown-features.mdx b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/markdown-features.mdx similarity index 91% rename from packages/docusaurus-init/templates/classic/docs/markdown-features.mdx rename to packages/docusaurus-init/templates/classic/docs/tutorial-basics/markdown-features.mdx index 9b9f9ca076..8855626051 100644 --- a/packages/docusaurus-init/templates/classic/docs/markdown-features.mdx +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-basics/markdown-features.mdx @@ -1,20 +1,24 @@ --- -title: Markdown Features +sidebar_position: 4 --- +# Markdown Features + Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. ## Front Matter -Markdown documents have metadata at the very top called [Front Matter](https://jekyllrb.com/docs/front-matter/): +Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): -```md +```text title="my-doc.md" +// highlight-start --- -id: my-doc +id: my-doc-id title: My document title description: My document description -sidebar_label: My doc +slug: /my-custom-url --- +// highlight-end ## Markdown heading diff --git a/packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json new file mode 100644 index 0000000000..ca3f8e0640 --- /dev/null +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Tutorial - Extras", + "position": 3 +} diff --git a/packages/docusaurus-init/templates/classic/docs/manage-docs-versions.md b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/manage-docs-versions.md similarity index 88% rename from packages/docusaurus-init/templates/classic/docs/manage-docs-versions.md rename to packages/docusaurus-init/templates/classic/docs/tutorial-extras/manage-docs-versions.md index 1f2cd1a8b3..6335b0ac94 100644 --- a/packages/docusaurus-init/templates/classic/docs/manage-docs-versions.md +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/manage-docs-versions.md @@ -1,7 +1,9 @@ --- -title: Manage Docs Versions +sidebar_position: 1 --- +# Manage Docs Versions + Docusaurus can manage multiple versions of your docs. ## Create a docs version @@ -12,7 +14,7 @@ Release a version 1.0 of your project: npm run docusaurus docs:version 1.0 ``` -The `docs` directory is copied into `versioned_docs/version-1.0` and `versions.json` is created. +The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. Your docs now have 2 versions: diff --git a/packages/docusaurus-init/templates/classic/docs/translate-your-site.md b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/translate-your-site.md similarity index 92% rename from packages/docusaurus-init/templates/classic/docs/translate-your-site.md rename to packages/docusaurus-init/templates/classic/docs/tutorial-extras/translate-your-site.md index 4c2eaf0c96..0606070cac 100644 --- a/packages/docusaurus-init/templates/classic/docs/translate-your-site.md +++ b/packages/docusaurus-init/templates/classic/docs/tutorial-extras/translate-your-site.md @@ -1,7 +1,9 @@ --- -title: Translate your site +sidebar_position: 2 --- +# Translate your site + Let's translate `docs/getting-started.md` to French. ## Configure i18n @@ -19,7 +21,7 @@ module.exports = { ## Translate a doc -Copy the `docs/getting-started.md` file to the `i18n/fr` directory: +Copy the `docs/getting-started.md` file to the `i18n/fr` folder: ```bash mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ @@ -39,7 +41,7 @@ npm run start -- --locale fr Your localized site is accessible at `http://localhost:3000/fr/` and the `Getting Started` page is translated. -:::warning +:::caution In development, you can only use one locale at a same time. diff --git a/packages/docusaurus-init/templates/classic/docusaurus.config.js b/packages/docusaurus-init/templates/classic/docusaurus.config.js index 023a5534a8..6dfe1342b1 100644 --- a/packages/docusaurus-init/templates/classic/docusaurus.config.js +++ b/packages/docusaurus-init/templates/classic/docusaurus.config.js @@ -19,7 +19,7 @@ module.exports = { items: [ { type: 'doc', - docId: 'getting-started', + docId: 'intro', position: 'left', label: 'Tutorial', }, @@ -38,8 +38,8 @@ module.exports = { title: 'Docs', items: [ { - label: 'Getting Started', - to: '/docs/', + label: 'Tutorial', + to: '/docs/intro', }, ], }, diff --git a/packages/docusaurus-init/templates/classic/sidebars.js b/packages/docusaurus-init/templates/classic/sidebars.js index 5ed8bc649a..981a73cd7a 100644 --- a/packages/docusaurus-init/templates/classic/sidebars.js +++ b/packages/docusaurus-init/templates/classic/sidebars.js @@ -1,22 +1,26 @@ +/** + * Creating a sidebar enables you to: + - create an ordered group of docs + - render a sidebar for each doc of that group + - provide next/previous navigation + + The sidebars can be generated from the filesystem, or explicitly defined here. + + Create as many sidebars as you want. + */ + module.exports = { - tutorial: [ + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], + + // But you can create a sidebar manually + /* + tutorialSidebar: [ { type: 'category', - label: 'Tutorial - Basics', - items: [ - 'getting-started', - 'create-a-page', - 'create-a-document', - 'create-a-blog-post', - 'markdown-features', - 'deploy-your-site', - 'congratulations', - ], - }, - { - type: 'category', - label: 'Tutorial - Extras', - items: ['manage-docs-versions', 'translate-your-site'], + label: 'Tutorial', + items: ['hello'], }, ], + */ }; diff --git a/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js b/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js index 19c618e64b..16f820b103 100644 --- a/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js +++ b/packages/docusaurus-init/templates/classic/src/components/HomepageFeatures.js @@ -41,8 +41,10 @@ function Feature({Svg, title, description}) {
-

{title}

-

{description}

+
+

{title}

+

{description}

+
); } diff --git a/packages/docusaurus-init/templates/classic/src/pages/index.js b/packages/docusaurus-init/templates/classic/src/pages/index.js index bbded37670..27c21e8f99 100644 --- a/packages/docusaurus-init/templates/classic/src/pages/index.js +++ b/packages/docusaurus-init/templates/classic/src/pages/index.js @@ -14,8 +14,10 @@ function HomepageHeader() {

{siteConfig.title}

{siteConfig.tagline}

- - Get Started - Docusaurus Tutorial + + Docusaurus Tutorial - 5min ⏱️
diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index 4956f93069..ffa19b92d1 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -20,6 +20,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "2.0.0-alpha.72", "@types/picomatch": "^2.2.1", + "@types/js-yaml": "^4.0.0", "commander": "^5.1.0", "picomatch": "^2.1.1" }, @@ -30,10 +31,12 @@ "@docusaurus/utils": "2.0.0-alpha.72", "@docusaurus/utils-validation": "2.0.0-alpha.72", "chalk": "^4.1.0", + "combine-promises": "^1.1.0", "execa": "^5.0.0", "fs-extra": "^9.1.0", "globby": "^11.0.2", "import-fresh": "^3.2.2", + "js-yaml": "^4.0.0", "loader-utils": "^1.2.3", "lodash": "^4.17.20", "remark-admonitions": "^1.2.1", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md new file mode 100644 index 0000000000..1ace6c84ba --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/0-getting-started.md @@ -0,0 +1,3 @@ +# Getting Started + +Getting started text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md new file mode 100644 index 0000000000..a8ef429a7a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/1-installation.md @@ -0,0 +1,3 @@ +# Installation + +Installation text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md new file mode 100644 index 0000000000..62db8a9152 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/00_api-overview.md @@ -0,0 +1,3 @@ +# API Overview + +API Overview text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md new file mode 100644 index 0000000000..a6e52ca91a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/0 --- Client API.md @@ -0,0 +1 @@ +Client API text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md new file mode 100644 index 0000000000..3730187399 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/01_Core APIs/1 --- Server API.md @@ -0,0 +1 @@ +Server API text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md new file mode 100644 index 0000000000..d7cfa7e3ea --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/0. Plugin API.md @@ -0,0 +1 @@ +Plugin API text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md new file mode 100644 index 0000000000..602b1f0340 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/1. Theme API.md @@ -0,0 +1 @@ +Theme API text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml new file mode 100644 index 0000000000..43ac56e8fb --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/02_Extension APIs/_category_.yml @@ -0,0 +1 @@ +label: 'Extension APIs (label from _category_.yml)' diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md new file mode 100644 index 0000000000..c3769f715a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/03_api-end.md @@ -0,0 +1,3 @@ +# API End + +API End text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json new file mode 100644 index 0000000000..927c1d7d4b --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/3-API/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "API (label from _category_.json)" +} diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md new file mode 100644 index 0000000000..92f2d85128 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/0-guide2.5.md @@ -0,0 +1,8 @@ +--- +id: guide2.5 +sidebar_position: 2.5 +--- + +# Guide 2.5 + +Guide 2.5 text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md new file mode 100644 index 0000000000..cf298d838f --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/02-guide2.md @@ -0,0 +1,7 @@ +--- +id: guide2 +--- + +# Guide 2 + +Guide 2 text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json new file mode 100644 index 0000000000..046edaa301 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 2 +} diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md new file mode 100644 index 0000000000..54c8c7be3a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/a-guide4.md @@ -0,0 +1,7 @@ +--- +id: guide4 +--- + +# Guide 4 + +Guide 4 text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md new file mode 100644 index 0000000000..b9311ebbba --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/b-guide5.md @@ -0,0 +1,7 @@ +--- +id: guide5 +--- + +# Guide 5 + +Guide 5 text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md new file mode 100644 index 0000000000..a11a28767c --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/guide3.md @@ -0,0 +1,8 @@ +--- +id: guide3 +sidebar_position: 3 +--- + +# Guide 3 + +Guide 3 text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md new file mode 100644 index 0000000000..d3e4f3bcb2 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docs/Guides/z-guide1.md @@ -0,0 +1,8 @@ +--- +id: guide1 +sidebar_position: 1 +--- + +# Guide 1 + +Guide 1 text diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js new file mode 100644 index 0000000000..6fa02ca102 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/docusaurus.config.js @@ -0,0 +1,14 @@ +/** + * 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. + */ + +module.exports = { + title: 'My Site', + tagline: 'The tagline of my site', + url: 'https://your-docusaurus-test-site.com', + baseUrl: '/', + favicon: 'img/favicon.ico', +}; diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js new file mode 100644 index 0000000000..65f30d493d --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/site-with-autogenerated-sidebar/partialAutogeneratedSidebars.js @@ -0,0 +1,23 @@ +/** + * 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. + */ + +module.exports = { + someSidebar: [ + {type: 'doc', id: 'API/api-end'}, + { + type: 'category', + label: 'Some category', + items: [ + {type: 'doc', id: 'API/api-overview'}, + { + type: 'autogenerated', + dirName: '3-API/02_Extension APIs', + }, + ], + }, + ], +}; diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 95a79b9d53..b5fda7366b 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -149,6 +149,7 @@ Object { \\"title\\": \\"Bar\\", \\"description\\": \\"This is custom description\\", \\"source\\": \\"@site/docs/foo/bar.md\\", + \\"sourceDirName\\": \\"foo\\", \\"slug\\": \\"/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\", \\"version\\": \\"current\\", @@ -170,6 +171,7 @@ Object { \\"title\\": \\"baz\\", \\"description\\": \\"Images\\", \\"source\\": \\"@site/docs/foo/baz.md\\", + \\"sourceDirName\\": \\"foo\\", \\"slug\\": \\"/foo/bazSlug.html\\", \\"permalink\\": \\"/docs/foo/bazSlug.html\\", \\"version\\": \\"current\\", @@ -195,6 +197,7 @@ Object { \\"title\\": \\"My heading as title\\", \\"description\\": \\"\\", \\"source\\": \\"@site/docs/headingAsTitle.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/headingAsTitle\\", \\"permalink\\": \\"/docs/headingAsTitle\\", \\"version\\": \\"current\\", @@ -207,6 +210,7 @@ Object { \\"title\\": \\"Hello, World !\\", \\"description\\": \\"Hi, Endilie here :)\\", \\"source\\": \\"@site/docs/hello.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/\\", \\"permalink\\": \\"/docs/\\", \\"version\\": \\"current\\", @@ -227,6 +231,7 @@ Object { \\"title\\": \\"ipsum\\", \\"description\\": \\"Lorem ipsum.\\", \\"source\\": \\"@site/docs/ipsum.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/ipsum\\", \\"permalink\\": \\"/docs/ipsum\\", \\"editUrl\\": null, @@ -242,6 +247,7 @@ Object { \\"title\\": \\"lorem\\", \\"description\\": \\"Lorem ipsum.\\", \\"source\\": \\"@site/docs/lorem.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/lorem\\", \\"permalink\\": \\"/docs/lorem\\", \\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\", @@ -258,6 +264,7 @@ Object { \\"title\\": \\"rootAbsoluteSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootAbsoluteSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/rootAbsoluteSlug\\", \\"version\\": \\"current\\", @@ -272,6 +279,7 @@ Object { \\"title\\": \\"rootRelativeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootRelativeSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/rootRelativeSlug\\", \\"permalink\\": \\"/docs/rootRelativeSlug\\", \\"version\\": \\"current\\", @@ -286,6 +294,7 @@ Object { \\"title\\": \\"rootResolvedSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootResolvedSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/hey/rootResolvedSlug\\", \\"permalink\\": \\"/docs/hey/rootResolvedSlug\\", \\"version\\": \\"current\\", @@ -300,6 +309,7 @@ Object { \\"title\\": \\"rootTryToEscapeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootTryToEscapeSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/rootTryToEscapeSlug\\", \\"permalink\\": \\"/docs/rootTryToEscapeSlug\\", \\"version\\": \\"current\\", @@ -314,6 +324,7 @@ Object { \\"title\\": \\"absoluteSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/absoluteSlug\\", \\"permalink\\": \\"/docs/absoluteSlug\\", \\"version\\": \\"current\\", @@ -328,6 +339,7 @@ Object { \\"title\\": \\"relativeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/relativeSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/slugs/relativeSlug\\", \\"version\\": \\"current\\", @@ -342,6 +354,7 @@ Object { \\"title\\": \\"resolvedSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\", \\"version\\": \\"current\\", @@ -356,6 +369,7 @@ Object { \\"title\\": \\"tryToEscapeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/tryToEscapeSlug\\", \\"version\\": \\"current\\", @@ -646,6 +660,134 @@ Array [ ] `; +exports[`site with custom sidebar items generator sidebarItemsGenerator is called with appropriate data 1`] = ` +Object { + "docs": Array [ + Object { + "frontMatter": Object {}, + "id": "API/Core APIs/Client API", + "sidebarPosition": 0, + "source": "@site/docs/3-API/01_Core APIs/0 --- Client API.md", + "sourceDirName": "3-API/01_Core APIs", + }, + Object { + "frontMatter": Object {}, + "id": "API/Core APIs/Server API", + "sidebarPosition": 1, + "source": "@site/docs/3-API/01_Core APIs/1 --- Server API.md", + "sourceDirName": "3-API/01_Core APIs", + }, + Object { + "frontMatter": Object {}, + "id": "API/Extension APIs/Plugin API", + "sidebarPosition": 0, + "source": "@site/docs/3-API/02_Extension APIs/0. Plugin API.md", + "sourceDirName": "3-API/02_Extension APIs", + }, + Object { + "frontMatter": Object {}, + "id": "API/Extension APIs/Theme API", + "sidebarPosition": 1, + "source": "@site/docs/3-API/02_Extension APIs/1. Theme API.md", + "sourceDirName": "3-API/02_Extension APIs", + }, + Object { + "frontMatter": Object {}, + "id": "API/api-end", + "sidebarPosition": 3, + "source": "@site/docs/3-API/03_api-end.md", + "sourceDirName": "3-API", + }, + Object { + "frontMatter": Object {}, + "id": "API/api-overview", + "sidebarPosition": 0, + "source": "@site/docs/3-API/00_api-overview.md", + "sourceDirName": "3-API", + }, + Object { + "frontMatter": Object { + "id": "guide1", + "sidebar_position": 1, + }, + "id": "Guides/guide1", + "sidebarPosition": 1, + "source": "@site/docs/Guides/z-guide1.md", + "sourceDirName": "Guides", + }, + Object { + "frontMatter": Object { + "id": "guide2", + }, + "id": "Guides/guide2", + "sidebarPosition": 2, + "source": "@site/docs/Guides/02-guide2.md", + "sourceDirName": "Guides", + }, + Object { + "frontMatter": Object { + "id": "guide2.5", + "sidebar_position": 2.5, + }, + "id": "Guides/guide2.5", + "sidebarPosition": 2.5, + "source": "@site/docs/Guides/0-guide2.5.md", + "sourceDirName": "Guides", + }, + Object { + "frontMatter": Object { + "id": "guide3", + "sidebar_position": 3, + }, + "id": "Guides/guide3", + "sidebarPosition": 3, + "source": "@site/docs/Guides/guide3.md", + "sourceDirName": "Guides", + }, + Object { + "frontMatter": Object { + "id": "guide4", + }, + "id": "Guides/guide4", + "sidebarPosition": undefined, + "source": "@site/docs/Guides/a-guide4.md", + "sourceDirName": "Guides", + }, + Object { + "frontMatter": Object { + "id": "guide5", + }, + "id": "Guides/guide5", + "sidebarPosition": undefined, + "source": "@site/docs/Guides/b-guide5.md", + "sourceDirName": "Guides", + }, + Object { + "frontMatter": Object {}, + "id": "getting-started", + "sidebarPosition": 0, + "source": "@site/docs/0-getting-started.md", + "sourceDirName": ".", + }, + Object { + "frontMatter": Object {}, + "id": "installation", + "sidebarPosition": 1, + "source": "@site/docs/1-installation.md", + "sourceDirName": ".", + }, + ], + "item": Object { + "dirName": ".", + "type": "autogenerated", + }, + "version": Object { + "contentPath": "docs", + "versionName": "current", + }, +} +`; + exports[`site with wrong sidebar file 1`] = ` "Bad sidebars file. These sidebar document ids do not exist: @@ -699,6 +841,7 @@ Object { \\"title\\": \\"team\\", \\"description\\": \\"Team 1.0.0\\", \\"source\\": \\"@site/community_versioned_docs/version-1.0.0/team.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/team\\", \\"permalink\\": \\"/community/team\\", \\"version\\": \\"1.0.0\\", @@ -712,6 +855,7 @@ Object { \\"title\\": \\"Team title translated\\", \\"description\\": \\"Team current version (translated)\\", \\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/team\\", \\"permalink\\": \\"/community/next/team\\", \\"version\\": \\"current\\", @@ -942,6 +1086,7 @@ Object { \\"title\\": \\"bar\\", \\"description\\": \\"This is next version of bar.\\", \\"source\\": \\"@site/docs/foo/bar.md\\", + \\"sourceDirName\\": \\"foo\\", \\"slug\\": \\"/foo/barSlug\\", \\"permalink\\": \\"/docs/next/foo/barSlug\\", \\"version\\": \\"current\\", @@ -961,6 +1106,7 @@ Object { \\"title\\": \\"hello\\", \\"description\\": \\"Hello next !\\", \\"source\\": \\"@site/docs/hello.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/\\", \\"permalink\\": \\"/docs/next/\\", \\"version\\": \\"current\\", @@ -978,6 +1124,7 @@ Object { \\"title\\": \\"absoluteSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/absoluteSlug\\", \\"permalink\\": \\"/docs/next/absoluteSlug\\", \\"version\\": \\"current\\", @@ -992,6 +1139,7 @@ Object { \\"title\\": \\"relativeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/relativeSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/next/slugs/relativeSlug\\", \\"version\\": \\"current\\", @@ -1006,6 +1154,7 @@ Object { \\"title\\": \\"resolvedSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\", \\"version\\": \\"current\\", @@ -1020,6 +1169,7 @@ Object { \\"title\\": \\"tryToEscapeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/next/tryToEscapeSlug\\", \\"version\\": \\"current\\", @@ -1034,6 +1184,7 @@ Object { \\"title\\": \\"hello\\", \\"description\\": \\"Hello 1.0.0 ! (translated en)\\", \\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/\\", \\"permalink\\": \\"/docs/1.0.0/\\", \\"version\\": \\"1.0.0\\", @@ -1051,6 +1202,7 @@ Object { \\"title\\": \\"bar\\", \\"description\\": \\"Bar 1.0.0 !\\", \\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/bar.md\\", + \\"sourceDirName\\": \\"foo\\", \\"slug\\": \\"/foo/barSlug\\", \\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\", \\"version\\": \\"1.0.0\\", @@ -1070,6 +1222,7 @@ Object { \\"title\\": \\"baz\\", \\"description\\": \\"Baz 1.0.0 ! This will be deleted in next subsequent versions.\\", \\"source\\": \\"@site/versioned_docs/version-1.0.0/foo/baz.md\\", + \\"sourceDirName\\": \\"foo\\", \\"slug\\": \\"/foo/baz\\", \\"permalink\\": \\"/docs/1.0.0/foo/baz\\", \\"version\\": \\"1.0.0\\", @@ -1091,6 +1244,7 @@ Object { \\"title\\": \\"bar\\", \\"description\\": \\"Bar 1.0.1 !\\", \\"source\\": \\"@site/versioned_docs/version-1.0.1/foo/bar.md\\", + \\"sourceDirName\\": \\"foo\\", \\"slug\\": \\"/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\", \\"version\\": \\"1.0.1\\", @@ -1108,6 +1262,7 @@ Object { \\"title\\": \\"hello\\", \\"description\\": \\"Hello 1.0.1 !\\", \\"source\\": \\"@site/versioned_docs/version-1.0.1/hello.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/\\", \\"permalink\\": \\"/docs/\\", \\"version\\": \\"1.0.1\\", @@ -1125,6 +1280,7 @@ Object { \\"title\\": \\"rootAbsoluteSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\", \\"version\\": \\"withSlugs\\", @@ -1140,6 +1296,7 @@ Object { \\"title\\": \\"rootRelativeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootRelativeSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/rootRelativeSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\", \\"version\\": \\"withSlugs\\", @@ -1154,6 +1311,7 @@ Object { \\"title\\": \\"rootResolvedSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootResolvedSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/hey/rootResolvedSlug\\", \\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\", \\"version\\": \\"withSlugs\\", @@ -1168,6 +1326,7 @@ Object { \\"title\\": \\"rootTryToEscapeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md\\", + \\"sourceDirName\\": \\".\\", \\"slug\\": \\"/rootTryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\", \\"version\\": \\"withSlugs\\", @@ -1182,6 +1341,7 @@ Object { \\"title\\": \\"absoluteSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/absoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\", \\"version\\": \\"withSlugs\\", @@ -1196,6 +1356,7 @@ Object { \\"title\\": \\"relativeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\", \\"version\\": \\"withSlugs\\", @@ -1210,6 +1371,7 @@ Object { \\"title\\": \\"resolvedSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\", \\"version\\": \\"withSlugs\\", @@ -1224,6 +1386,7 @@ Object { \\"title\\": \\"tryToEscapeSlug\\", \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md\\", + \\"sourceDirName\\": \\"slugs\\", \\"slug\\": \\"/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\", \\"version\\": \\"withSlugs\\", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index 0ffdeb3779..b1216944ea 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -177,6 +177,7 @@ describe('simple site', () => { version: 'current', id: 'foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/foo/bar', slug: '/foo/bar', @@ -192,6 +193,7 @@ describe('simple site', () => { version: 'current', id: 'hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/hello', slug: '/hello', @@ -220,6 +222,7 @@ describe('simple site', () => { version: 'current', id: 'hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: true, permalink: '/docs/', slug: '/', @@ -248,6 +251,7 @@ describe('simple site', () => { version: 'current', id: 'foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: true, permalink: '/docs/', slug: '/', @@ -279,6 +283,7 @@ describe('simple site', () => { version: 'current', id: 'foo/baz', unversionedId: 'foo/baz', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/foo/bazSlug.html', slug: '/foo/bazSlug.html', @@ -301,6 +306,7 @@ describe('simple site', () => { version: 'current', id: 'lorem', unversionedId: 'lorem', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/lorem', slug: '/lorem', @@ -336,6 +342,7 @@ describe('simple site', () => { version: 'current', id: 'foo/baz', unversionedId: 'foo/baz', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/foo/bazSlug.html', slug: '/foo/bazSlug.html', @@ -378,6 +385,7 @@ describe('simple site', () => { version: 'current', id: 'lorem', unversionedId: 'lorem', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/lorem', slug: '/lorem', @@ -549,6 +557,7 @@ describe('versioned site', () => { await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), { id: 'foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/next/foo/barSlug', slug: '/foo/barSlug', @@ -560,6 +569,7 @@ describe('versioned site', () => { await currentVersionTestUtils.testMeta(path.join('hello.md'), { id: 'hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/next/hello', slug: '/hello', @@ -576,6 +586,7 @@ describe('versioned site', () => { await version100TestUtils.testMeta(path.join('foo', 'bar.md'), { id: 'version-1.0.0/foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/1.0.0/foo/barSlug', slug: '/foo/barSlug', @@ -587,6 +598,7 @@ describe('versioned site', () => { await version100TestUtils.testMeta(path.join('hello.md'), { id: 'version-1.0.0/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/1.0.0/hello', slug: '/hello', @@ -600,6 +612,7 @@ describe('versioned site', () => { await version101TestUtils.testMeta(path.join('foo', 'bar.md'), { id: 'version-1.0.1/foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/foo/bar', slug: '/foo/bar', @@ -611,6 +624,7 @@ describe('versioned site', () => { await version101TestUtils.testMeta(path.join('hello.md'), { id: 'version-1.0.1/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/hello', slug: '/hello', @@ -701,6 +715,7 @@ describe('versioned site', () => { await testUtilsLocal.testMeta(path.join('hello.md'), { id: 'version-1.0.0/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/1.0.0/hello', slug: '/hello', @@ -741,6 +756,7 @@ describe('versioned site', () => { await testUtilsLocal.testMeta(path.join('hello.md'), { id: 'version-1.0.0/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/1.0.0/hello', slug: '/hello', @@ -773,6 +789,7 @@ describe('versioned site', () => { await testUtilsLocal.testMeta(path.join('hello.md'), { id: 'version-1.0.0/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/docs/1.0.0/hello', slug: '/hello', @@ -806,6 +823,7 @@ describe('versioned site', () => { await testUtilsLocal.testMeta(path.join('hello.md'), { id: 'version-1.0.0/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/fr/docs/1.0.0/hello', slug: '/hello', @@ -840,6 +858,7 @@ describe('versioned site', () => { await testUtilsLocal.testMeta(path.join('hello.md'), { id: 'version-1.0.0/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: false, permalink: '/fr/docs/1.0.0/hello', slug: '/hello', diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index d1ceaf4a1d..7c5bc8f94e 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -10,7 +10,7 @@ import path from 'path'; import {isMatch} from 'picomatch'; import commander from 'commander'; -import {kebabCase} from 'lodash'; +import {kebabCase, orderBy} from 'lodash'; import fs from 'fs-extra'; import pluginContentDocs from '../index'; @@ -24,7 +24,7 @@ import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; import * as cliDocs from '../cli'; import {OptionsSchema} from '../options'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; -import {DocMetadata, LoadedVersion} from '../types'; +import {DocMetadata, LoadedVersion, SidebarItemsGenerator} from '../types'; import {toSidebarsProp} from '../props'; // @ts-expect-error: TODO typedefs missing? @@ -33,6 +33,17 @@ import {validate} from 'webpack'; function findDocById(version: LoadedVersion, unversionedId: string) { return version.docs.find((item) => item.unversionedId === unversionedId); } +function getDocById(version: LoadedVersion, unversionedId: string) { + const doc = findDocById(version, unversionedId); + if (!doc) { + throw new Error( + `No doc found with id=${unversionedId} in version ${version.versionName}. +Available ids=\n- ${version.docs.map((d) => d.unversionedId).join('\n- ')}`, + ); + } + return doc; +} + const defaultDocMetadata: Partial = { next: undefined, previous: undefined, @@ -40,6 +51,7 @@ const defaultDocMetadata: Partial = { lastUpdatedAt: undefined, lastUpdatedBy: undefined, sidebar_label: undefined, + formattedLastUpdatedAt: undefined, }; const createFakeActions = (contentDir: string) => { @@ -203,6 +215,7 @@ describe('simple website', () => { "sidebars.json", "i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}", "docs/**/*.{md,mdx}", + "docs/**/_category_.{json,yml,yaml}", ] `); expect(isMatch('docs/hello.md', matchPattern)).toEqual(true); @@ -247,6 +260,7 @@ describe('simple website', () => { version: 'current', id: 'hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: true, permalink: '/docs/', slug: '/', @@ -268,11 +282,12 @@ describe('simple website', () => { }, }); - expect(findDocById(currentVersion, 'foo/bar')).toEqual({ + expect(getDocById(currentVersion, 'foo/bar')).toEqual({ ...defaultDocMetadata, version: 'current', id: 'foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: false, next: { title: 'baz', @@ -368,15 +383,19 @@ describe('versioned website', () => { "sidebars.json", "i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}", "docs/**/*.{md,mdx}", + "docs/**/_category_.{json,yml,yaml}", "versioned_sidebars/version-1.0.1-sidebars.json", "i18n/en/docusaurus-plugin-content-docs/version-1.0.1/**/*.{md,mdx}", "versioned_docs/version-1.0.1/**/*.{md,mdx}", + "versioned_docs/version-1.0.1/**/_category_.{json,yml,yaml}", "versioned_sidebars/version-1.0.0-sidebars.json", "i18n/en/docusaurus-plugin-content-docs/version-1.0.0/**/*.{md,mdx}", "versioned_docs/version-1.0.0/**/*.{md,mdx}", + "versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}", "versioned_sidebars/version-withSlugs-sidebars.json", "i18n/en/docusaurus-plugin-content-docs/version-withSlugs/**/*.{md,mdx}", "versioned_docs/version-withSlugs/**/*.{md,mdx}", + "versioned_docs/version-withSlugs/**/_category_.{json,yml,yaml}", ] `); expect(isMatch('docs/hello.md', matchPattern)).toEqual(true); @@ -427,10 +446,11 @@ describe('versioned website', () => { expect(findDocById(version101, 'foo/baz')).toBeUndefined(); expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined(); - expect(findDocById(currentVersion, 'foo/bar')).toEqual({ + expect(getDocById(currentVersion, 'foo/bar')).toEqual({ ...defaultDocMetadata, id: 'foo/bar', unversionedId: 'foo/bar', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/next/foo/barSlug', slug: '/foo/barSlug', @@ -452,10 +472,11 @@ describe('versioned website', () => { permalink: '/docs/next/', }, }); - expect(findDocById(currentVersion, 'hello')).toEqual({ + expect(getDocById(currentVersion, 'hello')).toEqual({ ...defaultDocMetadata, id: 'hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: true, permalink: '/docs/next/', slug: '/', @@ -474,10 +495,11 @@ describe('versioned website', () => { permalink: '/docs/next/foo/barSlug', }, }); - expect(findDocById(version101, 'hello')).toEqual({ + expect(getDocById(version101, 'hello')).toEqual({ ...defaultDocMetadata, id: 'version-1.0.1/hello', unversionedId: 'hello', + sourceDirName: '.', isDocsHomePage: true, permalink: '/docs/', slug: '/', @@ -496,10 +518,11 @@ describe('versioned website', () => { permalink: '/docs/foo/bar', }, }); - expect(findDocById(version100, 'foo/baz')).toEqual({ + expect(getDocById(version100, 'foo/baz')).toEqual({ ...defaultDocMetadata, id: 'version-1.0.0/foo/baz', unversionedId: 'foo/baz', + sourceDirName: 'foo', isDocsHomePage: false, permalink: '/docs/1.0.0/foo/baz', slug: '/foo/baz', @@ -611,9 +634,11 @@ describe('versioned website (community)', () => { "community_sidebars.json", "i18n/en/docusaurus-plugin-content-docs-community/current/**/*.{md,mdx}", "community/**/*.{md,mdx}", + "community/**/_category_.{json,yml,yaml}", "community_versioned_sidebars/version-1.0.0-sidebars.json", "i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0/**/*.{md,mdx}", "community_versioned_docs/version-1.0.0/**/*.{md,mdx}", + "community_versioned_docs/version-1.0.0/**/_category_.{json,yml,yaml}", ] `); expect(isMatch('community/team.md', matchPattern)).toEqual(true); @@ -644,10 +669,11 @@ describe('versioned website (community)', () => { expect(content.loadedVersions.length).toEqual(2); const [currentVersion, version100] = content.loadedVersions; - expect(findDocById(currentVersion, 'team')).toEqual({ + expect(getDocById(currentVersion, 'team')).toEqual({ ...defaultDocMetadata, id: 'team', unversionedId: 'team', + sourceDirName: '.', isDocsHomePage: false, permalink: '/community/next/team', slug: '/team', @@ -659,10 +685,11 @@ describe('versioned website (community)', () => { sidebar: 'community', frontMatter: {title: 'Team title translated'}, }); - expect(findDocById(version100, 'team')).toEqual({ + expect(getDocById(version100, 'team')).toEqual({ ...defaultDocMetadata, id: 'version-1.0.0/team', unversionedId: 'team', + sourceDirName: '.', isDocsHomePage: false, permalink: '/community/team', slug: '/team', @@ -709,7 +736,7 @@ describe('site with doc label', () => { }), ); - const content = await plugin.loadContent(); + const content = (await plugin.loadContent?.())!; return {content}; } @@ -730,3 +757,807 @@ describe('site with doc label', () => { expect(sidebarProps.docs[1].label).toBe('Hello 2 From Doc'); }); }); + +describe('site with full autogenerated sidebar', () => { + async function loadSite() { + const siteDir = path.join( + __dirname, + '__fixtures__', + 'site-with-autogenerated-sidebar', + ); + const context = await loadContext(siteDir); + const plugin = pluginContentDocs( + context, + normalizePluginOptions(OptionsSchema, { + path: 'docs', + }), + ); + + const content = (await plugin.loadContent?.())!; + + return {content, siteDir}; + } + + test('sidebar is fully autogenerated', async () => { + const {content} = await loadSite(); + const version = content.loadedVersions[0]; + + expect(version.sidebars).toEqual({ + defaultSidebar: [ + { + type: 'doc', + id: 'getting-started', + }, + { + type: 'doc', + id: 'installation', + }, + { + type: 'category', + label: 'Guides', + collapsed: true, + items: [ + { + type: 'doc', + id: 'Guides/guide1', + }, + { + type: 'doc', + id: 'Guides/guide2', + }, + { + type: 'doc', + id: 'Guides/guide2.5', + }, + { + type: 'doc', + id: 'Guides/guide3', + }, + { + type: 'doc', + id: 'Guides/guide4', + }, + { + type: 'doc', + id: 'Guides/guide5', + }, + ], + }, + { + type: 'category', + label: 'API (label from _category_.json)', + collapsed: true, + items: [ + { + type: 'doc', + id: 'API/api-overview', + }, + { + type: 'category', + label: 'Core APIs', + collapsed: true, + items: [ + { + type: 'doc', + + id: 'API/Core APIs/Client API', + }, + { + type: 'doc', + id: 'API/Core APIs/Server API', + }, + ], + }, + { + type: 'category', + label: 'Extension APIs (label from _category_.yml)', + collapsed: true, + items: [ + { + type: 'doc', + id: 'API/Extension APIs/Plugin API', + }, + { + type: 'doc', + id: 'API/Extension APIs/Theme API', + }, + ], + }, + { + type: 'doc', + id: 'API/api-end', + }, + ], + }, + ], + }); + }); + + test('docs in fully generated sidebar have correct metadatas', async () => { + const {content, siteDir} = await loadSite(); + const version = content.loadedVersions[0]; + + expect(getDocById(version, 'getting-started')).toEqual({ + ...defaultDocMetadata, + id: 'getting-started', + unversionedId: 'getting-started', + sourceDirName: '.', + isDocsHomePage: false, + permalink: '/docs/getting-started', + slug: '/getting-started', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '0-getting-started.md', + ), + title: 'Getting Started', + description: 'Getting started text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 0, + previous: undefined, + next: { + permalink: '/docs/installation', + title: 'Installation', + }, + }); + + expect(getDocById(version, 'installation')).toEqual({ + ...defaultDocMetadata, + id: 'installation', + unversionedId: 'installation', + sourceDirName: '.', + isDocsHomePage: false, + permalink: '/docs/installation', + slug: '/installation', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '1-installation.md', + ), + title: 'Installation', + description: 'Installation text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 1, + previous: { + permalink: '/docs/getting-started', + title: 'Getting Started', + }, + next: { + permalink: '/docs/Guides/guide1', + title: 'Guide 1', + }, + }); + + expect(getDocById(version, 'Guides/guide1')).toEqual({ + ...defaultDocMetadata, + id: 'Guides/guide1', + unversionedId: 'Guides/guide1', + sourceDirName: 'Guides', + isDocsHomePage: false, + permalink: '/docs/Guides/guide1', + slug: '/Guides/guide1', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + 'Guides', + 'z-guide1.md', + ), + title: 'Guide 1', + description: 'Guide 1 text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: { + id: 'guide1', + sidebar_position: 1, + }, + sidebarPosition: 1, + previous: { + permalink: '/docs/installation', + title: 'Installation', + }, + next: { + permalink: '/docs/Guides/guide2', + title: 'Guide 2', + }, + }); + + expect(getDocById(version, 'Guides/guide2')).toEqual({ + ...defaultDocMetadata, + id: 'Guides/guide2', + unversionedId: 'Guides/guide2', + sourceDirName: 'Guides', + isDocsHomePage: false, + permalink: '/docs/Guides/guide2', + slug: '/Guides/guide2', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + 'Guides', + '02-guide2.md', + ), + title: 'Guide 2', + description: 'Guide 2 text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: { + id: 'guide2', + }, + sidebarPosition: 2, + previous: { + permalink: '/docs/Guides/guide1', + title: 'Guide 1', + }, + next: { + permalink: '/docs/Guides/guide2.5', + title: 'Guide 2.5', + }, + }); + + expect(getDocById(version, 'Guides/guide2.5')).toEqual({ + ...defaultDocMetadata, + id: 'Guides/guide2.5', + unversionedId: 'Guides/guide2.5', + sourceDirName: 'Guides', + isDocsHomePage: false, + permalink: '/docs/Guides/guide2.5', + slug: '/Guides/guide2.5', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + 'Guides', + '0-guide2.5.md', + ), + title: 'Guide 2.5', + description: 'Guide 2.5 text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: { + id: 'guide2.5', + sidebar_position: 2.5, + }, + sidebarPosition: 2.5, + previous: { + permalink: '/docs/Guides/guide2', + title: 'Guide 2', + }, + next: { + permalink: '/docs/Guides/guide3', + title: 'Guide 3', + }, + }); + + expect(getDocById(version, 'Guides/guide3')).toEqual({ + ...defaultDocMetadata, + id: 'Guides/guide3', + unversionedId: 'Guides/guide3', + sourceDirName: 'Guides', + isDocsHomePage: false, + permalink: '/docs/Guides/guide3', + slug: '/Guides/guide3', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + 'Guides', + 'guide3.md', + ), + title: 'Guide 3', + description: 'Guide 3 text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: { + id: 'guide3', + sidebar_position: 3, + }, + sidebarPosition: 3, + previous: { + permalink: '/docs/Guides/guide2.5', + title: 'Guide 2.5', + }, + next: { + permalink: '/docs/Guides/guide4', + title: 'Guide 4', + }, + }); + + expect(getDocById(version, 'Guides/guide4')).toEqual({ + ...defaultDocMetadata, + id: 'Guides/guide4', + unversionedId: 'Guides/guide4', + sourceDirName: 'Guides', + isDocsHomePage: false, + permalink: '/docs/Guides/guide4', + slug: '/Guides/guide4', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + 'Guides', + 'a-guide4.md', + ), + title: 'Guide 4', + description: 'Guide 4 text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: { + id: 'guide4', + }, + sidebarPosition: undefined, + previous: { + permalink: '/docs/Guides/guide3', + title: 'Guide 3', + }, + next: { + permalink: '/docs/Guides/guide5', + title: 'Guide 5', + }, + }); + + expect(getDocById(version, 'Guides/guide5')).toEqual({ + ...defaultDocMetadata, + id: 'Guides/guide5', + unversionedId: 'Guides/guide5', + sourceDirName: 'Guides', + isDocsHomePage: false, + permalink: '/docs/Guides/guide5', + slug: '/Guides/guide5', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + 'Guides', + 'b-guide5.md', + ), + title: 'Guide 5', + description: 'Guide 5 text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: { + id: 'guide5', + }, + sidebarPosition: undefined, + previous: { + permalink: '/docs/Guides/guide4', + title: 'Guide 4', + }, + next: { + permalink: '/docs/API/api-overview', + title: 'API Overview', + }, + }); + + expect(getDocById(version, 'API/api-overview')).toEqual({ + ...defaultDocMetadata, + id: 'API/api-overview', + unversionedId: 'API/api-overview', + sourceDirName: '3-API', + isDocsHomePage: false, + permalink: '/docs/API/api-overview', + slug: '/API/api-overview', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '00_api-overview.md', + ), + title: 'API Overview', + description: 'API Overview text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 0, + previous: { + permalink: '/docs/Guides/guide5', + title: 'Guide 5', + }, + next: { + permalink: '/docs/API/Core APIs/Client API', + title: 'Client API', + }, + }); + + expect(getDocById(version, 'API/Core APIs/Client API')).toEqual({ + ...defaultDocMetadata, + id: 'API/Core APIs/Client API', + unversionedId: 'API/Core APIs/Client API', + sourceDirName: '3-API/01_Core APIs', + isDocsHomePage: false, + permalink: '/docs/API/Core APIs/Client API', + slug: '/API/Core APIs/Client API', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '01_Core APIs', + '0 --- Client API.md', + ), + title: 'Client API', + description: 'Client API text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 0, + previous: { + permalink: '/docs/API/api-overview', + title: 'API Overview', + }, + next: { + permalink: '/docs/API/Core APIs/Server API', + title: 'Server API', + }, + }); + + expect(getDocById(version, 'API/Core APIs/Server API')).toEqual({ + ...defaultDocMetadata, + id: 'API/Core APIs/Server API', + unversionedId: 'API/Core APIs/Server API', + sourceDirName: '3-API/01_Core APIs', + isDocsHomePage: false, + permalink: '/docs/API/Core APIs/Server API', + slug: '/API/Core APIs/Server API', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '01_Core APIs', + '1 --- Server API.md', + ), + title: 'Server API', + description: 'Server API text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 1, + previous: { + permalink: '/docs/API/Core APIs/Client API', + title: 'Client API', + }, + next: { + permalink: '/docs/API/Extension APIs/Plugin API', + title: 'Plugin API', + }, + }); + + expect(getDocById(version, 'API/Extension APIs/Plugin API')).toEqual({ + ...defaultDocMetadata, + id: 'API/Extension APIs/Plugin API', + unversionedId: 'API/Extension APIs/Plugin API', + sourceDirName: '3-API/02_Extension APIs', + isDocsHomePage: false, + permalink: '/docs/API/Extension APIs/Plugin API', + slug: '/API/Extension APIs/Plugin API', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '02_Extension APIs', + '0. Plugin API.md', + ), + title: 'Plugin API', + description: 'Plugin API text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 0, + previous: { + permalink: '/docs/API/Core APIs/Server API', + title: 'Server API', + }, + next: { + permalink: '/docs/API/Extension APIs/Theme API', + title: 'Theme API', + }, + }); + + expect(getDocById(version, 'API/Extension APIs/Theme API')).toEqual({ + ...defaultDocMetadata, + id: 'API/Extension APIs/Theme API', + unversionedId: 'API/Extension APIs/Theme API', + sourceDirName: '3-API/02_Extension APIs', + isDocsHomePage: false, + permalink: '/docs/API/Extension APIs/Theme API', + slug: '/API/Extension APIs/Theme API', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '02_Extension APIs', + '1. Theme API.md', + ), + title: 'Theme API', + description: 'Theme API text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 1, + previous: { + permalink: '/docs/API/Extension APIs/Plugin API', + title: 'Plugin API', + }, + next: { + permalink: '/docs/API/api-end', + title: 'API End', + }, + }); + + expect(getDocById(version, 'API/api-end')).toEqual({ + ...defaultDocMetadata, + id: 'API/api-end', + unversionedId: 'API/api-end', + sourceDirName: '3-API', + isDocsHomePage: false, + permalink: '/docs/API/api-end', + slug: '/API/api-end', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '03_api-end.md', + ), + title: 'API End', + description: 'API End text', + version: 'current', + sidebar: 'defaultSidebar', + frontMatter: {}, + sidebarPosition: 3, + previous: { + permalink: '/docs/API/Extension APIs/Theme API', + title: 'Theme API', + }, + next: undefined, + }); + }); +}); + +describe('site with partial autogenerated sidebars', () => { + async function loadSite() { + const siteDir = path.join( + __dirname, + '__fixtures__', + 'site-with-autogenerated-sidebar', + ); + const context = await loadContext(siteDir, {}); + const plugin = pluginContentDocs( + context, + normalizePluginOptions(OptionsSchema, { + path: 'docs', + sidebarPath: path.join( + __dirname, + '__fixtures__', + 'site-with-autogenerated-sidebar', + 'partialAutogeneratedSidebars.js', + ), + }), + ); + + const content = (await plugin.loadContent?.())!; + + return {content, siteDir}; + } + + test('sidebar is partially autogenerated', async () => { + const {content} = await loadSite(); + const version = content.loadedVersions[0]; + + expect(version.sidebars).toEqual({ + someSidebar: [ + { + type: 'doc', + id: 'API/api-end', + }, + { + type: 'category', + label: 'Some category', + collapsed: true, + items: [ + { + type: 'doc', + id: 'API/api-overview', + }, + { + type: 'doc', + id: 'API/Extension APIs/Plugin API', + }, + { + type: 'doc', + id: 'API/Extension APIs/Theme API', + }, + ], + }, + ], + }); + }); + + test('docs in partially generated sidebar have correct metadatas', async () => { + const {content, siteDir} = await loadSite(); + const version = content.loadedVersions[0]; + + // Only looking at the docs of the autogen sidebar, others metadatas should not be affected + + expect(getDocById(version, 'API/api-end')).toEqual({ + ...defaultDocMetadata, + id: 'API/api-end', + unversionedId: 'API/api-end', + sourceDirName: '3-API', + isDocsHomePage: false, + permalink: '/docs/API/api-end', + slug: '/API/api-end', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '03_api-end.md', + ), + title: 'API End', + description: 'API End text', + version: 'current', + sidebar: 'someSidebar', + frontMatter: {}, + sidebarPosition: 3, // ignored (not part of the autogenerated sidebar slice) + previous: undefined, + next: { + permalink: '/docs/API/api-overview', + title: 'API Overview', + }, + }); + + expect(getDocById(version, 'API/api-overview')).toEqual({ + ...defaultDocMetadata, + id: 'API/api-overview', + unversionedId: 'API/api-overview', + sourceDirName: '3-API', + isDocsHomePage: false, + permalink: '/docs/API/api-overview', + slug: '/API/api-overview', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '00_api-overview.md', + ), + title: 'API Overview', + description: 'API Overview text', + version: 'current', + sidebar: 'someSidebar', + frontMatter: {}, + sidebarPosition: 0, // ignored (not part of the autogenerated sidebar slice) + previous: { + permalink: '/docs/API/api-end', + title: 'API End', + }, + next: { + permalink: '/docs/API/Extension APIs/Plugin API', + title: 'Plugin API', + }, + }); + + expect(getDocById(version, 'API/Extension APIs/Plugin API')).toEqual({ + ...defaultDocMetadata, + id: 'API/Extension APIs/Plugin API', + unversionedId: 'API/Extension APIs/Plugin API', + sourceDirName: '3-API/02_Extension APIs', + isDocsHomePage: false, + permalink: '/docs/API/Extension APIs/Plugin API', + slug: '/API/Extension APIs/Plugin API', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '02_Extension APIs', + '0. Plugin API.md', + ), + title: 'Plugin API', + description: 'Plugin API text', + version: 'current', + sidebar: 'someSidebar', + frontMatter: {}, + sidebarPosition: 0, + previous: { + permalink: '/docs/API/api-overview', + title: 'API Overview', + }, + next: { + permalink: '/docs/API/Extension APIs/Theme API', + title: 'Theme API', + }, + }); + + expect(getDocById(version, 'API/Extension APIs/Theme API')).toEqual({ + ...defaultDocMetadata, + id: 'API/Extension APIs/Theme API', + unversionedId: 'API/Extension APIs/Theme API', + sourceDirName: '3-API/02_Extension APIs', + isDocsHomePage: false, + permalink: '/docs/API/Extension APIs/Theme API', + slug: '/API/Extension APIs/Theme API', + source: path.posix.join( + '@site', + posixPath(path.relative(siteDir, version.contentPath)), + '3-API', + '02_Extension APIs', + '1. Theme API.md', + ), + title: 'Theme API', + description: 'Theme API text', + version: 'current', + sidebar: 'someSidebar', + frontMatter: {}, + sidebarPosition: 1, + previous: { + permalink: '/docs/API/Extension APIs/Plugin API', + title: 'Plugin API', + }, + next: undefined, + }); + }); +}); + +describe('site with custom sidebar items generator', () => { + async function loadSite(sidebarItemsGenerator: SidebarItemsGenerator) { + const siteDir = path.join( + __dirname, + '__fixtures__', + 'site-with-autogenerated-sidebar', + ); + const context = await loadContext(siteDir); + const plugin = pluginContentDocs( + context, + normalizePluginOptions(OptionsSchema, { + path: 'docs', + sidebarItemsGenerator, + }), + ); + const content = (await plugin.loadContent?.())!; + return {content, siteDir}; + } + + test('sidebar is autogenerated according to custom sidebarItemsGenerator', async () => { + const customSidebarItemsGenerator: SidebarItemsGenerator = async () => { + return [ + {type: 'doc', id: 'API/api-overview'}, + {type: 'doc', id: 'API/api-end'}, + ]; + }; + + const customSidebarItemsGeneratorMock: SidebarItemsGenerator = jest.fn( + customSidebarItemsGenerator, + ); + + const {content} = await loadSite(customSidebarItemsGeneratorMock); + const version = content.loadedVersions[0]; + + expect(version.sidebars).toEqual({ + defaultSidebar: [ + {type: 'doc', id: 'API/api-overview'}, + {type: 'doc', id: 'API/api-end'}, + ], + }); + }); + + test('sidebarItemsGenerator is called with appropriate data', async () => { + type GeneratorArg = Parameters[0]; + + const customSidebarItemsGeneratorMock = jest.fn( + async (_arg: GeneratorArg) => [], + ); + const {siteDir} = await loadSite(customSidebarItemsGeneratorMock); + + const generatorArg: GeneratorArg = + customSidebarItemsGeneratorMock.mock.calls[0][0]; + + // Make test pass even if docs are in different order and paths are absolutes + function makeDeterministic(arg: GeneratorArg): GeneratorArg { + return { + ...arg, + docs: orderBy(arg.docs, 'id'), + version: { + ...arg.version, + contentPath: path.relative(siteDir, arg.version.contentPath), + }, + }; + } + + expect(makeDeterministic(generatorArg)).toMatchSnapshot(); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts new file mode 100644 index 0000000000..af116cb1db --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/numberPrefix.test.ts @@ -0,0 +1,115 @@ +/** + * 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 { + extractNumberPrefix, + stripNumberPrefix, + stripPathNumberPrefixes, +} from '../numberPrefix'; + +const BadNumberPrefixPatterns = [ + 'a1-My Doc', + 'My Doc-000', + '00abc01-My Doc', + 'My 001- Doc', + 'My -001 Doc', +]; + +describe('stripNumberPrefix', () => { + test('should strip number prefix if present', () => { + expect(stripNumberPrefix('1-My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('01-My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001-My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 - My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('999 - My Doc')).toEqual('My Doc'); + // + expect(stripNumberPrefix('1---My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('01---My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001---My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 --- My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('999 --- My Doc')).toEqual('My Doc'); + // + expect(stripNumberPrefix('1___My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('01___My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001___My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 ___ My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('999 ___ My Doc')).toEqual('My Doc'); + // + expect(stripNumberPrefix('1.My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('01.My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001.My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('001 . My Doc')).toEqual('My Doc'); + expect(stripNumberPrefix('999 . My Doc')).toEqual('My Doc'); + }); + + test('should not strip number prefix if pattern does not match', () => { + BadNumberPrefixPatterns.forEach((badPattern) => { + expect(stripNumberPrefix(badPattern)).toEqual(badPattern); + }); + }); +}); + +describe('stripPathNumberPrefix', () => { + test('should strip number prefixes in paths', () => { + expect( + stripPathNumberPrefixes( + '0-MyRootFolder0/1 - MySubFolder1/2. MyDeepFolder2/3 _MyDoc3', + ), + ).toEqual('MyRootFolder0/MySubFolder1/MyDeepFolder2/MyDoc3'); + }); +}); + +describe('extractNumberPrefix', () => { + test('should extract number prefix if present', () => { + expect(extractNumberPrefix('0-My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 0, + }); + expect(extractNumberPrefix('1-My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 1, + }); + expect(extractNumberPrefix('01-My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 1, + }); + expect(extractNumberPrefix('001-My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 1, + }); + expect(extractNumberPrefix('001 - My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 1, + }); + expect(extractNumberPrefix('001 - My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 1, + }); + expect(extractNumberPrefix('999 - My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 999, + }); + + expect(extractNumberPrefix('0046036 - My Doc')).toEqual({ + filename: 'My Doc', + numberPrefix: 46036, + }); + }); + + test('should not extract number prefix if pattern does not match', () => { + BadNumberPrefixPatterns.forEach((badPattern) => { + expect(extractNumberPrefix(badPattern)).toEqual({ + filename: badPattern, + numberPrefix: undefined, + }); + }); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts index fd1c61ddf0..2f9e0ec2f1 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts @@ -7,6 +7,7 @@ import {OptionsSchema, DEFAULT_OPTIONS} from '../options'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; +import {DefaultSidebarItemsGenerator} from '../sidebarItemsGenerator'; // the type of remark/rehype plugins is function const markdownPluginsFunctionStub = () => {}; @@ -26,6 +27,7 @@ describe('normalizeDocsPluginOptions', () => { homePageId: 'home', // Document id for docs home page. include: ['**/*.{md,mdx}'], // Extensions to include. sidebarPath: 'my-sidebar', // Path to sidebar configuration for showing a list of markdown pages. + sidebarItemsGenerator: DefaultSidebarItemsGenerator, docLayoutComponent: '@theme/DocPage', docItemComponent: '@theme/DocItem', remarkPlugins: [markdownPluginsObjectStub], diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts new file mode 100644 index 0000000000..38b8818cd5 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebarItemsGenerator.test.ts @@ -0,0 +1,268 @@ +/** + * 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 { + CategoryMetadatasFile, + DefaultSidebarItemsGenerator, +} from '../sidebarItemsGenerator'; +import {DefaultCategoryCollapsedValue} from '../sidebars'; +import {Sidebar, SidebarItemsGenerator} from '../types'; +import fs from 'fs-extra'; + +describe('DefaultSidebarItemsGenerator', () => { + function testDefaultSidebarItemsGenerator( + options: Partial[0]>, + ) { + return DefaultSidebarItemsGenerator({ + item: { + type: 'autogenerated', + dirName: '.', + }, + version: { + versionName: 'current', + contentPath: 'docs', + }, + docs: [], + ...options, + }); + } + + function mockCategoryMetadataFiles( + categoryMetadataFiles: Record>, + ) { + jest.spyOn(fs, 'pathExists').mockImplementation((metadataFilePath) => { + return typeof categoryMetadataFiles[metadataFilePath] !== 'undefined'; + }); + jest.spyOn(fs, 'readFile').mockImplementation( + // @ts-expect-error: annoying TS error due to overrides + async (metadataFilePath: string) => { + return JSON.stringify(categoryMetadataFiles[metadataFilePath]); + }, + ); + } + + test('generates empty sidebar slice when no docs and emit a warning', async () => { + const consoleWarn = jest.spyOn(console, 'warn'); + const sidebarSlice = await testDefaultSidebarItemsGenerator({ + docs: [], + }); + expect(sidebarSlice).toEqual([]); + expect(consoleWarn).toHaveBeenCalledWith( + expect.stringMatching( + /No docs found in dir .: can't auto-generate a sidebar/, + ), + ); + }); + + test('generates simple flat sidebar', async () => { + const sidebarSlice = await DefaultSidebarItemsGenerator({ + item: { + type: 'autogenerated', + dirName: '.', + }, + version: { + versionName: 'current', + contentPath: '', + }, + docs: [ + { + id: 'doc1', + source: 'doc1.md', + sourceDirName: '.', + sidebarPosition: 2, + frontMatter: { + sidebar_label: 'doc1 sidebar label', + }, + }, + { + id: 'doc2', + source: 'doc2.md', + sourceDirName: '.', + sidebarPosition: 3, + frontMatter: {}, + }, + { + id: 'doc3', + source: 'doc3.md', + sourceDirName: '.', + sidebarPosition: 1, + frontMatter: {}, + }, + { + id: 'doc4', + source: 'doc4.md', + sourceDirName: '.', + sidebarPosition: 1.5, + frontMatter: {}, + }, + { + id: 'doc5', + source: 'doc5.md', + sourceDirName: '.', + sidebarPosition: undefined, + frontMatter: {}, + }, + ], + }); + + expect(sidebarSlice).toEqual([ + {type: 'doc', id: 'doc3'}, + {type: 'doc', id: 'doc4'}, + {type: 'doc', id: 'doc1', label: 'doc1 sidebar label'}, + {type: 'doc', id: 'doc2'}, + {type: 'doc', id: 'doc5'}, + ] as Sidebar); + }); + + test('generates complex nested sidebar', async () => { + mockCategoryMetadataFiles({ + '02-Guides/_category_.json': {collapsed: false}, + '02-Guides/01-SubGuides/_category_.yml': { + label: 'SubGuides (metadata file label)', + }, + }); + + const sidebarSlice = await DefaultSidebarItemsGenerator({ + item: { + type: 'autogenerated', + dirName: '.', + }, + version: { + versionName: 'current', + contentPath: '', + }, + docs: [ + { + id: 'intro', + source: 'intro.md', + sourceDirName: '.', + sidebarPosition: 1, + frontMatter: {}, + }, + { + id: 'tutorial2', + source: 'tutorial2.md', + sourceDirName: '01-Tutorials', + sidebarPosition: 2, + frontMatter: {}, + }, + { + id: 'tutorial1', + source: 'tutorial1.md', + sourceDirName: '01-Tutorials', + sidebarPosition: 1, + frontMatter: {}, + }, + { + id: 'guide2', + source: 'guide2.md', + sourceDirName: '02-Guides', + sidebarPosition: 2, + frontMatter: {}, + }, + { + id: 'guide1', + source: 'guide1.md', + sourceDirName: '02-Guides', + sidebarPosition: 1, + frontMatter: {}, + }, + { + id: 'nested-guide', + source: 'nested-guide.md', + sourceDirName: '02-Guides/01-SubGuides', + sidebarPosition: undefined, + frontMatter: {}, + }, + { + id: 'end', + source: 'end.md', + sourceDirName: '.', + sidebarPosition: 3, + frontMatter: {}, + }, + ], + }); + + expect(sidebarSlice).toEqual([ + {type: 'doc', id: 'intro'}, + { + type: 'category', + label: 'Tutorials', + collapsed: DefaultCategoryCollapsedValue, + items: [ + {type: 'doc', id: 'tutorial1'}, + {type: 'doc', id: 'tutorial2'}, + ], + }, + { + type: 'category', + label: 'Guides', + collapsed: false, + items: [ + {type: 'doc', id: 'guide1'}, + { + type: 'category', + label: 'SubGuides (metadata file label)', + collapsed: DefaultCategoryCollapsedValue, + items: [{type: 'doc', id: 'nested-guide'}], + }, + {type: 'doc', id: 'guide2'}, + ], + }, + {type: 'doc', id: 'end'}, + ] as Sidebar); + }); + + test('generates subfolder sidebar', async () => { + const sidebarSlice = await DefaultSidebarItemsGenerator({ + item: { + type: 'autogenerated', + dirName: 'subfolder/subsubfolder', + }, + version: { + versionName: 'current', + contentPath: '', + }, + docs: [ + { + id: 'doc1', + source: 'doc1.md', + sourceDirName: 'subfolder/subsubfolder', + sidebarPosition: undefined, + frontMatter: {}, + }, + { + id: 'doc2', + source: 'doc2.md', + sourceDirName: 'subfolder', + sidebarPosition: undefined, + frontMatter: {}, + }, + { + id: 'doc3', + source: 'doc3.md', + sourceDirName: '.', + sidebarPosition: undefined, + frontMatter: {}, + }, + { + id: 'doc4', + source: 'doc4.md', + sourceDirName: 'subfolder/subsubfolder', + sidebarPosition: undefined, + frontMatter: {}, + }, + ], + }); + + expect(sidebarSlice).toEqual([ + {type: 'doc', id: 'doc1'}, + {type: 'doc', id: 'doc4'}, + ] as Sidebar); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts index 17b6f06d49..4e717cd10f 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts @@ -14,8 +14,16 @@ import { collectSidebarCategories, collectSidebarLinks, transformSidebarItems, + DefaultSidebars, + processSidebars, } from '../sidebars'; -import {Sidebar, Sidebars} from '../types'; +import { + Sidebar, + SidebarItem, + SidebarItemsGenerator, + Sidebars, + UnprocessedSidebars, +} from '../types'; /* eslint-disable global-require, import/no-dynamic-require */ @@ -124,7 +132,7 @@ describe('loadSidebars', () => { ); */ // See https://github.com/facebook/docusaurus/issues/3366 - expect(loadSidebars('badpath')).toEqual({}); + expect(loadSidebars('badpath')).toEqual(DefaultSidebars); }); test('undefined path', () => { @@ -443,6 +451,131 @@ describe('transformSidebarItems', () => { }); }); +describe('processSidebars', () => { + const StaticGeneratedSidebarSlice: SidebarItem[] = [ + {type: 'doc', id: 'doc-generated-id-1'}, + {type: 'doc', id: 'doc-generated-id-2'}, + ]; + + const StaticSidebarItemsGenerator: SidebarItemsGenerator = jest.fn( + async () => { + return StaticGeneratedSidebarSlice; + }, + ); + + async function testProcessSidebars(unprocessedSidebars: UnprocessedSidebars) { + return processSidebars({ + sidebarItemsGenerator: StaticSidebarItemsGenerator, + unprocessedSidebars, + docs: [], + // @ts-expect-error: useless for this test + version: {}, + }); + } + + test('let sidebars without autogenerated items untouched', async () => { + const unprocessedSidebars: UnprocessedSidebars = { + someSidebar: [ + {type: 'doc', id: 'doc1'}, + { + type: 'category', + collapsed: false, + items: [{type: 'doc', id: 'doc2'}], + label: 'Category', + }, + {type: 'link', href: 'https://facebook.com', label: 'FB'}, + ], + secondSidebar: [ + {type: 'doc', id: 'doc3'}, + {type: 'link', href: 'https://instagram.com', label: 'IG'}, + { + type: 'category', + collapsed: false, + items: [{type: 'doc', id: 'doc4'}], + label: 'Category', + }, + ], + }; + + const processedSidebar = await testProcessSidebars(unprocessedSidebars); + expect(processedSidebar).toEqual(unprocessedSidebars); + }); + + test('replace autogenerated items by generated sidebars slices', async () => { + const unprocessedSidebars: UnprocessedSidebars = { + someSidebar: [ + {type: 'doc', id: 'doc1'}, + { + type: 'category', + collapsed: false, + items: [ + {type: 'doc', id: 'doc2'}, + {type: 'autogenerated', dirName: 'dir1'}, + ], + label: 'Category', + }, + {type: 'link', href: 'https://facebook.com', label: 'FB'}, + ], + secondSidebar: [ + {type: 'doc', id: 'doc3'}, + {type: 'autogenerated', dirName: 'dir2'}, + {type: 'link', href: 'https://instagram.com', label: 'IG'}, + {type: 'autogenerated', dirName: 'dir3'}, + { + type: 'category', + collapsed: false, + items: [{type: 'doc', id: 'doc4'}], + label: 'Category', + }, + ], + }; + + const processedSidebar = await testProcessSidebars(unprocessedSidebars); + + expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3); + expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + item: {type: 'autogenerated', dirName: 'dir1'}, + docs: [], + version: {}, + }); + expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + item: {type: 'autogenerated', dirName: 'dir2'}, + docs: [], + version: {}, + }); + expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ + item: {type: 'autogenerated', dirName: 'dir3'}, + docs: [], + version: {}, + }); + + expect(processedSidebar).toEqual({ + someSidebar: [ + {type: 'doc', id: 'doc1'}, + { + type: 'category', + collapsed: false, + items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice], + label: 'Category', + }, + {type: 'link', href: 'https://facebook.com', label: 'FB'}, + ], + secondSidebar: [ + {type: 'doc', id: 'doc3'}, + ...StaticGeneratedSidebarSlice, + {type: 'link', href: 'https://instagram.com', label: 'IG'}, + ...StaticGeneratedSidebarSlice, + { + type: 'category', + collapsed: false, + items: [{type: 'doc', id: 'doc4'}], + label: 'Category', + }, + ], + } as Sidebars); + }); +}); + describe('createSidebarsUtils', () => { const sidebar1: Sidebar = [ { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts index 9e31cf3ad5..a49486d87d 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts @@ -15,6 +15,23 @@ describe('getSlug', () => { ); }); + test('can strip dir number prefixes', () => { + expect( + getSlug({ + baseID: 'doc', + dirName: '/001-dir1/002-dir2', + stripDirNumberPrefixes: true, + }), + ).toEqual('/dir1/dir2/doc'); + expect( + getSlug({ + baseID: 'doc', + dirName: '/001-dir1/002-dir2', + stripDirNumberPrefixes: false, + }), + ).toEqual('/001-dir1/002-dir2/doc'); + }); + // See https://github.com/facebook/docusaurus/issues/3223 test('should handle special chars in doc path', () => { expect( diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts index 8e17e6d8ad..66988167c4 100644 --- a/packages/docusaurus-plugin-content-docs/src/cli.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -12,7 +12,11 @@ import { } from './versions'; import fs from 'fs-extra'; import path from 'path'; -import {Sidebars, PathOptions, SidebarItem} from './types'; +import { + PathOptions, + UnprocessedSidebarItem, + UnprocessedSidebars, +} from './types'; import {loadSidebars} from './sidebars'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; @@ -90,10 +94,14 @@ export function cliDocsVersionCommand( // Load current sidebar and create a new versioned sidebars file. if (fs.existsSync(sidebarPath)) { - const loadedSidebars: Sidebars = loadSidebars(sidebarPath); + const loadedSidebars = loadSidebars(sidebarPath); + // TODO @slorber: this "version prefix" in versioned sidebars looks like a bad idea to me + // TODO try to get rid of it // Transform id in original sidebar to versioned id. - const normalizeItem = (item: SidebarItem): SidebarItem => { + const normalizeItem = ( + item: UnprocessedSidebarItem, + ): UnprocessedSidebarItem => { switch (item.type) { case 'category': return {...item, items: item.items.map(normalizeItem)}; @@ -108,14 +116,13 @@ export function cliDocsVersionCommand( } }; - const versionedSidebar: Sidebars = Object.entries(loadedSidebars).reduce( - (acc: Sidebars, [sidebarId, sidebarItems]) => { - const newVersionedSidebarId = `version-${version}/${sidebarId}`; - acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem); - return acc; - }, - {}, - ); + const versionedSidebar: UnprocessedSidebars = Object.entries( + loadedSidebars, + ).reduce((acc: UnprocessedSidebars, [sidebarId, sidebarItems]) => { + const newVersionedSidebarId = `version-${version}/${sidebarId}`; + acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem); + return acc; + }, {}); const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId); const newSidebarFile = path.join( diff --git a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts index b235bb779e..6ae0fe77b1 100644 --- a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts @@ -14,7 +14,9 @@ type DocFrontMatter = { description?: string; slug?: string; sidebar_label?: string; + sidebar_position?: number; custom_edit_url?: string; + strip_number_prefixes?: boolean; }; const DocFrontMatterSchema = Joi.object({ @@ -23,7 +25,9 @@ const DocFrontMatterSchema = Joi.object({ description: Joi.string(), slug: Joi.string(), sidebar_label: Joi.string(), + sidebar_position: Joi.number(), custom_edit_url: Joi.string().allow(null), + strip_number_prefixes: Joi.boolean(), }).unknown(); export function assertDocFrontMatter( diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 632113c60d..7cdfc1c724 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -30,6 +30,7 @@ import getSlug from './slug'; import {CURRENT_VERSION_NAME} from './constants'; import globby from 'globby'; import {getDocsDirPaths} from './versions'; +import {extractNumberPrefix, stripPathNumberPrefixes} from './numberPrefix'; import {assertDocFrontMatter} from './docFrontMatter'; type LastUpdateOptions = Pick< @@ -121,37 +122,66 @@ export function processDocMetadata({ }); assertDocFrontMatter(frontMatter); - // ex: api/myDoc -> api - // ex: myDoc -> . - const docsFileDirName = path.dirname(source); - const { sidebar_label: sidebarLabel, custom_edit_url: customEditURL, + + // Strip number prefixes by default (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc) by default, + // but ability to disable this behavior with frontmatterr + strip_number_prefixes: stripNumberPrefixes = true, } = frontMatter; - const baseID: string = - frontMatter.id || path.basename(source, path.extname(source)); + // ex: api/plugins/myDoc -> myDoc + // ex: myDoc -> myDoc + const sourceFileNameWithoutExtension = path.basename( + source, + path.extname(source), + ); + + // ex: api/plugins/myDoc -> api/plugins + // ex: myDoc -> . + const sourceDirName = path.dirname(source); + + const {filename: unprefixedFileName, numberPrefix} = stripNumberPrefixes + ? extractNumberPrefix(sourceFileNameWithoutExtension) + : {filename: sourceFileNameWithoutExtension, numberPrefix: undefined}; + + const baseID: string = frontMatter.id ?? unprefixedFileName; if (baseID.includes('/')) { throw new Error(`Document id [${baseID}] cannot include "/".`); } + // For autogenerated sidebars, sidebar position can come from filename number prefix or frontmatter + const sidebarPosition: number | undefined = + frontMatter.sidebar_position ?? numberPrefix; + // TODO legacy retrocompatibility // The same doc in 2 distinct version could keep the same id, // we just need to namespace the data by version - const versionIdPart = + const versionIdPrefix = versionMetadata.versionName === CURRENT_VERSION_NAME - ? '' - : `version-${versionMetadata.versionName}/`; + ? undefined + : `version-${versionMetadata.versionName}`; // TODO legacy retrocompatibility - // I think it's bad to affect the frontmatter id with the dirname - const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`; + // I think it's bad to affect the frontmatter id with the dirname? + function computeDirNameIdPrefix() { + if (sourceDirName === '.') { + return undefined; + } + // Eventually remove the number prefixes from intermediate directories + return stripNumberPrefixes + ? stripPathNumberPrefixes(sourceDirName) + : sourceDirName; + } - // TODO legacy composite id, requires a breaking change to modify this - const id = `${versionIdPart}${dirNameIdPart}${baseID}`; + const unversionedId = [computeDirNameIdPrefix(), baseID] + .filter(Boolean) + .join('/'); - const unversionedId = `${dirNameIdPart}${baseID}`; + // TODO is versioning the id very useful in practice? + // legacy versioned id, requires a breaking change to modify this + const id = [versionIdPrefix, unversionedId].filter(Boolean).join('/'); // TODO remove soon, deprecated homePageId const isDocsHomePage = unversionedId === (homePageId ?? '_index'); @@ -165,8 +195,9 @@ export function processDocMetadata({ ? '/' : getSlug({ baseID, - dirName: docsFileDirName, + dirName: sourceDirName, frontmatterSlug: frontMatter.slug, + stripDirNumberPrefixes: stripNumberPrefixes, }); // Default title is the id. @@ -212,6 +243,7 @@ export function processDocMetadata({ title, description, source: aliasedSitePath(filePath, siteDir), + sourceDirName, slug: docSlug, permalink, editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(), @@ -224,6 +256,7 @@ export function processDocMetadata({ ) : undefined, sidebar_label: sidebarLabel, + sidebarPosition, frontMatter, }; } diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 894da94cfe..72e12b8058 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -20,8 +20,7 @@ import { addTrailingPathSeparator, } from '@docusaurus/utils'; import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types'; - -import {loadSidebars, createSidebarsUtils} from './sidebars'; +import {loadSidebars, createSidebarsUtils, processSidebars} from './sidebars'; import {readVersionDocs, processDocMetadata} from './docs'; import {getDocsDirPaths, readVersionsMetadata} from './versions'; @@ -49,6 +48,7 @@ import { translateLoadedContent, getLoadedContentTranslationFiles, } from './translations'; +import {CategoryMetadataFilenamePattern} from './sidebarItemsGenerator'; export default function pluginContentDocs( context: LoadContext, @@ -127,6 +127,7 @@ export default function pluginContentDocs( ), ), ), + `${version.contentPath}/**/${CategoryMetadataFilenamePattern}`, ]; } @@ -162,8 +163,9 @@ export default function pluginContentDocs( async function loadVersion( versionMetadata: VersionMetadata, ): Promise { - const sidebars = loadSidebars(versionMetadata.sidebarFilePath); - const sidebarsUtils = createSidebarsUtils(sidebars); + const unprocessedSidebars = loadSidebars( + versionMetadata.sidebarFilePath, + ); const docsBase: DocMetadataBase[] = await loadVersionDocsBase( versionMetadata, @@ -173,6 +175,15 @@ export default function pluginContentDocs( (doc) => doc.id, ); + const sidebars = await processSidebars({ + sidebarItemsGenerator: options.sidebarItemsGenerator, + unprocessedSidebars, + docs: docsBase, + version: versionMetadata, + }); + + const sidebarsUtils = createSidebarsUtils(sidebars); + const validDocIds = Object.keys(docsBaseById); sidebarsUtils.checkSidebarsDocIds(validDocIds); diff --git a/packages/docusaurus-plugin-content-docs/src/numberPrefix.ts b/packages/docusaurus-plugin-content-docs/src/numberPrefix.ts new file mode 100644 index 0000000000..0caa02709a --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/numberPrefix.ts @@ -0,0 +1,35 @@ +/** + * 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. + */ + +const NumberPrefixRegex = /^(?\d+)(?\s*[-_.]+\s*)(?.*)$/; + +// 0-myDoc => myDoc +export function stripNumberPrefix(str: string) { + return NumberPrefixRegex.exec(str)?.groups?.suffix ?? str; +} + +// 0-myFolder/0-mySubfolder/0-myDoc => myFolder/mySubfolder/myDoc +export function stripPathNumberPrefixes(path: string) { + return path.split('/').map(stripNumberPrefix).join('/'); +} + +// 0-myDoc => {filename: myDoc, numberPrefix: 0} +// 003 - myDoc => {filename: myDoc, numberPrefix: 3} +export function extractNumberPrefix( + filename: string, +): {filename: string; numberPrefix?: number} { + const match = NumberPrefixRegex.exec(filename); + const cleanFileName = match?.groups?.suffix ?? filename; + const numberPrefixString = match?.groups?.numberPrefix; + const numberPrefix = numberPrefixString + ? parseInt(numberPrefixString, 10) + : undefined; + return { + filename: cleanFileName, + numberPrefix, + }; +} diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index e7f7bcb1c7..a38a9f58d4 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -15,13 +15,15 @@ import { import {OptionValidationContext, ValidationResult} from '@docusaurus/types'; import chalk from 'chalk'; import admonitions from 'remark-admonitions'; +import {DefaultSidebarItemsGenerator} from './sidebarItemsGenerator'; export const DEFAULT_OPTIONS: Omit = { path: 'docs', // Path to data on filesystem, relative to site dir. routeBasePath: 'docs', // URL Route. homePageId: undefined, // TODO remove soon, deprecated include: ['**/*.{md,mdx}'], // Extensions to include. - sidebarPath: 'sidebars.json', // Path to sidebar configuration for showing a list of markdown pages. + sidebarPath: 'sidebars.json', // Path to the sidebars configuration file + sidebarItemsGenerator: DefaultSidebarItemsGenerator, docLayoutComponent: '@theme/DocPage', docItemComponent: '@theme/DocItem', remarkPlugins: [], @@ -61,6 +63,9 @@ export const OptionsSchema = Joi.object({ homePageId: Joi.string().optional(), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), sidebarPath: Joi.string().allow('').default(DEFAULT_OPTIONS.sidebarPath), + sidebarItemsGenerator: Joi.function().default( + () => DEFAULT_OPTIONS.sidebarItemsGenerator, + ), docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent), docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent), remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), diff --git a/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts b/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts new file mode 100644 index 0000000000..f5a5bd5d51 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts @@ -0,0 +1,305 @@ +/** + * 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 { + SidebarItem, + SidebarItemDoc, + SidebarItemCategory, + SidebarItemsGenerator, + SidebarItemsGeneratorDoc, +} from './types'; +import {sortBy, take, last, orderBy} from 'lodash'; +import {addTrailingSlash, posixPath} from '@docusaurus/utils'; +import {Joi} from '@docusaurus/utils-validation'; +import {extractNumberPrefix} from './numberPrefix'; +import chalk from 'chalk'; +import path from 'path'; +import fs from 'fs-extra'; +import Yaml from 'js-yaml'; +import {DefaultCategoryCollapsedValue} from './sidebars'; + +const BreadcrumbSeparator = '/'; + +export const CategoryMetadataFilenameBase = '_category_'; +export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}'; + +export type CategoryMetadatasFile = { + label?: string; + position?: number; + collapsed?: boolean; + + // 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/ + // cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199 +}; + +type WithPosition = {position?: number}; +type SidebarItemWithPosition = SidebarItem & WithPosition; + +const CategoryMetadatasFileSchema = Joi.object({ + label: Joi.string().optional(), + position: Joi.number().optional(), + collapsed: Joi.boolean().optional(), +}); + +// TODO later if there is `CategoryFolder/index.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, +): Promise { + function assertCategoryMetadataFile( + content: unknown, + ): asserts content is CategoryMetadatasFile { + Joi.attempt(content, CategoryMetadatasFileSchema); + } + + async function tryReadFile( + fileNameWithExtension: string, + parse: (content: string) => unknown, + ): Promise { + // Simpler to use only posix paths for mocking file metadatas in tests + const filePath = posixPath( + path.join(categoryDirPath, fileNameWithExtension), + ); + if (await fs.pathExists(filePath)) { + const contentString = await fs.readFile(filePath, {encoding: 'utf8'}); + const unsafeContent: unknown = parse(contentString); + try { + assertCategoryMetadataFile(unsafeContent); + return unsafeContent; + } catch (e) { + console.error( + chalk.red( + `The docs sidebar category metadata file looks invalid!\nPath=${filePath}`, + ), + ); + throw e; + } + } + return null; + } + + return ( + (await tryReadFile(`${CategoryMetadataFilenameBase}.json`, JSON.parse)) ?? + (await tryReadFile(`${CategoryMetadataFilenameBase}.yml`, Yaml.load)) ?? + // eslint-disable-next-line no-return-await + (await tryReadFile(`${CategoryMetadataFilenameBase}.yaml`, Yaml.load)) + ); +} + +// [...parents, tail] +function parseBreadcrumb( + breadcrumb: string[], +): {parents: string[]; tail: string} { + return { + parents: take(breadcrumb, breadcrumb.length - 1), + tail: last(breadcrumb)!, + }; +} + +// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449 +export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async function defaultSidebarItemsGenerator({ + item, + docs: allDocs, + version, +}): Promise { + // Doc at the root of the autogenerated sidebar dir + function isRootDoc(doc: SidebarItemsGeneratorDoc) { + return doc.sourceDirName === item.dirName; + } + + // Doc inside a subfolder of the autogenerated sidebar dir + function isCategoryDoc(doc: SidebarItemsGeneratorDoc) { + if (isRootDoc(doc)) { + return false; + } + + return ( + // autogen dir is . and doc is in subfolder + item.dirName === '.' || + // autogen dir is not . and doc is in subfolder + // "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included) + doc.sourceDirName.startsWith(addTrailingSlash(item.dirName)) + ); + } + + function isInAutogeneratedDir(doc: SidebarItemsGeneratorDoc) { + return isRootDoc(doc) || isCategoryDoc(doc); + } + + // autogenDir=a/b and docDir=a/b/c/d => returns c/d + // autogenDir=a/b and docDir=a/b => returns . + function getDocDirRelativeToAutogenDir( + doc: SidebarItemsGeneratorDoc, + ): string { + if (!isInAutogeneratedDir(doc)) { + throw new Error( + 'getDocDirRelativeToAutogenDir() can only be called for subdocs of the sidebar autogen dir', + ); + } + // Is there a node API to compare 2 relative paths more easily? + // path.relative() does not give good results + if (item.dirName === '.') { + return doc.sourceDirName; + } else if (item.dirName === doc.sourceDirName) { + return '.'; + } else { + return doc.sourceDirName.replace(addTrailingSlash(item.dirName), ''); + } + } + + // Get only docs in the autogen dir + // Sort by folder+filename at once + const docs = sortBy(allDocs.filter(isInAutogeneratedDir), (d) => d.source); + + if (docs.length === 0) { + console.warn( + chalk.yellow( + `No docs found in dir ${item.dirName}: can't auto-generate a sidebar`, + ), + ); + } + + function createDocSidebarItem( + doc: SidebarItemsGeneratorDoc, + ): SidebarItemDoc & WithPosition { + return { + type: 'doc', + id: doc.id, + ...(doc.frontMatter.sidebar_label && { + label: doc.frontMatter.sidebar_label, + }), + ...(typeof doc.sidebarPosition !== 'undefined' && { + position: doc.sidebarPosition, + }), + }; + } + + async function createCategorySidebarItem({ + breadcrumb, + }: { + breadcrumb: string[]; + }): Promise { + const categoryDirPath = path.join( + version.contentPath, + breadcrumb.join(BreadcrumbSeparator), + ); + + const categoryMetadatas = await readCategoryMetadatasFile(categoryDirPath); + + const {tail} = parseBreadcrumb(breadcrumb); + + const {filename, numberPrefix} = extractNumberPrefix(tail); + + const position = categoryMetadatas?.position ?? numberPrefix; + + return { + type: 'category', + label: categoryMetadatas?.label ?? filename, + items: [], + collapsed: categoryMetadatas?.collapsed ?? DefaultCategoryCollapsedValue, + ...(typeof position !== 'undefined' && {position}), + }; + } + + // Not sure how to simplify this algorithm :/ + async function autogenerateSidebarItems(): Promise< + SidebarItemWithPosition[] + > { + const sidebarItems: SidebarItem[] = []; // mutable result + + const categoriesByBreadcrumb: Record = {}; // mutable cache of categories already created + + async function getOrCreateCategoriesForBreadcrumb( + breadcrumb: string[], + ): Promise { + if (breadcrumb.length === 0) { + return null; + } + const {parents} = parseBreadcrumb(breadcrumb); + const parentCategory = await getOrCreateCategoriesForBreadcrumb(parents); + const existingCategory = + categoriesByBreadcrumb[breadcrumb.join(BreadcrumbSeparator)]; + + if (existingCategory) { + return existingCategory; + } else { + const newCategory = await createCategorySidebarItem({ + breadcrumb, + }); + if (parentCategory) { + parentCategory.items.push(newCategory); + } else { + sidebarItems.push(newCategory); + } + categoriesByBreadcrumb[ + breadcrumb.join(BreadcrumbSeparator) + ] = newCategory; + return newCategory; + } + } + + // Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item) + function getRelativeBreadcrumb(doc: SidebarItemsGeneratorDoc): string[] { + const relativeDirPath = getDocDirRelativeToAutogenDir(doc); + if (relativeDirPath === '.') { + return []; + } else { + return relativeDirPath.split(BreadcrumbSeparator); + } + } + + async function handleDocItem(doc: SidebarItemsGeneratorDoc): Promise { + const breadcrumb = getRelativeBreadcrumb(doc); + const category = await getOrCreateCategoriesForBreadcrumb(breadcrumb); + + const docSidebarItem = createDocSidebarItem(doc); + if (category) { + category.items.push(docSidebarItem); + } else { + sidebarItems.push(docSidebarItem); + } + } + + // async process made sequential on purpose! order matters + for (const doc of docs) { + // eslint-disable-next-line no-await-in-loop + await handleDocItem(doc); + } + + return sidebarItems; + } + + const sidebarItems = await autogenerateSidebarItems(); + + return sortSidebarItems(sidebarItems); +}; + +// Recursively sort the categories/docs + remove the "position" attribute from final output +// Note: the "position" is only used to sort "inside" a sidebar slice +// It is not used to sort across multiple consecutive sidebar slices (ie a whole Category composed of multiple autogenerated items) +function sortSidebarItems( + sidebarItems: SidebarItemWithPosition[], +): SidebarItem[] { + const processedSidebarItems = sidebarItems.map((item) => { + if (item.type === 'category') { + return { + ...item, + items: sortSidebarItems(item.items), + }; + } + return item; + }); + + const sortedSidebarItems = orderBy( + processedSidebarItems, + (item) => item.position, + ['asc'], + ); + + return sortedSidebarItems.map(({position: _removed, ...item}) => item); +} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts index 0f4758d78c..56496e7038 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts @@ -16,9 +16,18 @@ import { Sidebar, SidebarItemCategory, SidebarItemType, + UnprocessedSidebarItem, + UnprocessedSidebars, + UnprocessedSidebar, + DocMetadataBase, + VersionMetadata, + SidebarItemsGenerator, + SidebarItemsGeneratorDoc, + SidebarItemsGeneratorVersion, } from './types'; -import {mapValues, flatten, flatMap, difference} from 'lodash'; +import {mapValues, flatten, flatMap, difference, pick, memoize} from 'lodash'; import {getElementsAround} from '@docusaurus/utils'; +import combinePromises from 'combine-promises'; type SidebarItemCategoryJSON = SidebarItemBase & { type: 'category'; @@ -27,12 +36,18 @@ type SidebarItemCategoryJSON = SidebarItemBase & { collapsed?: boolean; }; +type SidebarItemAutogeneratedJSON = SidebarItemBase & { + type: 'autogenerated'; + dirName: string; +}; + type SidebarItemJSON = | string | SidebarCategoryShorthandJSON | SidebarItemDoc | SidebarItemLink | SidebarItemCategoryJSON + | SidebarItemAutogeneratedJSON | { type: string; [key: string]: unknown; @@ -56,7 +71,7 @@ function isCategoryShorthand( } // categories are collapsed by default, unless user set collapsed = false -const defaultCategoryCollapsedValue = true; +export const DefaultCategoryCollapsedValue = true; /** * Convert {category1: [item1,item2]} shorthand syntax to long-form syntax @@ -66,7 +81,7 @@ function normalizeCategoryShorthand( ): SidebarItemCategoryJSON[] { return Object.entries(sidebar).map(([label, items]) => ({ type: 'category', - collapsed: defaultCategoryCollapsedValue, + collapsed: DefaultCategoryCollapsedValue, label, items, })); @@ -78,7 +93,7 @@ function normalizeCategoryShorthand( function assertItem( item: Record, keys: K[], -): asserts item is Record { +): asserts item is Record { const unknownKeys = Object.keys(item).filter( // @ts-expect-error: key is always string (key) => !keys.includes(key as string) && key !== 'type', @@ -115,6 +130,24 @@ function assertIsCategory( } } +function assertIsAutogenerated( + item: Record, +): asserts item is SidebarItemAutogeneratedJSON { + assertItem(item, ['dirName', 'customProps']); + if (typeof item.dirName !== 'string') { + throw new Error( + `Error loading ${JSON.stringify(item)}. "dirName" must be a string.`, + ); + } + if (item.dirName.startsWith('/') || item.dirName.endsWith('/')) { + throw new Error( + `Error loading ${JSON.stringify( + item, + )}. "dirName" must be a dir path relative to the docs folder root, and should not start or end with /`, + ); + } +} + function assertIsDoc( item: Record, ): asserts item is SidebarItemDoc { @@ -152,7 +185,7 @@ function assertIsLink( * Normalizes recursively item and all its children. Ensures that at the end * each item will be an object with the corresponding type. */ -function normalizeItem(item: SidebarItemJSON): SidebarItem[] { +function normalizeItem(item: SidebarItemJSON): UnprocessedSidebarItem[] { if (typeof item === 'string') { return [ { @@ -169,11 +202,14 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] { assertIsCategory(item); return [ { - collapsed: defaultCategoryCollapsedValue, + collapsed: DefaultCategoryCollapsedValue, ...item, items: flatMap(item.items, normalizeItem), }, ]; + case 'autogenerated': + assertIsAutogenerated(item); + return [item]; case 'link': assertIsLink(item); return [item]; @@ -195,7 +231,7 @@ function normalizeItem(item: SidebarItemJSON): SidebarItem[] { } } -function normalizeSidebar(sidebar: SidebarJSON) { +function normalizeSidebar(sidebar: SidebarJSON): UnprocessedSidebar { const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar) ? sidebar : normalizeCategoryShorthand(sidebar); @@ -203,21 +239,29 @@ function normalizeSidebar(sidebar: SidebarJSON) { return flatMap(normalizedSidebar, normalizeItem); } -function normalizeSidebars(sidebars: SidebarsJSON): Sidebars { +function normalizeSidebars(sidebars: SidebarsJSON): UnprocessedSidebars { return mapValues(sidebars, normalizeSidebar); } +export const DefaultSidebars: UnprocessedSidebars = { + defaultSidebar: [ + { + type: 'autogenerated', + dirName: '.', + }, + ], +}; + // TODO refactor: make async -export function loadSidebars(sidebarFilePath: string): Sidebars { +export function loadSidebars(sidebarFilePath: string): UnprocessedSidebars { if (!sidebarFilePath) { throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`); } - // sidebars file is optional, some users use docs without sidebars! - // See https://github.com/facebook/docusaurus/issues/3366 + // No sidebars file: by default we use the file-system structure to generate the sidebar + // See https://github.com/facebook/docusaurus/pull/4582 if (!fs.existsSync(sidebarFilePath)) { - // throw new Error(`No sidebar file exist at path: ${sidebarFilePath}`); - return {}; + return DefaultSidebars; } // We don't want sidebars to be cached because of hot reloading. @@ -225,6 +269,87 @@ export function loadSidebars(sidebarFilePath: string): Sidebars { return normalizeSidebars(sidebarJson); } +export function toSidebarItemsGeneratorDoc( + doc: DocMetadataBase, +): SidebarItemsGeneratorDoc { + return pick(doc, [ + 'id', + 'frontMatter', + 'source', + 'sourceDirName', + 'sidebarPosition', + ]); +} +export function toSidebarItemsGeneratorVersion( + version: VersionMetadata, +): SidebarItemsGeneratorVersion { + return pick(version, ['versionName', 'contentPath']); +} + +// Handle the generation of autogenerated sidebar items +export async function processSidebar({ + sidebarItemsGenerator, + unprocessedSidebar, + docs, + version, +}: { + sidebarItemsGenerator: SidebarItemsGenerator; + unprocessedSidebar: UnprocessedSidebar; + docs: DocMetadataBase[]; + version: VersionMetadata; +}): Promise { + // Just a minor lazy transformation optimization + const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({ + docs: docs.map(toSidebarItemsGeneratorDoc), + version: toSidebarItemsGeneratorVersion(version), + })); + + async function processRecursive( + item: UnprocessedSidebarItem, + ): Promise { + if (item.type === 'category') { + return [ + { + ...item, + items: (await Promise.all(item.items.map(processRecursive))).flat(), + }, + ]; + } + if (item.type === 'autogenerated') { + return sidebarItemsGenerator({ + item, + ...getSidebarItemsGeneratorDocsAndVersion(), + }); + } + return [item]; + } + + return (await Promise.all(unprocessedSidebar.map(processRecursive))).flat(); +} + +export async function processSidebars({ + sidebarItemsGenerator, + unprocessedSidebars, + docs, + version, +}: { + sidebarItemsGenerator: SidebarItemsGenerator; + unprocessedSidebars: UnprocessedSidebars; + docs: DocMetadataBase[]; + version: VersionMetadata; +}): Promise { + return combinePromises( + mapValues(unprocessedSidebars, (unprocessedSidebar) => + processSidebar({ + sidebarItemsGenerator, + unprocessedSidebar, + docs, + version, + }), + ), + ); +} + function collectSidebarItemsOfType< Type extends SidebarItemType, Item extends SidebarItem & {type: SidebarItemType} diff --git a/packages/docusaurus-plugin-content-docs/src/slug.ts b/packages/docusaurus-plugin-content-docs/src/slug.ts index 84d0397336..32556a8582 100644 --- a/packages/docusaurus-plugin-content-docs/src/slug.ts +++ b/packages/docusaurus-plugin-content-docs/src/slug.ts @@ -11,23 +11,31 @@ import { isValidPathname, resolvePathname, } from '@docusaurus/utils'; +import {stripPathNumberPrefixes} from './numberPrefix'; export default function getSlug({ baseID, frontmatterSlug, dirName, + stripDirNumberPrefixes = true, }: { baseID: string; frontmatterSlug?: string; dirName: string; + stripDirNumberPrefixes?: boolean; }): string { const baseSlug = frontmatterSlug || baseID; let slug: string; if (baseSlug.startsWith('/')) { slug = baseSlug; } else { + const dirNameStripped = stripDirNumberPrefixes + ? stripPathNumberPrefixes(dirName) + : dirName; const resolveDirname = - dirName === '.' ? '/' : addLeadingSlash(addTrailingSlash(dirName)); + dirName === '.' + ? '/' + : addLeadingSlash(addTrailingSlash(dirNameStripped)); slug = resolvePathname(baseSlug, resolveDirname); } diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 012e75ba0f..3c1633f0f5 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -83,6 +83,7 @@ export type PluginOptions = MetadataOptions & disableVersioning: boolean; excludeNextVersionDocs?: boolean; includeCurrentVersion: boolean; + sidebarItemsGenerator: SidebarItemsGenerator; }; export type SidebarItemBase = { @@ -108,6 +109,27 @@ export type SidebarItemCategory = SidebarItemBase & { collapsed: boolean; }; +export type UnprocessedSidebarItemAutogenerated = { + type: 'autogenerated'; + dirName: string; +}; + +export type UnprocessedSidebarItemCategory = SidebarItemBase & { + type: 'category'; + label: string; + items: UnprocessedSidebarItem[]; + collapsed: boolean; +}; + +export type UnprocessedSidebarItem = + | SidebarItemDoc + | SidebarItemLink + | UnprocessedSidebarItemCategory + | UnprocessedSidebarItemAutogenerated; + +export type UnprocessedSidebar = UnprocessedSidebarItem[]; +export type UnprocessedSidebars = Record; + export type SidebarItem = | SidebarItemDoc | SidebarItemLink @@ -115,9 +137,25 @@ export type SidebarItem = export type Sidebar = SidebarItem[]; export type SidebarItemType = SidebarItem['type']; - export type Sidebars = Record; +// Reduce API surface for options.sidebarItemsGenerator +// The user-provided generator fn should receive only a subset of metadatas +// A change to any of these metadatas can be considered as a breaking change +export type SidebarItemsGeneratorDoc = Pick< + DocMetadataBase, + 'id' | 'frontMatter' | 'source' | 'sourceDirName' | 'sidebarPosition' +>; +export type SidebarItemsGeneratorVersion = Pick< + VersionMetadata, + 'versionName' | 'contentPath' +>; +export type SidebarItemsGenerator = (generatorArgs: { + item: UnprocessedSidebarItemAutogenerated; + version: SidebarItemsGeneratorVersion; + docs: SidebarItemsGeneratorDoc[]; +}) => Promise; + export type OrderMetadata = { previous?: string; next?: string; @@ -143,10 +181,12 @@ export type DocMetadataBase = LastUpdateData & { title: string; description: string; source: string; + sourceDirName: string; // relative to the docs folder (can be ".") slug: string; permalink: string; // eslint-disable-next-line camelcase sidebar_label?: string; + sidebarPosition?: number; editUrl?: string | null; frontMatter: FrontMatter; }; diff --git a/packages/docusaurus-plugin-content-docs/src/versions.ts b/packages/docusaurus-plugin-content-docs/src/versions.ts index 732d9eecd7..de52d2d7d3 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions.ts @@ -434,6 +434,7 @@ function filterVersions( } } +// TODO make this async (requires plugin init to be async) export function readVersionsMetadata({ context, options, diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx index de6f07a928..77bbc902de 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx @@ -91,6 +91,7 @@ export default function CodeBlock({ children, className: languageClassName, metastring, + title, }: Props): JSX.Element { const {prism} = useThemeConfig(); @@ -107,9 +108,13 @@ export default function CodeBlock({ setMounted(true); }, []); + // TODO: the title is provided by MDX as props automatically + // so we probably don't need to parse the metastring + // (note: title="xyz" => title prop still has the quotes) + const codeBlockTitle = parseCodeBlockTitle(metastring) || title; + const button = useRef(null); let highlightLines: number[] = []; - const codeBlockTitle = parseCodeBlockTitle(metastring); const prismTheme = usePrismTheme(); diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 29a4b7abaf..a22bfcd7f1 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -55,6 +55,7 @@ declare module '@theme/CodeBlock' { readonly children: string; readonly className?: string; readonly metastring?: string; + readonly title?: string; }; const CodeBlock: (props: Props) => JSX.Element; diff --git a/packages/docusaurus-theme-classic/update-code-translations.test.js b/packages/docusaurus-theme-classic/update-code-translations.test.js index 80254c10ed..825ea04b6b 100644 --- a/packages/docusaurus-theme-classic/update-code-translations.test.js +++ b/packages/docusaurus-theme-classic/update-code-translations.test.js @@ -10,6 +10,9 @@ const path = require('path'); const fs = require('fs-extra'); const {mapValues, pickBy} = require('lodash'); +// Seems the 5s default timeout fails sometimes +jest.setTimeout(15000); + describe('update-code-translations', () => { test(`to have base.json contain all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => { const baseMessages = pickBy( diff --git a/website/community/support.md b/website/community/0-support.md similarity index 97% rename from website/community/support.md rename to website/community/0-support.md index 75f7e034b7..1db2482d68 100644 --- a/website/community/support.md +++ b/website/community/0-support.md @@ -1,8 +1,4 @@ ---- -id: support -title: Support -slug: /support ---- +# Support Docusaurus has a community of thousands of developers. diff --git a/website/community/team.mdx b/website/community/1-team.mdx similarity index 97% rename from website/community/team.mdx rename to website/community/1-team.mdx index 31d0a30b88..98bf339e27 100644 --- a/website/community/team.mdx +++ b/website/community/1-team.mdx @@ -1,8 +1,4 @@ ---- -id: team -title: Team -slug: /team ---- +# Team import { ActiveTeamRow, diff --git a/website/community/resources.md b/website/community/2-resources.md similarity index 97% rename from website/community/resources.md rename to website/community/2-resources.md index b021b869ff..1f0e348420 100644 --- a/website/community/resources.md +++ b/website/community/2-resources.md @@ -1,8 +1,4 @@ ---- -id: resources -title: Awesome Resources -slug: /resources ---- +# Awesome Resources A curated list of interesting Docusaurus community projects. diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index 4f4e62e200..690c2e279e 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -70,9 +70,16 @@ module.exports = { include: ['**/*.md', '**/*.mdx'], // Extensions to include. /** * Path to sidebar configuration for showing a list of markdown pages. - * Warning: will change */ - sidebarPath: '', + sidebarPath: 'sidebars.js', + /** + * Function used to replace the sidebar items of type "autogenerated" + * by real sidebar items (docs, categories, links...) + */ + sidebarItemsGenerator: function ({item, version, docs}) { + // Use the provided data to create a custom "sidebar slice" + return [{type: 'doc', id: 'doc1'}]; + }, /** * Theme components used by the docs pages */ @@ -154,14 +161,16 @@ Markdown documents can use the following markdown frontmatter metadata fields, e - `id`: A unique document id. If this field is not present, the document's `id` will default to its file name (without the extension) - `title`: The title of your document. If this field is not present, the document's `title` will default to its `id` -- `hide_title`: Whether to hide the title at the top of the doc. By default it is `false` +- `hide_title`: Whether to hide the title at the top of the doc. By default, it is `false` - `hide_table_of_contents`: Whether to hide the table of contents to the right. By default it is `false` - `sidebar_label`: The text shown in the document sidebar and in the next/previous button for this document. If this field is not present, the document's `sidebar_label` will default to its `title` +- `sidebar_position`: Permits to control the position of a doc inside the generated sidebar slice, when using `autogenerated` sidebar items. Can be Int or Float. +- `strip_number_prefixes`: When a document has a number prefix (`001 - My Doc.md`, `2. MyDoc.md`...), it is automatically removed, and the prefix is used as `sidebar_position`. Use `strip_number_prefixes: false` if you want to disable this behavior - `custom_edit_url`: The URL for editing this document. If this field is not present, the document's edit URL will fall back to `editUrl` from options fields passed to `docusaurus-plugin-content-docs` - `keywords`: Keywords meta tag for the document page, for search engines - `description`: The description of your document, which will become the `` and `` in ``, used by search engines. If this field is not present, it will default to the first line of the contents - `image`: Cover or thumbnail image that will be used when displaying the link to your post -- `slug`: Allows to customize the document url +- `slug`: Allows to customize the document url (`//`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /` Example: diff --git a/website/docs/guides/creating-pages.md b/website/docs/guides/creating-pages.md index 8e2d405cd8..f29c474e73 100644 --- a/website/docs/guides/creating-pages.md +++ b/website/docs/guides/creating-pages.md @@ -4,12 +4,20 @@ title: Creating Pages slug: /creating-pages --- -In this section, we will learn about creating ad-hoc pages in Docusaurus using React. This is most useful for creating one-off standalone pages like a showcase page, playground page or support page. +In this section, we will learn about creating pages in Docusaurus. + +This is useful for creating **one-off standalone pages** like a showcase page, playground page or support page. The functionality of pages is powered by `@docusaurus/plugin-content-pages`. You can use React components, or Markdown. +:::note + +Pages do not have sidebars, only [docs](./docs/docs-introduction.md) have. + +::: + ## Add a React page {#add-a-react-page} Create a file `/src/pages/helloReact.js`: diff --git a/website/docs/guides/docs/sidebar.md b/website/docs/guides/docs/sidebar.md index ec68bd3ca8..e6fb08f0af 100644 --- a/website/docs/guides/docs/sidebar.md +++ b/website/docs/guides/docs/sidebar.md @@ -4,170 +4,241 @@ title: Sidebar slug: /sidebar --- -To generate a sidebar to your Docusaurus site: +Creating a sidebar is useful to: + +- Group multiple **related documents** +- **Display a sidebar** on each of those documents +- Provide a **paginated navigation**, with next/previous button + +To use sidebars on your Docusaurus site: 1. Define a file that exports a [sidebar object](#sidebar-object). 1. Pass this object into the `@docusaurus/plugin-docs` plugin directly or via `@docusaurus/preset-classic`. -```js {8-9} title="docusaurus.config.js" +```js title="docusaurus.config.js" module.exports = { - // ... presets: [ [ '@docusaurus/preset-classic', { docs: { - // Sidebars filepath relative to the site dir. + // highlight-start sidebarPath: require.resolve('./sidebars.js'), + // highlight-end }, - // ... }, ], ], }; ``` -## Sidebar object {#sidebar-object} +## Default sidebar -A sidebar object contains [sidebar items](#understanding-sidebar-items) and it is defined like this: - -```typescript -type Sidebar = { - [sidebarId: string]: - | { - [sidebarCategory: string]: SidebarItem[]; - } - | SidebarItem[]; -}; -``` - -For example: +By default, Docusaurus [automatically generates a sidebar](#sidebar-item-autogenerated) for you, by using the filesystem structure of the `docs` folder: ```js title="sidebars.js" module.exports = { - docs: [ + mySidebar: [ { - type: 'category', - label: 'Getting Started', - items: ['greeting'], - }, - { - type: 'category', - label: 'Docusaurus', - items: ['doc1'], + type: 'autogenerated', + dirName: '.', // generate sidebar slice from the docs folder (or versioned_docs/) }, ], }; ``` -In this example, notice the following: +You can also define your sidebars explicitly. -- The key `docs` is the id of the sidebar. The id can be any value, not necessarily `docs`. -- `Getting Started` is a category within the sidebar. -- `greeting` and `doc1` are both [sidebar item](#understanding-sidebar-items). +## Sidebar object {#sidebar-object} -Shorthand notation can also be used: +A sidebar is a **tree of [sidebar items](#understanding-sidebar-items)**. + +```typescript +type Sidebar = + // Normal syntax + | SidebarItem[] + + // Shorthand syntax + | Record< + string, // category label + SidebarItem[] // category items + >; +``` + +A sidebars file can contain **multiple sidebar objects**. + +```typescript +type SidebarsFile = Record< + string, // sidebar id + Sidebar +>; +``` + +Example: ```js title="sidebars.js" module.exports = { - docs: { - 'Getting started': ['greeting'], - Docusaurus: ['doc1'], - }, + mySidebar: [ + { + type: 'category', + label: 'Getting Started', + items: ['doc1'], + }, + { + type: 'category', + label: 'Docusaurus', + items: ['doc2', 'doc3'], + }, + ], }; ``` -:::note +Notice the following: -Shorthand notation relies on the iteration order of JavaScript object keys for the category name. When using this notation, keep in mind that EcmaScript does not guarantee `Object.keys({a,b}) === ['a','b']`, yet this is generally true. +- There is a single sidebar `mySidebar`, containing 5 [sidebar items](#understanding-sidebar-items) +- `Getting Started` and `Docusaurus` are sidebar categories +- `doc1`, `doc2` and `doc3` are sidebar documents + +:::tip + +Use the **shorthand syntax** to express this sidebar more concisely: + +```js title="sidebars.js" +module.exports = { + mySidebar: { + 'Getting started': ['doc1'], + Docusaurus: ['doc2', 'doc3'], + }, +}; +``` ::: ## Using multiple sidebars {#using-multiple-sidebars} -You can have multiple sidebars for different Markdown files by adding more top-level keys to the exported object. +You can create a sidebar for each **set of markdown files** that you want to **group together**. + +:::tip + +The Docusaurus site is a good example of using multiple sidebars: + +- [Docs](../../introduction.md) +- [API](../../cli.md) + +::: Example: ```js title="sidebars.js" module.exports = { - firstSidebar: { - 'Category A': ['doc1'], - }, - secondSidebar: { - 'Category A': ['doc2'], - 'Category B': ['doc3'], + tutorialSidebar: { + 'Category A': ['doc1', 'doc2'], }, + apiSidebar: ['doc3', 'doc4'], }; ``` -By default, the doc page the user is reading will display the sidebar that it is part of. This can be customized with the [sidebar type](#understanding-sidebar-items). +:::note -For example, with the above example, when displaying the `doc2` page, the sidebar will contain the items of `secondSidebar` only. Another example of multiple sidebars is the `API` and `Docs` sections on the Docusaurus website. +The keys `tutorialSidebar` and `apiSidebar` are sidebar **technical ids** and do not matter much. -For more information about sidebars and how they relate to doc pages, see [Navbar doc link](../../api/themes/theme-configuration.md#navbar-doc-link). +::: + +When browsing: + +- `doc1` or `doc2`: the `tutorialSidebar` will be displayed +- `doc3` or `doc4`: the `apiSidebar` will be displayed + +A **paginated navigation** link documents inside the same sidebar with **next and previous buttons**. ## Understanding sidebar items {#understanding-sidebar-items} -As the name implies, `SidebarItem` is an item defined in a Sidebar. A SidebarItem as a `type` that defines what the item links to. +`SidebarItem` is an item defined in a Sidebar tree. -`type` supports the following values +There are different types of sidebar items: -- [Doc](#linking-to-a-doc-page) -- [Link](#creating-a-generic-link) -- [Ref](#creating-a-link-to-page-without-sidebar) -- [Category](#creating-a-hierarchy) +- **[Doc](#sidebar-item-doc)**: link to a doc page, assigning it to the sidebar +- **[Ref](#sidebar-item-ref)**: link to a doc page, without assigning it to the sidebar +- **[Link](#sidebar-item-link)**: link to any internal or external page +- **[Category](#sidebar-item-category)**: create a hierarchy of sidebar items +- **[Autogenerated](#sidebar-item-autogenerated)**: generate a sidebar slice automatically -### Linking to a doc page {#linking-to-a-doc-page} +### Doc: link to a doc {#sidebar-item-doc} -Set `type` to `doc` to link to a documentation page. This is the default type. +Use the `doc` type to link to a doc page and assign that doc to a sidebar: ```typescript type SidebarItemDoc = - | string + // Normal syntax | { type: 'doc'; id: string; label: string; // Sidebar label text - }; + } + + // Shorthand syntax + | string; // docId shortcut ``` Example: -```js -{ - type: 'doc', - id: 'doc1', // string - document id - label: 'Getting started' // Sidebar label text -} -``` - -The `sidebar_label` in the markdown frontmatter has a higher precedence over the `label` key in `SidebarItemDoc`. Using just the [Document ID](#document-id) is also valid, the following is equivalent to the above: - -```js -'doc1'; // string - document id - -``` - -Using this type will bind the linked doc to current sidebar. This means that if you access the `doc1` page, the sidebar displayed will be the sidebar that contains this doc page. - -In the example below, `doc1` is bound to `firstSidebar`. - ```js title="sidebars.js" module.exports = { - firstSidebar: { - 'Category A': ['doc1'], - }, - secondSidebar: { - 'Category A': ['doc2'], - 'Category B': ['doc3'], - }, + mySidebar: [ + // Normal syntax: + // highlight-start + { + type: 'doc', + id: 'doc1', // document id + label: 'Getting started', // sidebar label + }, + // highlight-end + + // Shorthand syntax: + // highlight-start + 'doc2', // document id + // highlight-end + ], }; ``` -### Creating a generic link {#creating-a-generic-link} +The `sidebar_label` markdown frontmatter has a higher precedence over the `label` key in `SidebarItemDoc`. -Set `type` to `link` to link to a non-documentation page. For example, to create an external link. +:::note + +Don't assign the same doc to multiple sidebars: use a [ref](#sidebar-item-ref) instead. + +::: + +### Ref: link to a doc, without sidebar {#sidebar-item-ref} + +Use the `ref` type to link to a doc page without assigning it to a sidebar. + +```typescript +type SidebarItemRef = { + type: 'ref'; + id: string; +}; +``` + +Example: + +```js title="sidebars.js" +module.exports = { + mySidebar: [ + { + type: 'ref', + id: 'doc1', // Document id (string). + }, + ], +}; +``` + +When browsing `doc1`, Docusaurus **will not display** the `mySidebar` sidebar. + +### Link: link to any page {#sidebar-item-link} + +Use the `link` type to link to any page (internal or external) that is not a doc. ```typescript type SidebarItemLink = { @@ -179,43 +250,41 @@ type SidebarItemLink = { Example: -```js -{ - type: 'link', - label: 'Custom Label', // The label that should be displayed (string). - href: 'https://example.com' // The target URL (string). -} -``` +```js title="sidebars.js" +module.exports = { + myLinksSidebar: [ + // highlight-start + // External link + { + type: 'link', + label: 'Facebook', // The link label + href: 'https://facebook.com', // The external URL + }, + // highlight-end -### Creating a link to page without sidebar {#creating-a-link-to-page-without-sidebar} - -Set `type` to `ref` to link to a documentation page without binding it to a sidebar. This means the sidebar disappears when the user displays the linked page. - -```typescript -type SidebarItemRef = { - type: 'ref'; - id: string; + // highlight-start + // Internal link + { + type: 'link', + label: 'Home', // The link label + href: '/', // The internal path + }, + // highlight-end + ], }; ``` -Example: +### Category: create a hierarchy {#sidebar-item-category} -```js -{ - type: 'ref', - id: 'doc1', // Document id (string). -} -``` - -### Creating a hierarchy {#creating-a-hierarchy} - -The Sidebar item type that creates a hierarchy in the sidebar. Set `type` to `category`. +Use the `category` type to create a hierarchy of sidebar items. ```typescript type SidebarItemCategory = { type: 'category'; label: string; // Sidebar label text. items: SidebarItem[]; // Array of sidebar items. + + // Category options: collapsed: boolean; // Set the category to be collapsed or open by default }; ``` @@ -225,16 +294,16 @@ Example: ```js title="sidebars.js" module.exports = { docs: [ - { - ... - }, { type: 'category', label: 'Guides', + collapsed: false, items: [ - 'guides/creating-pages', + 'creating-pages', { - Docs: ['docs-introduction', 'docs-sidebar', 'markdown-features', 'versioning'], + type: 'category', + label: 'Docs', + items: ['introduction', 'sidebar', 'markdown-features', 'versioning'], }, ], }, @@ -242,7 +311,9 @@ module.exports = { }; ``` -**Note**: it's possible to use the shorthand syntax to create nested categories: +:::tip + +Use the **shorthand syntax** when you don't need **category options**: ```js title="sidebars.js" module.exports = { @@ -250,28 +321,25 @@ module.exports = { Guides: [ 'creating-pages', { - Docs: [ - 'docs-introduction', - 'docs-sidebar', - 'markdown-features', - 'versioning', - ], + Docs: ['introduction', 'sidebar', 'markdown-features', 'versioning'], }, ], }, }; ``` +::: + #### Collapsible categories {#collapsible-categories} For sites with a sizable amount of content, we support the option to expand/collapse a category to toggle the display of its contents. Categories are collapsible by default. If you want them to be always expanded, set `themeConfig.sidebarCollapsible` to `false`: -```js {4} title="docusaurus.config.js" +```js title="docusaurus.config.js" module.exports = { - // ... themeConfig: { + // highlight-start sidebarCollapsible: false, - // ... + // highlight-end }, }; ``` @@ -296,16 +364,189 @@ module.exports = { }; ``` +### Autogenerated: generate a sidebar {#sidebar-item-autogenerated} + +Docusaurus can **create a sidebar automatically** from your **filesystem structure**: each folder creates a sidebar category. + +An `autogenerated` item is converted by Docusaurus to a **sidebar slice**: a list of items of type `doc` and `category`. + +```typescript +type SidebarItemAutogenerated = { + type: 'autogenerated'; + dirName: string; // Source folder to generate the sidebar slice from (relative to docs) +}; +``` + +Docusaurus can generate a sidebar from your docs folder: + +```js title="sidebars.js" +module.exports = { + myAutogeneratedSidebar: [ + // highlight-start + { + type: 'autogenerated', + dirName: '.', // '.' means the current docs folder + }, + // highlight-end + ], +}; +``` + +You can also use **multiple `autogenerated` items** in a sidebar, and interleave them with regular sidebar items: + +```js title="sidebars.js" +module.exports = { + mySidebar: [ + 'intro', + { + type: 'category', + label: 'Tutorials', + items: [ + 'tutorial-intro', + // highlight-start + { + type: 'autogenerated', + dirName: 'tutorials/easy', // Generate sidebar slice from docs/tutorials/easy + }, + // highlight-end + 'tutorial-medium', + // highlight-start + { + type: 'autogenerated', + dirName: 'tutorials/advanced', // Generate sidebar slice from docs/tutorials/hard + }, + // highlight-end + 'tutorial-end', + ], + }, + // highlight-start + { + type: 'autogenerated', + dirName: 'guides', // Generate sidebar slice from docs/guides + }, + // highlight-end + { + type: 'category', + label: 'Community', + items: ['team', 'chat'], + }, + ], +}; +``` + +#### Autogenerated sidebar metadatas {#autogenerated-sidebar-metadatas} + +By default, the sidebar slice will be generated in **alphabetical order** (using files and folders names). + +If the generated sidebar does not look good, you can assign additional metadatas to docs and categories. + +**For docs**: use additional frontmatter: + +```diff title="docs/tutorials/tutorial-easy.md" ++ --- ++ sidebar_label: Easy ++ sidebar_position: 2 ++ --- + + +# Easy Tutorial + +This is the easy tutorial! +``` + +**For categories**: add a `_category_.json` or `_category_.yml` file in the appropriate folder: + +```json title="docs/tutorials/_category_.json" +{ + "label": "Tutorial", + "position": 3 +} +``` + +```yaml title="docs/tutorials/_category_.yml" +label: 'Tutorial' +position: 2.5 # float position is supported +collapsed: false # keep the category open by default +``` + +:::info + +The position metadata is only used **inside a sidebar slice**: Docusaurus does not re-order other items of your sidebar. + +::: + +#### Using number prefixes + +A simple way to order an autogenerated sidebar is to prefix docs and folders by number prefixes: + +```bash +docs +├── 01-Intro.md +├── 02-Tutorial Easy +│   ├── 01-First Part.md +│   ├── 02-Second Part.md +│   └── 03-End.md +├── 03-Tutorial Hard +│   ├── 01-First Part.md +│   ├── 02-Second Part.md +│   ├── 03-Third Part.md +│   └── 04-End.md +└── 04-End.md +``` + +To make it **easier to adopt**, Docusaurus supports **multiple number prefix patterns**. + +By default, Docusaurus will **remove the number prefix** from the doc id, title, label and url paths. + +:::caution + +**Prefer using [additional metadatas](#autogenerated-sidebar-metadatas)**. + +Updating a number prefix can be annoying, as it can require **updating multiple existing markdown links**: + +```diff title="docs/02-Tutorial Easy/01-First Part.md" +- Check the [Tutorial End](../04-End.md); ++ Check the [Tutorial End](../05-End.md); +``` + +::: + +#### Customize the sidebar items generator + +You can provide a custom `sidebarItemsGenerator` function in the docs plugin (or preset) config: + +```js title="docusaurus.config.js" +module.exports = { + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + /** + * Function used to replace the sidebar items of type "autogenerated" + * by real sidebar items (docs, categories, links...) + */ + // highlight-start + sidebarItemsGenerator: function ({item, version, docs}) { + // Use the provided data to create a custom "sidebar slice" + return [{type: 'doc', id: 'doc1'}]; + }, + // highlight-end + }, + ], + ], +}; +``` + ## Hideable sidebar {#hideable-sidebar} Using the enabled `themeConfig.hideableSidebar` option, you can make the entire sidebar hidden, allowing you to better focus your users on the content. This is especially useful when content consumption on medium screens (e.g. on tablets). -```js {4} title="docusaurus.config.js" +```js title="docusaurus.config.js" module.exports = { - // ... themeConfig: { + // highlight-starrt hideableSidebar: true, - // ... + // highlight-end }, }; ``` @@ -323,3 +564,21 @@ To pass in custom props to a swizzled sidebar item, add the optional `customProp } } ``` + +## Complex sidebars example {#complex-sidebars-example} + +Real-world example from the Docusaurus site: + +```mdx-code-block +import CodeBlock from '@theme/CodeBlock'; + + + {require('!!raw-loader!@site/sidebars.js') + .default + .split('\n') + // remove comments + .map((line) => !['#','/*','*'].some(commentPattern => line.trim().startsWith(commentPattern)) && line) + .filter(Boolean) + .join('\n')} + +``` diff --git a/website/docs/i18n/i18n-crowdin.mdx b/website/docs/i18n/i18n-crowdin.mdx index cb78d51b0f..84b25b9ed8 100644 --- a/website/docs/i18n/i18n-crowdin.mdx +++ b/website/docs/i18n/i18n-crowdin.mdx @@ -514,13 +514,15 @@ It is currently **not possible to link to a specific file** in Crowdin. The **Docusaurus v2 configuration file** is a good example of using versioning and multi-instance: +```mdx-code-block import CrowdinConfigV2 from '!!raw-loader!@site/../crowdin-v2.yaml'; import CodeBlock from '@theme/CodeBlock'; - + {CrowdinConfigV2.split('\n') // remove comments .map((line) => !line.startsWith('#') && line) .filter(Boolean) .join('\n')} +``` diff --git a/website/sidebarsCommunity.js b/website/sidebarsCommunity.js index 7af164c055..e058dfa01a 100644 --- a/website/sidebarsCommunity.js +++ b/website/sidebarsCommunity.js @@ -7,9 +7,10 @@ module.exports = { community: [ - 'support', - 'team', - 'resources', + { + type: 'autogenerated', + dirName: '.', + }, { type: 'link', href: '/showcase', diff --git a/yarn.lock b/yarn.lock index 75ad17a0c8..9d789a1229 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3480,6 +3480,11 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/js-yaml@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb" + integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA== + "@types/jscodeshift@^0.7.1": version "0.7.1" resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.7.1.tgz#8afcda6c8ca2ce828c3b192f8a1ba0245987ac12" @@ -4504,6 +4509,11 @@ argparse@^1.0.10, argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -6292,6 +6302,11 @@ columnify@^1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" +combine-promises@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71" + integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg== + combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -12017,6 +12032,13 @@ js-yaml@^3.11.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"