mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 18:58:36 +02:00
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
This commit is contained in:
parent
836f92708a
commit
db79d462ab
67 changed files with 2887 additions and 306 deletions
|
@ -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', ...],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"label": "Tutorial - Basics",
|
||||
"position": 2
|
||||
}
|
|
@ -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?
|
||||
|
|
@ -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`:
|
||||
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"label": "Tutorial - Extras",
|
||||
"position": 3
|
||||
}
|
|
@ -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:
|
||||
|
|
@ -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.
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
|
|
@ -41,8 +41,10 @@ function Feature({Svg, title, description}) {
|
|||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} alt={title} />
|
||||
</div>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,8 +14,10 @@ function HomepageHeader() {
|
|||
<h1 className="hero__title">{siteConfig.title}</h1>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link className="button button--secondary button--lg" to="/docs">
|
||||
Get Started - Docusaurus Tutorial
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro">
|
||||
Docusaurus Tutorial - 5min ⏱️
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# Getting Started
|
||||
|
||||
Getting started text
|
|
@ -0,0 +1,3 @@
|
|||
# Installation
|
||||
|
||||
Installation text
|
|
@ -0,0 +1,3 @@
|
|||
# API Overview
|
||||
|
||||
API Overview text
|
|
@ -0,0 +1 @@
|
|||
Client API text
|
|
@ -0,0 +1 @@
|
|||
Server API text
|
|
@ -0,0 +1 @@
|
|||
Plugin API text
|
|
@ -0,0 +1 @@
|
|||
Theme API text
|
|
@ -0,0 +1 @@
|
|||
label: 'Extension APIs (label from _category_.yml)'
|
|
@ -0,0 +1,3 @@
|
|||
# API End
|
||||
|
||||
API End text
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"label": "API (label from _category_.json)"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
id: guide2.5
|
||||
sidebar_position: 2.5
|
||||
---
|
||||
|
||||
# Guide 2.5
|
||||
|
||||
Guide 2.5 text
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: guide2
|
||||
---
|
||||
|
||||
# Guide 2
|
||||
|
||||
Guide 2 text
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"position": 2
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: guide4
|
||||
---
|
||||
|
||||
# Guide 4
|
||||
|
||||
Guide 4 text
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: guide5
|
||||
---
|
||||
|
||||
# Guide 5
|
||||
|
||||
Guide 5 text
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
id: guide3
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Guide 3
|
||||
|
||||
Guide 3 text
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
id: guide1
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Guide 1
|
||||
|
||||
Guide 1 text
|
|
@ -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',
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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\\",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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<DocMetadata> = {
|
||||
next: undefined,
|
||||
previous: undefined,
|
||||
|
@ -40,6 +51,7 @@ const defaultDocMetadata: Partial<DocMetadata> = {
|
|||
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<SidebarItemsGenerator>[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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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],
|
||||
|
|
|
@ -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<Parameters<SidebarItemsGenerator>[0]>,
|
||||
) {
|
||||
return DefaultSidebarItemsGenerator({
|
||||
item: {
|
||||
type: 'autogenerated',
|
||||
dirName: '.',
|
||||
},
|
||||
version: {
|
||||
versionName: 'current',
|
||||
contentPath: 'docs',
|
||||
},
|
||||
docs: [],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function mockCategoryMetadataFiles(
|
||||
categoryMetadataFiles: Record<string, Partial<CategoryMetadatasFile>>,
|
||||
) {
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<DocFrontMatter>({
|
||||
|
@ -23,7 +25,9 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
|
|||
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(
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<LoadedVersion> {
|
||||
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);
|
||||
|
||||
|
|
35
packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
Normal file
35
packages/docusaurus-plugin-content-docs/src/numberPrefix.ts
Normal file
|
@ -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 = /^(?<numberPrefix>\d+)(?<separator>\s*[-_.]+\s*)(?<suffix>.*)$/;
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
|
@ -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<PluginOptions, 'id'> = {
|
||||
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),
|
||||
|
|
|
@ -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<CategoryMetadatasFile>({
|
||||
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<CategoryMetadatasFile | null> {
|
||||
function assertCategoryMetadataFile(
|
||||
content: unknown,
|
||||
): asserts content is CategoryMetadatasFile {
|
||||
Joi.attempt(content, CategoryMetadatasFileSchema);
|
||||
}
|
||||
|
||||
async function tryReadFile(
|
||||
fileNameWithExtension: string,
|
||||
parse: (content: string) => unknown,
|
||||
): Promise<CategoryMetadatasFile | null> {
|
||||
// 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<SidebarItem[]> {
|
||||
// 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<SidebarItemCategory & WithPosition> {
|
||||
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<string, SidebarItemCategory> = {}; // mutable cache of categories already created
|
||||
|
||||
async function getOrCreateCategoriesForBreadcrumb(
|
||||
breadcrumb: string[],
|
||||
): Promise<SidebarItemCategory | null> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
|
@ -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<K extends string>(
|
||||
item: Record<string, unknown>,
|
||||
keys: K[],
|
||||
): asserts item is Record<K, never> {
|
||||
): asserts item is Record<K, unknown> {
|
||||
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<string, unknown>,
|
||||
): 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<string, unknown>,
|
||||
): 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<Sidebar> {
|
||||
// Just a minor lazy transformation optimization
|
||||
const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({
|
||||
docs: docs.map(toSidebarItemsGeneratorDoc),
|
||||
version: toSidebarItemsGeneratorVersion(version),
|
||||
}));
|
||||
|
||||
async function processRecursive(
|
||||
item: UnprocessedSidebarItem,
|
||||
): Promise<SidebarItem[]> {
|
||||
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<Sidebars> {
|
||||
return combinePromises(
|
||||
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
|
||||
processSidebar({
|
||||
sidebarItemsGenerator,
|
||||
unprocessedSidebar,
|
||||
docs,
|
||||
version,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function collectSidebarItemsOfType<
|
||||
Type extends SidebarItemType,
|
||||
Item extends SidebarItem & {type: SidebarItemType}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string, UnprocessedSidebar>;
|
||||
|
||||
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<string, Sidebar>;
|
||||
|
||||
// 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<SidebarItem[]>;
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -434,6 +434,7 @@ function filterVersions(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO make this async (requires plugin init to be async)
|
||||
export function readVersionsMetadata({
|
||||
context,
|
||||
options,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
---
|
||||
id: support
|
||||
title: Support
|
||||
slug: /support
|
||||
---
|
||||
# Support
|
||||
|
||||
Docusaurus has a community of thousands of developers.
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
---
|
||||
id: team
|
||||
title: Team
|
||||
slug: /team
|
||||
---
|
||||
# Team
|
||||
|
||||
import {
|
||||
ActiveTeamRow,
|
|
@ -1,8 +1,4 @@
|
|||
---
|
||||
id: resources
|
||||
title: Awesome Resources
|
||||
slug: /resources
|
||||
---
|
||||
# Awesome Resources
|
||||
|
||||
A curated list of interesting Docusaurus community projects.
|
||||
|
|
@ -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 `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, 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 (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`
|
||||
|
||||
Example:
|
||||
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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/<version>)
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
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';
|
||||
|
||||
<CodeBlock className="language-js" title="sidebars.js">
|
||||
{require('!!raw-loader!@site/sidebars.js')
|
||||
.default
|
||||
.split('\n')
|
||||
// remove comments
|
||||
.map((line) => !['#','/*','*'].some(commentPattern => line.trim().startsWith(commentPattern)) && line)
|
||||
.filter(Boolean)
|
||||
.join('\n')}
|
||||
</CodeBlock>
|
||||
```
|
||||
|
|
|
@ -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';
|
||||
|
||||
<CodeBlock className="language-yaml" title="test">
|
||||
<CodeBlock className="language-yaml" title="crowdin.yml">
|
||||
{CrowdinConfigV2.split('\n')
|
||||
// remove comments
|
||||
.map((line) => !line.startsWith('#') && line)
|
||||
.filter(Boolean)
|
||||
.join('\n')}
|
||||
</CodeBlock>
|
||||
```
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
module.exports = {
|
||||
community: [
|
||||
'support',
|
||||
'team',
|
||||
'resources',
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: '.',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/showcase',
|
||||
|
|
22
yarn.lock
22
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"
|
||||
|
|
Loading…
Add table
Reference in a new issue