mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 23:57:22 +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
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue