mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat(content-docs): sidebar category linking to document or auto-generated index page (#5830)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com> Co-authored-by: Armano <armano2@users.noreply.github.com> Co-authored-by: Alexey Pyltsyn <lex61rus@gmail.com>
This commit is contained in:
parent
95f911efef
commit
cfae5d0933
105 changed files with 3904 additions and 816 deletions
|
@ -167,7 +167,6 @@ module.exports = {
|
|||
'head',
|
||||
'tail',
|
||||
'initial',
|
||||
'last',
|
||||
],
|
||||
message: 'These APIs have their ES counterparts.',
|
||||
},
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
"escape-html": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"github-slugger": "^1.4.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"mdast-util-to-string": "^2.0.0",
|
||||
"remark-emoji": "^2.1.0",
|
||||
|
@ -40,7 +39,6 @@
|
|||
"devDependencies": {
|
||||
"@docusaurus/types": "2.0.0-beta.9",
|
||||
"@types/escape-html": "^1.0.1",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/mdast": "^3.0.7",
|
||||
"@types/stringify-object": "^3.3.1",
|
||||
"@types/unist": "^2.0.6",
|
||||
|
|
|
@ -7,19 +7,16 @@
|
|||
|
||||
/* Based on remark-slug (https://github.com/remarkjs/remark-slug) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */
|
||||
|
||||
import {parseMarkdownHeadingId} from '@docusaurus/utils';
|
||||
import {parseMarkdownHeadingId, createSlugger} from '@docusaurus/utils';
|
||||
import visit, {Visitor} from 'unist-util-visit';
|
||||
import toString from 'mdast-util-to-string';
|
||||
import Slugger from 'github-slugger';
|
||||
import type {Transformer} from 'unified';
|
||||
import type {Parent} from 'unist';
|
||||
import type {Heading, Text} from 'mdast';
|
||||
|
||||
const slugs = new Slugger();
|
||||
|
||||
function headings(): Transformer {
|
||||
const transformer: Transformer = (ast) => {
|
||||
slugs.reset();
|
||||
const slugs = createSlugger();
|
||||
|
||||
const visitor: Visitor<Heading> = (headingNode) => {
|
||||
const data = headingNode.data || (headingNode.data = {});
|
||||
|
@ -29,7 +26,7 @@ function headings(): Transformer {
|
|||
let {id} = properties;
|
||||
|
||||
if (id) {
|
||||
id = slugs.slug(id, true);
|
||||
id = slugs.slug(id, {maintainCase: true});
|
||||
} else {
|
||||
const headingTextNodes = headingNode.children.filter(
|
||||
({type}) => !['html', 'jsx'].includes(type),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version-1.0.1/docs": {
|
||||
"VersionedSideBarNameDoesNotMatter/docs": {
|
||||
"Test": [
|
||||
"version-1.0.1/foo/bar"
|
||||
"foo/bar"
|
||||
],
|
||||
"Guides": [
|
||||
"version-1.0.1/hello"
|
||||
|
|
|
@ -2,53 +2,31 @@
|
|||
|
||||
exports[`docsVersion first time versioning 1`] = `
|
||||
Object {
|
||||
"version-1.0.0/docs": Array [
|
||||
"docs": Object {
|
||||
"Guides": Array [
|
||||
"hello",
|
||||
],
|
||||
"Test": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-1.0.0/foo/bar",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "version-1.0.0/foo/baz",
|
||||
"type": "doc",
|
||||
},
|
||||
"foo/bar",
|
||||
"foo/baz",
|
||||
],
|
||||
"label": "foo",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-1.0.0/rootAbsoluteSlug",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "version-1.0.0/rootRelativeSlug",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "version-1.0.0/rootResolvedSlug",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "version-1.0.0/rootTryToEscapeSlug",
|
||||
"type": "doc",
|
||||
},
|
||||
"rootAbsoluteSlug",
|
||||
"rootRelativeSlug",
|
||||
"rootResolvedSlug",
|
||||
"rootTryToEscapeSlug",
|
||||
],
|
||||
"label": "Slugs",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"id": "version-1.0.0/headingAsTitle",
|
||||
"id": "headingAsTitle",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
|
@ -57,67 +35,31 @@ Object {
|
|||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"id": "version-1.0.0/hello",
|
||||
"id": "hello",
|
||||
"type": "ref",
|
||||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-1.0.0/hello",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"label": "Guides",
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`docsVersion not the first time versioning 1`] = `
|
||||
Object {
|
||||
"version-2.0.0/docs": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-2.0.0/foo/bar",
|
||||
"type": "doc",
|
||||
},
|
||||
"docs": Object {
|
||||
"Guides": Array [
|
||||
"hello",
|
||||
],
|
||||
"label": "Test",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-2.0.0/hello",
|
||||
"type": "doc",
|
||||
},
|
||||
"Test": Array [
|
||||
"foo/bar",
|
||||
],
|
||||
"label": "Guides",
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`docsVersion second docs instance versioning 1`] = `
|
||||
Object {
|
||||
"version-2.0.0/community": Array [
|
||||
Object {
|
||||
"id": "version-2.0.0/team",
|
||||
"type": "doc",
|
||||
},
|
||||
"community": Array [
|
||||
"team",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -43,6 +43,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "foo",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -67,6 +68,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Slugs",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -84,6 +86,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -96,6 +99,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Guides",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
|
@ -614,12 +618,14 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"Bar\\",
|
||||
\\"href\\": \\"/docs/foo/bar\\"
|
||||
\\"href\\": \\"/docs/foo/bar\\",
|
||||
\\"docId\\": \\"foo/bar\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"baz\\",
|
||||
\\"href\\": \\"/docs/foo/bazSlug.html\\"
|
||||
\\"href\\": \\"/docs/foo/bazSlug.html\\",
|
||||
\\"docId\\": \\"foo/baz\\"
|
||||
}
|
||||
],
|
||||
\\"collapsible\\": true,
|
||||
|
@ -632,22 +638,26 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"rootAbsoluteSlug\\",
|
||||
\\"href\\": \\"/docs/rootAbsoluteSlug\\"
|
||||
\\"href\\": \\"/docs/rootAbsoluteSlug\\",
|
||||
\\"docId\\": \\"rootAbsoluteSlug\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"rootRelativeSlug\\",
|
||||
\\"href\\": \\"/docs/rootRelativeSlug\\"
|
||||
\\"href\\": \\"/docs/rootRelativeSlug\\",
|
||||
\\"docId\\": \\"rootRelativeSlug\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"rootResolvedSlug\\",
|
||||
\\"href\\": \\"/docs/hey/rootResolvedSlug\\"
|
||||
\\"href\\": \\"/docs/hey/rootResolvedSlug\\",
|
||||
\\"docId\\": \\"rootResolvedSlug\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"rootTryToEscapeSlug\\",
|
||||
\\"href\\": \\"/docs/rootTryToEscapeSlug\\"
|
||||
\\"href\\": \\"/docs/rootTryToEscapeSlug\\",
|
||||
\\"docId\\": \\"rootTryToEscapeSlug\\"
|
||||
}
|
||||
],
|
||||
\\"collapsible\\": true,
|
||||
|
@ -656,7 +666,8 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"My heading as title\\",
|
||||
\\"href\\": \\"/docs/headingAsTitle\\"
|
||||
\\"href\\": \\"/docs/headingAsTitle\\",
|
||||
\\"docId\\": \\"headingAsTitle\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
|
@ -666,7 +677,8 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"Hello sidebar_label\\",
|
||||
\\"href\\": \\"/docs/\\"
|
||||
\\"href\\": \\"/docs/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -679,11 +691,92 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"Hello sidebar_label\\",
|
||||
\\"href\\": \\"/docs/\\"
|
||||
\\"href\\": \\"/docs/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"foo/bar\\": {
|
||||
\\"id\\": \\"foo/bar\\",
|
||||
\\"title\\": \\"Bar\\",
|
||||
\\"description\\": \\"This is custom description\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"foo/baz\\": {
|
||||
\\"id\\": \\"foo/baz\\",
|
||||
\\"title\\": \\"baz\\",
|
||||
\\"description\\": \\"Images\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"headingAsTitle\\": {
|
||||
\\"id\\": \\"headingAsTitle\\",
|
||||
\\"title\\": \\"My heading as title\\",
|
||||
\\"description\\": \\"\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"hello\\": {
|
||||
\\"id\\": \\"hello\\",
|
||||
\\"title\\": \\"Hello, World !\\",
|
||||
\\"description\\": \\"Hi, Endilie here :)\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"ipsum\\": {
|
||||
\\"id\\": \\"ipsum\\",
|
||||
\\"title\\": \\"ipsum\\",
|
||||
\\"description\\": \\"Lorem ipsum.\\"
|
||||
},
|
||||
\\"lorem\\": {
|
||||
\\"id\\": \\"lorem\\",
|
||||
\\"title\\": \\"lorem\\",
|
||||
\\"description\\": \\"Lorem ipsum.\\"
|
||||
},
|
||||
\\"rootAbsoluteSlug\\": {
|
||||
\\"id\\": \\"rootAbsoluteSlug\\",
|
||||
\\"title\\": \\"rootAbsoluteSlug\\",
|
||||
\\"description\\": \\"Lorem\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"rootRelativeSlug\\": {
|
||||
\\"id\\": \\"rootRelativeSlug\\",
|
||||
\\"title\\": \\"rootRelativeSlug\\",
|
||||
\\"description\\": \\"Lorem\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"rootResolvedSlug\\": {
|
||||
\\"id\\": \\"rootResolvedSlug\\",
|
||||
\\"title\\": \\"rootResolvedSlug\\",
|
||||
\\"description\\": \\"Lorem\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"rootTryToEscapeSlug\\": {
|
||||
\\"id\\": \\"rootTryToEscapeSlug\\",
|
||||
\\"title\\": \\"rootTryToEscapeSlug\\",
|
||||
\\"description\\": \\"Lorem\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"slugs/absoluteSlug\\": {
|
||||
\\"id\\": \\"slugs/absoluteSlug\\",
|
||||
\\"title\\": \\"absoluteSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/relativeSlug\\": {
|
||||
\\"id\\": \\"slugs/relativeSlug\\",
|
||||
\\"title\\": \\"relativeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/resolvedSlug\\": {
|
||||
\\"id\\": \\"slugs/resolvedSlug\\",
|
||||
\\"title\\": \\"resolvedSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/tryToEscapeSlug\\": {
|
||||
\\"id\\": \\"slugs/tryToEscapeSlug\\",
|
||||
\\"title\\": \\"tryToEscapeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
}
|
||||
|
@ -958,6 +1051,7 @@ Object {
|
|||
"sidebarPosition": 0,
|
||||
"source": "@site/docs/3-API/01_Core APIs/0 --- Client API.md",
|
||||
"sourceDirName": "3-API/01_Core APIs",
|
||||
"unversionedId": "API/Core APIs/Client API",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -965,6 +1059,7 @@ Object {
|
|||
"sidebarPosition": 1,
|
||||
"source": "@site/docs/3-API/01_Core APIs/1 --- Server API.md",
|
||||
"sourceDirName": "3-API/01_Core APIs",
|
||||
"unversionedId": "API/Core APIs/Server API",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -972,6 +1067,7 @@ Object {
|
|||
"sidebarPosition": 0,
|
||||
"source": "@site/docs/3-API/02_Extension APIs/0. Plugin API.md",
|
||||
"sourceDirName": "3-API/02_Extension APIs",
|
||||
"unversionedId": "API/Extension APIs/Plugin API",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -979,6 +1075,7 @@ Object {
|
|||
"sidebarPosition": 1,
|
||||
"source": "@site/docs/3-API/02_Extension APIs/1. Theme API.md",
|
||||
"sourceDirName": "3-API/02_Extension APIs",
|
||||
"unversionedId": "API/Extension APIs/Theme API",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -986,6 +1083,7 @@ Object {
|
|||
"sidebarPosition": 3,
|
||||
"source": "@site/docs/3-API/03_api-end.md",
|
||||
"sourceDirName": "3-API",
|
||||
"unversionedId": "API/api-end",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -993,6 +1091,7 @@ Object {
|
|||
"sidebarPosition": 0,
|
||||
"source": "@site/docs/3-API/00_api-overview.md",
|
||||
"sourceDirName": "3-API",
|
||||
"unversionedId": "API/api-overview",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {
|
||||
|
@ -1003,6 +1102,7 @@ Object {
|
|||
"sidebarPosition": 1,
|
||||
"source": "@site/docs/Guides/z-guide1.md",
|
||||
"sourceDirName": "Guides",
|
||||
"unversionedId": "Guides/guide1",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {
|
||||
|
@ -1012,6 +1112,7 @@ Object {
|
|||
"sidebarPosition": 2,
|
||||
"source": "@site/docs/Guides/02-guide2.md",
|
||||
"sourceDirName": "Guides",
|
||||
"unversionedId": "Guides/guide2",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {
|
||||
|
@ -1022,6 +1123,7 @@ Object {
|
|||
"sidebarPosition": 2.5,
|
||||
"source": "@site/docs/Guides/0-guide2.5.md",
|
||||
"sourceDirName": "Guides",
|
||||
"unversionedId": "Guides/guide2.5",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {
|
||||
|
@ -1032,6 +1134,7 @@ Object {
|
|||
"sidebarPosition": 3,
|
||||
"source": "@site/docs/Guides/guide3.md",
|
||||
"sourceDirName": "Guides",
|
||||
"unversionedId": "Guides/guide3",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {
|
||||
|
@ -1041,6 +1144,7 @@ Object {
|
|||
"sidebarPosition": undefined,
|
||||
"source": "@site/docs/Guides/a-guide4.md",
|
||||
"sourceDirName": "Guides",
|
||||
"unversionedId": "Guides/guide4",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {
|
||||
|
@ -1050,6 +1154,7 @@ Object {
|
|||
"sidebarPosition": undefined,
|
||||
"source": "@site/docs/Guides/b-guide5.md",
|
||||
"sourceDirName": "Guides",
|
||||
"unversionedId": "Guides/guide5",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -1057,6 +1162,7 @@ Object {
|
|||
"sidebarPosition": 0,
|
||||
"source": "@site/docs/0-getting-started.md",
|
||||
"sourceDirName": ".",
|
||||
"unversionedId": "getting-started",
|
||||
},
|
||||
Object {
|
||||
"frontMatter": Object {},
|
||||
|
@ -1064,6 +1170,7 @@ Object {
|
|||
"sidebarPosition": 1,
|
||||
"source": "@site/docs/1-installation.md",
|
||||
"sourceDirName": ".",
|
||||
"unversionedId": "installation",
|
||||
},
|
||||
],
|
||||
"item": Object {
|
||||
|
@ -1151,9 +1258,18 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"team\\",
|
||||
\\"href\\": \\"/community/team\\"
|
||||
\\"href\\": \\"/community/team\\",
|
||||
\\"docId\\": \\"team\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"team\\": {
|
||||
\\"id\\": \\"team\\",
|
||||
\\"title\\": \\"team\\",
|
||||
\\"description\\": \\"Team 1.0.0\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/community\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
"version-current-metadata-prop-751.json": "{
|
||||
|
@ -1169,9 +1285,18 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"Team title translated\\",
|
||||
\\"href\\": \\"/community/next/team\\"
|
||||
\\"href\\": \\"/community/next/team\\",
|
||||
\\"docId\\": \\"team\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"team\\": {
|
||||
\\"id\\": \\"team\\",
|
||||
\\"title\\": \\"Team title translated\\",
|
||||
\\"description\\": \\"Team current version (translated)\\",
|
||||
\\"sidebar\\": \\"community\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
}
|
||||
|
@ -1279,6 +1404,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -1291,6 +1417,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Guides",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
|
@ -1299,17 +1426,18 @@ Object {
|
|||
|
||||
exports[`versioned website content: 101 version sidebars 1`] = `
|
||||
Object {
|
||||
"version-1.0.1/docs": Array [
|
||||
"VersionedSideBarNameDoesNotMatter/docs": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"collapsible": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-1.0.1/foo/bar",
|
||||
"id": "foo/bar",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -1322,6 +1450,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Guides",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
|
@ -1341,6 +1470,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -1353,6 +1483,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Guides",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
|
@ -1562,7 +1693,7 @@ Object {
|
|||
\\"tags\\": [],
|
||||
\\"version\\": \\"1.0.1\\",
|
||||
\\"frontMatter\\": {},
|
||||
\\"sidebar\\": \\"version-1.0.1/docs\\",
|
||||
\\"sidebar\\": \\"VersionedSideBarNameDoesNotMatter/docs\\",
|
||||
\\"next\\": {
|
||||
\\"title\\": \\"hello\\",
|
||||
\\"permalink\\": \\"/docs/\\"
|
||||
|
@ -1581,7 +1712,7 @@ Object {
|
|||
\\"tags\\": [],
|
||||
\\"version\\": \\"1.0.1\\",
|
||||
\\"frontMatter\\": {},
|
||||
\\"sidebar\\": \\"version-1.0.1/docs\\",
|
||||
\\"sidebar\\": \\"VersionedSideBarNameDoesNotMatter/docs\\",
|
||||
\\"previous\\": {
|
||||
\\"title\\": \\"bar\\",
|
||||
\\"permalink\\": \\"/docs/foo/bar\\"
|
||||
|
@ -1791,12 +1922,14 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"bar\\",
|
||||
\\"href\\": \\"/docs/1.0.0/foo/barSlug\\"
|
||||
\\"href\\": \\"/docs/1.0.0/foo/barSlug\\",
|
||||
\\"docId\\": \\"foo/bar\\"
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"baz\\",
|
||||
\\"href\\": \\"/docs/1.0.0/foo/baz\\"
|
||||
\\"href\\": \\"/docs/1.0.0/foo/baz\\",
|
||||
\\"docId\\": \\"foo/baz\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1809,11 +1942,32 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"hello\\",
|
||||
\\"href\\": \\"/docs/1.0.0/\\"
|
||||
\\"href\\": \\"/docs/1.0.0/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"foo/bar\\": {
|
||||
\\"id\\": \\"foo/bar\\",
|
||||
\\"title\\": \\"bar\\",
|
||||
\\"description\\": \\"Bar 1.0.0 !\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/docs\\"
|
||||
},
|
||||
\\"foo/baz\\": {
|
||||
\\"id\\": \\"foo/baz\\",
|
||||
\\"title\\": \\"baz\\",
|
||||
\\"description\\": \\"Baz 1.0.0 ! This will be deleted in next subsequent versions.\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/docs\\"
|
||||
},
|
||||
\\"hello\\": {
|
||||
\\"id\\": \\"hello\\",
|
||||
\\"title\\": \\"hello\\",
|
||||
\\"description\\": \\"Hello 1.0.0 ! (translated en)\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/docs\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
"version-1-0-1-metadata-prop-e87.json": "{
|
||||
|
@ -1825,7 +1979,7 @@ Object {
|
|||
\\"className\\": \\"docs-version-1.0.1\\",
|
||||
\\"isLast\\": true,
|
||||
\\"docsSidebars\\": {
|
||||
\\"version-1.0.1/docs\\": [
|
||||
\\"VersionedSideBarNameDoesNotMatter/docs\\": [
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
|
@ -1835,7 +1989,8 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"bar\\",
|
||||
\\"href\\": \\"/docs/foo/bar\\"
|
||||
\\"href\\": \\"/docs/foo/bar\\",
|
||||
\\"docId\\": \\"foo/bar\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1848,11 +2003,26 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"hello\\",
|
||||
\\"href\\": \\"/docs/\\"
|
||||
\\"href\\": \\"/docs/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"foo/bar\\": {
|
||||
\\"id\\": \\"foo/bar\\",
|
||||
\\"title\\": \\"bar\\",
|
||||
\\"description\\": \\"Bar 1.0.1 !\\",
|
||||
\\"sidebar\\": \\"VersionedSideBarNameDoesNotMatter/docs\\"
|
||||
},
|
||||
\\"hello\\": {
|
||||
\\"id\\": \\"hello\\",
|
||||
\\"title\\": \\"hello\\",
|
||||
\\"description\\": \\"Hello 1.0.1 !\\",
|
||||
\\"sidebar\\": \\"VersionedSideBarNameDoesNotMatter/docs\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
"version-current-metadata-prop-751.json": "{
|
||||
|
@ -1874,7 +2044,8 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"bar\\",
|
||||
\\"href\\": \\"/docs/next/foo/barSlug\\"
|
||||
\\"href\\": \\"/docs/next/foo/barSlug\\",
|
||||
\\"docId\\": \\"foo/bar\\"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1887,11 +2058,46 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"hello\\",
|
||||
\\"href\\": \\"/docs/next/\\"
|
||||
\\"href\\": \\"/docs/next/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"foo/bar\\": {
|
||||
\\"id\\": \\"foo/bar\\",
|
||||
\\"title\\": \\"bar\\",
|
||||
\\"description\\": \\"This is next version of bar.\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"hello\\": {
|
||||
\\"id\\": \\"hello\\",
|
||||
\\"title\\": \\"hello\\",
|
||||
\\"description\\": \\"Hello next !\\",
|
||||
\\"sidebar\\": \\"docs\\"
|
||||
},
|
||||
\\"slugs/absoluteSlug\\": {
|
||||
\\"id\\": \\"slugs/absoluteSlug\\",
|
||||
\\"title\\": \\"absoluteSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/relativeSlug\\": {
|
||||
\\"id\\": \\"slugs/relativeSlug\\",
|
||||
\\"title\\": \\"relativeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/resolvedSlug\\": {
|
||||
\\"id\\": \\"slugs/resolvedSlug\\",
|
||||
\\"title\\": \\"resolvedSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/tryToEscapeSlug\\": {
|
||||
\\"id\\": \\"slugs/tryToEscapeSlug\\",
|
||||
\\"title\\": \\"tryToEscapeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
"version-with-slugs-metadata-prop-2bf.json": "{
|
||||
|
@ -1913,11 +2119,55 @@ Object {
|
|||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"rootAbsoluteSlug\\",
|
||||
\\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\"
|
||||
\\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
|
||||
\\"docId\\": \\"rootAbsoluteSlug\\"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
\\"docs\\": {
|
||||
\\"rootAbsoluteSlug\\": {
|
||||
\\"id\\": \\"rootAbsoluteSlug\\",
|
||||
\\"title\\": \\"rootAbsoluteSlug\\",
|
||||
\\"description\\": \\"Lorem\\",
|
||||
\\"sidebar\\": \\"version-1.0.1/docs\\"
|
||||
},
|
||||
\\"rootRelativeSlug\\": {
|
||||
\\"id\\": \\"rootRelativeSlug\\",
|
||||
\\"title\\": \\"rootRelativeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"rootResolvedSlug\\": {
|
||||
\\"id\\": \\"rootResolvedSlug\\",
|
||||
\\"title\\": \\"rootResolvedSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"rootTryToEscapeSlug\\": {
|
||||
\\"id\\": \\"rootTryToEscapeSlug\\",
|
||||
\\"title\\": \\"rootTryToEscapeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/absoluteSlug\\": {
|
||||
\\"id\\": \\"slugs/absoluteSlug\\",
|
||||
\\"title\\": \\"absoluteSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/relativeSlug\\": {
|
||||
\\"id\\": \\"slugs/relativeSlug\\",
|
||||
\\"title\\": \\"relativeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/resolvedSlug\\": {
|
||||
\\"id\\": \\"slugs/resolvedSlug\\",
|
||||
\\"title\\": \\"resolvedSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
},
|
||||
\\"slugs/tryToEscapeSlug\\": {
|
||||
\\"id\\": \\"slugs/tryToEscapeSlug\\",
|
||||
\\"title\\": \\"tryToEscapeSlug\\",
|
||||
\\"description\\": \\"Lorem\\"
|
||||
}
|
||||
}
|
||||
}",
|
||||
}
|
||||
|
@ -1973,12 +2223,12 @@ Object {
|
|||
Object {
|
||||
"id": "foo/bar",
|
||||
"path": "/docs/foo/bar",
|
||||
"sidebar": "version-1.0.1/docs",
|
||||
"sidebar": "VersionedSideBarNameDoesNotMatter/docs",
|
||||
},
|
||||
Object {
|
||||
"id": "hello",
|
||||
"path": "/docs/",
|
||||
"sidebar": "version-1.0.1/docs",
|
||||
"sidebar": "VersionedSideBarNameDoesNotMatter/docs",
|
||||
},
|
||||
],
|
||||
"isLast": true,
|
||||
|
@ -2291,7 +2541,7 @@ Array [
|
|||
"content": "@site/versioned_docs/version-1.0.1/hello.md",
|
||||
},
|
||||
"path": "/docs/",
|
||||
"sidebar": "version-1.0.1/docs",
|
||||
"sidebar": "VersionedSideBarNameDoesNotMatter/docs",
|
||||
},
|
||||
Object {
|
||||
"component": "@theme/DocItem",
|
||||
|
@ -2300,7 +2550,7 @@ Array [
|
|||
"content": "@site/versioned_docs/version-1.0.1/foo/bar.md",
|
||||
},
|
||||
"path": "/docs/foo/bar",
|
||||
"sidebar": "version-1.0.1/docs",
|
||||
"sidebar": "VersionedSideBarNameDoesNotMatter/docs",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -2320,6 +2570,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -8,6 +8,14 @@ Array [
|
|||
"description": "The label for category Getting started in sidebar docs",
|
||||
"message": "Getting started",
|
||||
},
|
||||
"sidebar.docs.category.Getting started.link.generated-index.description": Object {
|
||||
"description": "The generated-index page description for category Getting started in sidebar docs",
|
||||
"message": "Getting started index description",
|
||||
},
|
||||
"sidebar.docs.category.Getting started.link.generated-index.title": Object {
|
||||
"description": "The generated-index page title for category Getting started in sidebar docs",
|
||||
"message": "Getting started index title",
|
||||
},
|
||||
"sidebar.docs.link.Link label": Object {
|
||||
"description": "The label for link Link label in sidebar docs, linking to https://facebook.com",
|
||||
"message": "Link label",
|
||||
|
@ -25,6 +33,14 @@ Array [
|
|||
"description": "The label for category Getting started in sidebar docs",
|
||||
"message": "Getting started",
|
||||
},
|
||||
"sidebar.docs.category.Getting started.link.generated-index.description": Object {
|
||||
"description": "The generated-index page description for category Getting started in sidebar docs",
|
||||
"message": "Getting started index description",
|
||||
},
|
||||
"sidebar.docs.category.Getting started.link.generated-index.title": Object {
|
||||
"description": "The generated-index page title for category Getting started in sidebar docs",
|
||||
"message": "Getting started index title",
|
||||
},
|
||||
"sidebar.docs.link.Link label": Object {
|
||||
"description": "The label for link Link label in sidebar docs, linking to https://facebook.com",
|
||||
"message": "Link label",
|
||||
|
@ -42,6 +58,14 @@ Array [
|
|||
"description": "The label for category Getting started in sidebar docs",
|
||||
"message": "Getting started",
|
||||
},
|
||||
"sidebar.docs.category.Getting started.link.generated-index.description": Object {
|
||||
"description": "The generated-index page description for category Getting started in sidebar docs",
|
||||
"message": "Getting started index description",
|
||||
},
|
||||
"sidebar.docs.category.Getting started.link.generated-index.title": Object {
|
||||
"description": "The generated-index page title for category Getting started in sidebar docs",
|
||||
"message": "Getting started index title",
|
||||
},
|
||||
"sidebar.docs.link.Link label": Object {
|
||||
"description": "The label for link Link label in sidebar docs, linking to https://facebook.com",
|
||||
"message": "Link label",
|
||||
|
@ -177,6 +201,13 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Getting started (translated)",
|
||||
"link": Object {
|
||||
"description": "Getting started index description (translated)",
|
||||
"permalink": "/docs/category/getting-started-index-slug",
|
||||
"slug": "/category/getting-started-index-slug",
|
||||
"title": "Getting started index title (translated)",
|
||||
"type": "generated-index",
|
||||
},
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -317,6 +348,13 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Getting started (translated)",
|
||||
"link": Object {
|
||||
"description": "Getting started index description (translated)",
|
||||
"permalink": "/docs/category/getting-started-index-slug",
|
||||
"slug": "/category/getting-started-index-slug",
|
||||
"title": "Getting started index title (translated)",
|
||||
"type": "generated-index",
|
||||
},
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -457,6 +495,13 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Getting started (translated)",
|
||||
"link": Object {
|
||||
"description": "Getting started index description (translated)",
|
||||
"permalink": "/docs/category/getting-started-index-slug",
|
||||
"slug": "/category/getting-started-index-slug",
|
||||
"title": "Getting started index title (translated)",
|
||||
"type": "generated-index",
|
||||
},
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
|
|
@ -11,7 +11,8 @@ import {
|
|||
processDocMetadata,
|
||||
readVersionDocs,
|
||||
readDocFile,
|
||||
handleNavigation,
|
||||
addDocNavigation,
|
||||
isConventionalDocIndex,
|
||||
} from '../docs';
|
||||
import {loadSidebars} from '../sidebars';
|
||||
import {readVersionsMetadata} from '../versions';
|
||||
|
@ -28,7 +29,8 @@ import type {LoadContext} from '@docusaurus/types';
|
|||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
import {DEFAULT_OPTIONS} from '../options';
|
||||
import {Optional} from 'utility-types';
|
||||
import {posixPath} from '@docusaurus/utils';
|
||||
import {createSlugger, posixPath} from '@docusaurus/utils';
|
||||
import {createSidebarsUtils} from '../sidebars/utils';
|
||||
|
||||
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||
|
||||
|
@ -119,7 +121,7 @@ function createTestUtils({
|
|||
|
||||
async function generateNavigation(
|
||||
docFiles: DocFile[],
|
||||
): Promise<[DocNavLink, DocNavLink][]> {
|
||||
): Promise<[DocNavLink | undefined, DocNavLink | undefined][]> {
|
||||
const rawDocs = await Promise.all(
|
||||
docFiles.map((docFile) =>
|
||||
processDocMetadata({
|
||||
|
@ -136,16 +138,19 @@ function createTestUtils({
|
|||
numberPrefixParser: options.numberPrefixParser,
|
||||
docs: rawDocs,
|
||||
version: versionMetadata,
|
||||
options: {
|
||||
sidebarOptions: {
|
||||
sidebarCollapsed: false,
|
||||
sidebarCollapsible: true,
|
||||
},
|
||||
categoryLabelSlugger: createSlugger(),
|
||||
});
|
||||
return handleNavigation(
|
||||
const sidebarsUtils = createSidebarsUtils(sidebars);
|
||||
|
||||
return addDocNavigation(
|
||||
rawDocs,
|
||||
sidebars,
|
||||
sidebarsUtils,
|
||||
versionMetadata.sidebarFilePath as string,
|
||||
).docs.map((doc) => [doc.previous, doc.next]);
|
||||
).map((doc) => [doc.previous, doc.next]);
|
||||
}
|
||||
|
||||
return {processDocFile, testMeta, testSlug, generateNavigation};
|
||||
|
@ -1022,3 +1027,113 @@ describe('versioned site', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConventionalDocIndex', () => {
|
||||
test('supports readme', () => {
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'readme.md',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'readme.mdx',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'README.md',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'parent/ReAdMe',
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('supports index', () => {
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'index.md',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'index.mdx',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'INDEX.md',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'parent/InDeX',
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('supports <categoryName>/<categoryName>.md', () => {
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'someCategory',
|
||||
source: 'someCategory',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'someCategory',
|
||||
source: 'someCategory.md',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'someCategory',
|
||||
source: 'someCategory.mdx',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'some_category',
|
||||
source: 'SOME_CATEGORY.md',
|
||||
}),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'some_category',
|
||||
source: 'parent/some_category',
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('reject other cases', () => {
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'someCategory',
|
||||
source: 'some_Category',
|
||||
}),
|
||||
).toEqual(false);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'read_me',
|
||||
}),
|
||||
).toEqual(false);
|
||||
expect(
|
||||
isConventionalDocIndex({
|
||||
sourceDirName: 'doesNotMatter',
|
||||
source: 'the index',
|
||||
}),
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -619,6 +619,32 @@ describe('versioned website', () => {
|
|||
{label: 'barTag 3', permalink: '/docs/next/tags/barTag-3-permalink'},
|
||||
],
|
||||
});
|
||||
expect(getDocById(version101, 'foo/bar')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
id: 'version-1.0.1/foo/bar',
|
||||
unversionedId: 'foo/bar',
|
||||
sourceDirName: 'foo',
|
||||
isDocsHomePage: false,
|
||||
permalink: '/docs/foo/bar',
|
||||
slug: '/foo/bar',
|
||||
source: path.posix.join(
|
||||
'@site',
|
||||
posixPath(path.relative(siteDir, version101.contentPath)),
|
||||
'foo',
|
||||
'bar.md',
|
||||
),
|
||||
title: 'bar',
|
||||
description: 'Bar 1.0.1 !',
|
||||
frontMatter: {},
|
||||
version: '1.0.1',
|
||||
sidebar: 'VersionedSideBarNameDoesNotMatter/docs',
|
||||
next: {
|
||||
title: 'hello',
|
||||
permalink: '/docs/',
|
||||
},
|
||||
tags: [],
|
||||
});
|
||||
|
||||
expect(getDocById(currentVersion, 'hello')).toEqual({
|
||||
...defaultDocMetadata,
|
||||
id: 'hello',
|
||||
|
@ -659,7 +685,7 @@ describe('versioned website', () => {
|
|||
description: 'Hello 1.0.1 !',
|
||||
frontMatter: {},
|
||||
version: '1.0.1',
|
||||
sidebar: 'version-1.0.1/docs',
|
||||
sidebar: 'VersionedSideBarNameDoesNotMatter/docs',
|
||||
previous: {
|
||||
title: 'bar',
|
||||
permalink: '/docs/foo/bar',
|
||||
|
|
|
@ -51,6 +51,8 @@ describe('normalizeDocsPluginOptions', () => {
|
|||
docItemComponent: '@theme/DocItem',
|
||||
docTagDocListComponent: '@theme/DocTagDocListPage',
|
||||
docTagsListComponent: '@theme/DocTagsListPage',
|
||||
docCategoryGeneratedIndexComponent:
|
||||
'@theme/DocCategoryGeneratedIndexPage',
|
||||
remarkPlugins: [markdownPluginsObjectStub],
|
||||
rehypePlugins: [markdownPluginsFunctionStub],
|
||||
beforeDefaultRehypePlugins: [],
|
||||
|
|
|
@ -9,24 +9,92 @@ import getSlug from '../slug';
|
|||
|
||||
describe('getSlug', () => {
|
||||
test('should default to dirname/id', () => {
|
||||
expect(getSlug({baseID: 'doc', dirName: '/dir'})).toEqual('/dir/doc');
|
||||
expect(getSlug({baseID: 'doc', dirName: '/dir/subdir'})).toEqual(
|
||||
'/dir/subdir/doc',
|
||||
);
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/doc.md',
|
||||
sourceDirName: '/dir',
|
||||
}),
|
||||
).toEqual('/dir/doc');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/doc.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/doc');
|
||||
});
|
||||
|
||||
test('should handle conventional doc indexes', () => {
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/index.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/inDEx.mdx',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/readme.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/reADMe.mdx',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/subdir.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/suBDir.mdx',
|
||||
sourceDirName: '/dir/subdir',
|
||||
}),
|
||||
).toEqual('/dir/subdir/');
|
||||
});
|
||||
|
||||
test('should ignore conventional doc index when explicit slug frontmatter is provided', () => {
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
source: '@site/docs/dir/subdir/index.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
frontmatterSlug: '/my/frontMatterSlug',
|
||||
}),
|
||||
).toEqual('/my/frontMatterSlug');
|
||||
});
|
||||
|
||||
test('can strip dir number prefixes', () => {
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
dirName: '/001-dir1/002-dir2',
|
||||
source: '@site/docs/001-dir1/002-dir2/doc.md',
|
||||
sourceDirName: '/001-dir1/002-dir2',
|
||||
stripDirNumberPrefixes: true,
|
||||
}),
|
||||
).toEqual('/dir1/dir2/doc');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'doc',
|
||||
dirName: '/001-dir1/002-dir2',
|
||||
source: '@site/docs/001-dir1/002-dir2/doc.md',
|
||||
sourceDirName: '/001-dir1/002-dir2',
|
||||
stripDirNumberPrefixes: false,
|
||||
}),
|
||||
).toEqual('/001-dir1/002-dir2/doc');
|
||||
|
@ -35,26 +103,45 @@ describe('getSlug', () => {
|
|||
// See https://github.com/facebook/docusaurus/issues/3223
|
||||
test('should handle special chars in doc path', () => {
|
||||
expect(
|
||||
getSlug({baseID: 'my dôc', dirName: '/dir with spâce/hey $hello'}),
|
||||
getSlug({
|
||||
baseID: 'my dôc',
|
||||
source: '@site/docs/dir with spâce/hey $hello/doc.md',
|
||||
sourceDirName: '/dir with spâce/hey $hello',
|
||||
}),
|
||||
).toEqual('/dir with spâce/hey $hello/my dôc');
|
||||
});
|
||||
|
||||
test('should handle current dir', () => {
|
||||
expect(getSlug({baseID: 'doc', dirName: '.'})).toEqual('/doc');
|
||||
expect(getSlug({baseID: 'doc', dirName: '/'})).toEqual('/doc');
|
||||
expect(
|
||||
getSlug({baseID: 'doc', source: '@site/docs/doc.md', sourceDirName: '.'}),
|
||||
).toEqual('/doc');
|
||||
expect(
|
||||
getSlug({baseID: 'doc', source: '@site/docs/doc.md', sourceDirName: '/'}),
|
||||
).toEqual('/doc');
|
||||
});
|
||||
|
||||
test('should resolve absolute slug frontmatter', () => {
|
||||
expect(
|
||||
getSlug({baseID: 'any', dirName: '.', frontmatterSlug: '/abc/def'}),
|
||||
).toEqual('/abc/def');
|
||||
expect(
|
||||
getSlug({baseID: 'any', dirName: './any', frontmatterSlug: '/abc/def'}),
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
source: '@site/docs/doc.md',
|
||||
sourceDirName: '.',
|
||||
frontmatterSlug: '/abc/def',
|
||||
}),
|
||||
).toEqual('/abc/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
dirName: './any/any',
|
||||
source: '@site/docs/any/doc.md',
|
||||
sourceDirName: './any',
|
||||
frontmatterSlug: '/abc/def',
|
||||
}),
|
||||
).toEqual('/abc/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
source: '@site/docs/any/any/doc.md',
|
||||
sourceDirName: './any/any',
|
||||
frontmatterSlug: '/abc/def',
|
||||
}),
|
||||
).toEqual('/abc/def');
|
||||
|
@ -62,46 +149,66 @@ describe('getSlug', () => {
|
|||
|
||||
test('should resolve relative slug frontmatter', () => {
|
||||
expect(
|
||||
getSlug({baseID: 'any', dirName: '.', frontmatterSlug: 'abc/def'}),
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
source: '@site/docs/doc.md',
|
||||
sourceDirName: '.',
|
||||
frontmatterSlug: 'abc/def',
|
||||
}),
|
||||
).toEqual('/abc/def');
|
||||
expect(
|
||||
getSlug({baseID: 'any', dirName: '/dir', frontmatterSlug: 'abc/def'}),
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
source: '@site/docs/dir/doc.md',
|
||||
sourceDirName: '/dir',
|
||||
frontmatterSlug: 'abc/def',
|
||||
}),
|
||||
).toEqual('/dir/abc/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
dirName: 'unslashedDir',
|
||||
source: '@site/docs/unslashedDir/doc.md',
|
||||
sourceDirName: 'unslashedDir',
|
||||
frontmatterSlug: 'abc/def',
|
||||
}),
|
||||
).toEqual('/unslashedDir/abc/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
dirName: 'dir/subdir',
|
||||
source: '@site/docs/dir/subdir/doc.md',
|
||||
sourceDirName: 'dir/subdir',
|
||||
frontmatterSlug: 'abc/def',
|
||||
}),
|
||||
).toEqual('/dir/subdir/abc/def');
|
||||
expect(
|
||||
getSlug({baseID: 'any', dirName: '/dir', frontmatterSlug: './abc/def'}),
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
source: '@site/docs/dir/doc.md',
|
||||
sourceDirName: '/dir',
|
||||
frontmatterSlug: './abc/def',
|
||||
}),
|
||||
).toEqual('/dir/abc/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
dirName: '/dir',
|
||||
source: '@site/docs/dir/doc.md',
|
||||
sourceDirName: '/dir',
|
||||
frontmatterSlug: './abc/../def',
|
||||
}),
|
||||
).toEqual('/dir/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
dirName: '/dir/subdir',
|
||||
source: '@site/docs/dir/subdir/doc.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
frontmatterSlug: '../abc/def',
|
||||
}),
|
||||
).toEqual('/dir/abc/def');
|
||||
expect(
|
||||
getSlug({
|
||||
baseID: 'any',
|
||||
dirName: '/dir/subdir',
|
||||
source: '@site/docs/dir/subdirdoc.md',
|
||||
sourceDirName: '/dir/subdir',
|
||||
frontmatterSlug: '../../../../../abc/../def',
|
||||
}),
|
||||
).toEqual('/def');
|
||||
|
|
|
@ -68,6 +68,13 @@ function createSampleVersion(
|
|||
type: 'category',
|
||||
label: 'Getting started',
|
||||
collapsed: false,
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: '/category/getting-started-index-slug',
|
||||
permalink: '/docs/category/getting-started-index-slug',
|
||||
title: 'Getting started index title',
|
||||
description: 'Getting started index description',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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 {CategoryGeneratedIndexMetadata, DocMetadataBase} from './types';
|
||||
import {SidebarItemCategoryWithGeneratedIndex} from './sidebars/types';
|
||||
import {SidebarsUtils, toNavigationLink} from './sidebars/utils';
|
||||
import {createDocsByIdIndex} from './docs';
|
||||
|
||||
function getCategoryGeneratedIndexMetadata({
|
||||
category,
|
||||
sidebarsUtils,
|
||||
docsById,
|
||||
}: {
|
||||
category: SidebarItemCategoryWithGeneratedIndex;
|
||||
sidebarsUtils: SidebarsUtils;
|
||||
docsById: Record<string, DocMetadataBase>;
|
||||
}): CategoryGeneratedIndexMetadata {
|
||||
const {sidebarName, previous, next} =
|
||||
sidebarsUtils.getCategoryGeneratedIndexNavigation(category.link.permalink);
|
||||
if (!sidebarName) {
|
||||
throw new Error('unexpected');
|
||||
}
|
||||
|
||||
return {
|
||||
title: category.link.title ?? category.label,
|
||||
description: category.link.description,
|
||||
slug: category.link.slug,
|
||||
permalink: category.link.permalink,
|
||||
sidebar: sidebarName,
|
||||
previous: toNavigationLink(previous, docsById),
|
||||
next: toNavigationLink(next, docsById),
|
||||
};
|
||||
}
|
||||
|
||||
export function getCategoryGeneratedIndexMetadataList({
|
||||
docs,
|
||||
sidebarsUtils,
|
||||
}: {
|
||||
sidebarsUtils: SidebarsUtils;
|
||||
docs: DocMetadataBase[];
|
||||
}): CategoryGeneratedIndexMetadata[] {
|
||||
const docsById = createDocsByIdIndex(docs);
|
||||
|
||||
const categoryGeneratedIndexItems =
|
||||
sidebarsUtils.getCategoryGeneratedIndexList();
|
||||
return categoryGeneratedIndexItems.map((category) =>
|
||||
getCategoryGeneratedIndexMetadata({
|
||||
category,
|
||||
sidebarsUtils,
|
||||
docsById,
|
||||
}),
|
||||
);
|
||||
}
|
|
@ -13,9 +13,7 @@ import {
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import type {PathOptions, SidebarOptions} from './types';
|
||||
import {transformSidebarItems} from './sidebars/utils';
|
||||
import type {SidebarItem, NormalizedSidebars, Sidebar} from './sidebars/types';
|
||||
import {loadUnprocessedSidebars, resolveSidebarPathOption} from './sidebars';
|
||||
import {loadSidebarsFile, resolveSidebarPathOption} from './sidebars';
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
|
||||
function createVersionedSidebarFile({
|
||||
|
@ -23,47 +21,20 @@ function createVersionedSidebarFile({
|
|||
pluginId,
|
||||
sidebarPath,
|
||||
version,
|
||||
options,
|
||||
}: {
|
||||
siteDir: string;
|
||||
pluginId: string;
|
||||
sidebarPath: string | false | undefined;
|
||||
version: string;
|
||||
options: SidebarOptions;
|
||||
}) {
|
||||
// Load current sidebar and create a new versioned sidebars file (if needed).
|
||||
const loadedSidebars = loadUnprocessedSidebars(sidebarPath, options);
|
||||
// Note: we don't need the sidebars file to be normalized: it's ok to let plugin option changes to impact older, versioned sidebars
|
||||
const sidebars = loadSidebarsFile(sidebarPath);
|
||||
|
||||
// Do not create a useless versioned sidebars file if sidebars file is empty or sidebars are disabled/false)
|
||||
const shouldCreateVersionedSidebarFile =
|
||||
Object.keys(loadedSidebars).length > 0;
|
||||
const shouldCreateVersionedSidebarFile = Object.keys(sidebars).length > 0;
|
||||
|
||||
if (shouldCreateVersionedSidebarFile) {
|
||||
// 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 prependVersion = (item: SidebarItem): SidebarItem => {
|
||||
if (item.type === 'ref' || item.type === 'doc') {
|
||||
return {
|
||||
type: item.type,
|
||||
id: `version-${version}/${item.id}`,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
const versionedSidebar = Object.entries(loadedSidebars).reduce(
|
||||
(acc: NormalizedSidebars, [sidebarId, sidebar]) => {
|
||||
const versionedId = `version-${version}/${sidebarId}`;
|
||||
acc[versionedId] = transformSidebarItems(
|
||||
sidebar as Sidebar,
|
||||
prependVersion,
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId);
|
||||
const newSidebarFile = path.join(
|
||||
versionedSidebarsDir,
|
||||
|
@ -72,7 +43,7 @@ function createVersionedSidebarFile({
|
|||
fs.ensureDirSync(path.dirname(newSidebarFile));
|
||||
fs.writeFileSync(
|
||||
newSidebarFile,
|
||||
`${JSON.stringify(versionedSidebar, null, 2)}\n`,
|
||||
`${JSON.stringify(sidebars, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
@ -155,7 +126,6 @@ export function cliDocsVersionCommand(
|
|||
pluginId,
|
||||
version,
|
||||
sidebarPath: resolveSidebarPathOption(siteDir, sidebarPath),
|
||||
options,
|
||||
});
|
||||
|
||||
// Update versions.json file.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import {keyBy} from 'lodash';
|
||||
import {keyBy, last} from 'lodash';
|
||||
import {
|
||||
aliasedSitePath,
|
||||
getEditUrl,
|
||||
|
@ -38,8 +38,11 @@ import {CURRENT_VERSION_NAME} from './constants';
|
|||
import {getDocsDirPaths} from './versions';
|
||||
import {stripPathNumberPrefixes} from './numberPrefix';
|
||||
import {validateDocFrontMatter} from './docFrontMatter';
|
||||
import type {Sidebars} from './sidebars/types';
|
||||
import {createSidebarsUtils} from './sidebars/utils';
|
||||
import {
|
||||
SidebarsUtils,
|
||||
toDocNavigationLink,
|
||||
toNavigationLink,
|
||||
} from './sidebars/utils';
|
||||
|
||||
type LastUpdateOptions = Pick<
|
||||
PluginOptions,
|
||||
|
@ -205,7 +208,8 @@ function doProcessDocMetadata({
|
|||
? '/'
|
||||
: getSlug({
|
||||
baseID,
|
||||
dirName: sourceDirName,
|
||||
source,
|
||||
sourceDirName,
|
||||
frontmatterSlug: frontMatter.slug,
|
||||
stripDirNumberPrefixes: parseNumberPrefixes,
|
||||
numberPrefixParser: options.numberPrefixParser,
|
||||
|
@ -291,68 +295,76 @@ export function processDocMetadata(args: {
|
|||
}
|
||||
}
|
||||
|
||||
export function handleNavigation(
|
||||
export function addDocNavigation(
|
||||
docsBase: DocMetadataBase[],
|
||||
sidebars: Sidebars,
|
||||
sidebarsUtils: SidebarsUtils,
|
||||
sidebarFilePath: string,
|
||||
): Pick<LoadedVersion, 'mainDocId' | 'docs'> {
|
||||
const docsBaseById = keyBy(docsBase, (doc) => doc.id);
|
||||
const {checkSidebarsDocIds, getDocNavigation, getFirstDocIdOfFirstSidebar} =
|
||||
createSidebarsUtils(sidebars);
|
||||
): LoadedVersion['docs'] {
|
||||
const docsById = createDocsByIdIndex(docsBase);
|
||||
|
||||
const validDocIds = Object.keys(docsBaseById);
|
||||
checkSidebarsDocIds(validDocIds, sidebarFilePath);
|
||||
sidebarsUtils.checkSidebarsDocIds(
|
||||
docsBase.flatMap(getDocIds),
|
||||
sidebarFilePath,
|
||||
);
|
||||
|
||||
// Add sidebar/next/previous to the docs
|
||||
function addNavData(doc: DocMetadataBase): DocMetadata {
|
||||
const {sidebarName, previousId, nextId} = getDocNavigation(doc.id);
|
||||
const toDocNavLink = (
|
||||
const navigation = sidebarsUtils.getDocNavigation(
|
||||
doc.unversionedId,
|
||||
doc.id,
|
||||
);
|
||||
|
||||
const toNavigationLinkByDocId = (
|
||||
docId: string | null | undefined,
|
||||
type: 'prev' | 'next',
|
||||
): DocNavLink | undefined => {
|
||||
if (!docId) {
|
||||
return undefined;
|
||||
}
|
||||
if (!docsBaseById[docId]) {
|
||||
const navDoc = docsById[docId];
|
||||
if (!navDoc) {
|
||||
// This could only happen if user provided the ID through front matter
|
||||
throw new Error(
|
||||
`Error when loading ${doc.id} in ${doc.sourceDirName}: the pagination_${type} front matter points to a non-existent ID ${docId}.`,
|
||||
);
|
||||
}
|
||||
const {
|
||||
title,
|
||||
permalink,
|
||||
frontMatter: {
|
||||
pagination_label: paginationLabel,
|
||||
sidebar_label: sidebarLabel,
|
||||
},
|
||||
} = docsBaseById[docId];
|
||||
return {title: paginationLabel ?? sidebarLabel ?? title, permalink};
|
||||
return toDocNavigationLink(navDoc);
|
||||
};
|
||||
const {
|
||||
frontMatter: {
|
||||
pagination_next: paginationNext = nextId,
|
||||
pagination_prev: paginationPrev = previousId,
|
||||
},
|
||||
} = doc;
|
||||
const previous = toDocNavLink(paginationPrev, 'prev');
|
||||
const next = toDocNavLink(paginationNext, 'next');
|
||||
return {...doc, sidebar: sidebarName, previous, next};
|
||||
}
|
||||
const docs = docsBase.map(addNavData);
|
||||
// sort to ensure consistent output for tests
|
||||
docs.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
/**
|
||||
const previous: DocNavLink | undefined = doc.frontMatter.pagination_prev
|
||||
? toNavigationLinkByDocId(doc.frontMatter.pagination_prev, 'prev')
|
||||
: toNavigationLink(navigation.previous, docsById);
|
||||
const next: DocNavLink | undefined = doc.frontMatter.pagination_next
|
||||
? toNavigationLinkByDocId(doc.frontMatter.pagination_next, 'next')
|
||||
: toNavigationLink(navigation.next, docsById);
|
||||
|
||||
return {...doc, sidebar: navigation.sidebarName, previous, next};
|
||||
}
|
||||
|
||||
const docsWithNavigation = docsBase.map(addNavData);
|
||||
// sort to ensure consistent output for tests
|
||||
docsWithNavigation.sort((a, b) => a.id.localeCompare(b.id));
|
||||
return docsWithNavigation;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "main doc" is the "version entry point"
|
||||
* We browse this doc by clicking on a version:
|
||||
* - the "home" doc (at '/docs/')
|
||||
* - the first doc of the first sidebar
|
||||
* - a random doc (if no docs are in any sidebar... edge case)
|
||||
*/
|
||||
export function getMainDocId({
|
||||
docs,
|
||||
sidebarsUtils,
|
||||
}: {
|
||||
docs: DocMetadataBase[];
|
||||
sidebarsUtils: SidebarsUtils;
|
||||
}): string {
|
||||
function getMainDoc(): DocMetadata {
|
||||
const versionHomeDoc = docs.find((doc) => doc.slug === '/');
|
||||
const firstDocIdOfFirstSidebar = getFirstDocIdOfFirstSidebar();
|
||||
const firstDocIdOfFirstSidebar =
|
||||
sidebarsUtils.getFirstDocIdOfFirstSidebar();
|
||||
if (versionHomeDoc) {
|
||||
return versionHomeDoc;
|
||||
} else if (firstDocIdOfFirstSidebar) {
|
||||
|
@ -362,5 +374,51 @@ export function handleNavigation(
|
|||
}
|
||||
}
|
||||
|
||||
return {mainDocId: getMainDoc().unversionedId, docs};
|
||||
return getMainDoc().unversionedId;
|
||||
}
|
||||
|
||||
function getLastPathSegment(str: string): string {
|
||||
return last(str.split('/'))!;
|
||||
}
|
||||
|
||||
// By convention, Docusaurus considers some docs are "indexes":
|
||||
// - index.md
|
||||
// - readme.md
|
||||
// - <folder>/<folder>.md
|
||||
//
|
||||
// Those index docs produce a different behavior
|
||||
// - Slugs do not end with a weird "/index" suffix
|
||||
// - Auto-generated sidebar categories link to them as intro
|
||||
export function isConventionalDocIndex(doc: {
|
||||
source: DocMetadataBase['slug'];
|
||||
sourceDirName: DocMetadataBase['sourceDirName'];
|
||||
}): boolean {
|
||||
// "@site/docs/folder/subFolder/subSubFolder/myDoc.md" => "myDoc"
|
||||
const docName = path.parse(doc.source).name;
|
||||
|
||||
// "folder/subFolder/subSubFolder" => "subSubFolder"
|
||||
const lastDirName = getLastPathSegment(doc.sourceDirName);
|
||||
|
||||
const eligibleDocIndexNames = ['index', 'readme', lastDirName.toLowerCase()];
|
||||
|
||||
return eligibleDocIndexNames.includes(docName.toLowerCase());
|
||||
}
|
||||
|
||||
// Return both doc ids
|
||||
// TODO legacy retro-compatibility due to old versioned sidebars using versioned doc ids
|
||||
// ("id" should be removed & "versionedId" should be renamed to "id")
|
||||
export function getDocIds(doc: DocMetadataBase): [string, string] {
|
||||
return [doc.unversionedId, doc.id];
|
||||
}
|
||||
|
||||
// docs are indexed by both versioned and unversioned ids at the same time
|
||||
// TODO legacy retro-compatibility due to old versioned sidebars using versioned doc ids
|
||||
// ("id" should be removed & "versionedId" should be renamed to "id")
|
||||
export function createDocsByIdIndex<
|
||||
Doc extends {id: string; unversionedId: string},
|
||||
>(docs: Doc[]): Record<string, Doc> {
|
||||
return {
|
||||
...keyBy(docs, (doc) => doc.unversionedId),
|
||||
...keyBy(docs, (doc) => doc.id),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,11 +16,17 @@ import {
|
|||
posixPath,
|
||||
addTrailingPathSeparator,
|
||||
createAbsoluteFilePathMatcher,
|
||||
createSlugger,
|
||||
} from '@docusaurus/utils';
|
||||
import type {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import {loadSidebars} from './sidebars';
|
||||
import {CategoryMetadataFilenamePattern} from './sidebars/generator';
|
||||
import {readVersionDocs, processDocMetadata, handleNavigation} from './docs';
|
||||
import {
|
||||
readVersionDocs,
|
||||
processDocMetadata,
|
||||
addDocNavigation,
|
||||
getMainDocId,
|
||||
} from './docs';
|
||||
import {getDocsDirPaths, readVersionsMetadata} from './versions';
|
||||
|
||||
import {
|
||||
|
@ -28,7 +34,6 @@ import {
|
|||
LoadedContent,
|
||||
SourceToPermalink,
|
||||
DocMetadataBase,
|
||||
DocMetadata,
|
||||
GlobalPluginData,
|
||||
VersionMetadata,
|
||||
LoadedVersion,
|
||||
|
@ -41,14 +46,17 @@ import {cliDocsVersionCommand} from './cli';
|
|||
import {VERSIONS_JSON_FILE} from './constants';
|
||||
import {keyBy, mapValues} from 'lodash';
|
||||
import {toGlobalDataVersion} from './globalData';
|
||||
import {toTagDocListProp, toVersionMetadataProp} from './props';
|
||||
import {toTagDocListProp} from './props';
|
||||
import {
|
||||
translateLoadedContent,
|
||||
getLoadedContentTranslationFiles,
|
||||
} from './translations';
|
||||
import chalk from 'chalk';
|
||||
import {getVersionTags} from './tags';
|
||||
import {createVersionRoutes} from './routes';
|
||||
import type {PropTagsListPage} from '@docusaurus/plugin-content-docs';
|
||||
import {createSidebarsUtils} from './sidebars/utils';
|
||||
import {getCategoryGeneratedIndexMetadataList} from './categoryGeneratedIndex';
|
||||
|
||||
export default function pluginContentDocs(
|
||||
context: LoadContext,
|
||||
|
@ -157,28 +165,37 @@ export default function pluginContentDocs(
|
|||
async function doLoadVersion(
|
||||
versionMetadata: VersionMetadata,
|
||||
): Promise<LoadedVersion> {
|
||||
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
|
||||
const docs: DocMetadataBase[] = await loadVersionDocsBase(
|
||||
versionMetadata,
|
||||
);
|
||||
|
||||
const sidebars = await loadSidebars(versionMetadata.sidebarFilePath, {
|
||||
sidebarItemsGenerator: options.sidebarItemsGenerator,
|
||||
numberPrefixParser: options.numberPrefixParser,
|
||||
docs: docsBase,
|
||||
docs,
|
||||
version: versionMetadata,
|
||||
options: {
|
||||
sidebarOptions: {
|
||||
sidebarCollapsed: options.sidebarCollapsed,
|
||||
sidebarCollapsible: options.sidebarCollapsible,
|
||||
},
|
||||
categoryLabelSlugger: createSlugger(),
|
||||
});
|
||||
|
||||
const sidebarsUtils = createSidebarsUtils(sidebars);
|
||||
|
||||
return {
|
||||
...versionMetadata,
|
||||
...handleNavigation(
|
||||
docsBase,
|
||||
sidebars,
|
||||
docs: addDocNavigation(
|
||||
docs,
|
||||
sidebarsUtils,
|
||||
versionMetadata.sidebarFilePath as string,
|
||||
),
|
||||
sidebars,
|
||||
mainDocId: getMainDocId({docs, sidebarsUtils}),
|
||||
categoryGeneratedIndices: getCategoryGeneratedIndexMetadataList({
|
||||
docs,
|
||||
sidebarsUtils,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -206,45 +223,17 @@ export default function pluginContentDocs(
|
|||
|
||||
async contentLoaded({content, actions}) {
|
||||
const {loadedVersions} = content;
|
||||
const {docLayoutComponent, docItemComponent} = options;
|
||||
const {
|
||||
docLayoutComponent,
|
||||
docItemComponent,
|
||||
docCategoryGeneratedIndexComponent,
|
||||
} = options;
|
||||
const {addRoute, createData, setGlobalData} = actions;
|
||||
|
||||
const createDocRoutes = async (
|
||||
docs: DocMetadata[],
|
||||
): Promise<RouteConfig[]> => {
|
||||
const routes = await Promise.all(
|
||||
docs.map(async (metadataItem) => {
|
||||
await createData(
|
||||
// Note that this created data path must be in sync with
|
||||
// metadataPath provided to mdx-loader.
|
||||
`${docuHash(metadataItem.source)}.json`,
|
||||
JSON.stringify(metadataItem, null, 2),
|
||||
);
|
||||
|
||||
const docRoute: RouteConfig = {
|
||||
path: metadataItem.permalink,
|
||||
component: docItemComponent,
|
||||
exact: true,
|
||||
modules: {
|
||||
content: metadataItem.source,
|
||||
},
|
||||
// Because the parent (DocPage) comp need to access it easily
|
||||
// This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state)
|
||||
...(metadataItem.sidebar && {
|
||||
sidebar: metadataItem.sidebar,
|
||||
}),
|
||||
};
|
||||
|
||||
return docRoute;
|
||||
}),
|
||||
);
|
||||
|
||||
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
||||
};
|
||||
|
||||
async function createVersionTagsRoutes(loadedVersion: LoadedVersion) {
|
||||
const versionTags = getVersionTags(loadedVersion.docs);
|
||||
async function createVersionTagsRoutes(version: LoadedVersion) {
|
||||
const versionTags = getVersionTags(version.docs);
|
||||
|
||||
// TODO tags should be a sub route of the version route
|
||||
async function createTagsListPage() {
|
||||
const tagsProp: PropTagsListPage['tags'] = Object.values(
|
||||
versionTags,
|
||||
|
@ -257,11 +246,11 @@ export default function pluginContentDocs(
|
|||
// Only create /tags page if there are tags.
|
||||
if (Object.keys(tagsProp).length > 0) {
|
||||
const tagsPropPath = await createData(
|
||||
`${docuHash(`tags-list-${loadedVersion.versionName}-prop`)}.json`,
|
||||
`${docuHash(`tags-list-${version.versionName}-prop`)}.json`,
|
||||
JSON.stringify(tagsProp, null, 2),
|
||||
);
|
||||
addRoute({
|
||||
path: loadedVersion.tagsPath,
|
||||
path: version.tagsPath,
|
||||
exact: true,
|
||||
component: options.docTagsListComponent,
|
||||
modules: {
|
||||
|
@ -271,11 +260,12 @@ export default function pluginContentDocs(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO tags should be a sub route of the version route
|
||||
async function createTagDocListPage(tag: VersionTag) {
|
||||
const tagProps = toTagDocListProp({
|
||||
allTagsPath: loadedVersion.tagsPath,
|
||||
allTagsPath: version.tagsPath,
|
||||
tag,
|
||||
docs: loadedVersion.docs,
|
||||
docs: version.docs,
|
||||
});
|
||||
const tagPropPath = await createData(
|
||||
`${docuHash(`tag-${tag.permalink}`)}.json`,
|
||||
|
@ -295,50 +285,22 @@ export default function pluginContentDocs(
|
|||
await Promise.all(Object.values(versionTags).map(createTagDocListPage));
|
||||
}
|
||||
|
||||
async function doCreateVersionRoutes(
|
||||
loadedVersion: LoadedVersion,
|
||||
): Promise<void> {
|
||||
await createVersionTagsRoutes(loadedVersion);
|
||||
|
||||
const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion);
|
||||
const versionMetadataPropPath = await createData(
|
||||
`${docuHash(
|
||||
`version-${loadedVersion.versionName}-metadata-prop`,
|
||||
)}.json`,
|
||||
JSON.stringify(versionMetadata, null, 2),
|
||||
);
|
||||
|
||||
addRoute({
|
||||
path: loadedVersion.versionPath,
|
||||
// allow matching /docs/* as well
|
||||
exact: false,
|
||||
// main docs component (DocPage)
|
||||
component: docLayoutComponent,
|
||||
// sub-routes for each doc
|
||||
routes: await createDocRoutes(loadedVersion.docs),
|
||||
modules: {
|
||||
versionMetadata: aliasedSource(versionMetadataPropPath),
|
||||
},
|
||||
priority: loadedVersion.routePriority,
|
||||
});
|
||||
}
|
||||
|
||||
async function createVersionRoutes(
|
||||
loadedVersion: LoadedVersion,
|
||||
): Promise<void> {
|
||||
try {
|
||||
return await doCreateVersionRoutes(loadedVersion);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Can't create version routes for version "${loadedVersion.versionName}"`,
|
||||
await Promise.all(
|
||||
loadedVersions.map((loadedVersion) =>
|
||||
createVersionRoutes({
|
||||
loadedVersion,
|
||||
docItemComponent,
|
||||
docLayoutComponent,
|
||||
docCategoryGeneratedIndexComponent,
|
||||
pluginId,
|
||||
aliasedSource,
|
||||
actions,
|
||||
}),
|
||||
),
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(loadedVersions.map(createVersionRoutes));
|
||||
// TODO tags should be a sub route of the version route
|
||||
await Promise.all(loadedVersions.map(createVersionTagsRoutes));
|
||||
|
||||
setGlobalData<GlobalPluginData>({
|
||||
path: normalizeUrl([baseUrl, options.routeBasePath]),
|
||||
|
|
|
@ -39,6 +39,7 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
|
|||
docItemComponent: '@theme/DocItem',
|
||||
docTagDocListComponent: '@theme/DocTagDocListPage',
|
||||
docTagsListComponent: '@theme/DocTagsListPage',
|
||||
docCategoryGeneratedIndexComponent: '@theme/DocCategoryGeneratedIndexPage',
|
||||
remarkPlugins: [],
|
||||
rehypePlugins: [],
|
||||
beforeDefaultRemarkPlugins: [],
|
||||
|
@ -109,6 +110,9 @@ export const OptionsSchema = Joi.object({
|
|||
docTagDocListComponent: Joi.string().default(
|
||||
DEFAULT_OPTIONS.docTagDocListComponent,
|
||||
),
|
||||
docCategoryGeneratedIndexComponent: Joi.string().default(
|
||||
DEFAULT_OPTIONS.docCategoryGeneratedIndexComponent,
|
||||
),
|
||||
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
|
||||
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
|
||||
beforeDefaultRemarkPlugins: RemarkPluginsSchema.default(
|
||||
|
|
|
@ -15,6 +15,18 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
|
||||
export type {GlobalDataVersion, GlobalDataDoc};
|
||||
|
||||
export type PropNavigationLink = {
|
||||
readonly title: string;
|
||||
readonly permalink: string;
|
||||
};
|
||||
export type PropNavigation = {
|
||||
readonly previous?: PropNavigationLink;
|
||||
readonly next?: PropNavigationLink;
|
||||
};
|
||||
|
||||
export type PropVersionDoc = import('./sidebars/types').PropVersionDoc;
|
||||
export type PropVersionDocs = import('./sidebars/types').PropVersionDocs;
|
||||
|
||||
export type PropVersionMetadata = {
|
||||
pluginId: string;
|
||||
version: string;
|
||||
|
@ -24,12 +36,23 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
className: string;
|
||||
isLast: boolean;
|
||||
docsSidebars: PropSidebars;
|
||||
docs: PropVersionDocs;
|
||||
};
|
||||
|
||||
export type PropSidebarItemLink = import('./sidebars/types').SidebarItemLink;
|
||||
export type PropCategoryGeneratedIndex = {
|
||||
title: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
permalink: string;
|
||||
navigation: PropNavigation;
|
||||
};
|
||||
|
||||
export type PropSidebarItemLink =
|
||||
import('./sidebars/types').PropSidebarItemLink;
|
||||
export type PropSidebarItemCategory =
|
||||
import('./sidebars/types').PropSidebarItemCategory;
|
||||
export type PropSidebarItem = import('./sidebars/types').PropSidebarItem;
|
||||
export type PropSidebar = import('./sidebars/types').PropSidebar;
|
||||
export type PropSidebars = import('./sidebars/types').PropSidebars;
|
||||
|
||||
export type PropTagDocListDoc = {
|
||||
|
@ -56,7 +79,10 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
|
||||
declare module '@theme/DocItem' {
|
||||
import type {TOCItem} from '@docusaurus/types';
|
||||
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';
|
||||
import type {
|
||||
PropNavigationLink,
|
||||
PropVersionMetadata,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export type DocumentRoute = {
|
||||
readonly component: () => JSX.Element;
|
||||
|
@ -85,8 +111,8 @@ declare module '@theme/DocItem' {
|
|||
readonly formattedLastUpdatedAt?: string;
|
||||
readonly lastUpdatedBy?: string;
|
||||
readonly version?: string;
|
||||
readonly previous?: {readonly permalink: string; readonly title: string};
|
||||
readonly next?: {readonly permalink: string; readonly title: string};
|
||||
readonly previous?: PropNavigationLink;
|
||||
readonly next?: PropNavigationLink;
|
||||
readonly tags: readonly {
|
||||
readonly label: string;
|
||||
readonly permalink: string;
|
||||
|
@ -109,6 +135,38 @@ declare module '@theme/DocItem' {
|
|||
export default DocItem;
|
||||
}
|
||||
|
||||
declare module '@theme/DocCard' {
|
||||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
readonly item: PropSidebarItem;
|
||||
}
|
||||
|
||||
export default function DocCard(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocCardList' {
|
||||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
readonly items: PropSidebarItem[];
|
||||
}
|
||||
|
||||
export default function DocCardList(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocCategoryGeneratedIndexPage' {
|
||||
import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
readonly categoryGeneratedIndex: PropCategoryGeneratedIndex;
|
||||
}
|
||||
|
||||
export default function DocCategoryGeneratedIndexPage(
|
||||
props: Props,
|
||||
): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocItemFooter' {
|
||||
import type {Props} from '@theme/DocItem';
|
||||
|
||||
|
@ -132,14 +190,19 @@ declare module '@theme/DocTagDocListPage' {
|
|||
}
|
||||
|
||||
declare module '@theme/DocVersionBanner' {
|
||||
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
readonly versionMetadata: PropVersionMetadata;
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
const DocVersionBanner: (props: Props) => JSX.Element;
|
||||
export default DocVersionBanner;
|
||||
export default function DocVersionBanner(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocVersionBadge' {
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function DocVersionBadge(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/DocPage' {
|
||||
|
|
|
@ -8,25 +8,28 @@
|
|||
import type {LoadedVersion, VersionTag, DocMetadata} from './types';
|
||||
import type {
|
||||
SidebarItemDoc,
|
||||
SidebarItemLink,
|
||||
SidebarItem,
|
||||
SidebarItemCategory,
|
||||
SidebarItemCategoryLink,
|
||||
PropVersionDocs,
|
||||
} from './sidebars/types';
|
||||
import type {
|
||||
PropSidebars,
|
||||
PropVersionMetadata,
|
||||
PropSidebarItem,
|
||||
PropSidebarItemCategory,
|
||||
PropTagDocList,
|
||||
PropTagDocListDoc,
|
||||
PropSidebarItemLink,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import {compact, keyBy, mapValues} from 'lodash';
|
||||
import {createDocsByIdIndex} from './docs';
|
||||
|
||||
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
|
||||
const docsById = keyBy(loadedVersion.docs, (doc) => doc.id);
|
||||
const docsById = createDocsByIdIndex(loadedVersion.docs);
|
||||
|
||||
const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => {
|
||||
const docId = item.id;
|
||||
function getDocById(docId: string): DocMetadata {
|
||||
const docMetadata = docsById[docId];
|
||||
|
||||
if (!docMetadata) {
|
||||
throw new Error(
|
||||
`Invalid sidebars file. The document with id "${docId}" was used in the sidebar, but no document with this id could be found.
|
||||
|
@ -34,26 +37,49 @@ Available document ids are:
|
|||
- ${Object.keys(docsById).sort().join('\n- ')}`,
|
||||
);
|
||||
}
|
||||
return docMetadata;
|
||||
}
|
||||
|
||||
const convertDocLink = (item: SidebarItemDoc): PropSidebarItemLink => {
|
||||
const docMetadata = getDocById(item.id);
|
||||
const {
|
||||
title,
|
||||
permalink,
|
||||
frontMatter: {sidebar_label: sidebarLabel},
|
||||
} = docMetadata;
|
||||
|
||||
return {
|
||||
type: 'link',
|
||||
label: sidebarLabel || item.label || title,
|
||||
href: permalink,
|
||||
className: item.className,
|
||||
customProps: item.customProps,
|
||||
docId: docMetadata.unversionedId,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeItem = (item: SidebarItem): PropSidebarItem => {
|
||||
function getCategoryLinkHref(
|
||||
link: SidebarItemCategoryLink | undefined,
|
||||
): string | undefined {
|
||||
switch (link?.type) {
|
||||
case 'doc':
|
||||
return getDocById(link.id).permalink;
|
||||
case 'generated-index':
|
||||
return link.permalink;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function convertCategory(item: SidebarItemCategory): PropSidebarItemCategory {
|
||||
const {link, ...rest} = item;
|
||||
const href = getCategoryLinkHref(link);
|
||||
return {...rest, items: item.items.map(normalizeItem), ...(href && {href})};
|
||||
}
|
||||
|
||||
function normalizeItem(item: SidebarItem): PropSidebarItem {
|
||||
switch (item.type) {
|
||||
case 'category':
|
||||
return {...item, items: item.items.map(normalizeItem)};
|
||||
return convertCategory(item);
|
||||
case 'ref':
|
||||
case 'doc':
|
||||
return convertDocLink(item);
|
||||
|
@ -61,7 +87,7 @@ Available document ids are:
|
|||
default:
|
||||
return item;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Transform the sidebar so that all sidebar item will be in the
|
||||
// form of 'link' or 'category' only.
|
||||
|
@ -69,6 +95,18 @@ Available document ids are:
|
|||
return mapValues(loadedVersion.sidebars, (items) => items.map(normalizeItem));
|
||||
}
|
||||
|
||||
function toVersionDocsProp(loadedVersion: LoadedVersion): PropVersionDocs {
|
||||
return mapValues(
|
||||
keyBy(loadedVersion.docs, (doc) => doc.unversionedId),
|
||||
(doc) => ({
|
||||
id: doc.unversionedId,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
sidebar: doc.sidebar,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function toVersionMetadataProp(
|
||||
pluginId: string,
|
||||
loadedVersion: LoadedVersion,
|
||||
|
@ -82,6 +120,7 @@ export function toVersionMetadataProp(
|
|||
className: loadedVersion.versionClassName,
|
||||
isLast: loadedVersion.isLast,
|
||||
docsSidebars: toSidebarsProp(loadedVersion),
|
||||
docs: toVersionDocsProp(loadedVersion),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
173
packages/docusaurus-plugin-content-docs/src/routes.ts
Normal file
173
packages/docusaurus-plugin-content-docs/src/routes.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* 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 {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types';
|
||||
import {docuHash, createSlugger} from '@docusaurus/utils';
|
||||
import {
|
||||
CategoryGeneratedIndexMetadata,
|
||||
DocMetadata,
|
||||
LoadedVersion,
|
||||
} from './types';
|
||||
import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs';
|
||||
import {toVersionMetadataProp} from './props';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function createCategoryGeneratedIndexRoutes({
|
||||
version,
|
||||
actions,
|
||||
docCategoryGeneratedIndexComponent,
|
||||
}: {
|
||||
version: LoadedVersion;
|
||||
actions: PluginContentLoadedActions;
|
||||
docCategoryGeneratedIndexComponent: string;
|
||||
}): Promise<RouteConfig[]> {
|
||||
const slugs = createSlugger();
|
||||
|
||||
async function createCategoryGeneratedIndexRoute(
|
||||
categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
|
||||
): Promise<RouteConfig> {
|
||||
const {sidebar, title, description, slug, permalink, previous, next} =
|
||||
categoryGeneratedIndex;
|
||||
|
||||
const propFileName = slugs.slug(
|
||||
`${version.versionPath}-${categoryGeneratedIndex.sidebar}-category-${categoryGeneratedIndex.title}`,
|
||||
);
|
||||
|
||||
const prop: PropCategoryGeneratedIndex = {
|
||||
title,
|
||||
description,
|
||||
slug,
|
||||
permalink,
|
||||
navigation: {
|
||||
previous,
|
||||
next,
|
||||
},
|
||||
};
|
||||
|
||||
const propData = await actions.createData(
|
||||
`${docuHash(`category/${propFileName}`)}.json`,
|
||||
JSON.stringify(prop, null, 2),
|
||||
);
|
||||
|
||||
return {
|
||||
path: permalink,
|
||||
component: docCategoryGeneratedIndexComponent,
|
||||
exact: true,
|
||||
modules: {
|
||||
categoryGeneratedIndex: propData,
|
||||
},
|
||||
// Same as doc, this sidebar route attribute permits to associate this subpage to the given sidebar
|
||||
...(sidebar && {sidebar}),
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
version.categoryGeneratedIndices.map(createCategoryGeneratedIndexRoute),
|
||||
);
|
||||
}
|
||||
|
||||
export async function createDocRoutes({
|
||||
docs,
|
||||
actions,
|
||||
docItemComponent,
|
||||
}: {
|
||||
docs: DocMetadata[];
|
||||
actions: PluginContentLoadedActions;
|
||||
docItemComponent: string;
|
||||
}): Promise<RouteConfig[]> {
|
||||
return Promise.all(
|
||||
docs.map(async (metadataItem) => {
|
||||
await actions.createData(
|
||||
// Note that this created data path must be in sync with
|
||||
// metadataPath provided to mdx-loader.
|
||||
`${docuHash(metadataItem.source)}.json`,
|
||||
JSON.stringify(metadataItem, null, 2),
|
||||
);
|
||||
|
||||
const docRoute: RouteConfig = {
|
||||
path: metadataItem.permalink,
|
||||
component: docItemComponent,
|
||||
exact: true,
|
||||
modules: {
|
||||
content: metadataItem.source,
|
||||
},
|
||||
// Because the parent (DocPage) comp need to access it easily
|
||||
// This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state)
|
||||
...(metadataItem.sidebar && {
|
||||
sidebar: metadataItem.sidebar,
|
||||
}),
|
||||
};
|
||||
|
||||
return docRoute;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function createVersionRoutes({
|
||||
loadedVersion,
|
||||
actions,
|
||||
docItemComponent,
|
||||
docLayoutComponent,
|
||||
docCategoryGeneratedIndexComponent,
|
||||
pluginId,
|
||||
aliasedSource,
|
||||
}: {
|
||||
loadedVersion: LoadedVersion;
|
||||
actions: PluginContentLoadedActions;
|
||||
docLayoutComponent: string;
|
||||
docItemComponent: string;
|
||||
docCategoryGeneratedIndexComponent: string;
|
||||
pluginId: string;
|
||||
aliasedSource: (str: string) => string;
|
||||
}): Promise<void> {
|
||||
async function doCreateVersionRoutes(version: LoadedVersion): Promise<void> {
|
||||
const versionMetadata = toVersionMetadataProp(pluginId, version);
|
||||
const versionMetadataPropPath = await actions.createData(
|
||||
`${docuHash(`version-${version.versionName}-metadata-prop`)}.json`,
|
||||
JSON.stringify(versionMetadata, null, 2),
|
||||
);
|
||||
|
||||
async function createVersionSubRoutes() {
|
||||
const [docRoutes, sidebarsRoutes] = await Promise.all([
|
||||
createDocRoutes({docs: version.docs, actions, docItemComponent}),
|
||||
createCategoryGeneratedIndexRoutes({
|
||||
version,
|
||||
actions,
|
||||
docCategoryGeneratedIndexComponent,
|
||||
}),
|
||||
]);
|
||||
|
||||
const routes = [...docRoutes, ...sidebarsRoutes];
|
||||
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
actions.addRoute({
|
||||
path: version.versionPath,
|
||||
// allow matching /docs/* as well
|
||||
exact: false,
|
||||
// main docs component (DocPage)
|
||||
component: docLayoutComponent,
|
||||
// sub-routes for each doc
|
||||
routes: await createVersionSubRoutes(),
|
||||
modules: {
|
||||
versionMetadata: aliasedSource(versionMetadataPropPath),
|
||||
},
|
||||
priority: version.routePriority,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await doCreateVersionRoutes(loadedVersion);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Can't create version routes for version "${loadedVersion.versionName}"`,
|
||||
),
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`loadUnprocessedSidebars sidebars link 1`] = `
|
||||
exports[`loadNormalizedSidebars sidebars link 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -14,13 +14,14 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadUnprocessedSidebars sidebars with category.collapsed property 1`] = `
|
||||
exports[`loadNormalizedSidebars sidebars with category.collapsed property 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -37,10 +38,12 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Introduction",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -57,17 +60,19 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Powering MDX",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
"label": "Reference",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadUnprocessedSidebars sidebars with category.collapsed property at first level 1`] = `
|
||||
exports[`loadNormalizedSidebars sidebars with category.collapsed property at first level 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -80,6 +85,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Introduction",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -92,13 +98,14 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Powering MDX",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadUnprocessedSidebars sidebars with deep level of category 1`] = `
|
||||
exports[`loadNormalizedSidebars sidebars with deep level of category 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -139,14 +146,17 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "deeper more more",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
"label": "level 4",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
"label": "level 3",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -155,17 +165,19 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "level 2",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
"label": "level 1",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`loadUnprocessedSidebars sidebars with first level not a category 1`] = `
|
||||
exports[`loadNormalizedSidebars sidebars with first level not a category 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -178,6 +190,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Getting Started",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -188,7 +201,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`loadUnprocessedSidebars sidebars with known sidebar item type 1`] = `
|
||||
exports[`loadNormalizedSidebars sidebars with known sidebar item type 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -214,6 +227,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Test",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
|
@ -226,6 +240,7 @@ Object {
|
|||
},
|
||||
],
|
||||
"label": "Guides",
|
||||
"link": undefined,
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -129,9 +129,15 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
|
||||
test('generates complex nested sidebar', async () => {
|
||||
mockCategoryMetadataFiles({
|
||||
'02-Guides/_category_.json': {collapsed: false},
|
||||
'02-Guides/_category_.json': {collapsed: false} as CategoryMetadataFile,
|
||||
'02-Guides/01-SubGuides/_category_.yml': {
|
||||
label: 'SubGuides (metadata file label)',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'subguides-generated-index-slug',
|
||||
title: 'subguides-title',
|
||||
description: 'subguides-description',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -153,6 +159,13 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
sidebarPosition: 1,
|
||||
frontMatter: {},
|
||||
},
|
||||
{
|
||||
id: 'tutorials-index',
|
||||
source: 'index.md',
|
||||
sourceDirName: '01-Tutorials',
|
||||
sidebarPosition: 2,
|
||||
frontMatter: {},
|
||||
},
|
||||
{
|
||||
id: 'tutorial2',
|
||||
source: 'tutorial2.md',
|
||||
|
@ -167,6 +180,12 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
sidebarPosition: 1,
|
||||
frontMatter: {},
|
||||
},
|
||||
{
|
||||
id: 'guides-index',
|
||||
source: '02-Guides.md', // TODO should we allow to just use "Guides.md" to have an index?
|
||||
sourceDirName: '02-Guides',
|
||||
frontMatter: {},
|
||||
},
|
||||
{
|
||||
id: 'guide2',
|
||||
source: 'guide2.md',
|
||||
|
@ -209,6 +228,10 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
label: 'Tutorials',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'tutorials-index',
|
||||
},
|
||||
items: [
|
||||
{type: 'doc', id: 'tutorial1'},
|
||||
{type: 'doc', id: 'tutorial2'},
|
||||
|
@ -219,6 +242,10 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
label: 'Guides',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'guides-index',
|
||||
},
|
||||
items: [
|
||||
{type: 'doc', id: 'guide1'},
|
||||
{
|
||||
|
@ -227,6 +254,12 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
collapsed: true,
|
||||
collapsible: true,
|
||||
items: [{type: 'doc', id: 'nested-guide'}],
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'subguides-generated-index-slug',
|
||||
title: 'subguides-title',
|
||||
description: 'subguides-description',
|
||||
},
|
||||
},
|
||||
{type: 'doc', id: 'guide2'},
|
||||
],
|
||||
|
@ -354,4 +387,75 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
},
|
||||
] as Sidebar);
|
||||
});
|
||||
|
||||
test('uses explicit link over the index/readme.{md,mdx} naming convention', async () => {
|
||||
mockCategoryMetadataFiles({
|
||||
'Category/_category_.yml': {
|
||||
label: 'Category label',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'doc3', // Using a "local doc id" ("doc1" instead of "parent/doc1") on purpose
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sidebarSlice = await DefaultSidebarItemsGenerator({
|
||||
numberPrefixParser: DefaultNumberPrefixParser,
|
||||
item: {
|
||||
type: 'autogenerated',
|
||||
dirName: '.',
|
||||
},
|
||||
version: {
|
||||
versionName: 'current',
|
||||
contentPath: '',
|
||||
},
|
||||
docs: [
|
||||
{
|
||||
id: 'parent/doc1',
|
||||
source: 'index.md',
|
||||
sourceDirName: 'Category',
|
||||
frontMatter: {},
|
||||
},
|
||||
{
|
||||
id: 'parent/doc2',
|
||||
source: 'index.md',
|
||||
sourceDirName: 'Category',
|
||||
frontMatter: {},
|
||||
},
|
||||
{
|
||||
id: 'parent/doc3',
|
||||
source: 'doc3.md',
|
||||
sourceDirName: 'Category',
|
||||
frontMatter: {},
|
||||
},
|
||||
],
|
||||
options: {
|
||||
sidebarCollapsed: true,
|
||||
sidebarCollapsible: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sidebarSlice).toEqual([
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category label',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
link: {
|
||||
id: 'parent/doc3',
|
||||
type: 'doc',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'parent/doc1',
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
id: 'parent/doc2',
|
||||
type: 'doc',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as Sidebar);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,27 +7,31 @@
|
|||
|
||||
import path from 'path';
|
||||
import {
|
||||
loadUnprocessedSidebars,
|
||||
loadNormalizedSidebars,
|
||||
DefaultSidebars,
|
||||
DisabledSidebars,
|
||||
} from '../index';
|
||||
import type {SidebarOptions} from '../../types';
|
||||
import type {NormalizeSidebarsParams, VersionMetadata} from '../../types';
|
||||
|
||||
describe('loadUnprocessedSidebars', () => {
|
||||
describe('loadNormalizedSidebars', () => {
|
||||
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
||||
const options: SidebarOptions = {
|
||||
const options: NormalizeSidebarsParams = {
|
||||
sidebarCollapsed: true,
|
||||
sidebarCollapsible: true,
|
||||
version: {
|
||||
versionName: 'version',
|
||||
versionPath: 'versionPath',
|
||||
} as VersionMetadata,
|
||||
};
|
||||
test('sidebars with known sidebar item type', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
||||
const result = loadUnprocessedSidebars(sidebarPath, options);
|
||||
const result = loadNormalizedSidebars(sidebarPath, options);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars with deep level of category', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
||||
const result = loadUnprocessedSidebars(sidebarPath, options);
|
||||
const result = loadNormalizedSidebars(sidebarPath, options);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -37,8 +41,8 @@ describe('loadUnprocessedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-shorthand.js',
|
||||
);
|
||||
const sidebar1 = loadUnprocessedSidebars(sidebarPath1, options);
|
||||
const sidebar2 = loadUnprocessedSidebars(sidebarPath2, options);
|
||||
const sidebar1 = loadNormalizedSidebars(sidebarPath1, options);
|
||||
const sidebar2 = loadNormalizedSidebars(sidebarPath2, options);
|
||||
expect(sidebar1).toEqual(sidebar2);
|
||||
});
|
||||
|
||||
|
@ -47,7 +51,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-wrong-items.json',
|
||||
);
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"category\\",
|
||||
|
@ -64,7 +68,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-wrong-label.json',
|
||||
);
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"category\\",
|
||||
|
@ -83,7 +87,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-doc-id-not-string.json',
|
||||
);
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"doc\\",
|
||||
|
@ -101,19 +105,19 @@ describe('loadUnprocessedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-first-level-not-category.js',
|
||||
);
|
||||
const result = loadUnprocessedSidebars(sidebarPath, options);
|
||||
const result = loadNormalizedSidebars(sidebarPath, options);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars link', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
||||
const result = loadUnprocessedSidebars(sidebarPath, options);
|
||||
const result = loadNormalizedSidebars(sidebarPath, options);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars link wrong label', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json');
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"link\\",
|
||||
|
@ -127,7 +131,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
|
||||
test('sidebars link wrong href', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json');
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"link\\",
|
||||
|
@ -143,7 +147,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
|
||||
test('sidebars with unknown sidebar item type', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"superman\\",
|
||||
|
@ -156,7 +160,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
|
||||
test('sidebars with known sidebar item type but wrong field', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
||||
expect(() => loadUnprocessedSidebars(sidebarPath, options))
|
||||
expect(() => loadNormalizedSidebars(sidebarPath, options))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"category\\",
|
||||
|
@ -170,24 +174,22 @@ describe('loadUnprocessedSidebars', () => {
|
|||
});
|
||||
|
||||
test('unexisting path', () => {
|
||||
expect(loadUnprocessedSidebars('badpath', options)).toEqual(
|
||||
expect(loadNormalizedSidebars('badpath', options)).toEqual(
|
||||
DisabledSidebars,
|
||||
);
|
||||
});
|
||||
|
||||
test('undefined path', () => {
|
||||
expect(loadUnprocessedSidebars(undefined, options)).toEqual(
|
||||
DefaultSidebars,
|
||||
);
|
||||
expect(loadNormalizedSidebars(undefined, options)).toEqual(DefaultSidebars);
|
||||
});
|
||||
|
||||
test('literal false path', () => {
|
||||
expect(loadUnprocessedSidebars(false, options)).toEqual(DisabledSidebars);
|
||||
expect(loadNormalizedSidebars(false, options)).toEqual(DisabledSidebars);
|
||||
});
|
||||
|
||||
test('sidebars with category.collapsed property', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
||||
const result = loadUnprocessedSidebars(sidebarPath, options);
|
||||
const result = loadNormalizedSidebars(sidebarPath, options);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -196,7 +198,7 @@ describe('loadUnprocessedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-collapsed-first-level.json',
|
||||
);
|
||||
const result = loadUnprocessedSidebars(sidebarPath, options);
|
||||
const result = loadNormalizedSidebars(sidebarPath, options);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {processSidebars} from '../processor';
|
||||
import {processSidebars, SidebarProcessorParams} from '../processor';
|
||||
import type {
|
||||
SidebarItem,
|
||||
SidebarItemsGenerator,
|
||||
|
@ -13,23 +13,50 @@ import type {
|
|||
NormalizedSidebars,
|
||||
} from '../types';
|
||||
import {DefaultSidebarItemsGenerator} from '../generator';
|
||||
import {createSlugger} from '@docusaurus/utils';
|
||||
import {VersionMetadata} from '../../types';
|
||||
import {DefaultNumberPrefixParser} from '../../numberPrefix';
|
||||
|
||||
describe('processSidebars', () => {
|
||||
function createStaticSidebarItemGenerator(
|
||||
sidebarSlice: SidebarItem[],
|
||||
): SidebarItemsGenerator {
|
||||
return jest.fn(async () => sidebarSlice);
|
||||
}
|
||||
|
||||
const StaticGeneratedSidebarSlice: SidebarItem[] = [
|
||||
{type: 'doc', id: 'doc-generated-id-1'},
|
||||
{type: 'doc', id: 'doc-generated-id-2'},
|
||||
];
|
||||
|
||||
const StaticSidebarItemsGenerator: SidebarItemsGenerator = jest.fn(
|
||||
async () => StaticGeneratedSidebarSlice,
|
||||
);
|
||||
const StaticSidebarItemsGenerator: SidebarItemsGenerator =
|
||||
createStaticSidebarItemGenerator(StaticGeneratedSidebarSlice);
|
||||
|
||||
async function testProcessSidebars(unprocessedSidebars: NormalizedSidebars) {
|
||||
return processSidebars(unprocessedSidebars, {
|
||||
// @ts-expect-error: good enough for this test
|
||||
const version: VersionMetadata = {
|
||||
versionName: '1.0.0',
|
||||
versionPath: '/docs/1.0.0',
|
||||
};
|
||||
|
||||
const params: SidebarProcessorParams = {
|
||||
sidebarItemsGenerator: StaticSidebarItemsGenerator,
|
||||
docs: [],
|
||||
// @ts-expect-error: useless for this test
|
||||
version: {},
|
||||
version,
|
||||
numberPrefixParser: DefaultNumberPrefixParser,
|
||||
categoryLabelSlugger: createSlugger(),
|
||||
sidebarOptions: {
|
||||
sidebarCollapsed: true,
|
||||
sidebarCollapsible: true,
|
||||
},
|
||||
};
|
||||
|
||||
async function testProcessSidebars(
|
||||
unprocessedSidebars: NormalizedSidebars,
|
||||
paramsOverrides: Partial<SidebarProcessorParams> = {},
|
||||
) {
|
||||
return processSidebars(unprocessedSidebars, {
|
||||
...params,
|
||||
...paramsOverrides,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -69,13 +96,18 @@ describe('processSidebars', () => {
|
|||
{type: 'doc', id: 'doc1'},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'category-generated-index-slug',
|
||||
permalink: 'category-generated-index-permalink',
|
||||
},
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
items: [
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{type: 'autogenerated', dirName: 'dir1'},
|
||||
],
|
||||
label: 'Category',
|
||||
},
|
||||
{type: 'link', href: 'https://facebook.com', label: 'FB'},
|
||||
],
|
||||
|
@ -86,10 +118,10 @@ describe('processSidebars', () => {
|
|||
{type: 'autogenerated', dirName: 'dir3'},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
items: [{type: 'doc', id: 'doc4'}],
|
||||
label: 'Category',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -100,20 +132,32 @@ describe('processSidebars', () => {
|
|||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
item: {type: 'autogenerated', dirName: 'dir1'},
|
||||
docs: [],
|
||||
version: {},
|
||||
docs: params.docs,
|
||||
version: {
|
||||
versionName: version.versionName,
|
||||
},
|
||||
numberPrefixParser: DefaultNumberPrefixParser,
|
||||
options: params.sidebarOptions,
|
||||
});
|
||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
item: {type: 'autogenerated', dirName: 'dir2'},
|
||||
docs: [],
|
||||
version: {},
|
||||
docs: params.docs,
|
||||
version: {
|
||||
versionName: version.versionName,
|
||||
},
|
||||
numberPrefixParser: DefaultNumberPrefixParser,
|
||||
options: params.sidebarOptions,
|
||||
});
|
||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
item: {type: 'autogenerated', dirName: 'dir3'},
|
||||
docs: [],
|
||||
version: {},
|
||||
docs: params.docs,
|
||||
version: {
|
||||
versionName: version.versionName,
|
||||
},
|
||||
numberPrefixParser: DefaultNumberPrefixParser,
|
||||
options: params.sidebarOptions,
|
||||
});
|
||||
|
||||
expect(processedSidebar).toEqual({
|
||||
|
@ -121,10 +165,15 @@ describe('processSidebars', () => {
|
|||
{type: 'doc', id: 'doc1'},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'category-generated-index-slug',
|
||||
permalink: 'category-generated-index-permalink',
|
||||
},
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
|
||||
label: 'Category',
|
||||
},
|
||||
{type: 'link', href: 'https://facebook.com', label: 'FB'},
|
||||
],
|
||||
|
@ -135,10 +184,52 @@ describe('processSidebars', () => {
|
|||
...StaticGeneratedSidebarSlice,
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
items: [{type: 'doc', id: 'doc4'}],
|
||||
label: 'Category',
|
||||
},
|
||||
],
|
||||
} as Sidebars);
|
||||
});
|
||||
|
||||
test('ensure generated items are normalized', async () => {
|
||||
const sidebarSliceContainingCategoryGeneratedIndex: SidebarItem[] = [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Generated category',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'generated-cat-index-slug',
|
||||
// @ts-expect-error: TODO undefined should be allowed here, typing error needing refactor
|
||||
permalink: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const unprocessedSidebars: NormalizedSidebars = {
|
||||
someSidebar: [{type: 'autogenerated', dirName: 'dir2'}],
|
||||
};
|
||||
|
||||
const processedSidebar = await testProcessSidebars(unprocessedSidebars, {
|
||||
sidebarItemsGenerator: createStaticSidebarItemGenerator(
|
||||
sidebarSliceContainingCategoryGeneratedIndex,
|
||||
),
|
||||
});
|
||||
|
||||
expect(processedSidebar).toEqual({
|
||||
someSidebar: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Generated category',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'generated-cat-index-slug',
|
||||
permalink: '/docs/1.0.0/generated-cat-index-slug',
|
||||
},
|
||||
items: [],
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
},
|
||||
],
|
||||
} as Sidebars);
|
||||
|
|
|
@ -12,8 +12,12 @@ import {
|
|||
collectSidebarLinks,
|
||||
transformSidebarItems,
|
||||
collectSidebarsDocIds,
|
||||
SidebarNavigation,
|
||||
toDocNavigationLink,
|
||||
toNavigationLink,
|
||||
} from '../utils';
|
||||
import type {Sidebar, Sidebars} from '../types';
|
||||
import {DocMetadataBase, DocNavLink} from '../../types';
|
||||
|
||||
describe('createSidebarsUtils', () => {
|
||||
const sidebar1: Sidebar = [
|
||||
|
@ -21,13 +25,13 @@ describe('createSidebarsUtils', () => {
|
|||
type: 'category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
label: 'Category1',
|
||||
label: 'S1 Category',
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
label: 'Subcategory 1',
|
||||
label: 'S1 Subcategory',
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
{type: 'doc', id: 'doc2'},
|
||||
|
@ -40,7 +44,7 @@ describe('createSidebarsUtils', () => {
|
|||
type: 'category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
label: 'Category2',
|
||||
label: 'S2 Category',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc3'},
|
||||
{type: 'doc', id: 'doc4'},
|
||||
|
@ -48,10 +52,58 @@ describe('createSidebarsUtils', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const sidebars: Sidebars = {sidebar1, sidebar2};
|
||||
const sidebar3: Sidebar = [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
label: 'S3 Category',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'doc5',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
label: 'S3 SubCategory',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: '/s3-subcategory-index-slug',
|
||||
permalink: '/s3-subcategory-index-permalink',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
label: 'S3 SubSubCategory',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: '/s3-subsubcategory-slug',
|
||||
permalink: '/s3-subsubcategory-index-permalink',
|
||||
},
|
||||
items: [
|
||||
{type: 'doc', id: 'doc6'},
|
||||
{type: 'doc', id: 'doc7'},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const {getFirstDocIdOfFirstSidebar, getSidebarNameByDocId, getDocNavigation} =
|
||||
createSidebarsUtils(sidebars);
|
||||
const sidebars: Sidebars = {sidebar1, sidebar2, sidebar3};
|
||||
|
||||
const {
|
||||
getFirstDocIdOfFirstSidebar,
|
||||
getSidebarNameByDocId,
|
||||
getDocNavigation,
|
||||
getCategoryGeneratedIndexNavigation,
|
||||
getCategoryGeneratedIndexList,
|
||||
} = createSidebarsUtils(sidebars);
|
||||
|
||||
test('getSidebarNameByDocId', async () => {
|
||||
expect(getFirstDocIdOfFirstSidebar()).toEqual('doc1');
|
||||
|
@ -62,32 +114,117 @@ describe('createSidebarsUtils', () => {
|
|||
expect(getSidebarNameByDocId('doc2')).toEqual('sidebar1');
|
||||
expect(getSidebarNameByDocId('doc3')).toEqual('sidebar2');
|
||||
expect(getSidebarNameByDocId('doc4')).toEqual('sidebar2');
|
||||
expect(getSidebarNameByDocId('doc5')).toEqual(undefined);
|
||||
expect(getSidebarNameByDocId('doc6')).toEqual(undefined);
|
||||
expect(getSidebarNameByDocId('doc5')).toEqual('sidebar3');
|
||||
expect(getSidebarNameByDocId('doc6')).toEqual('sidebar3');
|
||||
expect(getSidebarNameByDocId('doc7')).toEqual('sidebar3');
|
||||
expect(getSidebarNameByDocId('unknown_id')).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('getDocNavigation', async () => {
|
||||
expect(getDocNavigation('doc1')).toEqual({
|
||||
sidebarName: 'sidebar1',
|
||||
previousId: undefined,
|
||||
nextId: 'doc2',
|
||||
});
|
||||
previous: undefined,
|
||||
next: {
|
||||
type: 'doc',
|
||||
id: 'doc2',
|
||||
},
|
||||
} as SidebarNavigation);
|
||||
expect(getDocNavigation('doc2')).toEqual({
|
||||
sidebarName: 'sidebar1',
|
||||
previousId: 'doc1',
|
||||
nextId: undefined,
|
||||
});
|
||||
previous: {
|
||||
type: 'doc',
|
||||
id: 'doc1',
|
||||
},
|
||||
next: undefined,
|
||||
} as SidebarNavigation);
|
||||
|
||||
expect(getDocNavigation('doc3')).toEqual({
|
||||
sidebarName: 'sidebar2',
|
||||
previousId: undefined,
|
||||
nextId: 'doc4',
|
||||
});
|
||||
previous: undefined,
|
||||
next: {
|
||||
type: 'doc',
|
||||
id: 'doc4',
|
||||
},
|
||||
} as SidebarNavigation);
|
||||
expect(getDocNavigation('doc4')).toEqual({
|
||||
sidebarName: 'sidebar2',
|
||||
previousId: 'doc3',
|
||||
nextId: undefined,
|
||||
previous: {
|
||||
type: 'doc',
|
||||
id: 'doc3',
|
||||
},
|
||||
next: undefined,
|
||||
} as SidebarNavigation);
|
||||
|
||||
expect(getDocNavigation('doc5')).toMatchObject({
|
||||
sidebarName: 'sidebar3',
|
||||
previous: undefined,
|
||||
next: {
|
||||
type: 'category',
|
||||
label: 'S3 SubCategory',
|
||||
},
|
||||
} as SidebarNavigation);
|
||||
expect(getDocNavigation('doc6')).toMatchObject({
|
||||
sidebarName: 'sidebar3',
|
||||
previous: {
|
||||
type: 'category',
|
||||
label: 'S3 SubSubCategory',
|
||||
},
|
||||
next: {
|
||||
type: 'doc',
|
||||
id: 'doc7',
|
||||
},
|
||||
} as SidebarNavigation);
|
||||
expect(getDocNavigation('doc7')).toMatchObject({
|
||||
sidebarName: 'sidebar3',
|
||||
previous: {
|
||||
type: 'doc',
|
||||
id: 'doc6',
|
||||
},
|
||||
next: undefined,
|
||||
} as SidebarNavigation);
|
||||
});
|
||||
|
||||
test('getCategoryGeneratedIndexNavigation', async () => {
|
||||
expect(
|
||||
getCategoryGeneratedIndexNavigation('/s3-subcategory-index-permalink'),
|
||||
).toMatchObject({
|
||||
sidebarName: 'sidebar3',
|
||||
previous: {
|
||||
type: 'category',
|
||||
label: 'S3 Category',
|
||||
},
|
||||
next: {
|
||||
type: 'category',
|
||||
label: 'S3 SubSubCategory',
|
||||
},
|
||||
} as SidebarNavigation);
|
||||
|
||||
expect(
|
||||
getCategoryGeneratedIndexNavigation('/s3-subsubcategory-index-permalink'),
|
||||
).toMatchObject({
|
||||
sidebarName: 'sidebar3',
|
||||
previous: {
|
||||
type: 'category',
|
||||
label: 'S3 SubCategory',
|
||||
},
|
||||
next: {
|
||||
type: 'doc',
|
||||
id: 'doc6',
|
||||
},
|
||||
} as SidebarNavigation);
|
||||
});
|
||||
|
||||
test('getCategoryGeneratedIndexList', async () => {
|
||||
expect(getCategoryGeneratedIndexList()).toMatchObject([
|
||||
{
|
||||
type: 'category',
|
||||
label: 'S3 SubCategory',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'S3 SubSubCategory',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -393,3 +530,166 @@ describe('transformSidebarItems', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDocNavigationLink', () => {
|
||||
type TestDoc = Pick<DocMetadataBase, 'permalink' | 'title' | 'frontMatter'>;
|
||||
function testDoc(data: TestDoc) {
|
||||
return data as DocMetadataBase;
|
||||
}
|
||||
|
||||
test('with no frontmatter', () => {
|
||||
expect(
|
||||
toDocNavigationLink(
|
||||
testDoc({
|
||||
title: 'Doc Title',
|
||||
permalink: '/docPermalink',
|
||||
frontMatter: {},
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
title: 'Doc Title',
|
||||
permalink: '/docPermalink',
|
||||
} as DocNavLink);
|
||||
});
|
||||
|
||||
test('with pagination_label frontmatter', () => {
|
||||
expect(
|
||||
toDocNavigationLink(
|
||||
testDoc({
|
||||
title: 'Doc Title',
|
||||
permalink: '/docPermalink',
|
||||
frontMatter: {
|
||||
pagination_label: 'pagination_label',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
title: 'pagination_label',
|
||||
permalink: '/docPermalink',
|
||||
} as DocNavLink);
|
||||
});
|
||||
|
||||
test('with sidebar_label frontmatter', () => {
|
||||
expect(
|
||||
toDocNavigationLink(
|
||||
testDoc({
|
||||
title: 'Doc Title',
|
||||
permalink: '/docPermalink',
|
||||
frontMatter: {
|
||||
sidebar_label: 'sidebar_label',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
title: 'sidebar_label',
|
||||
permalink: '/docPermalink',
|
||||
} as DocNavLink);
|
||||
});
|
||||
|
||||
test('with pagination_label + sidebar_label frontmatter', () => {
|
||||
expect(
|
||||
toDocNavigationLink(
|
||||
testDoc({
|
||||
title: 'Doc Title',
|
||||
permalink: '/docPermalink',
|
||||
frontMatter: {
|
||||
pagination_label: 'pagination_label',
|
||||
sidebar_label: 'sidebar_label',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
title: 'pagination_label',
|
||||
permalink: '/docPermalink',
|
||||
} as DocNavLink);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toNavigationLink', () => {
|
||||
type TestDoc = Pick<DocMetadataBase, 'permalink' | 'title'>;
|
||||
function testDoc(data: TestDoc) {
|
||||
return {...data, frontMatter: {}} as DocMetadataBase;
|
||||
}
|
||||
|
||||
const docsById: Record<string, DocMetadataBase> = {
|
||||
doc1: testDoc({
|
||||
title: 'Doc 1',
|
||||
permalink: '/doc1',
|
||||
}),
|
||||
doc2: testDoc({
|
||||
title: 'Doc 1',
|
||||
permalink: '/doc1',
|
||||
}),
|
||||
};
|
||||
|
||||
test('with doc items', () => {
|
||||
expect(toNavigationLink({type: 'doc', id: 'doc1'}, docsById)).toEqual(
|
||||
toDocNavigationLink(docsById.doc1),
|
||||
);
|
||||
expect(toNavigationLink({type: 'doc', id: 'doc2'}, docsById)).toEqual(
|
||||
toDocNavigationLink(docsById.doc2),
|
||||
);
|
||||
expect(() =>
|
||||
toNavigationLink({type: 'doc', id: 'doc3'}, docsById),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't create navigation link: no doc found with id=doc3"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('with category item and doc link', () => {
|
||||
expect(
|
||||
toNavigationLink(
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
items: [],
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'doc1',
|
||||
},
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
},
|
||||
docsById,
|
||||
),
|
||||
).toEqual(toDocNavigationLink(docsById.doc1));
|
||||
expect(() =>
|
||||
toNavigationLink(
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
items: [],
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'doc3',
|
||||
},
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
},
|
||||
docsById,
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't create navigation link: no doc found with id=doc3"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('with category item and generated-index link', () => {
|
||||
expect(
|
||||
toNavigationLink(
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
items: [],
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'slug',
|
||||
permalink: 'generated-index-permalink',
|
||||
},
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
},
|
||||
docsById,
|
||||
),
|
||||
).toEqual({title: 'Category', permalink: 'generated-index-permalink'});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* 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 {validateSidebars, validateCategoryMetadataFile} from '../validation';
|
||||
import {CategoryMetadataFile} from '../generator';
|
||||
import {SidebarsConfig} from '../types';
|
||||
|
||||
describe('validateSidebars', () => {
|
||||
// TODO add more tests
|
||||
|
||||
// TODO it seems many error cases are not validated properly
|
||||
// and error messages are quite bad
|
||||
test('throw for bad value', async () => {
|
||||
expect(() => validateSidebars({sidebar: [{type: 42}]}))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": 42,
|
||||
[41m\\"undefined\\"[0m[31m [1]: -- missing --[0m
|
||||
}
|
||||
[31m
|
||||
[1] Unknown sidebar item type \\"42\\".[0m"
|
||||
`);
|
||||
});
|
||||
|
||||
test('accept empty object', async () => {
|
||||
const sidebars: SidebarsConfig = {};
|
||||
validateSidebars(sidebars);
|
||||
});
|
||||
|
||||
test('accept valid values', async () => {
|
||||
const sidebars: SidebarsConfig = {
|
||||
sidebar1: [
|
||||
{type: 'doc', id: 'doc1'},
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Category',
|
||||
items: [{type: 'doc', id: 'doc3'}],
|
||||
},
|
||||
],
|
||||
};
|
||||
validateSidebars(sidebars);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCategoryMetadataFile', () => {
|
||||
// TODO add more tests
|
||||
|
||||
test('throw for bad value', async () => {
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile(42),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('accept empty object', async () => {
|
||||
const content: CategoryMetadataFile = {};
|
||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
test('accept valid values', async () => {
|
||||
const content: CategoryMetadataFile = {
|
||||
className: 'className',
|
||||
label: 'Category Label',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'slug',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
position: 3,
|
||||
};
|
||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
test('rejects permalink', async () => {
|
||||
const content: CategoryMetadataFile = {
|
||||
className: 'className',
|
||||
label: 'Category Label',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'slug',
|
||||
// @ts-expect-error: rejected on purpose
|
||||
permalink: 'somePermalink',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
position: 3,
|
||||
};
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile(content),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"link.permalink\\" is not allowed"`,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -11,19 +11,27 @@ import type {
|
|||
SidebarItemCategory,
|
||||
SidebarItemsGenerator,
|
||||
SidebarItemsGeneratorDoc,
|
||||
SidebarItemCategoryLink,
|
||||
SidebarItemCategoryLinkConfig,
|
||||
} from './types';
|
||||
import {keyBy, sortBy} from 'lodash';
|
||||
import {sortBy, last} from 'lodash';
|
||||
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
|
||||
import {Joi} from '@docusaurus/utils-validation';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import Yaml from 'js-yaml';
|
||||
import {validateCategoryMetadataFile} from './validation';
|
||||
import {createDocsByIdIndex, isConventionalDocIndex} from '../docs';
|
||||
|
||||
const BreadcrumbSeparator = '/';
|
||||
// To avoid possible name clashes with a folder of the same name as the ID
|
||||
const docIdPrefix = '$doc$/';
|
||||
|
||||
// Just an alias to the make code more explicit
|
||||
function getLocalDocId(docId: string): string {
|
||||
return last(docId.split('/'))!;
|
||||
}
|
||||
|
||||
export const CategoryMetadataFilenameBase = '_category_';
|
||||
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
|
||||
|
||||
|
@ -33,6 +41,7 @@ export type CategoryMetadataFile = {
|
|||
collapsed?: boolean;
|
||||
collapsible?: boolean;
|
||||
className?: string;
|
||||
link?: SidebarItemCategoryLinkConfig;
|
||||
|
||||
// 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/
|
||||
|
@ -50,17 +59,9 @@ type Dir = {
|
|||
[item: string]: Dir | null;
|
||||
};
|
||||
|
||||
const CategoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
||||
label: Joi.string(),
|
||||
position: Joi.number(),
|
||||
collapsed: Joi.boolean(),
|
||||
collapsible: Joi.boolean(),
|
||||
className: Joi.string(),
|
||||
});
|
||||
|
||||
// TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata
|
||||
// Example use-case being able to disable number prefix parsing at the folder level, or customize the default route path segment for an intermediate directory...
|
||||
// TODO later if there is `CategoryFolder/index.md`, we may want to read the metadata as yaml on it
|
||||
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it
|
||||
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
|
||||
async function readCategoryMetadataFile(
|
||||
categoryDirPath: string,
|
||||
|
@ -69,7 +70,7 @@ async function readCategoryMetadataFile(
|
|||
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
|
||||
const unsafeContent = Yaml.load(contentString);
|
||||
try {
|
||||
return Joi.attempt(unsafeContent, CategoryMetadataFileSchema);
|
||||
return validateCategoryMetadataFile(unsafeContent);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
|
@ -100,6 +101,21 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
item: {dirName: autogenDir},
|
||||
version,
|
||||
}) => {
|
||||
const docsById = createDocsByIdIndex(allDocs);
|
||||
const findDoc = (docId: string): SidebarItemsGeneratorDoc | undefined =>
|
||||
docsById[docId];
|
||||
const getDoc = (docId: string): SidebarItemsGeneratorDoc => {
|
||||
const doc = findDoc(docId);
|
||||
if (!doc) {
|
||||
throw new Error(
|
||||
`Can't find any doc with id=${docId}.\nAvailable doc ids:\n- ${Object.keys(
|
||||
docsById,
|
||||
).join('\n- ')}`,
|
||||
);
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Step 1. Extract the docs that are in the autogen dir.
|
||||
*/
|
||||
|
@ -163,12 +179,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
* (From a record to an array of items, akin to normalizing shorthand)
|
||||
*/
|
||||
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
|
||||
const docsById = keyBy(allDocs, (doc) => doc.id);
|
||||
function createDocItem(id: string): WithPosition<SidebarItemDoc> {
|
||||
const {
|
||||
sidebarPosition: position,
|
||||
frontMatter: {sidebar_label: label, sidebar_class_name: className},
|
||||
} = docsById[id];
|
||||
} = getDoc(id);
|
||||
return {
|
||||
type: 'doc',
|
||||
id,
|
||||
|
@ -187,6 +202,57 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
const categoryMetadata = await readCategoryMetadataFile(categoryPath);
|
||||
const className = categoryMetadata?.className;
|
||||
const {filename, numberPrefix} = numberPrefixParser(folderName);
|
||||
const allItems = await Promise.all(
|
||||
Object.entries(dir).map(([key, content]) =>
|
||||
dirToItem(content, key, `${fullPath}/${key}`),
|
||||
),
|
||||
);
|
||||
|
||||
// Try to match a doc inside the category folder,
|
||||
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
|
||||
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
|
||||
return allItems.find(
|
||||
(item) => item.type === 'doc' && getLocalDocId(item.id) === localId,
|
||||
) as SidebarItemDoc | undefined;
|
||||
}
|
||||
|
||||
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
|
||||
return allItems.find(
|
||||
(item) =>
|
||||
item.type === 'doc' && isConventionalDocIndex(getDoc(item.id)),
|
||||
) as SidebarItemDoc | undefined;
|
||||
}
|
||||
|
||||
function getCategoryLinkedDocId(): string | undefined {
|
||||
const link = categoryMetadata?.link;
|
||||
if (link) {
|
||||
if (link.type === 'doc') {
|
||||
return findDocByLocalId(link.id)?.id || getDoc(link.id).id;
|
||||
} else {
|
||||
// We don't continue for other link types on purpose!
|
||||
// IE if user decide to use type "generated-index", we should not pick a README.md file as the linked doc
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
// Apply default convention to pick index.md, README.md or <categoryName>.md as the category doc
|
||||
return findConventionalCategoryDocLink()?.id;
|
||||
}
|
||||
|
||||
const categoryLinkedDocId = getCategoryLinkedDocId();
|
||||
|
||||
const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId
|
||||
? {
|
||||
type: 'doc',
|
||||
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
|
||||
}
|
||||
: // TODO typing issue
|
||||
(categoryMetadata?.link as SidebarItemCategoryLink | undefined);
|
||||
|
||||
// If a doc is linked, remove it from the category subItems
|
||||
const items = allItems.filter(
|
||||
(item) => !(item.type === 'doc' && item.id === categoryLinkedDocId),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'category',
|
||||
label: categoryMetadata?.label ?? filename,
|
||||
|
@ -195,11 +261,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
|
||||
position: categoryMetadata?.position ?? numberPrefix,
|
||||
...(className !== undefined && {className}),
|
||||
items: await Promise.all(
|
||||
Object.entries(dir).map(([key, content]) =>
|
||||
dirToItem(content, key, `${fullPath}/${key}`),
|
||||
),
|
||||
),
|
||||
items,
|
||||
...(link && {link}),
|
||||
};
|
||||
}
|
||||
async function dirToItem(
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import fs from 'fs-extra';
|
||||
import importFresh from 'import-fresh';
|
||||
import type {SidebarsConfig, Sidebars, NormalizedSidebars} from './types';
|
||||
import type {PluginOptions} from '../types';
|
||||
import type {NormalizeSidebarsParams, PluginOptions} from '../types';
|
||||
import {validateSidebars} from './validation';
|
||||
import {normalizeSidebars} from './normalization';
|
||||
import {processSidebars, SidebarProcessorProps} from './processor';
|
||||
import {processSidebars, SidebarProcessorParams} from './processor';
|
||||
import path from 'path';
|
||||
import {createSlugger} from '@docusaurus/utils';
|
||||
|
||||
export const DefaultSidebars: SidebarsConfig = {
|
||||
defaultSidebar: [
|
||||
|
@ -36,7 +37,7 @@ export function resolveSidebarPathOption(
|
|||
: sidebarPathOption;
|
||||
}
|
||||
|
||||
function loadSidebarFile(
|
||||
function loadSidebarsFileUnsafe(
|
||||
sidebarFilePath: string | false | undefined,
|
||||
): SidebarsConfig {
|
||||
// false => no sidebars
|
||||
|
@ -60,25 +61,34 @@ function loadSidebarFile(
|
|||
return importFresh(sidebarFilePath);
|
||||
}
|
||||
|
||||
export function loadUnprocessedSidebars(
|
||||
export function loadSidebarsFile(
|
||||
sidebarFilePath: string | false | undefined,
|
||||
options: SidebarProcessorProps['options'],
|
||||
): NormalizedSidebars {
|
||||
const sidebarsConfig = loadSidebarFile(sidebarFilePath);
|
||||
): SidebarsConfig {
|
||||
const sidebarsConfig = loadSidebarsFileUnsafe(sidebarFilePath);
|
||||
validateSidebars(sidebarsConfig);
|
||||
return sidebarsConfig;
|
||||
}
|
||||
|
||||
const normalizedSidebars = normalizeSidebars(sidebarsConfig, options);
|
||||
return normalizedSidebars;
|
||||
export function loadNormalizedSidebars(
|
||||
sidebarFilePath: string | false | undefined,
|
||||
params: NormalizeSidebarsParams,
|
||||
): NormalizedSidebars {
|
||||
return normalizeSidebars(loadSidebarsFile(sidebarFilePath), params);
|
||||
}
|
||||
|
||||
// Note: sidebarFilePath must be absolute, use resolveSidebarPathOption
|
||||
export async function loadSidebars(
|
||||
sidebarFilePath: string | false | undefined,
|
||||
options: SidebarProcessorProps,
|
||||
options: SidebarProcessorParams,
|
||||
): Promise<Sidebars> {
|
||||
const unprocessedSidebars = loadUnprocessedSidebars(
|
||||
const normalizeSidebarsParams: NormalizeSidebarsParams = {
|
||||
...options.sidebarOptions,
|
||||
version: options.version,
|
||||
categoryLabelSlugger: createSlugger(),
|
||||
};
|
||||
const normalizedSidebars = loadNormalizedSidebars(
|
||||
sidebarFilePath,
|
||||
options.options,
|
||||
normalizeSidebarsParams,
|
||||
);
|
||||
return processSidebars(unprocessedSidebars, options);
|
||||
return processSidebars(normalizedSidebars, options);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {SidebarOptions} from '../types';
|
||||
import type {NormalizeSidebarsParams, SidebarOptions} from '../types';
|
||||
import type {
|
||||
NormalizedSidebarItem,
|
||||
NormalizedSidebar,
|
||||
|
@ -15,9 +15,31 @@ import type {
|
|||
SidebarItemConfig,
|
||||
SidebarConfig,
|
||||
SidebarsConfig,
|
||||
SidebarItemCategoryLink,
|
||||
NormalizedSidebarItemCategory,
|
||||
} from './types';
|
||||
import {mapValues} from 'lodash';
|
||||
import {isCategoriesShorthand} from './utils';
|
||||
import {mapValues} from 'lodash';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
|
||||
function normalizeCategoryLink(
|
||||
category: SidebarItemCategoryConfig,
|
||||
params: NormalizeSidebarsParams,
|
||||
): SidebarItemCategoryLink | undefined {
|
||||
if (category.link?.type === 'generated-index') {
|
||||
// default slug logic can be improved
|
||||
const getDefaultSlug = () =>
|
||||
`/category/${params.categoryLabelSlugger.slug(category.label)}`;
|
||||
const slug = category.link.slug ?? getDefaultSlug();
|
||||
const permalink = normalizeUrl([params.version.versionPath, slug]);
|
||||
return {
|
||||
...category.link,
|
||||
slug,
|
||||
permalink,
|
||||
};
|
||||
}
|
||||
return category.link;
|
||||
}
|
||||
|
||||
function normalizeCategoriesShorthand(
|
||||
sidebar: SidebarCategoriesShorthand,
|
||||
|
@ -36,9 +58,9 @@ function normalizeCategoriesShorthand(
|
|||
* Normalizes recursively item and all its children. Ensures that at the end
|
||||
* each item will be an object with the corresponding type.
|
||||
*/
|
||||
function normalizeItem(
|
||||
export function normalizeItem(
|
||||
item: SidebarItemConfig,
|
||||
options: SidebarOptions,
|
||||
options: NormalizeSidebarsParams,
|
||||
): NormalizedSidebarItem[] {
|
||||
if (typeof item === 'string') {
|
||||
return [
|
||||
|
@ -49,40 +71,42 @@ function normalizeItem(
|
|||
];
|
||||
}
|
||||
if (isCategoriesShorthand(item)) {
|
||||
return normalizeCategoriesShorthand(item, options).flatMap((subitem) =>
|
||||
normalizeItem(subitem, options),
|
||||
return normalizeCategoriesShorthand(item, options).flatMap((subItem) =>
|
||||
normalizeItem(subItem, options),
|
||||
);
|
||||
}
|
||||
return item.type === 'category'
|
||||
? [
|
||||
{
|
||||
if (item.type === 'category') {
|
||||
const link = normalizeCategoryLink(item, options);
|
||||
const normalizedCategory: NormalizedSidebarItemCategory = {
|
||||
...item,
|
||||
items: item.items.flatMap((subItem) =>
|
||||
link,
|
||||
items: (item.items ?? []).flatMap((subItem) =>
|
||||
normalizeItem(subItem, options),
|
||||
),
|
||||
collapsible: item.collapsible ?? options.sidebarCollapsible,
|
||||
collapsed: item.collapsed ?? options.sidebarCollapsed,
|
||||
},
|
||||
]
|
||||
: [item];
|
||||
};
|
||||
return [normalizedCategory];
|
||||
}
|
||||
return [item];
|
||||
}
|
||||
|
||||
function normalizeSidebar(
|
||||
sidebar: SidebarConfig,
|
||||
options: SidebarOptions,
|
||||
options: NormalizeSidebarsParams,
|
||||
): NormalizedSidebar {
|
||||
const normalizedSidebar = Array.isArray(sidebar)
|
||||
? sidebar
|
||||
: normalizeCategoriesShorthand(sidebar, options);
|
||||
|
||||
return normalizedSidebar.flatMap((subitem) =>
|
||||
normalizeItem(subitem, options),
|
||||
return normalizedSidebar.flatMap((subItem) =>
|
||||
normalizeItem(subItem, options),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeSidebars(
|
||||
sidebars: SidebarsConfig,
|
||||
options: SidebarOptions,
|
||||
params: NormalizeSidebarsParams,
|
||||
): NormalizedSidebars {
|
||||
return mapValues(sidebars, (subitem) => normalizeSidebar(subitem, options));
|
||||
return mapValues(sidebars, (items) => normalizeSidebar(items, params));
|
||||
}
|
||||
|
|
|
@ -21,18 +21,24 @@ import type {
|
|||
SidebarItemsGeneratorOption,
|
||||
SidebarItemsGeneratorDoc,
|
||||
SidebarItemsGeneratorVersion,
|
||||
NormalizedSidebarItemCategory,
|
||||
SidebarItemCategory,
|
||||
SidebarItemAutogenerated,
|
||||
} from './types';
|
||||
import {transformSidebarItems} from './utils';
|
||||
import {DefaultSidebarItemsGenerator} from './generator';
|
||||
import {mapValues, memoize, pick} from 'lodash';
|
||||
import combinePromises from 'combine-promises';
|
||||
import {normalizeItem} from './normalization';
|
||||
import {Slugger} from '@docusaurus/utils';
|
||||
|
||||
export type SidebarProcessorProps = {
|
||||
export type SidebarProcessorParams = {
|
||||
sidebarItemsGenerator: SidebarItemsGeneratorOption;
|
||||
numberPrefixParser: NumberPrefixParser;
|
||||
docs: DocMetadataBase[];
|
||||
version: VersionMetadata;
|
||||
options: SidebarOptions;
|
||||
categoryLabelSlugger: Slugger;
|
||||
sidebarOptions: SidebarOptions;
|
||||
};
|
||||
|
||||
function toSidebarItemsGeneratorDoc(
|
||||
|
@ -40,6 +46,7 @@ function toSidebarItemsGeneratorDoc(
|
|||
): SidebarItemsGeneratorDoc {
|
||||
return pick(doc, [
|
||||
'id',
|
||||
'unversionedId',
|
||||
'frontMatter',
|
||||
'source',
|
||||
'sourceDirName',
|
||||
|
@ -56,48 +63,71 @@ function toSidebarItemsGeneratorVersion(
|
|||
// Handle the generation of autogenerated sidebar items and other post-processing checks
|
||||
async function processSidebar(
|
||||
unprocessedSidebar: NormalizedSidebar,
|
||||
{
|
||||
params: SidebarProcessorParams,
|
||||
): Promise<Sidebar> {
|
||||
const {
|
||||
sidebarItemsGenerator,
|
||||
numberPrefixParser,
|
||||
docs,
|
||||
version,
|
||||
options,
|
||||
}: SidebarProcessorProps,
|
||||
): Promise<Sidebar> {
|
||||
sidebarOptions,
|
||||
} = params;
|
||||
|
||||
// Just a minor lazy transformation optimization
|
||||
const getSidebarItemsGeneratorDocsAndVersion = memoize(() => ({
|
||||
docs: docs.map(toSidebarItemsGeneratorDoc),
|
||||
version: toSidebarItemsGeneratorVersion(version),
|
||||
}));
|
||||
|
||||
async function handleAutoGeneratedItems(
|
||||
item: NormalizedSidebarItem,
|
||||
): Promise<SidebarItem[]> {
|
||||
if (item.type === 'category') {
|
||||
return [
|
||||
{
|
||||
async function processCategoryItem(
|
||||
item: NormalizedSidebarItemCategory,
|
||||
): Promise<SidebarItemCategory> {
|
||||
return {
|
||||
...item,
|
||||
items: (
|
||||
await Promise.all(item.items.map(handleAutoGeneratedItems))
|
||||
).flat(),
|
||||
},
|
||||
];
|
||||
items: (await Promise.all(item.items.map(processItem))).flat(),
|
||||
};
|
||||
}
|
||||
if (item.type === 'autogenerated') {
|
||||
return sidebarItemsGenerator({
|
||||
|
||||
async function processAutoGeneratedItem(
|
||||
item: SidebarItemAutogenerated,
|
||||
): Promise<SidebarItem[]> {
|
||||
// TODO the returned type can't be trusted in practice (generator can be user-provided)
|
||||
const generatedItems = await sidebarItemsGenerator({
|
||||
item,
|
||||
numberPrefixParser,
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
...getSidebarItemsGeneratorDocsAndVersion(),
|
||||
options,
|
||||
options: sidebarOptions,
|
||||
});
|
||||
// TODO validate generated items: user can generate bad items
|
||||
|
||||
const generatedItemsNormalized = generatedItems.flatMap((generatedItem) =>
|
||||
normalizeItem(generatedItem, {...params, ...sidebarOptions}),
|
||||
);
|
||||
|
||||
// Process again... weird but sidebar item generated might generate some auto-generated items?
|
||||
return processItems(generatedItemsNormalized);
|
||||
}
|
||||
|
||||
async function processItem(
|
||||
item: NormalizedSidebarItem,
|
||||
): Promise<SidebarItem[]> {
|
||||
if (item.type === 'category') {
|
||||
return [await processCategoryItem(item)];
|
||||
}
|
||||
if (item.type === 'autogenerated') {
|
||||
return processAutoGeneratedItem(item);
|
||||
}
|
||||
return [item];
|
||||
}
|
||||
|
||||
const processedSidebar = (
|
||||
await Promise.all(unprocessedSidebar.map(handleAutoGeneratedItems))
|
||||
).flat();
|
||||
async function processItems(
|
||||
items: NormalizedSidebarItem[],
|
||||
): Promise<SidebarItem[]> {
|
||||
return (await Promise.all(items.map(processItem))).flat();
|
||||
}
|
||||
|
||||
const processedSidebar = await processItems(unprocessedSidebar);
|
||||
|
||||
const fixSidebarItemInconsistencies = (item: SidebarItem): SidebarItem => {
|
||||
// A non-collapsible category can't be collapsed!
|
||||
|
@ -114,11 +144,11 @@ async function processSidebar(
|
|||
|
||||
export async function processSidebars(
|
||||
unprocessedSidebars: NormalizedSidebars,
|
||||
props: SidebarProcessorProps,
|
||||
params: SidebarProcessorParams,
|
||||
): Promise<Sidebars> {
|
||||
return combinePromises(
|
||||
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
|
||||
processSidebar(unprocessedSidebar, props),
|
||||
processSidebar(unprocessedSidebar, params),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
NumberPrefixParser,
|
||||
SidebarOptions,
|
||||
} from '../types';
|
||||
import {Required} from 'utility-types';
|
||||
|
||||
// Makes all properties visible when hovering over the type
|
||||
type Expand<T extends Record<string, unknown>> = {[P in keyof T]: T[P]};
|
||||
|
@ -45,10 +46,35 @@ type SidebarItemCategoryBase = SidebarItemBase & {
|
|||
collapsible: boolean;
|
||||
};
|
||||
|
||||
export type SidebarItemCategoryLinkDoc = {type: 'doc'; id: string};
|
||||
|
||||
export type SidebarItemCategoryLinkGeneratedIndexConfig = {
|
||||
type: 'generated-index';
|
||||
slug?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
export type SidebarItemCategoryLinkGeneratedIndex = {
|
||||
type: 'generated-index';
|
||||
slug: string;
|
||||
permalink: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type SidebarItemCategoryLinkConfig =
|
||||
| SidebarItemCategoryLinkDoc
|
||||
| SidebarItemCategoryLinkGeneratedIndexConfig;
|
||||
|
||||
export type SidebarItemCategoryLink =
|
||||
| SidebarItemCategoryLinkDoc
|
||||
| SidebarItemCategoryLinkGeneratedIndex;
|
||||
|
||||
// The user-given configuration in sidebars.js, before normalization
|
||||
export type SidebarItemCategoryConfig = Expand<
|
||||
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
|
||||
items: SidebarItemConfig[];
|
||||
link?: SidebarItemCategoryLinkConfig;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -73,6 +99,7 @@ export type SidebarsConfig = {
|
|||
export type NormalizedSidebarItemCategory = Expand<
|
||||
SidebarItemCategoryBase & {
|
||||
items: NormalizedSidebarItem[];
|
||||
link?: SidebarItemCategoryLink;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -90,14 +117,25 @@ export type NormalizedSidebars = {
|
|||
export type SidebarItemCategory = Expand<
|
||||
SidebarItemCategoryBase & {
|
||||
items: SidebarItem[];
|
||||
link?: SidebarItemCategoryLink;
|
||||
}
|
||||
>;
|
||||
|
||||
export type SidebarItemCategoryWithLink = Required<SidebarItemCategory, 'link'>;
|
||||
|
||||
export type SidebarItemCategoryWithGeneratedIndex =
|
||||
SidebarItemCategoryWithLink & {link: SidebarItemCategoryLinkGeneratedIndex};
|
||||
|
||||
export type SidebarItem =
|
||||
| SidebarItemDoc
|
||||
| SidebarItemLink
|
||||
| SidebarItemCategory;
|
||||
|
||||
// A sidebar item that is part of the previous/next ordered navigation
|
||||
export type SidebarNavigationItem =
|
||||
| SidebarItemDoc
|
||||
| SidebarItemCategoryWithLink;
|
||||
|
||||
export type Sidebar = SidebarItem[];
|
||||
export type SidebarItemType = SidebarItem['type'];
|
||||
export type Sidebars = {
|
||||
|
@ -108,21 +146,42 @@ export type Sidebars = {
|
|||
export type PropSidebarItemCategory = Expand<
|
||||
SidebarItemCategoryBase & {
|
||||
items: PropSidebarItem[];
|
||||
href?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export type PropSidebarItem = SidebarItemLink | PropSidebarItemCategory;
|
||||
// we may want to use a union type in props instead of this generic link?
|
||||
export type PropSidebarItemLink = SidebarItemLink & {
|
||||
docId?: string;
|
||||
};
|
||||
|
||||
export type PropSidebarItem = PropSidebarItemLink | PropSidebarItemCategory;
|
||||
export type PropSidebar = PropSidebarItem[];
|
||||
export type PropSidebars = {
|
||||
[sidebarId: string]: PropSidebar;
|
||||
};
|
||||
|
||||
export type PropVersionDoc = {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
sidebar?: string;
|
||||
};
|
||||
export type PropVersionDocs = {
|
||||
[docId: string]: PropVersionDoc;
|
||||
};
|
||||
|
||||
// Reduce API surface for options.sidebarItemsGenerator
|
||||
// The user-provided generator fn should receive only a subset of metadata
|
||||
// A change to any of these metadata can be considered as a breaking change
|
||||
export type SidebarItemsGeneratorDoc = Pick<
|
||||
DocMetadataBase,
|
||||
'id' | 'frontMatter' | 'source' | 'sourceDirName' | 'sidebarPosition'
|
||||
| 'id'
|
||||
| 'unversionedId'
|
||||
| 'frontMatter'
|
||||
| 'source'
|
||||
| 'sourceDirName'
|
||||
| 'sidebarPosition'
|
||||
>;
|
||||
export type SidebarItemsGeneratorVersion = Pick<
|
||||
VersionMetadata,
|
||||
|
@ -138,7 +197,9 @@ export type SidebarItemsGeneratorArgs = {
|
|||
};
|
||||
export type SidebarItemsGenerator = (
|
||||
generatorArgs: SidebarItemsGeneratorArgs,
|
||||
) => Promise<SidebarItem[]>;
|
||||
) => // TODO TS issue: the generator can generate un-normalized items!
|
||||
Promise<SidebarItem[]>;
|
||||
// Promise<SidebarItemConfig[]>;
|
||||
|
||||
// Also inject the default generator to conveniently wrap/enhance/sort the default sidebar gen logic
|
||||
// see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320
|
||||
|
|
|
@ -16,8 +16,15 @@ import type {
|
|||
SidebarCategoriesShorthand,
|
||||
SidebarItemConfig,
|
||||
} from './types';
|
||||
import {mapValues, difference} from 'lodash';
|
||||
|
||||
import {mapValues, difference, uniq} from 'lodash';
|
||||
import {getElementsAround, toMessageRelativeFilePath} from '@docusaurus/utils';
|
||||
import {DocMetadataBase, DocNavLink} from '../types';
|
||||
import {
|
||||
SidebarItemCategoryWithGeneratedIndex,
|
||||
SidebarItemCategoryWithLink,
|
||||
SidebarNavigationItem,
|
||||
} from './types';
|
||||
|
||||
export function isCategoriesShorthand(
|
||||
item: SidebarItemConfig,
|
||||
|
@ -41,21 +48,24 @@ export function transformSidebarItems(
|
|||
return sidebar.map(transformRecursive);
|
||||
}
|
||||
|
||||
// Flatten sidebar items into a single flat array (containing categories/docs on the same level)
|
||||
// /!\ order matters (useful for next/prev nav), top categories appear before their child elements
|
||||
function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] {
|
||||
function flattenRecursive(item: SidebarItem): SidebarItem[] {
|
||||
return item.type === 'category'
|
||||
? [item, ...item.items.flatMap(flattenRecursive)]
|
||||
: [item];
|
||||
}
|
||||
return items.flatMap(flattenRecursive);
|
||||
}
|
||||
|
||||
function collectSidebarItemsOfType<
|
||||
Type extends SidebarItemType,
|
||||
Item extends SidebarItem & {type: SidebarItemType},
|
||||
>(type: Type, sidebar: Sidebar): Item[] {
|
||||
function collectRecursive(item: SidebarItem): Item[] {
|
||||
const currentItemsCollected: Item[] =
|
||||
item.type === type ? [item as Item] : [];
|
||||
|
||||
const childItemsCollected: Item[] =
|
||||
item.type === 'category' ? item.items.flatMap(collectRecursive) : [];
|
||||
|
||||
return [...currentItemsCollected, ...childItemsCollected];
|
||||
}
|
||||
|
||||
return sidebar.flatMap(collectRecursive);
|
||||
return flattenSidebarItems(sidebar).filter(
|
||||
(item) => item.type === type,
|
||||
) as Item[];
|
||||
}
|
||||
|
||||
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
|
||||
|
@ -70,25 +80,72 @@ export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] {
|
|||
return collectSidebarItemsOfType('link', sidebar);
|
||||
}
|
||||
|
||||
// /!\ docId order matters for navigation!
|
||||
export function collectSidebarDocIds(sidebar: Sidebar): string[] {
|
||||
return flattenSidebarItems(sidebar).flatMap((item) => {
|
||||
if (item.type === 'category') {
|
||||
return item.link?.type === 'doc' ? [item.link.id] : [];
|
||||
}
|
||||
if (item.type === 'doc') {
|
||||
return [item.id];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
export function collectSidebarNavigation(
|
||||
sidebar: Sidebar,
|
||||
): SidebarNavigationItem[] {
|
||||
return flattenSidebarItems(sidebar).flatMap((item) => {
|
||||
if (item.type === 'category' && item.link) {
|
||||
return [item as SidebarNavigationItem];
|
||||
}
|
||||
if (item.type === 'doc') {
|
||||
return [item];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
export function collectSidebarsDocIds(
|
||||
sidebars: Sidebars,
|
||||
): Record<string, string[]> {
|
||||
return mapValues(sidebars, (sidebar) =>
|
||||
collectSidebarDocItems(sidebar).map((docItem) => docItem.id),
|
||||
);
|
||||
return mapValues(sidebars, collectSidebarDocIds);
|
||||
}
|
||||
|
||||
export function createSidebarsUtils(sidebars: Sidebars): {
|
||||
export function collectSidebarsNavigations(
|
||||
sidebars: Sidebars,
|
||||
): Record<string, SidebarNavigationItem[]> {
|
||||
return mapValues(sidebars, collectSidebarNavigation);
|
||||
}
|
||||
|
||||
export type SidebarNavigation = {
|
||||
sidebarName: string | undefined;
|
||||
previous: SidebarNavigationItem | undefined;
|
||||
next: SidebarNavigationItem | undefined;
|
||||
};
|
||||
|
||||
// A convenient and performant way to query the sidebars content
|
||||
export type SidebarsUtils = {
|
||||
sidebars: Sidebars;
|
||||
getFirstDocIdOfFirstSidebar: () => string | undefined;
|
||||
getSidebarNameByDocId: (docId: string) => string | undefined;
|
||||
getDocNavigation: (docId: string) => {
|
||||
sidebarName: string | undefined;
|
||||
previousId: string | undefined;
|
||||
nextId: string | undefined;
|
||||
};
|
||||
getDocNavigation: (
|
||||
unversionedId: string,
|
||||
versionedId: string,
|
||||
) => SidebarNavigation;
|
||||
getCategoryGeneratedIndexList: () => SidebarItemCategoryWithGeneratedIndex[];
|
||||
getCategoryGeneratedIndexNavigation: (
|
||||
categoryGeneratedIndexPermalink: string,
|
||||
) => SidebarNavigation;
|
||||
|
||||
checkSidebarsDocIds: (validDocIds: string[], sidebarFilePath: string) => void;
|
||||
} {
|
||||
};
|
||||
|
||||
export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
|
||||
const sidebarNameToDocIds = collectSidebarsDocIds(sidebars);
|
||||
const sidebarNameToNavigationItems = collectSidebarsNavigations(sidebars);
|
||||
|
||||
// Reverse mapping
|
||||
const docIdToSidebarName = Object.fromEntries(
|
||||
Object.entries(sidebarNameToDocIds).flatMap(([sidebarName, docIds]) =>
|
||||
|
@ -104,28 +161,92 @@ export function createSidebarsUtils(sidebars: Sidebars): {
|
|||
return docIdToSidebarName[docId];
|
||||
}
|
||||
|
||||
function getDocNavigation(docId: string): {
|
||||
sidebarName: string | undefined;
|
||||
previousId: string | undefined;
|
||||
nextId: string | undefined;
|
||||
} {
|
||||
const sidebarName = getSidebarNameByDocId(docId);
|
||||
if (sidebarName) {
|
||||
const docIds = sidebarNameToDocIds[sidebarName];
|
||||
const currentIndex = docIds.indexOf(docId);
|
||||
const {previous, next} = getElementsAround(docIds, currentIndex);
|
||||
return {
|
||||
sidebarName,
|
||||
previousId: previous,
|
||||
nextId: next,
|
||||
};
|
||||
} else {
|
||||
function emptySidebarNavigation(): SidebarNavigation {
|
||||
return {
|
||||
sidebarName: undefined,
|
||||
previousId: undefined,
|
||||
nextId: undefined,
|
||||
previous: undefined,
|
||||
next: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getDocNavigation(
|
||||
unversionedId: string,
|
||||
versionedId: string,
|
||||
): SidebarNavigation {
|
||||
// TODO legacy id retro-compatibility!
|
||||
let docId = unversionedId;
|
||||
let sidebarName = getSidebarNameByDocId(docId);
|
||||
if (!sidebarName) {
|
||||
docId = versionedId;
|
||||
sidebarName = getSidebarNameByDocId(docId);
|
||||
}
|
||||
|
||||
if (sidebarName) {
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex((item) => {
|
||||
if (item.type === 'doc') {
|
||||
return item.id === docId;
|
||||
}
|
||||
if (item.type === 'category' && item.link.type === 'doc') {
|
||||
return item.link.id === docId;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
} else {
|
||||
return emptySidebarNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
function getCategoryGeneratedIndexList(): SidebarItemCategoryWithGeneratedIndex[] {
|
||||
return Object.values(sidebarNameToNavigationItems)
|
||||
.flat()
|
||||
.flatMap((item) => {
|
||||
if (item.type === 'category' && item.link.type === 'generated-index') {
|
||||
return [item as SidebarItemCategoryWithGeneratedIndex];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// We identity the category generated index by its permalink (should be unique)
|
||||
// More reliable than using object identity
|
||||
function getCategoryGeneratedIndexNavigation(
|
||||
categoryGeneratedIndexPermalink: string,
|
||||
): SidebarNavigation {
|
||||
function isCurrentCategoryGeneratedIndexItem(
|
||||
item: SidebarNavigationItem,
|
||||
): boolean {
|
||||
return (
|
||||
item.type === 'category' &&
|
||||
item.link?.type === 'generated-index' &&
|
||||
item.link.permalink === categoryGeneratedIndexPermalink
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarName = Object.entries(sidebarNameToNavigationItems).find(
|
||||
([, navigationItems]) =>
|
||||
navigationItems.find(isCurrentCategoryGeneratedIndexItem),
|
||||
)?.[0];
|
||||
|
||||
if (sidebarName) {
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex(
|
||||
isCurrentCategoryGeneratedIndexItem,
|
||||
);
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
} else {
|
||||
return emptySidebarNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
function checkSidebarsDocIds(validDocIds: string[], sidebarFilePath: string) {
|
||||
|
@ -140,15 +261,69 @@ These sidebar document ids do not exist:
|
|||
- ${invalidSidebarDocIds.sort().join('\n- ')}
|
||||
|
||||
Available document ids are:
|
||||
- ${validDocIds.sort().join('\n- ')}`,
|
||||
- ${uniq(validDocIds).sort().join('\n- ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sidebars,
|
||||
getFirstDocIdOfFirstSidebar,
|
||||
getSidebarNameByDocId,
|
||||
getDocNavigation,
|
||||
getCategoryGeneratedIndexList,
|
||||
getCategoryGeneratedIndexNavigation,
|
||||
checkSidebarsDocIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function toDocNavigationLink(doc: DocMetadataBase): DocNavLink {
|
||||
const {
|
||||
title,
|
||||
permalink,
|
||||
frontMatter: {
|
||||
pagination_label: paginationLabel,
|
||||
sidebar_label: sidebarLabel,
|
||||
},
|
||||
} = doc;
|
||||
return {title: paginationLabel ?? sidebarLabel ?? title, permalink};
|
||||
}
|
||||
|
||||
export function toNavigationLink(
|
||||
navigationItem: SidebarNavigationItem | undefined,
|
||||
docsById: Record<string, DocMetadataBase>,
|
||||
): DocNavLink | undefined {
|
||||
function getDocById(docId: string) {
|
||||
const doc = docsById[docId];
|
||||
if (!doc) {
|
||||
throw new Error(
|
||||
`Can't create navigation link: no doc found with id=${docId}`,
|
||||
);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function handleCategory(category: SidebarItemCategoryWithLink): DocNavLink {
|
||||
if (category.link.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(category.link.id));
|
||||
} else if (category.link.type === 'generated-index') {
|
||||
return {
|
||||
title: category.label,
|
||||
permalink: category.link.permalink,
|
||||
};
|
||||
} else {
|
||||
throw new Error('unexpected category link type');
|
||||
}
|
||||
}
|
||||
if (!navigationItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (navigationItem.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(navigationItem.id));
|
||||
} else if (navigationItem.type === 'category') {
|
||||
return handleCategory(navigationItem);
|
||||
} else {
|
||||
throw new Error('unexpected navigation item');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,13 @@ import type {
|
|||
SidebarItemDoc,
|
||||
SidebarItemLink,
|
||||
SidebarItemCategoryConfig,
|
||||
SidebarItemCategoryLink,
|
||||
SidebarsConfig,
|
||||
SidebarItemCategoryLinkDoc,
|
||||
SidebarItemCategoryLinkGeneratedIndex,
|
||||
} from './types';
|
||||
import {isCategoriesShorthand} from './utils';
|
||||
import {CategoryMetadataFile} from './generator';
|
||||
|
||||
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
|
||||
className: Joi.string(),
|
||||
|
@ -48,6 +52,36 @@ const sidebarItemLinkSchema = sidebarItemBaseSchema.append<SidebarItemLink>({
|
|||
.messages({'any.unknown': '"label" must be a string'}),
|
||||
});
|
||||
|
||||
const sidebarItemCategoryLinkSchema = Joi.object<SidebarItemCategoryLink>()
|
||||
.when('.type', {
|
||||
switch: [
|
||||
{
|
||||
is: 'doc',
|
||||
then: Joi.object<SidebarItemCategoryLinkDoc>({
|
||||
type: 'doc',
|
||||
id: Joi.string().required(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
is: 'generated-index',
|
||||
then: Joi.object<SidebarItemCategoryLinkGeneratedIndex>({
|
||||
type: 'generated-index',
|
||||
slug: Joi.string().optional(),
|
||||
// permalink: Joi.string().optional(), // No, this one is not in the user config, only in the normalized version
|
||||
title: Joi.string().optional(),
|
||||
description: Joi.string().optional(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
is: Joi.string().required(),
|
||||
then: Joi.forbidden().messages({
|
||||
'any.unknown': 'Unknown sidebar category link type "{.type}".',
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
.id('sidebarCategoryLinkSchema');
|
||||
|
||||
const sidebarItemCategorySchema =
|
||||
sidebarItemBaseSchema.append<SidebarItemCategoryConfig>({
|
||||
type: 'category',
|
||||
|
@ -58,6 +92,7 @@ const sidebarItemCategorySchema =
|
|||
items: Joi.array()
|
||||
.required()
|
||||
.messages({'any.unknown': '"items" must be an array'}), // .items(Joi.link('#sidebarItemSchema')),
|
||||
link: sidebarItemCategoryLinkSchema,
|
||||
collapsed: Joi.boolean().messages({
|
||||
'any.unknown': '"collapsed" must be a boolean',
|
||||
}),
|
||||
|
@ -77,14 +112,7 @@ const sidebarItemSchema: Joi.Schema<SidebarItemConfig> = Joi.object()
|
|||
{is: 'autogenerated', then: sidebarItemAutogeneratedSchema},
|
||||
{is: 'category', then: sidebarItemCategorySchema},
|
||||
{
|
||||
is: 'subcategory',
|
||||
then: Joi.forbidden().messages({
|
||||
'any.unknown':
|
||||
'Docusaurus v2: "subcategory" has been renamed as "category".',
|
||||
}),
|
||||
},
|
||||
{
|
||||
is: Joi.string().required(),
|
||||
is: Joi.any().required(),
|
||||
then: Joi.forbidden().messages({
|
||||
'any.unknown': 'Unknown sidebar item type "{.type}".',
|
||||
}),
|
||||
|
@ -105,6 +133,7 @@ function validateSidebarItem(item: unknown): asserts item is SidebarItemConfig {
|
|||
);
|
||||
} else {
|
||||
Joi.assert(item, sidebarItemSchema);
|
||||
|
||||
if ((item as SidebarItemCategoryConfig).type === 'category') {
|
||||
(item as SidebarItemCategoryConfig).items.forEach(validateSidebarItem);
|
||||
}
|
||||
|
@ -122,3 +151,18 @@ export function validateSidebars(
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
||||
label: Joi.string(),
|
||||
position: Joi.number(),
|
||||
collapsed: Joi.boolean(),
|
||||
collapsible: Joi.boolean(),
|
||||
className: Joi.string(),
|
||||
link: sidebarItemCategoryLinkSchema,
|
||||
});
|
||||
|
||||
export function validateCategoryMetadataFile(
|
||||
unsafeContent: unknown,
|
||||
): CategoryMetadataFile {
|
||||
return Joi.attempt(unsafeContent, categoryMetadataFileSchema);
|
||||
}
|
||||
|
|
|
@ -15,39 +15,52 @@ import {
|
|||
DefaultNumberPrefixParser,
|
||||
stripPathNumberPrefixes,
|
||||
} from './numberPrefix';
|
||||
import type {NumberPrefixParser} from './types';
|
||||
import type {DocMetadataBase, NumberPrefixParser} from './types';
|
||||
import {isConventionalDocIndex} from './docs';
|
||||
|
||||
export default function getSlug({
|
||||
baseID,
|
||||
frontmatterSlug,
|
||||
dirName,
|
||||
source,
|
||||
sourceDirName,
|
||||
stripDirNumberPrefixes = true,
|
||||
numberPrefixParser = DefaultNumberPrefixParser,
|
||||
}: {
|
||||
baseID: string;
|
||||
frontmatterSlug?: string;
|
||||
dirName: string;
|
||||
source: DocMetadataBase['slug'];
|
||||
sourceDirName: DocMetadataBase['sourceDirName'];
|
||||
stripDirNumberPrefixes?: boolean;
|
||||
numberPrefixParser?: NumberPrefixParser;
|
||||
}): string {
|
||||
const baseSlug = frontmatterSlug || baseID;
|
||||
let slug: string;
|
||||
if (baseSlug.startsWith('/')) {
|
||||
slug = baseSlug;
|
||||
} else {
|
||||
function getDirNameSlug(): string {
|
||||
const dirNameStripped = stripDirNumberPrefixes
|
||||
? stripPathNumberPrefixes(dirName, numberPrefixParser)
|
||||
: dirName;
|
||||
? stripPathNumberPrefixes(sourceDirName, numberPrefixParser)
|
||||
: sourceDirName;
|
||||
const resolveDirname =
|
||||
dirName === '.'
|
||||
sourceDirName === '.'
|
||||
? '/'
|
||||
: addLeadingSlash(addTrailingSlash(dirNameStripped));
|
||||
slug = resolvePathname(baseSlug, resolveDirname);
|
||||
return resolveDirname;
|
||||
}
|
||||
|
||||
function computeSlug(): string {
|
||||
if (frontmatterSlug?.startsWith('/')) {
|
||||
return frontmatterSlug;
|
||||
} else {
|
||||
const dirNameSlug = getDirNameSlug();
|
||||
if (!frontmatterSlug && isConventionalDocIndex({source, sourceDirName})) {
|
||||
return dirNameSlug;
|
||||
}
|
||||
const baseSlug = frontmatterSlug || baseID;
|
||||
return resolvePathname(baseSlug, getDirNameSlug());
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidSlug(slug: string): string {
|
||||
if (!isValidPathname(slug)) {
|
||||
throw new Error(
|
||||
`We couldn't compute a valid slug for document with id "${baseID}" in "${dirName}" directory.
|
||||
`We couldn't compute a valid slug for document with id "${baseID}" in "${sourceDirName}" directory.
|
||||
The slug we computed looks invalid: ${slug}.
|
||||
Maybe your slug frontmatter is incorrect or you use weird chars in the file path?
|
||||
By using the slug frontmatter, you should be able to fix this error, by using the slug of your choice:
|
||||
|
@ -59,6 +72,8 @@ slug: /my/customDocPath
|
|||
`,
|
||||
);
|
||||
}
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
||||
return ensureValidSlug(computeSlug());
|
||||
}
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
*/
|
||||
|
||||
import type {LoadedVersion, LoadedContent} from './types';
|
||||
import type {Sidebar, Sidebars} from './sidebars/types';
|
||||
import type {
|
||||
Sidebar,
|
||||
SidebarItemCategory,
|
||||
SidebarItemCategoryLink,
|
||||
Sidebars,
|
||||
} from './sidebars/types';
|
||||
|
||||
import {chain, mapValues, keyBy} from 'lodash';
|
||||
import {
|
||||
|
@ -21,6 +26,7 @@ import type {
|
|||
} from '@docusaurus/types';
|
||||
import {mergeTranslations} from '@docusaurus/utils';
|
||||
import {CURRENT_VERSION_NAME} from './constants';
|
||||
import {TranslationMessage} from '@docusaurus/types';
|
||||
|
||||
function getVersionFileName(versionName: string): string {
|
||||
if (versionName === CURRENT_VERSION_NAME) {
|
||||
|
@ -96,14 +102,48 @@ function getSidebarTranslationFileContent(
|
|||
sidebar: Sidebar,
|
||||
sidebarName: string,
|
||||
): TranslationFileContent {
|
||||
type TranslationMessageEntry = [string, TranslationMessage];
|
||||
|
||||
const categories = collectSidebarCategories(sidebar);
|
||||
const categoryContent: TranslationFileContent = chain(categories)
|
||||
.keyBy((category) => `sidebar.${sidebarName}.category.${category.label}`)
|
||||
.mapValues((category) => ({
|
||||
|
||||
const categoryContent: TranslationFileContent = Object.fromEntries(
|
||||
categories.flatMap((category) => {
|
||||
const entries: TranslationMessageEntry[] = [];
|
||||
|
||||
entries.push([
|
||||
`sidebar.${sidebarName}.category.${category.label}`,
|
||||
{
|
||||
message: category.label,
|
||||
description: `The label for category ${category.label} in sidebar ${sidebarName}`,
|
||||
}))
|
||||
.value();
|
||||
},
|
||||
]);
|
||||
|
||||
if (category.link) {
|
||||
if (category.link.type === 'generated-index') {
|
||||
if (category.link.title) {
|
||||
entries.push([
|
||||
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`,
|
||||
{
|
||||
message: category.link.title,
|
||||
description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (category.link.description) {
|
||||
entries.push([
|
||||
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`,
|
||||
{
|
||||
message: category.link.description,
|
||||
description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}),
|
||||
);
|
||||
|
||||
const links = collectSidebarLinks(sidebar);
|
||||
const linksContent: TranslationFileContent = chain(links)
|
||||
|
@ -126,13 +166,39 @@ function translateSidebar({
|
|||
sidebarName: string;
|
||||
sidebarsTranslations: TranslationFileContent;
|
||||
}): Sidebar {
|
||||
function transformSidebarCategoryLink(
|
||||
category: SidebarItemCategory,
|
||||
): SidebarItemCategoryLink | undefined {
|
||||
if (!category.link) {
|
||||
return undefined;
|
||||
}
|
||||
if (category.link.type === 'generated-index') {
|
||||
const title =
|
||||
sidebarsTranslations[
|
||||
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`
|
||||
]?.message ?? category.link.title;
|
||||
const description =
|
||||
sidebarsTranslations[
|
||||
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`
|
||||
]?.message ?? category.link.description;
|
||||
return {
|
||||
...category.link,
|
||||
title,
|
||||
description,
|
||||
};
|
||||
}
|
||||
return category.link;
|
||||
}
|
||||
|
||||
return transformSidebarItems(sidebar, (item) => {
|
||||
if (item.type === 'category') {
|
||||
const link = transformSidebarCategoryLink(item);
|
||||
return {
|
||||
...item,
|
||||
label:
|
||||
sidebarsTranslations[`sidebar.${sidebarName}.category.${item.label}`]
|
||||
?.message ?? item.label,
|
||||
...(link && {link}),
|
||||
};
|
||||
}
|
||||
if (item.type === 'link') {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
|
||||
import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
|
||||
import type {Tag, FrontMatterTag} from '@docusaurus/utils';
|
||||
import type {Tag, FrontMatterTag, Slugger} from '@docusaurus/utils';
|
||||
import type {
|
||||
BrokenMarkdownLink as IBrokenMarkdownLink,
|
||||
ContentPaths,
|
||||
|
@ -86,6 +86,11 @@ export type SidebarOptions = {
|
|||
sidebarCollapsed: boolean;
|
||||
};
|
||||
|
||||
export type NormalizeSidebarsParams = SidebarOptions & {
|
||||
version: VersionMetadata;
|
||||
categoryLabelSlugger: Slugger;
|
||||
};
|
||||
|
||||
export type PluginOptions = MetadataOptions &
|
||||
PathOptions &
|
||||
VersionsOptions &
|
||||
|
@ -98,6 +103,7 @@ export type PluginOptions = MetadataOptions &
|
|||
docItemComponent: string;
|
||||
docTagDocListComponent: string;
|
||||
docTagsListComponent: string;
|
||||
docCategoryGeneratedIndexComponent: string;
|
||||
admonitions: Record<string, unknown>;
|
||||
disableVersioning: boolean;
|
||||
includeCurrentVersion: boolean;
|
||||
|
@ -135,14 +141,14 @@ export type DocFrontMatter = {
|
|||
};
|
||||
|
||||
export type DocMetadataBase = LastUpdateData & {
|
||||
id: string; // TODO legacy versioned id => try to remove
|
||||
unversionedId: string; // TODO new unversioned id => try to rename to "id"
|
||||
version: VersionName;
|
||||
unversionedId: string;
|
||||
id: string;
|
||||
isDocsHomePage: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
source: string;
|
||||
sourceDirName: string; // relative to the docs folder (can be ".")
|
||||
source: string; // @site aliased source => "@site/docs/folder/subFolder/subSubFolder/myDoc.md"
|
||||
sourceDirName: string; // relative to the versioned docs folder (can be ".") => "folder/subFolder/subSubFolder"
|
||||
slug: string;
|
||||
permalink: string;
|
||||
sidebarPosition?: number;
|
||||
|
@ -162,6 +168,16 @@ export type DocMetadata = DocMetadataBase & {
|
|||
next?: DocNavLink;
|
||||
};
|
||||
|
||||
export type CategoryGeneratedIndexMetadata = {
|
||||
title: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
permalink: string;
|
||||
sidebar: string;
|
||||
previous?: DocNavLink;
|
||||
next?: DocNavLink;
|
||||
};
|
||||
|
||||
export type SourceToPermalink = {
|
||||
[source: string]: string;
|
||||
};
|
||||
|
@ -180,6 +196,7 @@ export type LoadedVersion = VersionMetadata & {
|
|||
mainDocId: string;
|
||||
docs: DocMetadata[];
|
||||
sidebars: Sidebars;
|
||||
categoryGeneratedIndices: CategoryGeneratedIndexMetadata[];
|
||||
};
|
||||
|
||||
export type LoadedContent = {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"clsx": "^1.1.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"globby": "^11.0.2",
|
||||
"infima": "0.2.0-alpha.34",
|
||||
"infima": "0.2.0-alpha.36",
|
||||
"lodash": "^4.17.20",
|
||||
"postcss": "^8.3.7",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
|
|
|
@ -107,11 +107,12 @@ declare module '@theme/CodeBlock' {
|
|||
}
|
||||
|
||||
declare module '@theme/DocPaginator' {
|
||||
import type {PropNavigation} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
type PageInfo = {readonly permalink: string; readonly title: string};
|
||||
|
||||
export interface Props {
|
||||
readonly metadata: {readonly previous?: PageInfo; readonly next?: PageInfo};
|
||||
}
|
||||
// May be simpler to provide a {navigation: PropNavigation} prop?
|
||||
export interface Props extends PropNavigation {}
|
||||
|
||||
const DocPaginator: (props: Props) => JSX.Element;
|
||||
export default DocPaginator;
|
||||
|
@ -138,7 +139,7 @@ declare module '@theme/DocSidebarItem' {
|
|||
|
||||
type DocSidebarPropsBase = {
|
||||
readonly activePath: string;
|
||||
readonly onItemClick?: () => void;
|
||||
readonly onItemClick?: (item: PropSidebarItem) => void;
|
||||
readonly level: number;
|
||||
readonly tabIndex?: number;
|
||||
};
|
||||
|
|
100
packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx
Normal file
100
packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import {
|
||||
PropSidebarItemCategory,
|
||||
PropSidebarItemLink,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import type {Props} from '@theme/DocCard';
|
||||
import {findFirstCategoryLink, useDocById} from '@docusaurus/theme-common';
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
import isInternalUrl from '@docusaurus/isInternalUrl';
|
||||
|
||||
function CardContainer({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href?: string;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
const className = clsx(
|
||||
'card margin-bottom--lg padding--lg',
|
||||
styles.cardContainer,
|
||||
href && styles.cardContainerLink,
|
||||
);
|
||||
return href ? (
|
||||
<Link href={href} className={className}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={className}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardLayout({
|
||||
href,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
href?: string;
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<CardContainer href={href}>
|
||||
<h2 className={clsx('text--truncate', styles.cardTitle)} title={title}>
|
||||
{icon} {title}
|
||||
</h2>
|
||||
<div
|
||||
className={clsx('text--truncate', styles.cardDescription)}
|
||||
title={description}>
|
||||
{description}
|
||||
</div>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function CardCategory({item}: {item: PropSidebarItemCategory}): JSX.Element {
|
||||
const href = findFirstCategoryLink(item);
|
||||
return (
|
||||
<CardLayout
|
||||
href={href}
|
||||
icon="🗃️"
|
||||
title={item.label}
|
||||
description={`${item.items.length} items`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardLink({item}: {item: PropSidebarItemLink}): JSX.Element {
|
||||
const icon = isInternalUrl(item.href) ? '📄️' : '🔗';
|
||||
const doc = useDocById(item.docId ?? undefined);
|
||||
return (
|
||||
<CardLayout
|
||||
href={item.href}
|
||||
icon={icon}
|
||||
title={item.label}
|
||||
description={doc?.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocCard({item}: Props): JSX.Element {
|
||||
switch (item.type) {
|
||||
case 'link':
|
||||
return <CardLink item={item} />;
|
||||
case 'category':
|
||||
return <CardCategory item={item} />;
|
||||
default:
|
||||
throw new Error(`unknown item type ${JSON.stringify(item)}`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.cardContainer {
|
||||
height: 8rem;
|
||||
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
--ifm-link-color: var(--ifm-color-emphasis-800);
|
||||
--ifm-link-hover-color: var(--ifm-color-emphasis-800);
|
||||
--ifm-link-hover-decoration: none;
|
||||
|
||||
/* box-shadow: var(--ifm-global-shadow-lw); */
|
||||
box-shadow: 0 1.5px 3px 0 rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
transition: box-shadow var(--ifm-transition-fast) ease,
|
||||
background-color var(--ifm-transition-fast) ease;
|
||||
}
|
||||
|
||||
.cardContainer.cardContainerLink:hover {
|
||||
/* box-shadow: var(--ifm-global-shadow-md); */
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
html[data-theme='dark'] .cardContainer.cardContainerLink:hover {
|
||||
--ifm-card-background-color: #2d2d2d; /* original, non-hovered color is #242526 */
|
||||
}
|
||||
|
||||
.cardContainer:not(.cardContainerLink) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.2rem;
|
||||
min-height: 1.2rem;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
font-size: 0.8rem;
|
||||
min-height: 0.8rem;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import DocCard from '@theme/DocCard';
|
||||
import {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export default function DocCardList({
|
||||
items,
|
||||
}: {
|
||||
items: PropSidebarItem[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="row">
|
||||
{items.map((item, index) => (
|
||||
<article key={index} className="col col--6">
|
||||
<DocCard item={item} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
import type {Props} from '@theme/DocCategoryGeneratedIndexPage';
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import DocPaginator from '@theme/DocPaginator';
|
||||
import Seo from '@theme/Seo';
|
||||
import DocVersionBanner from '@theme/DocVersionBanner';
|
||||
import DocVersionBadge from '@theme/DocVersionBadge';
|
||||
import {MainHeading} from '@theme/Heading';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function DocCategoryGeneratedIndexPage({
|
||||
categoryGeneratedIndex,
|
||||
}: Props): JSX.Element {
|
||||
const category = useCurrentSidebarCategory();
|
||||
return (
|
||||
<>
|
||||
<Seo
|
||||
title={categoryGeneratedIndex.title}
|
||||
description={categoryGeneratedIndex.description}
|
||||
/>
|
||||
<div className={styles.generatedIndexPage}>
|
||||
<DocVersionBanner />
|
||||
<DocVersionBadge />
|
||||
<header>
|
||||
<MainHeading className={styles.title}>
|
||||
{categoryGeneratedIndex.title}
|
||||
</MainHeading>
|
||||
{categoryGeneratedIndex.description && (
|
||||
<p>{categoryGeneratedIndex.description}</p>
|
||||
)}
|
||||
</header>
|
||||
<main className="margin-top--lg">
|
||||
<DocCardList items={category.items} />
|
||||
</main>
|
||||
<footer className="margin-top--lg">
|
||||
<DocPaginator
|
||||
previous={categoryGeneratedIndex.navigation.previous}
|
||||
next={categoryGeneratedIndex.navigation.next}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@media (min-width: 997px) {
|
||||
.generatedIndexPage {
|
||||
max-width: 75% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Duplicated from .markdown h1 */
|
||||
.title {
|
||||
--ifm-h1-font-size: 3rem;
|
||||
margin-bottom: calc(1.25 * var(--ifm-leading));
|
||||
}
|
|
@ -10,6 +10,7 @@ import clsx from 'clsx';
|
|||
import useWindowSize from '@theme/hooks/useWindowSize';
|
||||
import DocPaginator from '@theme/DocPaginator';
|
||||
import DocVersionBanner from '@theme/DocVersionBanner';
|
||||
import DocVersionBadge from '@theme/DocVersionBadge';
|
||||
import Seo from '@theme/Seo';
|
||||
import type {Props} from '@theme/DocItem';
|
||||
import DocItemFooter from '@theme/DocItemFooter';
|
||||
|
@ -20,7 +21,7 @@ import styles from './styles.module.css';
|
|||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||
|
||||
export default function DocItem(props: Props): JSX.Element {
|
||||
const {content: DocContent, versionMetadata} = props;
|
||||
const {content: DocContent} = props;
|
||||
const {metadata, frontMatter} = DocContent;
|
||||
const {
|
||||
image,
|
||||
|
@ -55,18 +56,10 @@ export default function DocItem(props: Props): JSX.Element {
|
|||
className={clsx('col', {
|
||||
[styles.docItemCol]: !hideTableOfContents,
|
||||
})}>
|
||||
<DocVersionBanner versionMetadata={versionMetadata} />
|
||||
<DocVersionBanner />
|
||||
<div className={styles.docItemContainer}>
|
||||
<article>
|
||||
{versionMetadata.badge && (
|
||||
<span
|
||||
className={clsx(
|
||||
ThemeClassNames.docs.docVersionBadge,
|
||||
'badge badge--secondary',
|
||||
)}>
|
||||
Version: {versionMetadata.label}
|
||||
</span>
|
||||
)}
|
||||
<DocVersionBadge />
|
||||
|
||||
{canRenderTOC && (
|
||||
<TOCCollapsible
|
||||
|
@ -95,7 +88,7 @@ export default function DocItem(props: Props): JSX.Element {
|
|||
<DocItemFooter {...props} />
|
||||
</article>
|
||||
|
||||
<DocPaginator metadata={metadata} />
|
||||
<DocPaginator previous={metadata.previous} next={metadata.next} />
|
||||
</div>
|
||||
</div>
|
||||
{renderTocDesktop && (
|
||||
|
|
|
@ -23,27 +23,30 @@ import {translate} from '@docusaurus/Translate';
|
|||
|
||||
import clsx from 'clsx';
|
||||
import styles from './styles.module.css';
|
||||
import {ThemeClassNames, docVersionSearchTag} from '@docusaurus/theme-common';
|
||||
import {
|
||||
ThemeClassNames,
|
||||
docVersionSearchTag,
|
||||
DocsSidebarProvider,
|
||||
useDocsSidebar,
|
||||
DocsVersionProvider,
|
||||
} from '@docusaurus/theme-common';
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
type DocPageContentProps = {
|
||||
readonly currentDocRoute: DocumentRoute;
|
||||
readonly versionMetadata: PropVersionMetadata;
|
||||
readonly children: ReactNode;
|
||||
readonly sidebarName: string | undefined;
|
||||
};
|
||||
|
||||
function DocPageContent({
|
||||
currentDocRoute,
|
||||
versionMetadata,
|
||||
children,
|
||||
sidebarName,
|
||||
}: DocPageContentProps): JSX.Element {
|
||||
const sidebar = useDocsSidebar();
|
||||
const {pluginId, version} = versionMetadata;
|
||||
|
||||
const sidebarName = currentDocRoute.sidebar;
|
||||
const sidebar = sidebarName
|
||||
? versionMetadata.docsSidebars[sidebarName]
|
||||
: undefined;
|
||||
|
||||
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
|
||||
const [hiddenSidebar, setHiddenSidebar] = useState(false);
|
||||
const toggleSidebar = useCallback(() => {
|
||||
|
@ -150,17 +153,30 @@ function DocPage(props: Props): JSX.Element {
|
|||
if (!currentDocRoute) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
// For now, the sidebarName is added as route config: not ideal!
|
||||
const sidebarName = currentDocRoute.sidebar;
|
||||
|
||||
const sidebar = sidebarName
|
||||
? versionMetadata.docsSidebars[sidebarName]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* TODO we should add a core addRoute({htmlClassName}) generic plugin option */}
|
||||
<html className={versionMetadata.className} />
|
||||
</Head>
|
||||
<DocsVersionProvider version={versionMetadata}>
|
||||
<DocsSidebarProvider sidebar={sidebar}>
|
||||
<DocPageContent
|
||||
currentDocRoute={currentDocRoute}
|
||||
versionMetadata={versionMetadata}>
|
||||
versionMetadata={versionMetadata}
|
||||
sidebarName={sidebarName}>
|
||||
{renderRoutes(docRoutes, {versionMetadata})}
|
||||
</DocPageContent>
|
||||
</DocsSidebarProvider>
|
||||
</DocsVersionProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
import type {Props} from '@theme/DocPaginator';
|
||||
import type {PropNavigation} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
function DocPaginator(props: Props): JSX.Element {
|
||||
const {metadata} = props;
|
||||
function DocPaginator(props: PropNavigation): JSX.Element {
|
||||
const {previous, next} = props;
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
@ -22,10 +22,8 @@ function DocPaginator(props: Props): JSX.Element {
|
|||
description: 'The ARIA label for the docs pagination',
|
||||
})}>
|
||||
<div className="pagination-nav__item">
|
||||
{metadata.previous && (
|
||||
<Link
|
||||
className="pagination-nav__link"
|
||||
to={metadata.previous.permalink}>
|
||||
{previous && (
|
||||
<Link className="pagination-nav__link" to={previous.permalink}>
|
||||
<div className="pagination-nav__sublabel">
|
||||
<Translate
|
||||
id="theme.docs.paginator.previous"
|
||||
|
@ -34,14 +32,14 @@ function DocPaginator(props: Props): JSX.Element {
|
|||
</Translate>
|
||||
</div>
|
||||
<div className="pagination-nav__label">
|
||||
« {metadata.previous.title}
|
||||
« {previous.title}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="pagination-nav__item pagination-nav__item--next">
|
||||
{metadata.next && (
|
||||
<Link className="pagination-nav__link" to={metadata.next.permalink}>
|
||||
{next && (
|
||||
<Link className="pagination-nav__link" to={next.permalink}>
|
||||
<div className="pagination-nav__sublabel">
|
||||
<Translate
|
||||
id="theme.docs.paginator.next"
|
||||
|
@ -49,9 +47,7 @@ function DocPaginator(props: Props): JSX.Element {
|
|||
Next
|
||||
</Translate>
|
||||
</div>
|
||||
<div className="pagination-nav__label">
|
||||
{metadata.next.title} »
|
||||
</div>
|
||||
<div className="pagination-nav__label">{next.title} »</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -100,7 +100,15 @@ const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
|
|||
<DocSidebarItems
|
||||
items={sidebar}
|
||||
activePath={path}
|
||||
onItemClick={() => toggleSidebar()}
|
||||
onItemClick={(item) => {
|
||||
// Mobile sidebar should only be closed if the category has a link
|
||||
if (item.type === 'category' && item.href) {
|
||||
toggleSidebar();
|
||||
}
|
||||
if (item.type === 'link') {
|
||||
toggleSidebar();
|
||||
}
|
||||
}}
|
||||
level={1}
|
||||
/>
|
||||
</ul>
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useEffect, memo} from 'react';
|
||||
import React, {useEffect, memo, useMemo} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
isSamePath,
|
||||
isActiveSidebarItem,
|
||||
usePrevious,
|
||||
Collapsible,
|
||||
useCollapsible,
|
||||
findFirstCategoryLink,
|
||||
ThemeClassNames,
|
||||
} from '@docusaurus/theme-common';
|
||||
import Link from '@docusaurus/Link';
|
||||
import isInternalUrl from '@docusaurus/isInternalUrl';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import IconExternalLink from '@theme/IconExternalLink';
|
||||
|
||||
import type {Props, DocSidebarItemsProps} from '@theme/DocSidebarItem';
|
||||
|
@ -25,21 +27,7 @@ import type {
|
|||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const isActiveSidebarItem = (
|
||||
item: Props['item'],
|
||||
activePath: string,
|
||||
): boolean => {
|
||||
if (item.type === 'link') {
|
||||
return isSamePath(item.href, activePath);
|
||||
}
|
||||
if (item.type === 'category') {
|
||||
return item.items.some((subItem) =>
|
||||
isActiveSidebarItem(subItem, activePath),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
|
||||
// Optimize sidebar at each "level"
|
||||
// TODO this item should probably not receive the "activePath" props
|
||||
|
@ -93,6 +81,28 @@ function useAutoExpandActiveCategory({
|
|||
}, [isActive, wasActive, collapsed, setCollapsed]);
|
||||
}
|
||||
|
||||
// When a collapsible category has no link, we still link it to its first child during SSR as a temporary fallback
|
||||
// This allows to be able to navigate inside the category even when JS fails to load, is delayed or simply disabled
|
||||
// React hydration becomes an optional progressive enhancement
|
||||
// see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
|
||||
// see https://github.com/facebook/docusaurus/issues/3030
|
||||
function useCategoryHrefWithSSRFallback(
|
||||
item: PropSidebarItemCategory,
|
||||
): string | undefined {
|
||||
const isBrowser = useIsBrowser();
|
||||
return useMemo(() => {
|
||||
if (item.href) {
|
||||
return item.href;
|
||||
}
|
||||
// In these cases, it's not necessary to render a fallback
|
||||
// We skip the "findFirstCategoryLink" computation
|
||||
if (isBrowser || !item.collapsible) {
|
||||
return undefined;
|
||||
}
|
||||
return findFirstCategoryLink(item);
|
||||
}, [item, isBrowser]);
|
||||
}
|
||||
|
||||
function DocSidebarItemCategory({
|
||||
item,
|
||||
onItemClick,
|
||||
|
@ -100,7 +110,8 @@ function DocSidebarItemCategory({
|
|||
level,
|
||||
...props
|
||||
}: Props & {item: PropSidebarItemCategory}) {
|
||||
const {items, label, collapsible, className} = item;
|
||||
const {items, label, collapsible, className, href} = item;
|
||||
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);
|
||||
|
||||
const isActive = isActiveSidebarItem(item, activePath);
|
||||
|
||||
|
@ -128,25 +139,53 @@ function DocSidebarItemCategory({
|
|||
},
|
||||
className,
|
||||
)}>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a
|
||||
<div className="menu__list-item-collapsible">
|
||||
<Link
|
||||
className={clsx('menu__link', {
|
||||
'menu__link--sublist': collapsible,
|
||||
'menu__link--active': collapsible && isActive,
|
||||
'menu__link--sublist': collapsible && !href,
|
||||
'menu__link--active': isActive,
|
||||
[styles.menuLinkText]: !collapsible,
|
||||
[styles.hasHref]: !!hrefWithSSRFallback,
|
||||
})}
|
||||
onClick={
|
||||
collapsible
|
||||
? (e) => {
|
||||
onItemClick?.(item);
|
||||
if (href) {
|
||||
setCollapsed(false);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
href={collapsible ? '#' : undefined}
|
||||
: () => {
|
||||
onItemClick?.(item);
|
||||
}
|
||||
}
|
||||
href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback}
|
||||
{...props}>
|
||||
{label}
|
||||
</a>
|
||||
</Link>
|
||||
{href && collapsible && (
|
||||
<button
|
||||
aria-label={translate(
|
||||
{
|
||||
id: 'theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel',
|
||||
message: "Toggle the collapsible sidebar category '{label}'",
|
||||
description:
|
||||
'The ARIA label to toggle the collapsible sidebar category',
|
||||
},
|
||||
{label},
|
||||
)}
|
||||
type="button"
|
||||
className="clean-btn menu__caret"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
|
||||
<DocSidebarItems
|
||||
|
@ -186,7 +225,7 @@ function DocSidebarItemLink({
|
|||
aria-current={isActive ? 'page' : undefined}
|
||||
to={href}
|
||||
{...(isInternalUrl(href) && {
|
||||
onClick: onItemClick,
|
||||
onClick: onItemClick ? () => onItemClick(item) : undefined,
|
||||
})}
|
||||
{...props}>
|
||||
{isInternalUrl(href) ? (
|
||||
|
|
|
@ -12,4 +12,8 @@
|
|||
.menuLinkText:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.menuLinkText.hasHref {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {ThemeClassNames, useDocsVersion} from '@docusaurus/theme-common';
|
||||
import clsx from 'clsx';
|
||||
import type {Props} from '@theme/DocVersionBadge';
|
||||
|
||||
export default function DocVersionBadge({
|
||||
className,
|
||||
}: Props): JSX.Element | null {
|
||||
const versionMetadata = useDocsVersion();
|
||||
if (versionMetadata.badge) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
className,
|
||||
ThemeClassNames.docs.docVersionBadge,
|
||||
'badge badge--secondary',
|
||||
)}>
|
||||
Version: {versionMetadata.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -17,15 +17,19 @@ import {
|
|||
import {
|
||||
ThemeClassNames,
|
||||
useDocsPreferredVersion,
|
||||
useDocsVersion,
|
||||
} from '@docusaurus/theme-common';
|
||||
|
||||
import type {Props} from '@theme/DocVersionBanner';
|
||||
import clsx from 'clsx';
|
||||
import type {VersionBanner} from '@docusaurus/plugin-content-docs';
|
||||
import type {
|
||||
VersionBanner,
|
||||
PropVersionMetadata,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
type BannerLabelComponentProps = {
|
||||
siteTitle: string;
|
||||
versionMetadata: Props['versionMetadata'];
|
||||
versionMetadata: PropVersionMetadata;
|
||||
};
|
||||
|
||||
function UnreleasedVersionLabel({
|
||||
|
@ -114,7 +118,12 @@ function LatestVersionSuggestionLabel({
|
|||
);
|
||||
}
|
||||
|
||||
function DocVersionBannerEnabled({versionMetadata}: Props): JSX.Element {
|
||||
function DocVersionBannerEnabled({
|
||||
className,
|
||||
versionMetadata,
|
||||
}: Props & {
|
||||
versionMetadata: PropVersionMetadata;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
siteConfig: {title: siteTitle},
|
||||
} = useDocusaurusContext();
|
||||
|
@ -136,6 +145,7 @@ function DocVersionBannerEnabled({versionMetadata}: Props): JSX.Element {
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
ThemeClassNames.docs.docVersionBanner,
|
||||
'alert alert--warning margin-bottom--md',
|
||||
)}
|
||||
|
@ -154,11 +164,17 @@ function DocVersionBannerEnabled({versionMetadata}: Props): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
function DocVersionBanner({versionMetadata}: Props): JSX.Element | null {
|
||||
export default function DocVersionBanner({
|
||||
className,
|
||||
}: Props): JSX.Element | null {
|
||||
const versionMetadata = useDocsVersion();
|
||||
if (versionMetadata.banner) {
|
||||
return <DocVersionBannerEnabled versionMetadata={versionMetadata} />;
|
||||
return (
|
||||
<DocVersionBannerEnabled
|
||||
className={className}
|
||||
versionMetadata={versionMetadata}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default DocVersionBanner;
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-beta.9",
|
||||
"@docusaurus/types": "2.0.0-beta.9",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"lodash": "^4.17.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
@ -32,7 +32,18 @@ export {
|
|||
|
||||
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||
|
||||
export {isDocsPluginEnabled} from './utils/docsUtils';
|
||||
export {
|
||||
isDocsPluginEnabled,
|
||||
DocsVersionProvider,
|
||||
useDocsVersion,
|
||||
useDocById,
|
||||
DocsSidebarProvider,
|
||||
useDocsSidebar,
|
||||
findSidebarCategory,
|
||||
findFirstCategoryLink,
|
||||
useCurrentSidebarCategory,
|
||||
isActiveSidebarItem,
|
||||
} from './utils/docsUtils';
|
||||
|
||||
export {isSamePath} from './utils/pathUtils';
|
||||
|
||||
|
|
|
@ -0,0 +1,331 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {renderHook} from '@testing-library/react-hooks';
|
||||
import {
|
||||
findFirstCategoryLink,
|
||||
isActiveSidebarItem,
|
||||
DocsVersionProvider,
|
||||
useDocsVersion,
|
||||
useDocById,
|
||||
useDocsSidebar,
|
||||
DocsSidebarProvider,
|
||||
findSidebarCategory,
|
||||
} from '../docsUtils';
|
||||
import {
|
||||
PropSidebar,
|
||||
PropSidebarItem,
|
||||
PropSidebarItemCategory,
|
||||
PropVersionMetadata,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
// Make tests more readable with some useful category item defaults
|
||||
function testCategory(
|
||||
data?: Partial<PropSidebarItemCategory>,
|
||||
): PropSidebarItemCategory {
|
||||
return {
|
||||
type: 'category',
|
||||
href: undefined,
|
||||
label: 'Category label',
|
||||
items: [],
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
function testVersion(data?: Partial<PropVersionMetadata>): PropVersionMetadata {
|
||||
return {
|
||||
version: 'versionName',
|
||||
label: 'Version Label',
|
||||
className: 'version className',
|
||||
badge: true,
|
||||
banner: 'unreleased',
|
||||
docs: {},
|
||||
docsSidebars: {},
|
||||
isLast: false,
|
||||
pluginId: 'default',
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
describe('docsUtils', () => {
|
||||
describe('useDocsVersion', () => {
|
||||
test('should throw if context provider is missing', () => {
|
||||
expect(
|
||||
() => renderHook(() => useDocsVersion()).result.current,
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"This hook requires usage of <DocsVersionProvider>"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('should read value from context provider', () => {
|
||||
const version = testVersion();
|
||||
const {result} = renderHook(() => useDocsVersion(), {
|
||||
wrapper: ({children}) => (
|
||||
<DocsVersionProvider version={version}>
|
||||
{children}
|
||||
</DocsVersionProvider>
|
||||
),
|
||||
});
|
||||
expect(result.current).toBe(version);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDocsSidebar', () => {
|
||||
test('should throw if context provider is missing', () => {
|
||||
expect(
|
||||
() => renderHook(() => useDocsSidebar()).result.current,
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"This hook requires usage of <DocsSidebarProvider>"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('should read value from context provider', () => {
|
||||
const sidebar: PropSidebar = [];
|
||||
const {result} = renderHook(() => useDocsSidebar(), {
|
||||
wrapper: ({children}) => (
|
||||
<DocsSidebarProvider sidebar={sidebar}>
|
||||
{children}
|
||||
</DocsSidebarProvider>
|
||||
),
|
||||
});
|
||||
expect(result.current).toBe(sidebar);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDocById', () => {
|
||||
const version = testVersion({
|
||||
docs: {
|
||||
doc1: {
|
||||
id: 'doc1',
|
||||
title: 'Doc 1',
|
||||
description: 'desc1',
|
||||
sidebar: 'sidebar1',
|
||||
},
|
||||
doc2: {
|
||||
id: 'doc2',
|
||||
title: 'Doc 2',
|
||||
description: 'desc2',
|
||||
sidebar: 'sidebar2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function callHook(docId: string | undefined) {
|
||||
const {result} = renderHook(() => useDocById(docId), {
|
||||
wrapper: ({children}) => (
|
||||
<DocsVersionProvider version={version}>
|
||||
{children}
|
||||
</DocsVersionProvider>
|
||||
),
|
||||
});
|
||||
return result.current;
|
||||
}
|
||||
|
||||
test('should accept undefined', () => {
|
||||
expect(callHook(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should find doc1', () => {
|
||||
expect(callHook('doc1')).toMatchObject({id: 'doc1'});
|
||||
});
|
||||
test('should find doc2', () => {
|
||||
expect(callHook('doc2')).toMatchObject({id: 'doc2'});
|
||||
});
|
||||
|
||||
test('should throw for doc3', () => {
|
||||
expect(() => callHook('doc3')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"no version doc found by id=doc3"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findSidebarCategory', () => {
|
||||
test('should be able to return undefined', () => {
|
||||
expect(findSidebarCategory([], () => false)).toBeUndefined();
|
||||
expect(
|
||||
findSidebarCategory([testCategory(), testCategory()], () => false),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return first element matching predicate', () => {
|
||||
const first = testCategory();
|
||||
const second = testCategory();
|
||||
const third = testCategory();
|
||||
const sidebar = [first, second, third];
|
||||
expect(findSidebarCategory(sidebar, () => true)).toEqual(first);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === first)).toEqual(
|
||||
first,
|
||||
);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === second)).toEqual(
|
||||
second,
|
||||
);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === third)).toEqual(
|
||||
third,
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to search in sub items', () => {
|
||||
const subsub1 = testCategory();
|
||||
const subsub2 = testCategory();
|
||||
const sub1 = testCategory({
|
||||
items: [subsub1, subsub2],
|
||||
});
|
||||
const sub2 = testCategory();
|
||||
const parent = testCategory({
|
||||
items: [sub1, sub2],
|
||||
});
|
||||
const sidebar = [parent];
|
||||
|
||||
expect(findSidebarCategory(sidebar, () => true)).toEqual(parent);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === sub1)).toEqual(
|
||||
sub1,
|
||||
);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === sub2)).toEqual(
|
||||
sub2,
|
||||
);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === subsub1)).toEqual(
|
||||
subsub1,
|
||||
);
|
||||
expect(findSidebarCategory(sidebar, (item) => item === subsub2)).toEqual(
|
||||
subsub2,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findFirstCategoryLink', () => {
|
||||
test('category without link nor child', () => {
|
||||
expect(
|
||||
findFirstCategoryLink(
|
||||
testCategory({
|
||||
href: undefined,
|
||||
}),
|
||||
),
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('category with link', () => {
|
||||
expect(
|
||||
findFirstCategoryLink(
|
||||
testCategory({
|
||||
href: '/itemPath',
|
||||
}),
|
||||
),
|
||||
).toEqual('/itemPath');
|
||||
});
|
||||
|
||||
test('category with deeply nested category link', () => {
|
||||
expect(
|
||||
findFirstCategoryLink(
|
||||
testCategory({
|
||||
href: undefined,
|
||||
items: [
|
||||
testCategory({
|
||||
href: undefined,
|
||||
items: [
|
||||
testCategory({
|
||||
href: '/itemPath',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toEqual('/itemPath');
|
||||
});
|
||||
|
||||
test('category with deeply nested link', () => {
|
||||
expect(
|
||||
findFirstCategoryLink(
|
||||
testCategory({
|
||||
href: undefined,
|
||||
items: [
|
||||
testCategory({
|
||||
href: undefined,
|
||||
items: [{type: 'link', href: '/itemPath', label: 'Label'}],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toEqual('/itemPath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActiveSidebarItem', () => {
|
||||
test('with link href', () => {
|
||||
const item: PropSidebarItem = {
|
||||
type: 'link',
|
||||
href: '/itemPath',
|
||||
label: 'Label',
|
||||
};
|
||||
|
||||
expect(isActiveSidebarItem(item, '/unexistingPath')).toEqual(false);
|
||||
|
||||
expect(isActiveSidebarItem(item, '/itemPath')).toEqual(true);
|
||||
|
||||
// Ensure it's not trailing slash sensitive:
|
||||
expect(isActiveSidebarItem(item, '/itemPath/')).toEqual(true);
|
||||
expect(
|
||||
isActiveSidebarItem({...item, href: '/itemPath/'}, '/itemPath'),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('with category href', () => {
|
||||
const item: PropSidebarItem = testCategory({
|
||||
href: '/itemPath',
|
||||
});
|
||||
|
||||
expect(isActiveSidebarItem(item, '/unexistingPath')).toEqual(false);
|
||||
|
||||
expect(isActiveSidebarItem(item, '/itemPath')).toEqual(true);
|
||||
|
||||
// Ensure it's not trailing slash sensitive:
|
||||
expect(isActiveSidebarItem(item, '/itemPath/')).toEqual(true);
|
||||
expect(
|
||||
isActiveSidebarItem({...item, href: '/itemPath/'}, '/itemPath'),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('with category nested items', () => {
|
||||
const item: PropSidebarItem = testCategory({
|
||||
href: '/category-path',
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
href: '/sub-link-path',
|
||||
label: 'Label',
|
||||
},
|
||||
testCategory({
|
||||
href: '/sub-category-path',
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
href: '/sub-sub-link-path',
|
||||
label: 'Label',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(isActiveSidebarItem(item, '/unexistingPath')).toEqual(false);
|
||||
|
||||
expect(isActiveSidebarItem(item, '/category-path')).toEqual(true);
|
||||
expect(isActiveSidebarItem(item, '/sub-link-path')).toEqual(true);
|
||||
expect(isActiveSidebarItem(item, '/sub-category-path')).toEqual(true);
|
||||
expect(isActiveSidebarItem(item, '/sub-sub-link-path')).toEqual(true);
|
||||
|
||||
// Ensure it's not trailing slash sensitive:
|
||||
expect(isActiveSidebarItem(item, '/category-path/')).toEqual(true);
|
||||
expect(isActiveSidebarItem(item, '/sub-link-path/')).toEqual(true);
|
||||
expect(isActiveSidebarItem(item, '/sub-category-path/')).toEqual(true);
|
||||
expect(isActiveSidebarItem(item, '/sub-sub-link-path/')).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {useAllDocsData} from '@theme/hooks/useDocs';
|
||||
|
||||
// TODO not ideal, see also "useDocs"
|
||||
export const isDocsPluginEnabled: boolean = !!useAllDocsData;
|
185
packages/docusaurus-theme-common/src/utils/docsUtils.tsx
Normal file
185
packages/docusaurus-theme-common/src/utils/docsUtils.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {createContext, ReactNode, useContext} from 'react';
|
||||
import {useAllDocsData} from '@theme/hooks/useDocs';
|
||||
import {
|
||||
PropSidebar,
|
||||
PropSidebarItem,
|
||||
PropSidebarItemCategory,
|
||||
PropVersionDoc,
|
||||
PropVersionMetadata,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import {isSamePath} from './pathUtils';
|
||||
import {useLocation} from '@docusaurus/router';
|
||||
|
||||
// TODO not ideal, see also "useDocs"
|
||||
export const isDocsPluginEnabled: boolean = !!useAllDocsData;
|
||||
|
||||
// Using a Symbol because null is a valid context value (a doc can have no sidebar)
|
||||
// Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx
|
||||
const EmptyContextValue: unique symbol = Symbol('EmptyContext');
|
||||
|
||||
const DocsVersionContext = createContext<
|
||||
PropVersionMetadata | typeof EmptyContextValue
|
||||
>(EmptyContextValue);
|
||||
|
||||
export function DocsVersionProvider({
|
||||
children,
|
||||
version,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
version: PropVersionMetadata | typeof EmptyContextValue;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<DocsVersionContext.Provider value={version}>
|
||||
{children}
|
||||
</DocsVersionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDocsVersion(): PropVersionMetadata {
|
||||
const version = useContext(DocsVersionContext);
|
||||
if (version === EmptyContextValue) {
|
||||
throw new Error('This hook requires usage of <DocsVersionProvider>');
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
export function useDocById(id: string): PropVersionDoc;
|
||||
export function useDocById(id: string | undefined): PropVersionDoc | undefined;
|
||||
export function useDocById(id: string | undefined): PropVersionDoc | undefined {
|
||||
const version = useDocsVersion();
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
const doc = version.docs[id];
|
||||
if (!doc) {
|
||||
throw new Error(`no version doc found by id=${id}`);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
const DocsSidebarContext = createContext<
|
||||
PropSidebar | null | typeof EmptyContextValue
|
||||
>(EmptyContextValue);
|
||||
|
||||
export function DocsSidebarProvider({
|
||||
children,
|
||||
sidebar,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
sidebar: PropSidebar | null;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<DocsSidebarContext.Provider value={sidebar}>
|
||||
{children}
|
||||
</DocsSidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDocsSidebar(): PropSidebar | null {
|
||||
const sidebar = useContext(DocsSidebarContext);
|
||||
if (sidebar === EmptyContextValue) {
|
||||
throw new Error('This hook requires usage of <DocsSidebarProvider>');
|
||||
}
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
// Use the components props and the sidebar in context
|
||||
// to get back the related sidebar category that we want to render
|
||||
export function findSidebarCategory(
|
||||
sidebar: PropSidebar,
|
||||
predicate: (category: PropSidebarItemCategory) => boolean,
|
||||
): PropSidebarItemCategory | undefined {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const item of sidebar) {
|
||||
if (item.type === 'category') {
|
||||
if (predicate(item)) {
|
||||
return item;
|
||||
} else {
|
||||
const subItem = findSidebarCategory(item.items, predicate);
|
||||
if (subItem) {
|
||||
return subItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If a category card has no link => link to the first subItem having a link
|
||||
export function findFirstCategoryLink(
|
||||
item: PropSidebarItemCategory,
|
||||
): string | undefined {
|
||||
if (item.href) {
|
||||
return item.href;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const subItem of item.items) {
|
||||
if (subItem.type === 'link') {
|
||||
return subItem.href;
|
||||
}
|
||||
if (subItem.type === 'category') {
|
||||
const categoryLink = findFirstCategoryLink(subItem);
|
||||
if (categoryLink) {
|
||||
return categoryLink;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unexpected category item type for ${JSON.stringify(subItem)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function useCurrentSidebarCategory(): PropSidebarItemCategory {
|
||||
const {pathname} = useLocation();
|
||||
const sidebar = useDocsSidebar();
|
||||
if (!sidebar) {
|
||||
throw new Error('Unexpected: cant find current sidebar in context');
|
||||
}
|
||||
const category = findSidebarCategory(sidebar, (item) =>
|
||||
isSamePath(item.href, pathname),
|
||||
);
|
||||
if (!category) {
|
||||
throw new Error(
|
||||
`Unexpected: sidebar category could not be found for pathname='${pathname}'.
|
||||
Hook useCurrentSidebarCategory() should only be used on Category pages`,
|
||||
);
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
function containsActiveSidebarItem(
|
||||
items: PropSidebarItem[],
|
||||
activePath: string,
|
||||
): boolean {
|
||||
return items.some((subItem) => isActiveSidebarItem(subItem, activePath));
|
||||
}
|
||||
|
||||
export function isActiveSidebarItem(
|
||||
item: PropSidebarItem,
|
||||
activePath: string,
|
||||
): boolean {
|
||||
const isActive = (testedPath: string | undefined) =>
|
||||
typeof testedPath !== 'undefined' && isSamePath(testedPath, activePath);
|
||||
|
||||
if (item.type === 'link') {
|
||||
return isActive(item.href);
|
||||
}
|
||||
|
||||
if (item.type === 'category') {
|
||||
return (
|
||||
isActive(item.href) || containsActiveSidebarItem(item.items, activePath)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "تم النسخ",
|
||||
"theme.CodeBlock.copy": "نسخ",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "نسخ الرمز إلى الحافظة",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "لم نتمكن من العثور على ما كنت تبحث عنه.",
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
"theme.CodeBlock.copy___DESCRIPTION": "The copy button label on code blocks",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copy code to clipboard",
|
||||
"theme.CodeBlock.copyButtonAriaLabel___DESCRIPTION": "The ARIA label for copy code blocks button",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel___DESCRIPTION": "The ARIA label to toggle the collapsible sidebar category",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.title___DESCRIPTION": "The title of the fallback page when the page crashed",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "কপিড",
|
||||
"theme.CodeBlock.copy": "কপি",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "ক্লিপবোর্ডে কোড কপি করুন",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "আপনি যা খুঁজছিলেন তা আমরা খুঁজে পাইনি।",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Zkopírováno",
|
||||
"theme.CodeBlock.copy": "Zkopírovat",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Zkopírovat kód do schránky",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Nepodařilo se nám najít co jste hledal(a).",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Kopieret",
|
||||
"theme.CodeBlock.copy": "Kopier",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Kopier kode til udklipsholder",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Vi kunne ikke finde det, du søgte.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Kopiert",
|
||||
"theme.CodeBlock.copy": "Kopieren",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "In die Zwischenablage kopieren",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Copiado",
|
||||
"theme.CodeBlock.copy": "Copiar",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copiar código al portapapeles",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "No pudimos encontrar lo que buscaba.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "کپی شد",
|
||||
"theme.CodeBlock.copy": "کپی",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "کپی به کلیپ بورد",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "صفحه ای که دنبال آن بودید پیدا نشد.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Kinopya",
|
||||
"theme.CodeBlock.copy": "Kopyahin",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Kopyahin ang code sa clipboard",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Hindi namin mahanap ang iyong hinananap.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Copié",
|
||||
"theme.CodeBlock.copy": "Copier",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copier le code",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "Cette page a planté.",
|
||||
"theme.ErrorPageContent.tryAgain": "Réessayer",
|
||||
"theme.NotFound.p1": "Nous n'avons pas trouvé ce que vous recherchez.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "הועתק",
|
||||
"theme.CodeBlock.copy": "העתק",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "העתק קוד ללוח העריכה",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "אנחנו לא מוצאים את מה שאתה מנסה לחפש.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "कॉपीड",
|
||||
"theme.CodeBlock.copy": "कॉपी",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "क्लिपबोर्ड पर कोड कॉपी करें",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "हमें वह नहीं मिला, जिसकी आपको तलाश थी।",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "コピーしました",
|
||||
"theme.CodeBlock.copy": "コピー",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "クリップボードにコードをコピー",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "お探しのページが見つかりませんでした。",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "복사했습니다",
|
||||
"theme.CodeBlock.copy": "복사",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "클립보드에 코드 복사",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "원하는 페이지를 찾을 수 없습니다.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Skopiowano!",
|
||||
"theme.CodeBlock.copy": "Kopiuj",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Kopiuj do schowka",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Nie mogliśmy znaleźć strony której szukasz.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Copiado",
|
||||
"theme.CodeBlock.copy": "Copiar",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copiar código para a área de transferência",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Não foi possível encontrar o que você está procurando.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Copiado",
|
||||
"theme.CodeBlock.copy": "Copiar",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copiar código para a área de transferência",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Não foi possível encontrar o que procura.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Скопировано",
|
||||
"theme.CodeBlock.copy": "Скопировать",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Скопировать в буфер обмена",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "К сожалению, мы не смогли найти запрашиваемую вами страницу.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Копирано",
|
||||
"theme.CodeBlock.copy": "Копирај",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Копирај код у меморију",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Тражени резултат не постоји.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Kopyalandı",
|
||||
"theme.CodeBlock.copy": "Kopyala",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Kodu panoya kopyala",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Aradığınız şeyi bulamadık.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "Đã sao chép",
|
||||
"theme.CodeBlock.copy": "Sao chép",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Sao chép code vào bộ nhớ tạm",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "This page crashed.",
|
||||
"theme.ErrorPageContent.tryAgain": "Try again",
|
||||
"theme.NotFound.p1": "Chúng tôi không thể tìm thấy những gì bạn đang tìm kiếm.",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "复制成功",
|
||||
"theme.CodeBlock.copy": "复制",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "复制代码到剪贴板",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "页面已崩溃。",
|
||||
"theme.ErrorPageContent.tryAgain": "重试",
|
||||
"theme.NotFound.p1": "我们找不到您要找的页面。",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"theme.CodeBlock.copied": "複製成功",
|
||||
"theme.CodeBlock.copy": "複製",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "複製代碼至剪貼簿",
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
|
||||
"theme.ErrorPageContent.title": "頁面已崩潰。",
|
||||
"theme.ErrorPageContent.tryAgain": "重試",
|
||||
"theme.NotFound.p1": "我們找不到您要找的頁面。",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"chalk": "^4.1.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"github-slugger": "^1.4.0",
|
||||
"globby": "^11.0.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lodash": "^4.17.20",
|
||||
|
@ -37,6 +38,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/dedent": "^0.7.0",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/micromatch": "^4.0.2",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
"dedent": "^0.7.0"
|
||||
|
|
27
packages/docusaurus-utils/src/__tests__/slugger.test.ts
Normal file
27
packages/docusaurus-utils/src/__tests__/slugger.test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* 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 {createSlugger} from '../slugger';
|
||||
|
||||
describe('createSlugger', () => {
|
||||
test('can create unique slugs', () => {
|
||||
const slugger = createSlugger();
|
||||
expect(slugger.slug('Some$/vaLue$!^')).toEqual('somevalue');
|
||||
expect(slugger.slug('Some$/vaLue$!^')).toEqual('somevalue-1');
|
||||
expect(slugger.slug('Some$/vaLue$!^')).toEqual('somevalue-2');
|
||||
expect(slugger.slug('Some$/vaLue$!^-1')).toEqual('somevalue-1-1');
|
||||
});
|
||||
|
||||
test('can create unique slugs respecting case', () => {
|
||||
const slugger = createSlugger();
|
||||
const opt = {maintainCase: true};
|
||||
expect(slugger.slug('Some$/vaLue$!^', opt)).toEqual('SomevaLue');
|
||||
expect(slugger.slug('Some$/vaLue$!^', opt)).toEqual('SomevaLue-1');
|
||||
expect(slugger.slug('Some$/vaLue$!^', opt)).toEqual('SomevaLue-2');
|
||||
expect(slugger.slug('Some$/vaLue$!^-1', opt)).toEqual('SomevaLue-1-1');
|
||||
});
|
||||
});
|
|
@ -33,6 +33,7 @@ export const posixPath = posixPathImport;
|
|||
export * from './markdownParser';
|
||||
export * from './markdownLinks';
|
||||
export * from './escapePath';
|
||||
export * from './slugger';
|
||||
export {md5Hash, simpleHash, docuHash} from './hashUtils';
|
||||
export {
|
||||
Globby,
|
||||
|
|
24
packages/docusaurus-utils/src/slugger.ts
Normal file
24
packages/docusaurus-utils/src/slugger.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import GithubSlugger from 'github-slugger';
|
||||
|
||||
// We create our own abstraction on top of the lib:
|
||||
// - unify usage everywhere in the codebase
|
||||
// - ability to add extra options
|
||||
export type SluggerOptions = {maintainCase?: boolean};
|
||||
|
||||
export type Slugger = {
|
||||
slug: (value: string, options?: SluggerOptions) => string;
|
||||
};
|
||||
|
||||
export function createSlugger(): Slugger {
|
||||
const githubSlugger = new GithubSlugger();
|
||||
return {
|
||||
slug: (value, options) => githubSlugger.slug(value, options?.maintainCase),
|
||||
};
|
||||
}
|
|
@ -36,7 +36,6 @@
|
|||
"@types/copy-webpack-plugin": "^8.0.1",
|
||||
"@types/css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"@types/detect-port": "^1.3.0",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
|
@ -82,7 +81,6 @@
|
|||
"eta": "^1.12.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"github-slugger": "^1.4.0",
|
||||
"globby": "^11.0.2",
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
"html-tags": "^3.1.0",
|
||||
|
|
|
@ -9,51 +9,51 @@ import {
|
|||
transformMarkdownHeadingLine,
|
||||
transformMarkdownContent,
|
||||
} from '../writeHeadingIds';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import {createSlugger} from '@docusaurus/utils';
|
||||
|
||||
describe('transformMarkdownHeadingLine', () => {
|
||||
test('throws when not a heading', () => {
|
||||
expect(() =>
|
||||
transformMarkdownHeadingLine('ABC', new GithubSlugger()),
|
||||
transformMarkdownHeadingLine('ABC', createSlugger()),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Line is not a Markdown heading: ABC."`,
|
||||
);
|
||||
});
|
||||
|
||||
test('works for simple level-2 heading', () => {
|
||||
expect(transformMarkdownHeadingLine('## ABC', new GithubSlugger())).toEqual(
|
||||
expect(transformMarkdownHeadingLine('## ABC', createSlugger())).toEqual(
|
||||
'## ABC {#abc}',
|
||||
);
|
||||
});
|
||||
|
||||
test('works for simple level-3 heading', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine('### ABC', new GithubSlugger()),
|
||||
).toEqual('### ABC {#abc}');
|
||||
expect(transformMarkdownHeadingLine('### ABC', createSlugger())).toEqual(
|
||||
'### ABC {#abc}',
|
||||
);
|
||||
});
|
||||
|
||||
test('works for simple level-4 heading', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine('#### ABC', new GithubSlugger()),
|
||||
).toEqual('#### ABC {#abc}');
|
||||
expect(transformMarkdownHeadingLine('#### ABC', createSlugger())).toEqual(
|
||||
'#### ABC {#abc}',
|
||||
);
|
||||
});
|
||||
|
||||
test('works for simple level-2 heading', () => {
|
||||
expect(transformMarkdownHeadingLine('## ABC', new GithubSlugger())).toEqual(
|
||||
expect(transformMarkdownHeadingLine('## ABC', createSlugger())).toEqual(
|
||||
'## ABC {#abc}',
|
||||
);
|
||||
});
|
||||
|
||||
test('unwraps markdown links', () => {
|
||||
const input = `## hello [facebook](https://facebook.com) [crowdin](https://crowdin.com/translate/docusaurus-v2/126/en-fr?filter=basic&value=0)`;
|
||||
expect(transformMarkdownHeadingLine(input, new GithubSlugger())).toEqual(
|
||||
expect(transformMarkdownHeadingLine(input, createSlugger())).toEqual(
|
||||
`${input} {#hello-facebook-crowdin}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('can slugify complex headings', () => {
|
||||
const input = '## abc [Hello] How are you %Sébastien_-_$)( ## -56756';
|
||||
expect(transformMarkdownHeadingLine(input, new GithubSlugger())).toEqual(
|
||||
expect(transformMarkdownHeadingLine(input, createSlugger())).toEqual(
|
||||
`${input} {#abc-hello-how-are-you-sébastien_-_---56756}`,
|
||||
);
|
||||
});
|
||||
|
@ -62,7 +62,7 @@ describe('transformMarkdownHeadingLine', () => {
|
|||
expect(
|
||||
transformMarkdownHeadingLine(
|
||||
'## hello world {#hello-world}',
|
||||
new GithubSlugger(),
|
||||
createSlugger(),
|
||||
),
|
||||
).toEqual('## hello world {#hello-world}');
|
||||
});
|
||||
|
@ -71,7 +71,7 @@ describe('transformMarkdownHeadingLine', () => {
|
|||
expect(
|
||||
transformMarkdownHeadingLine(
|
||||
'## New heading {#old-heading}',
|
||||
new GithubSlugger(),
|
||||
createSlugger(),
|
||||
),
|
||||
).toEqual('## New heading {#old-heading}');
|
||||
});
|
||||
|
@ -80,7 +80,7 @@ describe('transformMarkdownHeadingLine', () => {
|
|||
expect(
|
||||
transformMarkdownHeadingLine(
|
||||
'## New heading {#old-heading}',
|
||||
new GithubSlugger(),
|
||||
createSlugger(),
|
||||
{overwrite: true},
|
||||
),
|
||||
).toEqual('## New heading {#new-heading}');
|
||||
|
@ -88,7 +88,7 @@ describe('transformMarkdownHeadingLine', () => {
|
|||
|
||||
test('maintains casing when asked to', () => {
|
||||
expect(
|
||||
transformMarkdownHeadingLine('## getDataFromAPI()', new GithubSlugger(), {
|
||||
transformMarkdownHeadingLine('## getDataFromAPI()', createSlugger(), {
|
||||
maintainCase: true,
|
||||
}),
|
||||
).toEqual('## getDataFromAPI() {#getDataFromAPI}');
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import chalk from 'chalk';
|
||||
import {loadContext, loadPluginConfigs} from '../server';
|
||||
import initPlugins from '../server/plugins/init';
|
||||
|
||||
import {parseMarkdownHeadingId} from '@docusaurus/utils';
|
||||
import {
|
||||
parseMarkdownHeadingId,
|
||||
createSlugger,
|
||||
Slugger,
|
||||
} from '@docusaurus/utils';
|
||||
import {safeGlobby} from '../server/utils';
|
||||
|
||||
type Options = {
|
||||
|
@ -25,7 +28,7 @@ function unwrapMarkdownLinks(line: string): string {
|
|||
|
||||
function addHeadingId(
|
||||
line: string,
|
||||
slugger: GithubSlugger,
|
||||
slugger: Slugger,
|
||||
maintainCase: boolean,
|
||||
): string {
|
||||
let headingLevel = 0;
|
||||
|
@ -36,7 +39,7 @@ function addHeadingId(
|
|||
const headingText = line.slice(headingLevel).trimEnd();
|
||||
const headingHashes = line.slice(0, headingLevel);
|
||||
const slug = slugger
|
||||
.slug(unwrapMarkdownLinks(headingText).trim(), maintainCase)
|
||||
.slug(unwrapMarkdownLinks(headingText).trim(), {maintainCase})
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
|
||||
|
@ -45,7 +48,7 @@ function addHeadingId(
|
|||
|
||||
export function transformMarkdownHeadingLine(
|
||||
line: string,
|
||||
slugger: GithubSlugger,
|
||||
slugger: Slugger,
|
||||
options: Options = {maintainCase: false, overwrite: false},
|
||||
): string {
|
||||
const {maintainCase = false, overwrite = false} = options;
|
||||
|
@ -64,7 +67,7 @@ export function transformMarkdownHeadingLine(
|
|||
|
||||
function transformMarkdownLine(
|
||||
line: string,
|
||||
slugger: GithubSlugger,
|
||||
slugger: Slugger,
|
||||
options?: Options,
|
||||
): string {
|
||||
// Ignore h1 headings on purpose, as we don't create anchor links for those
|
||||
|
@ -77,7 +80,7 @@ function transformMarkdownLine(
|
|||
|
||||
function transformMarkdownLines(lines: string[], options?: Options): string[] {
|
||||
let inCode = false;
|
||||
const slugger = new GithubSlugger();
|
||||
const slugger = createSlugger();
|
||||
|
||||
return lines.map((line) => {
|
||||
if (line.startsWith('```')) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"label": "Category Links",
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"slug": "/category-links-generated-index-slug"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Readme
|
||||
|
||||
This `readme.md` should not be used as the category index due to the `_category_.json` link
|
|
@ -0,0 +1,3 @@
|
|||
# Sample doc
|
||||
|
||||
Lorem Ipsum
|
|
@ -0,0 +1,3 @@
|
|||
# Sample doc
|
||||
|
||||
Lorem Ipsum
|
|
@ -0,0 +1,22 @@
|
|||
# Category with a doc of category's name
|
||||
|
||||
You should be able to click on the category and browse this `<categoryName>/<categoryName>.md` doc
|
||||
|
||||
## Intro
|
||||
|
||||
Some intro text
|
||||
|
||||
:::tip
|
||||
|
||||
It is also possible to render the card items for that category thanks to MDX:
|
||||
|
||||
:::
|
||||
|
||||
## Category content
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items}/>
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
# Category with index.md doc
|
||||
|
||||
You should be able to click on the category and browse this `index.md` doc
|
|
@ -0,0 +1,3 @@
|
|||
# Sample doc
|
||||
|
||||
Lorem Ipsum
|
|
@ -0,0 +1,3 @@
|
|||
# Category with readme.md doc
|
||||
|
||||
You should be able to click on the category and browse this `readme.md` doc
|
|
@ -0,0 +1,3 @@
|
|||
# Sample doc
|
||||
|
||||
Lorem Ipsum
|
|
@ -5,7 +5,8 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
sidebar: [
|
||||
{
|
||||
type: 'doc',
|
||||
|
@ -16,11 +17,19 @@ module.exports = {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'Tests',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'tests',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: 'External Link test',
|
||||
href: 'https://docusaurus.io',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -40,7 +49,7 @@ module.exports = {
|
|||
label: 'Test Test test test test test test',
|
||||
},
|
||||
],
|
||||
...generateHugeSidebarItems(4),
|
||||
...generateHugeSidebarItems(),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -62,6 +71,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
};
|
||||
module.exports = sidebars;
|
||||
|
||||
function generateHugeSidebarItems() {
|
||||
const linksCount = 8;
|
||||
|
@ -87,5 +97,5 @@ function generateHugeSidebarItems() {
|
|||
return [...linkItems, ...categoryItems];
|
||||
}
|
||||
|
||||
return generateRecursive(4);
|
||||
return generateRecursive(3);
|
||||
}
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
label: Plugins
|
||||
position: 2
|
||||
link:
|
||||
type: doc
|
||||
id: api/plugins/plugins-overview # Dogfood using a "qualified id"
|
||||
|
|
|
@ -48,6 +48,7 @@ Accepted fields:
|
|||
| `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. |
|
||||
| `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page |
|
||||
| `docTagDocListComponent` | `string` | `'@theme/DocTagDocListPage'` | Root component of the "docs containing tag" page. |
|
||||
| `docCategoryGeneratedIndexComponent` | `string` | `'@theme/DocCategoryGeneratedIndexPage'` | Root component of the generated category index page. |
|
||||
| `remarkPlugins` | `any[]` | `[]` | Remark plugins passed to MDX. |
|
||||
| `rehypePlugins` | `any[]` | `[]` | Rehype plugins passed to MDX. |
|
||||
| `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. |
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
label: Themes
|
||||
position: 3
|
||||
link:
|
||||
type: doc
|
||||
id: themes-overview # Dogfood using a "local id"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
sidebar_position: 1
|
||||
id: theme-configuration
|
||||
title: 'Theme configuration'
|
||||
sidebar_label: 'Configuration'
|
||||
slug: '/api/themes/configuration'
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue