mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
refactor(content-docs): clean up sidebars logic; validate generator returns (#6596)
* refactor(content-docs): clean up sidebars logic; validate generator returns * remove another TODO * fix types * refactors * refactor...
This commit is contained in:
parent
d6bdf7e804
commit
e3fd3e74ce
23 changed files with 750 additions and 615 deletions
|
@ -770,8 +770,6 @@ Object {
|
||||||
\\"docs\\": [
|
\\"docs\\": [
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Test\\",
|
\\"label\\": \\"Test\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -791,8 +789,8 @@ Object {
|
||||||
\\"docId\\": \\"foo/baz\\"
|
\\"docId\\": \\"foo/baz\\"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"collapsible\\": true,
|
\\"collapsed\\": true,
|
||||||
\\"collapsed\\": true
|
\\"collapsible\\": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
|
@ -823,8 +821,8 @@ Object {
|
||||||
\\"docId\\": \\"rootTryToEscapeSlug\\"
|
\\"docId\\": \\"rootTryToEscapeSlug\\"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"collapsed\\": true,
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true,
|
||||||
\\"href\\": \\"/docs/category/slugs\\"
|
\\"href\\": \\"/docs/category/slugs\\"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -844,12 +842,12 @@ Object {
|
||||||
\\"href\\": \\"/docs/\\",
|
\\"href\\": \\"/docs/\\",
|
||||||
\\"docId\\": \\"hello\\"
|
\\"docId\\": \\"hello\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Guides\\",
|
\\"label\\": \\"Guides\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -858,7 +856,9 @@ Object {
|
||||||
\\"href\\": \\"/docs/\\",
|
\\"href\\": \\"/docs/\\",
|
||||||
\\"docId\\": \\"hello\\"
|
\\"docId\\": \\"hello\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3188,8 +3188,6 @@ Object {
|
||||||
\\"version-1.0.0/docs\\": [
|
\\"version-1.0.0/docs\\": [
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Test\\",
|
\\"label\\": \\"Test\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3204,12 +3202,12 @@ Object {
|
||||||
\\"href\\": \\"/docs/1.0.0/foo/baz\\",
|
\\"href\\": \\"/docs/1.0.0/foo/baz\\",
|
||||||
\\"docId\\": \\"foo/baz\\"
|
\\"docId\\": \\"foo/baz\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Guides\\",
|
\\"label\\": \\"Guides\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3218,7 +3216,9 @@ Object {
|
||||||
\\"href\\": \\"/docs/1.0.0/\\",
|
\\"href\\": \\"/docs/1.0.0/\\",
|
||||||
\\"docId\\": \\"hello\\"
|
\\"docId\\": \\"hello\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3255,8 +3255,6 @@ Object {
|
||||||
\\"VersionedSideBarNameDoesNotMatter/docs\\": [
|
\\"VersionedSideBarNameDoesNotMatter/docs\\": [
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Test\\",
|
\\"label\\": \\"Test\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3265,12 +3263,12 @@ Object {
|
||||||
\\"href\\": \\"/docs/foo/bar\\",
|
\\"href\\": \\"/docs/foo/bar\\",
|
||||||
\\"docId\\": \\"foo/bar\\"
|
\\"docId\\": \\"foo/bar\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Guides\\",
|
\\"label\\": \\"Guides\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3279,7 +3277,9 @@ Object {
|
||||||
\\"href\\": \\"/docs/\\",
|
\\"href\\": \\"/docs/\\",
|
||||||
\\"docId\\": \\"hello\\"
|
\\"docId\\": \\"hello\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3310,8 +3310,6 @@ Object {
|
||||||
\\"docs\\": [
|
\\"docs\\": [
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Test\\",
|
\\"label\\": \\"Test\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3320,12 +3318,12 @@ Object {
|
||||||
\\"href\\": \\"/docs/next/foo/barSlug\\",
|
\\"href\\": \\"/docs/next/foo/barSlug\\",
|
||||||
\\"docId\\": \\"foo/bar\\"
|
\\"docId\\": \\"foo/bar\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Guides\\",
|
\\"label\\": \\"Guides\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3334,7 +3332,9 @@ Object {
|
||||||
\\"href\\": \\"/docs/next/\\",
|
\\"href\\": \\"/docs/next/\\",
|
||||||
\\"docId\\": \\"hello\\"
|
\\"docId\\": \\"hello\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3385,8 +3385,6 @@ Object {
|
||||||
\\"version-1.0.1/docs\\": [
|
\\"version-1.0.1/docs\\": [
|
||||||
{
|
{
|
||||||
\\"type\\": \\"category\\",
|
\\"type\\": \\"category\\",
|
||||||
\\"collapsed\\": true,
|
|
||||||
\\"collapsible\\": true,
|
|
||||||
\\"label\\": \\"Test\\",
|
\\"label\\": \\"Test\\",
|
||||||
\\"items\\": [
|
\\"items\\": [
|
||||||
{
|
{
|
||||||
|
@ -3395,7 +3393,9 @@ Object {
|
||||||
\\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
|
\\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
|
||||||
\\"docId\\": \\"rootAbsoluteSlug\\"
|
\\"docId\\": \\"rootAbsoluteSlug\\"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
\\"collapsed\\": true,
|
||||||
|
\\"collapsible\\": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
||||||
PathOptions,
|
PathOptions,
|
||||||
SidebarOptions,
|
SidebarOptions,
|
||||||
} from '@docusaurus/plugin-content-docs';
|
} from '@docusaurus/plugin-content-docs';
|
||||||
import {loadSidebarsFile, resolveSidebarPathOption} from './sidebars';
|
import {loadSidebarsFileUnsafe, resolveSidebarPathOption} from './sidebars';
|
||||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
|
|
||||||
|
@ -34,7 +34,8 @@ async function createVersionedSidebarFile({
|
||||||
// Load current sidebar and create a new versioned sidebars file (if needed).
|
// Load current sidebar and create a new versioned sidebars file (if needed).
|
||||||
// Note: we don't need the sidebars file to be normalized: it's ok to let
|
// Note: we don't need the sidebars file to be normalized: it's ok to let
|
||||||
// plugin option changes to impact older, versioned sidebars
|
// plugin option changes to impact older, versioned sidebars
|
||||||
const sidebars = await loadSidebarsFile(sidebarPath);
|
// We don't validate here, assuming the user has already built the version
|
||||||
|
const sidebars = await loadSidebarsFileUnsafe(sidebarPath);
|
||||||
|
|
||||||
// Do not create a useless versioned sidebars file if sidebars file is empty
|
// Do not create a useless versioned sidebars file if sidebars file is empty
|
||||||
// or sidebars are disabled/false)
|
// or sidebars are disabled/false)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Sidebars
|
||||||
|
|
||||||
|
This part is very complicated and hard to navigate. Sidebars are loaded through the following steps:
|
||||||
|
|
||||||
|
1. **Loading**. The sidebars file is read. Returns `SidebarsConfig`.
|
||||||
|
2. **Normalization**. The shorthands are expanded. This step is very lenient about the sidebars' shapes. Returns `NormalizedSidebars`.
|
||||||
|
3. **Validation**. The normalized sidebars are validated. This step happens after normalization, because the normalized sidebars are easier to validate, and allows us to repeatedly validate & generate in the future.
|
||||||
|
4. **Generation**. This step is done through the "processor" (naming is hard). The `autogenerated` items are unwrapped. In the future, steps 3 and 4 may be repeatedly done until all autogenerated items are unwrapped. Returns `ProcessedSidebars`.
|
||||||
|
5. **Post-processing**. Defaults are applied (collapsed states), category links are resolved, empty categories are flattened. Returns `Sidebars`.
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"docs": {
|
|
||||||
"Test": [
|
|
||||||
{
|
|
||||||
"type": "category",
|
|
||||||
"label": true,
|
|
||||||
"items": ["doc1"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"docs": {
|
|
||||||
"Test": [
|
|
||||||
{
|
|
||||||
"type": "doc",
|
|
||||||
"id": ["doc1"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"docs": {
|
|
||||||
"Test": [
|
|
||||||
{
|
|
||||||
"type": "link",
|
|
||||||
"label": "GitHub",
|
|
||||||
"href": ["example.com"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"docs": {
|
|
||||||
"Test": [
|
|
||||||
{
|
|
||||||
"type": "link",
|
|
||||||
"label": false,
|
|
||||||
"href": "https://github.com"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"docs": {
|
|
||||||
"Test": [
|
|
||||||
"foo/bar",
|
|
||||||
"foo/baz",
|
|
||||||
{
|
|
||||||
"type": "superman"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Guides": [
|
|
||||||
"hello"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"docs": {
|
|
||||||
"Test": [
|
|
||||||
"foo/bar",
|
|
||||||
"foo/baz",
|
|
||||||
{
|
|
||||||
"type": "category",
|
|
||||||
"label": "category",
|
|
||||||
"href": "https://github.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "ref",
|
|
||||||
"id": "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Guides": [
|
|
||||||
"hello"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`loadNormalizedSidebars sidebars link 1`] = `
|
exports[`loadSidebars sidebars link 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -21,7 +21,7 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadNormalizedSidebars sidebars with category.collapsed property 1`] = `
|
exports[`loadSidebars sidebars with category.collapsed property 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -72,7 +72,7 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadNormalizedSidebars sidebars with category.collapsed property at first level 1`] = `
|
exports[`loadSidebars sidebars with category.collapsed property at first level 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -105,7 +105,7 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadNormalizedSidebars sidebars with deep level of category 1`] = `
|
exports[`loadSidebars sidebars with deep level of category 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -177,7 +177,7 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadNormalizedSidebars sidebars with first level not a category 1`] = `
|
exports[`loadSidebars sidebars with first level not a category 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -201,7 +201,7 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`loadNormalizedSidebars sidebars with known sidebar item type 1`] = `
|
exports[`loadSidebars sidebars with known sidebar item type 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -246,3 +246,23 @@ Object {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`loadSidebars undefined path 1`] = `
|
||||||
|
Object {
|
||||||
|
"defaultSidebar": Array [
|
||||||
|
Object {
|
||||||
|
"collapsed": true,
|
||||||
|
"collapsible": true,
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "bar",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "foo",
|
||||||
|
"link": undefined,
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -214,8 +214,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Tutorials',
|
label: 'Tutorials',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
link: {
|
link: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
id: 'tutorials-index',
|
id: 'tutorials-index',
|
||||||
|
@ -228,19 +226,16 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Guides',
|
label: 'Guides',
|
||||||
collapsed: false,
|
|
||||||
collapsible: true,
|
|
||||||
link: {
|
link: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
id: 'guides-index',
|
id: 'guides-index',
|
||||||
},
|
},
|
||||||
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
{type: 'doc', id: 'guide1', className: 'foo'},
|
{type: 'doc', id: 'guide1', className: 'foo'},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'SubGuides (metadata file label)',
|
label: 'SubGuides (metadata file label)',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
items: [{type: 'doc', id: 'nested-guide'}],
|
items: [{type: 'doc', id: 'nested-guide'}],
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
|
@ -279,8 +274,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
'subfolder/subsubfolder/subsubsubfolder3': {
|
'subfolder/subsubfolder/subsubsubfolder3': {
|
||||||
position: 1,
|
position: 1,
|
||||||
label: 'subsubsubfolder3 (_category_.json label)',
|
label: 'subsubsubfolder3 (_category_.json label)',
|
||||||
collapsible: false,
|
|
||||||
collapsed: false,
|
|
||||||
link: {
|
link: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
id: 'doc1', // This is a "fully-qualified" ID that can't be found locally
|
id: 'doc1', // This is a "fully-qualified" ID that can't be found locally
|
||||||
|
@ -355,8 +348,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'subsubsubfolder3 (_category_.json label)',
|
label: 'subsubsubfolder3 (_category_.json label)',
|
||||||
collapsed: false,
|
|
||||||
collapsible: false,
|
|
||||||
link: {
|
link: {
|
||||||
id: 'doc1',
|
id: 'doc1',
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
|
@ -369,8 +360,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'subsubsubfolder2 (_category_.yml label)',
|
label: 'subsubsubfolder2 (_category_.yml label)',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
className: 'bar',
|
className: 'bar',
|
||||||
items: [{type: 'doc', id: 'doc6'}],
|
items: [{type: 'doc', id: 'doc6'}],
|
||||||
},
|
},
|
||||||
|
@ -379,8 +368,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'subsubsubfolder',
|
label: 'subsubsubfolder',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
items: [{type: 'doc', id: 'doc5'}],
|
items: [{type: 'doc', id: 'doc5'}],
|
||||||
},
|
},
|
||||||
] as Sidebar);
|
] as Sidebar);
|
||||||
|
@ -458,8 +445,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Category label',
|
label: 'Category label',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
link: {
|
link: {
|
||||||
id: 'parent/doc3',
|
id: 'parent/doc3',
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
|
@ -478,8 +463,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Category 2 label',
|
label: 'Category 2 label',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'parent/doc4',
|
id: 'parent/doc4',
|
||||||
|
@ -583,8 +566,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Tutorials',
|
label: 'Tutorials',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
link: {
|
link: {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
id: 'tutorials-index',
|
id: 'tutorials-index',
|
||||||
|
@ -597,8 +578,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Guides',
|
label: 'Guides',
|
||||||
collapsed: true,
|
|
||||||
collapsible: true,
|
|
||||||
items: [
|
items: [
|
||||||
{type: 'doc', id: 'guide1', className: 'foo'},
|
{type: 'doc', id: 'guide1', className: 'foo'},
|
||||||
{type: 'doc', id: 'guide2'},
|
{type: 'doc', id: 'guide2'},
|
||||||
|
|
|
@ -6,32 +6,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import {loadSidebars, DisabledSidebars} from '../index';
|
||||||
loadNormalizedSidebars,
|
import type {SidebarProcessorParams} from '../types';
|
||||||
DefaultSidebars,
|
import {DefaultSidebarItemsGenerator} from '../generator';
|
||||||
DisabledSidebars,
|
|
||||||
} from '../index';
|
|
||||||
import type {NormalizeSidebarsParams, VersionMetadata} from '../../types';
|
|
||||||
|
|
||||||
describe('loadNormalizedSidebars', () => {
|
describe('loadSidebars', () => {
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
||||||
const options: NormalizeSidebarsParams = {
|
const params: SidebarProcessorParams = {
|
||||||
sidebarCollapsed: true,
|
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||||
sidebarCollapsible: true,
|
numberPrefixParser: (filename) => ({filename}),
|
||||||
version: {
|
docs: [
|
||||||
versionName: 'version',
|
{
|
||||||
versionPath: 'versionPath',
|
source: '@site/docs/foo/bar.md',
|
||||||
} as VersionMetadata,
|
sourceDirName: 'foo',
|
||||||
|
id: 'bar',
|
||||||
|
frontMatter: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
version: {contentPath: 'docs/foo', contentPathLocalized: 'docs/foo'},
|
||||||
|
categoryLabelSlugger: null,
|
||||||
|
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||||
};
|
};
|
||||||
test('sidebars with known sidebar item type', async () => {
|
test('sidebars with known sidebar item type', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
||||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
const result = await loadSidebars(sidebarPath, params);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with deep level of category', async () => {
|
test('sidebars with deep level of category', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
||||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
const result = await loadSidebars(sidebarPath, params);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,8 +45,8 @@ describe('loadNormalizedSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-category-shorthand.js',
|
'sidebars-category-shorthand.js',
|
||||||
);
|
);
|
||||||
const sidebar1 = await loadNormalizedSidebars(sidebarPath1, options);
|
const sidebar1 = await loadSidebars(sidebarPath1, params);
|
||||||
const sidebar2 = await loadNormalizedSidebars(sidebarPath2, options);
|
const sidebar2 = await loadSidebars(sidebarPath2, params);
|
||||||
expect(sidebar1).toEqual(sidebar2);
|
expect(sidebar1).toEqual(sidebar2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,53 +55,11 @@ describe('loadNormalizedSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-category-wrong-items.json',
|
'sidebars-category-wrong-items.json',
|
||||||
);
|
);
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
await expect(() =>
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
loadSidebars(sidebarPath, params),
|
||||||
"{
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||||
\\"type\\": \\"category\\",
|
`"Invalid category {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}: items must be an array"`,
|
||||||
\\"label\\": \\"Category Label\\",
|
|
||||||
\\"items\\" [31m[1][0m: \\"doc1\\"
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] \\"items\\" must be an array[0m"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sidebars with category but category label is not a string', async () => {
|
|
||||||
const sidebarPath = path.join(
|
|
||||||
fixtureDir,
|
|
||||||
'sidebars-category-wrong-label.json',
|
|
||||||
);
|
);
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
\\"type\\": \\"category\\",
|
|
||||||
\\"items\\": [
|
|
||||||
\\"doc1\\"
|
|
||||||
],
|
|
||||||
\\"label\\" [31m[1][0m: true
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] \\"label\\" must be a string[0m"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sidebars item doc but id is not a string', async () => {
|
|
||||||
const sidebarPath = path.join(
|
|
||||||
fixtureDir,
|
|
||||||
'sidebars-doc-id-not-string.json',
|
|
||||||
);
|
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
\\"type\\": \\"doc\\",
|
|
||||||
\\"id\\" [31m[1][0m: [
|
|
||||||
\\"doc1\\"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] \\"id\\" must be a string[0m"
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with first level not a category', async () => {
|
test('sidebars with first level not a category', async () => {
|
||||||
|
@ -105,95 +67,35 @@ describe('loadNormalizedSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-first-level-not-category.js',
|
'sidebars-first-level-not-category.js',
|
||||||
);
|
);
|
||||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
const result = await loadSidebars(sidebarPath, params);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars link', async () => {
|
test('sidebars link', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
||||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
const result = await loadSidebars(sidebarPath, params);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars link wrong label', async () => {
|
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json');
|
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
\\"type\\": \\"link\\",
|
|
||||||
\\"href\\": \\"https://github.com\\",
|
|
||||||
\\"label\\" [31m[1][0m: false
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] \\"label\\" must be a string[0m"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sidebars link wrong href', async () => {
|
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json');
|
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
\\"type\\": \\"link\\",
|
|
||||||
\\"label\\": \\"GitHub\\",
|
|
||||||
\\"href\\" [31m[1][0m: [
|
|
||||||
\\"example.com\\"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] \\"href\\" contains an invalid value[0m"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sidebars with unknown sidebar item type', async () => {
|
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
\\"type\\": \\"superman\\",
|
|
||||||
[41m\\"undefined\\"[0m[31m [1]: -- missing --[0m
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] Unknown sidebar item type \\"superman\\".[0m"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sidebars with known sidebar item type but wrong field', async () => {
|
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
|
||||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
|
||||||
"{
|
|
||||||
\\"type\\": \\"category\\",
|
|
||||||
\\"label\\": \\"category\\",
|
|
||||||
\\"href\\": \\"https://github.com\\",
|
|
||||||
[41m\\"items\\"[0m[31m [1]: -- missing --[0m
|
|
||||||
}
|
|
||||||
[31m
|
|
||||||
[1] \\"items\\" is required[0m"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('unexisting path', async () => {
|
test('unexisting path', async () => {
|
||||||
await expect(loadNormalizedSidebars('badpath', options)).resolves.toEqual(
|
await expect(loadSidebars('badpath', params)).resolves.toEqual(
|
||||||
DisabledSidebars,
|
DisabledSidebars,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('undefined path', async () => {
|
test('undefined path', async () => {
|
||||||
await expect(loadNormalizedSidebars(undefined, options)).resolves.toEqual(
|
await expect(loadSidebars(undefined, params)).resolves.toMatchSnapshot();
|
||||||
DefaultSidebars,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('literal false path', async () => {
|
test('literal false path', async () => {
|
||||||
await expect(loadNormalizedSidebars(false, options)).resolves.toEqual(
|
await expect(loadSidebars(false, params)).resolves.toEqual(
|
||||||
DisabledSidebars,
|
DisabledSidebars,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with category.collapsed property', async () => {
|
test('sidebars with category.collapsed property', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
||||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
const result = await loadSidebars(sidebarPath, params);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,7 +104,7 @@ describe('loadNormalizedSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-collapsed-first-level.json',
|
'sidebars-collapsed-first-level.json',
|
||||||
);
|
);
|
||||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
const result = await loadSidebars(sidebarPath, params);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
/**
|
||||||
|
* 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 {postProcessSidebars} from '../postProcessor';
|
||||||
|
|
||||||
|
describe('postProcess', () => {
|
||||||
|
test('transforms category without subitems', () => {
|
||||||
|
const processedSidebar = postProcessSidebars(
|
||||||
|
{
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Category',
|
||||||
|
link: {
|
||||||
|
type: 'generated-index',
|
||||||
|
slug: 'generated/permalink',
|
||||||
|
},
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Category 2',
|
||||||
|
link: {
|
||||||
|
type: 'doc',
|
||||||
|
id: 'doc ID',
|
||||||
|
},
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||||
|
version: {versionPath: 'version'},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(processedSidebar).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"sidebar": Array [
|
||||||
|
Object {
|
||||||
|
"href": "version/generated/permalink",
|
||||||
|
"label": "Category",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "doc ID",
|
||||||
|
"label": "Category 2",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
postProcessSidebars(
|
||||||
|
{
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Bad category',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||||
|
version: {versionPath: 'version'},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Sidebar category Bad category has neither any subitem nor a link. This makes this item not able to link to anything."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corrects collapsed state inconsistencies', () => {
|
||||||
|
expect(
|
||||||
|
postProcessSidebars(
|
||||||
|
{
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Category',
|
||||||
|
collapsed: true,
|
||||||
|
collapsible: false,
|
||||||
|
items: [{type: 'doc', id: 'foo'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: true},
|
||||||
|
version: {versionPath: 'version'},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"sidebar": Array [
|
||||||
|
Object {
|
||||||
|
"collapsed": false,
|
||||||
|
"collapsible": false,
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "foo",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Category",
|
||||||
|
"link": undefined,
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
postProcessSidebars(
|
||||||
|
{
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Category',
|
||||||
|
collapsed: true,
|
||||||
|
items: [{type: 'doc', id: 'foo'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
sidebarOptions: {sidebarCollapsed: false, sidebarCollapsible: false},
|
||||||
|
version: {versionPath: 'version'},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"sidebar": Array [
|
||||||
|
Object {
|
||||||
|
"collapsed": false,
|
||||||
|
"collapsible": false,
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "foo",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Category",
|
||||||
|
"link": undefined,
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
postProcessSidebars(
|
||||||
|
{
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Category',
|
||||||
|
items: [{type: 'doc', id: 'foo'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
sidebarOptions: {sidebarCollapsed: true, sidebarCollapsible: false},
|
||||||
|
version: {versionPath: 'version'},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"sidebar": Array [
|
||||||
|
Object {
|
||||||
|
"collapsed": false,
|
||||||
|
"collapsible": false,
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "foo",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Category",
|
||||||
|
"link": undefined,
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,12 +5,15 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {processSidebars, type SidebarProcessorParams} from '../processor';
|
import {processSidebars} from '../processor';
|
||||||
import type {
|
import type {
|
||||||
SidebarItem,
|
SidebarItem,
|
||||||
SidebarItemsGenerator,
|
SidebarItemsGenerator,
|
||||||
Sidebars,
|
NormalizedSidebar,
|
||||||
NormalizedSidebars,
|
NormalizedSidebars,
|
||||||
|
SidebarProcessorParams,
|
||||||
|
CategoryMetadataFile,
|
||||||
|
ProcessedSidebars,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {DefaultSidebarItemsGenerator} from '../generator';
|
import {DefaultSidebarItemsGenerator} from '../generator';
|
||||||
import {createSlugger} from '@docusaurus/utils';
|
import {createSlugger} from '@docusaurus/utils';
|
||||||
|
@ -25,7 +28,7 @@ describe('processSidebars', () => {
|
||||||
return jest.fn(async () => sidebarSlice);
|
return jest.fn(async () => sidebarSlice);
|
||||||
}
|
}
|
||||||
|
|
||||||
const StaticGeneratedSidebarSlice: SidebarItem[] = [
|
const StaticGeneratedSidebarSlice: NormalizedSidebar = [
|
||||||
{type: 'doc', id: 'doc-generated-id-1'},
|
{type: 'doc', id: 'doc-generated-id-1'},
|
||||||
{type: 'doc', id: 'doc-generated-id-2'},
|
{type: 'doc', id: 'doc-generated-id-2'},
|
||||||
];
|
];
|
||||||
|
@ -53,9 +56,10 @@ describe('processSidebars', () => {
|
||||||
|
|
||||||
async function testProcessSidebars(
|
async function testProcessSidebars(
|
||||||
unprocessedSidebars: NormalizedSidebars,
|
unprocessedSidebars: NormalizedSidebars,
|
||||||
|
categoriesMetadata: Record<string, CategoryMetadataFile> = {},
|
||||||
paramsOverrides: Partial<SidebarProcessorParams> = {},
|
paramsOverrides: Partial<SidebarProcessorParams> = {},
|
||||||
) {
|
) {
|
||||||
return processSidebars(unprocessedSidebars, {
|
return processSidebars(unprocessedSidebars, categoriesMetadata, {
|
||||||
...params,
|
...params,
|
||||||
...paramsOverrides,
|
...paramsOverrides,
|
||||||
});
|
});
|
||||||
|
@ -101,10 +105,7 @@ describe('processSidebars', () => {
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
slug: 'category-generated-index-slug',
|
slug: 'category-generated-index-slug',
|
||||||
permalink: 'category-generated-index-permalink',
|
|
||||||
},
|
},
|
||||||
collapsed: true, // A suspicious bad config that will be normalized
|
|
||||||
collapsible: false,
|
|
||||||
items: [
|
items: [
|
||||||
{type: 'doc', id: 'doc2'},
|
{type: 'doc', id: 'doc2'},
|
||||||
{type: 'autogenerated', dirName: 'dir1'},
|
{type: 'autogenerated', dirName: 'dir1'},
|
||||||
|
@ -131,6 +132,7 @@ describe('processSidebars', () => {
|
||||||
|
|
||||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
|
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
|
||||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||||
|
categoriesMetadata: {},
|
||||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||||
item: {type: 'autogenerated', dirName: 'dir1'},
|
item: {type: 'autogenerated', dirName: 'dir1'},
|
||||||
docs: params.docs,
|
docs: params.docs,
|
||||||
|
@ -143,6 +145,7 @@ describe('processSidebars', () => {
|
||||||
});
|
});
|
||||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||||
|
categoriesMetadata: {},
|
||||||
item: {type: 'autogenerated', dirName: 'dir2'},
|
item: {type: 'autogenerated', dirName: 'dir2'},
|
||||||
docs: params.docs,
|
docs: params.docs,
|
||||||
version: {
|
version: {
|
||||||
|
@ -154,6 +157,7 @@ describe('processSidebars', () => {
|
||||||
});
|
});
|
||||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||||
|
categoriesMetadata: {},
|
||||||
item: {type: 'autogenerated', dirName: 'dir3'},
|
item: {type: 'autogenerated', dirName: 'dir3'},
|
||||||
docs: params.docs,
|
docs: params.docs,
|
||||||
version: {
|
version: {
|
||||||
|
@ -173,10 +177,7 @@ describe('processSidebars', () => {
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
slug: 'category-generated-index-slug',
|
slug: 'category-generated-index-slug',
|
||||||
permalink: 'category-generated-index-permalink',
|
|
||||||
},
|
},
|
||||||
collapsed: false,
|
|
||||||
collapsible: false,
|
|
||||||
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
|
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
|
||||||
},
|
},
|
||||||
{type: 'link', href: 'https://facebook.com', label: 'FB'},
|
{type: 'link', href: 'https://facebook.com', label: 'FB'},
|
||||||
|
@ -194,20 +195,17 @@ describe('processSidebars', () => {
|
||||||
items: [{type: 'doc', id: 'doc4'}],
|
items: [{type: 'doc', id: 'doc4'}],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as Sidebars);
|
} as ProcessedSidebars);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ensure generated items are normalized', async () => {
|
test('ensure generated items are normalized', async () => {
|
||||||
const sidebarSliceContainingCategoryGeneratedIndex: SidebarItem[] = [
|
const sidebarSliceContainingCategoryGeneratedIndex: NormalizedSidebar = [
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Generated category',
|
label: 'Generated category',
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
slug: 'generated-cat-index-slug',
|
slug: 'generated-cat-index-slug',
|
||||||
// @ts-expect-error: TODO undefined should be allowed here,
|
|
||||||
// typing error needing refactor
|
|
||||||
permalink: undefined,
|
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
@ -218,15 +216,19 @@ describe('processSidebars', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const unprocessedSidebars: NormalizedSidebars = {
|
const unprocessedSidebars = {
|
||||||
someSidebar: [{type: 'autogenerated', dirName: 'dir2'}],
|
someSidebar: [{type: 'autogenerated', dirName: 'dir2'}],
|
||||||
};
|
};
|
||||||
|
|
||||||
const processedSidebar = await testProcessSidebars(unprocessedSidebars, {
|
const processedSidebar = await testProcessSidebars(
|
||||||
sidebarItemsGenerator: createStaticSidebarItemGenerator(
|
unprocessedSidebars,
|
||||||
sidebarSliceContainingCategoryGeneratedIndex,
|
{},
|
||||||
),
|
{
|
||||||
});
|
sidebarItemsGenerator: createStaticSidebarItemGenerator(
|
||||||
|
sidebarSliceContainingCategoryGeneratedIndex,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(processedSidebar).toEqual({
|
expect(processedSidebar).toEqual({
|
||||||
someSidebar: [
|
someSidebar: [
|
||||||
|
@ -236,7 +238,6 @@ describe('processSidebars', () => {
|
||||||
link: {
|
link: {
|
||||||
type: 'generated-index',
|
type: 'generated-index',
|
||||||
slug: 'generated-cat-index-slug',
|
slug: 'generated-cat-index-slug',
|
||||||
permalink: '/docs/1.0.0/generated-cat-index-slug',
|
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
@ -244,67 +245,8 @@ describe('processSidebars', () => {
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
collapsible: true,
|
|
||||||
collapsed: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as Sidebars);
|
} as ProcessedSidebars);
|
||||||
});
|
|
||||||
|
|
||||||
test('transforms category without subitems', async () => {
|
|
||||||
const sidebarSlice: SidebarItem[] = [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category',
|
|
||||||
link: {
|
|
||||||
type: 'generated-index',
|
|
||||||
permalink: 'generated/permalink',
|
|
||||||
},
|
|
||||||
items: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category 2',
|
|
||||||
link: {
|
|
||||||
type: 'doc',
|
|
||||||
id: 'doc ID',
|
|
||||||
},
|
|
||||||
items: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const processedSidebar = await testProcessSidebars(
|
|
||||||
{sidebar: sidebarSlice},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(processedSidebar).toEqual({
|
|
||||||
sidebar: [
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
label: 'Category',
|
|
||||||
href: 'generated/permalink',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'doc',
|
|
||||||
label: 'Category 2',
|
|
||||||
id: 'doc ID',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as Sidebars);
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await testProcessSidebars({
|
|
||||||
sidebar: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Bad category',
|
|
||||||
items: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Sidebar category Bad category has neither any subitem nor a link. This makes this item not able to link to anything."`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,14 +6,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {validateSidebars, validateCategoryMetadataFile} from '../validation';
|
import {validateSidebars, validateCategoryMetadataFile} from '../validation';
|
||||||
import type {CategoryMetadataFile} from '../generator';
|
import type {SidebarsConfig, CategoryMetadataFile} from '../types';
|
||||||
import type {SidebarsConfig} from '../types';
|
|
||||||
|
|
||||||
describe('validateSidebars', () => {
|
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 () => {
|
test('throw for bad value', async () => {
|
||||||
expect(() => validateSidebars({sidebar: [{type: 42}]}))
|
expect(() => validateSidebars({sidebar: [{type: 42}]}))
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
@ -45,10 +40,193 @@ describe('validateSidebars', () => {
|
||||||
};
|
};
|
||||||
validateSidebars(sidebars);
|
validateSidebars(sidebars);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('html item type', () => {
|
test('sidebar category wrong label', () => {
|
||||||
test('requires a value', () => {
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: true,
|
||||||
|
items: [{type: 'doc', id: 'doc1'}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"category\\",
|
||||||
|
\\"items\\": [
|
||||||
|
{
|
||||||
|
\\"type\\": \\"doc\\",
|
||||||
|
\\"id\\": \\"doc1\\"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\\"label\\" [31m[1][0m: true
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] \\"label\\" must be a string[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebars link wrong label', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: false,
|
||||||
|
href: 'https://github.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"link\\",
|
||||||
|
\\"href\\": \\"https://github.com\\",
|
||||||
|
\\"label\\" [31m[1][0m: false
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] \\"label\\" must be a string[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebars link wrong href', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: 'GitHub',
|
||||||
|
href: ['example.com'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"link\\",
|
||||||
|
\\"label\\": \\"GitHub\\",
|
||||||
|
\\"href\\" [31m[1][0m: [
|
||||||
|
\\"example.com\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] \\"href\\" contains an invalid value[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebars with unknown sidebar item type', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'superman',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"superman\\",
|
||||||
|
[41m\\"undefined\\"[0m[31m [1]: -- missing --[0m
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] Unknown sidebar item type \\"superman\\".[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebars category missing items', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'category',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
id: 'hello',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"category\\",
|
||||||
|
\\"label\\": \\"category\\",
|
||||||
|
[41m\\"items\\"[0m[31m [1]: -- missing --[0m
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] \\"items\\" is required[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebars category wrong field', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'category',
|
||||||
|
items: [],
|
||||||
|
href: 'https://google.com',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'ref',
|
||||||
|
id: 'hello',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"category\\",
|
||||||
|
\\"label\\": \\"category\\",
|
||||||
|
\\"items\\": [],
|
||||||
|
\\"href\\" [31m[1][0m: \\"https://google.com\\"
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] \\"href\\" is not allowed[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebar category wrong items', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: {
|
||||||
|
Test: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Category Label',
|
||||||
|
items: 'doc1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"sidebar.forEach is not a function"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebars item doc but id is not a string', async () => {
|
||||||
|
expect(() =>
|
||||||
|
validateSidebars({
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
id: ['doc1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"type\\": \\"doc\\",
|
||||||
|
\\"id\\" [31m[1][0m: [
|
||||||
|
\\"doc1\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
[31m
|
||||||
|
[1] \\"id\\" must be a string[0m"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTML type requires a value', () => {
|
||||||
const sidebars: SidebarsConfig = {
|
const sidebars: SidebarsConfig = {
|
||||||
sidebar1: [
|
sidebar1: [
|
||||||
{
|
{
|
||||||
|
@ -68,7 +246,7 @@ describe('html item type', () => {
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepts valid values', () => {
|
test('HTML type accepts valid values', () => {
|
||||||
const sidebars: SidebarsConfig = {
|
const sidebars: SidebarsConfig = {
|
||||||
sidebar1: [
|
sidebar1: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
SidebarItem,
|
|
||||||
SidebarItemDoc,
|
SidebarItemDoc,
|
||||||
SidebarItemCategory,
|
|
||||||
SidebarItemsGenerator,
|
SidebarItemsGenerator,
|
||||||
SidebarItemsGeneratorDoc,
|
SidebarItemsGeneratorDoc,
|
||||||
SidebarItemCategoryLink,
|
NormalizedSidebarItemCategory,
|
||||||
|
NormalizedSidebarItem,
|
||||||
|
SidebarItemCategoryLinkConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {sortBy, last} from 'lodash';
|
import {sortBy, last} from 'lodash';
|
||||||
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
|
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
|
||||||
|
@ -48,7 +48,6 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
numberPrefixParser,
|
numberPrefixParser,
|
||||||
isCategoryIndex,
|
isCategoryIndex,
|
||||||
docs: allDocs,
|
docs: allDocs,
|
||||||
options,
|
|
||||||
item: {dirName: autogenDir},
|
item: {dirName: autogenDir},
|
||||||
categoriesMetadata,
|
categoriesMetadata,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -125,7 +124,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
* Step 3. Recursively transform the tree-like structure to sidebar items.
|
* Step 3. Recursively transform the tree-like structure to sidebar items.
|
||||||
* (From a record to an array of items, akin to normalizing shorthand)
|
* (From a record to an array of items, akin to normalizing shorthand)
|
||||||
*/
|
*/
|
||||||
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
|
function generateSidebar(
|
||||||
|
fsModel: Dir,
|
||||||
|
): Promise<WithPosition<NormalizedSidebarItem>[]> {
|
||||||
function createDocItem(id: string): WithPosition<SidebarItemDoc> {
|
function createDocItem(id: string): WithPosition<SidebarItemDoc> {
|
||||||
const {
|
const {
|
||||||
sidebarPosition: position,
|
sidebarPosition: position,
|
||||||
|
@ -145,7 +146,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
fullPath: string,
|
fullPath: string,
|
||||||
folderName: string,
|
folderName: string,
|
||||||
): Promise<WithPosition<SidebarItemCategory>> {
|
): Promise<WithPosition<NormalizedSidebarItemCategory>> {
|
||||||
const categoryMetadata =
|
const categoryMetadata =
|
||||||
categoriesMetadata[posixPath(path.join(autogenDir, fullPath))];
|
categoriesMetadata[posixPath(path.join(autogenDir, fullPath))];
|
||||||
const className = categoryMetadata?.className;
|
const className = categoryMetadata?.className;
|
||||||
|
@ -160,18 +161,19 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
|
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
|
||||||
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
|
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
|
||||||
return allItems.find(
|
return allItems.find(
|
||||||
(item) => item.type === 'doc' && getLocalDocId(item.id) === localId,
|
(item): item is SidebarItemDoc =>
|
||||||
) as SidebarItemDoc | undefined;
|
item.type === 'doc' && getLocalDocId(item.id) === localId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
|
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
|
||||||
return allItems.find((item) => {
|
return allItems.find((item): item is SidebarItemDoc => {
|
||||||
if (item.type !== 'doc') {
|
if (item.type !== 'doc') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const doc = getDoc(item.id);
|
const doc = getDoc(item.id);
|
||||||
return isCategoryIndex(toCategoryIndexMatcherParam(doc));
|
return isCategoryIndex(toCategoryIndexMatcherParam(doc));
|
||||||
}) as SidebarItemDoc | undefined;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryLinkedDocId(): string | undefined {
|
function getCategoryLinkedDocId(): string | undefined {
|
||||||
|
@ -190,13 +192,13 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
|
|
||||||
const categoryLinkedDocId = getCategoryLinkedDocId();
|
const categoryLinkedDocId = getCategoryLinkedDocId();
|
||||||
|
|
||||||
const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId
|
const link: SidebarItemCategoryLinkConfig | null | undefined =
|
||||||
? {
|
categoryLinkedDocId
|
||||||
type: 'doc',
|
? {
|
||||||
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
|
type: 'doc',
|
||||||
}
|
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
|
||||||
: // TODO typing issue
|
}
|
||||||
(categoryMetadata?.link as SidebarItemCategoryLink | undefined);
|
: categoryMetadata?.link;
|
||||||
|
|
||||||
// If a doc is linked, remove it from the category subItems
|
// If a doc is linked, remove it from the category subItems
|
||||||
const items = allItems.filter(
|
const items = allItems.filter(
|
||||||
|
@ -206,9 +208,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
return {
|
return {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: categoryMetadata?.label ?? filename,
|
label: categoryMetadata?.label ?? filename,
|
||||||
collapsible:
|
collapsible: categoryMetadata?.collapsible,
|
||||||
categoryMetadata?.collapsible ?? options.sidebarCollapsible,
|
collapsed: categoryMetadata?.collapsed,
|
||||||
collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
|
|
||||||
position: categoryMetadata?.position ?? numberPrefix,
|
position: categoryMetadata?.position ?? numberPrefix,
|
||||||
...(className !== undefined && {className}),
|
...(className !== undefined && {className}),
|
||||||
items,
|
items,
|
||||||
|
@ -219,7 +220,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
dir: Dir | null, // The directory item to be transformed.
|
dir: Dir | null, // The directory item to be transformed.
|
||||||
itemKey: string, // For docs, it's the doc ID; for categories, it's used to generate the next `relativePath`.
|
itemKey: string, // For docs, it's the doc ID; for categories, it's used to generate the next `relativePath`.
|
||||||
fullPath: string, // `dir`'s full path relative to the autogen dir.
|
fullPath: string, // `dir`'s full path relative to the autogen dir.
|
||||||
): Promise<WithPosition<SidebarItem>> {
|
): Promise<WithPosition<NormalizedSidebarItem>> {
|
||||||
return dir
|
return dir
|
||||||
? createCategoryItem(dir, fullPath, itemKey)
|
? createCategoryItem(dir, fullPath, itemKey)
|
||||||
: createDocItem(itemKey.substring(docIdPrefix.length));
|
: createDocItem(itemKey.substring(docIdPrefix.length));
|
||||||
|
@ -238,7 +239,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
||||||
* consecutive sidebar slices (i.e. a whole category composed of multiple
|
* consecutive sidebar slices (i.e. a whole category composed of multiple
|
||||||
* autogenerated items)
|
* autogenerated items)
|
||||||
*/
|
*/
|
||||||
function sortItems(sidebarItems: WithPosition<SidebarItem>[]): SidebarItem[] {
|
function sortItems(
|
||||||
|
sidebarItems: WithPosition<NormalizedSidebarItem>[],
|
||||||
|
): NormalizedSidebarItem[] {
|
||||||
const processedSidebarItems = sidebarItems.map((item) => {
|
const processedSidebarItems = sidebarItems.map((item) => {
|
||||||
if (item.type === 'category') {
|
if (item.type === 'category') {
|
||||||
return {...item, items: sortItems(item.items)};
|
return {...item, items: sortItems(item.items)};
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import importFresh from 'import-fresh';
|
import importFresh from 'import-fresh';
|
||||||
import type {SidebarsConfig, Sidebars, NormalizedSidebars} from './types';
|
import type {SidebarsConfig, Sidebars, SidebarProcessorParams} from './types';
|
||||||
import type {NormalizeSidebarsParams} from '../types';
|
|
||||||
import {validateSidebars, validateCategoryMetadataFile} from './validation';
|
import {validateSidebars, validateCategoryMetadataFile} from './validation';
|
||||||
import {normalizeSidebars} from './normalization';
|
import {normalizeSidebars} from './normalization';
|
||||||
import {processSidebars, type SidebarProcessorParams} from './processor';
|
import {processSidebars} from './processor';
|
||||||
|
import {postProcessSidebars} from './postProcessor';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {createSlugger, Globby} from '@docusaurus/utils';
|
import {Globby} from '@docusaurus/utils';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
|
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
|
||||||
import Yaml from 'js-yaml';
|
import Yaml from 'js-yaml';
|
||||||
|
@ -69,7 +69,7 @@ async function readCategoriesMetadata(contentPath: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSidebarsFileUnsafe(
|
export async function loadSidebarsFileUnsafe(
|
||||||
sidebarFilePath: string | false | undefined,
|
sidebarFilePath: string | false | undefined,
|
||||||
): Promise<SidebarsConfig> {
|
): Promise<SidebarsConfig> {
|
||||||
// false => no sidebars
|
// false => no sidebars
|
||||||
|
@ -93,37 +93,21 @@ async function loadSidebarsFileUnsafe(
|
||||||
return importFresh(sidebarFilePath);
|
return importFresh(sidebarFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSidebarsFile(
|
|
||||||
sidebarFilePath: string | false | undefined,
|
|
||||||
): Promise<SidebarsConfig> {
|
|
||||||
const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath);
|
|
||||||
validateSidebars(sidebarsConfig);
|
|
||||||
return sidebarsConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadNormalizedSidebars(
|
|
||||||
sidebarFilePath: string | false | undefined,
|
|
||||||
params: NormalizeSidebarsParams,
|
|
||||||
): Promise<NormalizedSidebars> {
|
|
||||||
return normalizeSidebars(await loadSidebarsFile(sidebarFilePath), params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: sidebarFilePath must be absolute, use resolveSidebarPathOption
|
// Note: sidebarFilePath must be absolute, use resolveSidebarPathOption
|
||||||
export async function loadSidebars(
|
export async function loadSidebars(
|
||||||
sidebarFilePath: string | false | undefined,
|
sidebarFilePath: string | false | undefined,
|
||||||
options: Omit<SidebarProcessorParams, 'categoriesMetadata'>,
|
options: SidebarProcessorParams,
|
||||||
): Promise<Sidebars> {
|
): Promise<Sidebars> {
|
||||||
const normalizeSidebarsParams: NormalizeSidebarsParams = {
|
const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath);
|
||||||
...options.sidebarOptions,
|
const normalizedSidebars = normalizeSidebars(sidebarsConfig);
|
||||||
version: options.version,
|
validateSidebars(normalizedSidebars);
|
||||||
categoryLabelSlugger: createSlugger(),
|
|
||||||
};
|
|
||||||
const normalizedSidebars = await loadNormalizedSidebars(
|
|
||||||
sidebarFilePath,
|
|
||||||
normalizeSidebarsParams,
|
|
||||||
);
|
|
||||||
const categoriesMetadata = await readCategoriesMetadata(
|
const categoriesMetadata = await readCategoriesMetadata(
|
||||||
options.version.contentPath,
|
options.version.contentPath,
|
||||||
);
|
);
|
||||||
return processSidebars(normalizedSidebars, {...options, categoriesMetadata});
|
const processedSidebars = await processSidebars(
|
||||||
|
normalizedSidebars,
|
||||||
|
categoriesMetadata,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
return postProcessSidebars(processedSidebars, options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {NormalizeSidebarsParams} from '../types';
|
|
||||||
import type {
|
import type {
|
||||||
NormalizedSidebarItem,
|
NormalizedSidebarItem,
|
||||||
NormalizedSidebar,
|
NormalizedSidebar,
|
||||||
|
@ -15,41 +14,16 @@ import type {
|
||||||
SidebarItemConfig,
|
SidebarItemConfig,
|
||||||
SidebarConfig,
|
SidebarConfig,
|
||||||
SidebarsConfig,
|
SidebarsConfig,
|
||||||
SidebarItemCategoryLink,
|
|
||||||
NormalizedSidebarItemCategory,
|
NormalizedSidebarItemCategory,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {isCategoriesShorthand} from './utils';
|
import {isCategoriesShorthand} from './utils';
|
||||||
import {mapValues} from 'lodash';
|
import {mapValues} from 'lodash';
|
||||||
import {normalizeUrl} from '@docusaurus/utils';
|
|
||||||
import type {SidebarOptions} from '@docusaurus/plugin-content-docs';
|
|
||||||
|
|
||||||
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(
|
function normalizeCategoriesShorthand(
|
||||||
sidebar: SidebarCategoriesShorthand,
|
sidebar: SidebarCategoriesShorthand,
|
||||||
options: SidebarOptions,
|
|
||||||
): SidebarItemCategoryConfig[] {
|
): SidebarItemCategoryConfig[] {
|
||||||
return Object.entries(sidebar).map(([label, items]) => ({
|
return Object.entries(sidebar).map(([label, items]) => ({
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: options.sidebarCollapsed,
|
|
||||||
collapsible: options.sidebarCollapsible,
|
|
||||||
label,
|
label,
|
||||||
items,
|
items,
|
||||||
}));
|
}));
|
||||||
|
@ -61,7 +35,6 @@ function normalizeCategoriesShorthand(
|
||||||
*/
|
*/
|
||||||
export function normalizeItem(
|
export function normalizeItem(
|
||||||
item: SidebarItemConfig,
|
item: SidebarItemConfig,
|
||||||
options: NormalizeSidebarsParams,
|
|
||||||
): NormalizedSidebarItem[] {
|
): NormalizedSidebarItem[] {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
return [
|
return [
|
||||||
|
@ -72,42 +45,35 @@ export function normalizeItem(
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (isCategoriesShorthand(item)) {
|
if (isCategoriesShorthand(item)) {
|
||||||
return normalizeCategoriesShorthand(item, options).flatMap((subItem) =>
|
return normalizeCategoriesShorthand(item).flatMap((subItem) =>
|
||||||
normalizeItem(subItem, options),
|
normalizeItem(subItem),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (item.type === 'category') {
|
if (item.type === 'category') {
|
||||||
const link = normalizeCategoryLink(item, options);
|
if (typeof item.items !== 'undefined' && !Array.isArray(item.items)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid category ${JSON.stringify(item)}: items must be an array`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const normalizedCategory: NormalizedSidebarItemCategory = {
|
const normalizedCategory: NormalizedSidebarItemCategory = {
|
||||||
...item,
|
...item,
|
||||||
link,
|
items: item.items.flatMap((subItem) => normalizeItem(subItem)),
|
||||||
items: (item.items ?? []).flatMap((subItem) =>
|
|
||||||
normalizeItem(subItem, options),
|
|
||||||
),
|
|
||||||
collapsible: item.collapsible ?? options.sidebarCollapsible,
|
|
||||||
collapsed: item.collapsed ?? options.sidebarCollapsed,
|
|
||||||
};
|
};
|
||||||
return [normalizedCategory];
|
return [normalizedCategory];
|
||||||
}
|
}
|
||||||
return [item];
|
return [item];
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSidebar(
|
function normalizeSidebar(sidebar: SidebarConfig): NormalizedSidebar {
|
||||||
sidebar: SidebarConfig,
|
|
||||||
options: NormalizeSidebarsParams,
|
|
||||||
): NormalizedSidebar {
|
|
||||||
const normalizedSidebar = Array.isArray(sidebar)
|
const normalizedSidebar = Array.isArray(sidebar)
|
||||||
? sidebar
|
? sidebar
|
||||||
: normalizeCategoriesShorthand(sidebar, options);
|
: normalizeCategoriesShorthand(sidebar);
|
||||||
|
|
||||||
return normalizedSidebar.flatMap((subItem) =>
|
return normalizedSidebar.flatMap((subItem) => normalizeItem(subItem));
|
||||||
normalizeItem(subItem, options),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeSidebars(
|
export function normalizeSidebars(
|
||||||
sidebars: SidebarsConfig,
|
sidebars: SidebarsConfig,
|
||||||
params: NormalizeSidebarsParams,
|
|
||||||
): NormalizedSidebars {
|
): NormalizedSidebars {
|
||||||
return mapValues(sidebars, (items) => normalizeSidebar(items, params));
|
return mapValues(sidebars, normalizeSidebar);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* 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 {normalizeUrl} from '@docusaurus/utils';
|
||||||
|
import type {
|
||||||
|
SidebarItem,
|
||||||
|
Sidebars,
|
||||||
|
SidebarProcessorParams,
|
||||||
|
ProcessedSidebarItemCategory,
|
||||||
|
ProcessedSidebarItem,
|
||||||
|
ProcessedSidebars,
|
||||||
|
SidebarItemCategoryLink,
|
||||||
|
} from './types';
|
||||||
|
import {mapValues} from 'lodash';
|
||||||
|
|
||||||
|
function normalizeCategoryLink(
|
||||||
|
category: ProcessedSidebarItemCategory,
|
||||||
|
params: SidebarProcessorParams,
|
||||||
|
): 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 postProcessSidebarItem(
|
||||||
|
item: ProcessedSidebarItem,
|
||||||
|
params: SidebarProcessorParams,
|
||||||
|
): SidebarItem {
|
||||||
|
if (item.type === 'category') {
|
||||||
|
const category = {
|
||||||
|
...item,
|
||||||
|
collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed,
|
||||||
|
collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible,
|
||||||
|
link: normalizeCategoryLink(item, params),
|
||||||
|
items: item.items.map((subItem) =>
|
||||||
|
postProcessSidebarItem(subItem, params),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
// If the current category doesn't have subitems, we render a normal link
|
||||||
|
// instead.
|
||||||
|
if (category.items.length === 0) {
|
||||||
|
if (!category.link) {
|
||||||
|
throw new Error(
|
||||||
|
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
switch (category.link.type) {
|
||||||
|
case 'doc':
|
||||||
|
return {
|
||||||
|
type: 'doc',
|
||||||
|
label: category.label,
|
||||||
|
id: category.link.id,
|
||||||
|
};
|
||||||
|
case 'generated-index':
|
||||||
|
return {
|
||||||
|
type: 'link',
|
||||||
|
label: category.label,
|
||||||
|
href: category.link.permalink,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error('Unexpected sidebar category link type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A non-collapsible category can't be collapsed!
|
||||||
|
if (category.collapsible === false) {
|
||||||
|
category.collapsed = false;
|
||||||
|
}
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postProcessSidebars(
|
||||||
|
sidebars: ProcessedSidebars,
|
||||||
|
params: SidebarProcessorParams,
|
||||||
|
): Sidebars {
|
||||||
|
return mapValues(sidebars, (sidebar) =>
|
||||||
|
sidebar.map((item) => postProcessSidebarItem(item, params)),
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,41 +7,23 @@
|
||||||
|
|
||||||
import type {DocMetadataBase, VersionMetadata} from '../types';
|
import type {DocMetadataBase, VersionMetadata} from '../types';
|
||||||
import type {
|
import type {
|
||||||
Sidebars,
|
|
||||||
Sidebar,
|
|
||||||
SidebarItem,
|
|
||||||
NormalizedSidebarItem,
|
NormalizedSidebarItem,
|
||||||
NormalizedSidebar,
|
NormalizedSidebar,
|
||||||
NormalizedSidebars,
|
NormalizedSidebars,
|
||||||
SidebarItemsGeneratorOption,
|
|
||||||
SidebarItemsGeneratorDoc,
|
SidebarItemsGeneratorDoc,
|
||||||
SidebarItemsGeneratorVersion,
|
SidebarItemsGeneratorVersion,
|
||||||
NormalizedSidebarItemCategory,
|
|
||||||
SidebarItemCategory,
|
|
||||||
SidebarItemAutogenerated,
|
SidebarItemAutogenerated,
|
||||||
|
ProcessedSidebarItem,
|
||||||
|
ProcessedSidebar,
|
||||||
|
ProcessedSidebars,
|
||||||
|
SidebarProcessorParams,
|
||||||
CategoryMetadataFile,
|
CategoryMetadataFile,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {transformSidebarItems} from './utils';
|
|
||||||
import {DefaultSidebarItemsGenerator} from './generator';
|
import {DefaultSidebarItemsGenerator} from './generator';
|
||||||
|
import {validateSidebars} from './validation';
|
||||||
import {mapValues, memoize, pick} from 'lodash';
|
import {mapValues, memoize, pick} from 'lodash';
|
||||||
import combinePromises from 'combine-promises';
|
import combinePromises from 'combine-promises';
|
||||||
import {normalizeItem} from './normalization';
|
|
||||||
import {isCategoryIndex} from '../docs';
|
import {isCategoryIndex} from '../docs';
|
||||||
import type {Slugger} from '@docusaurus/utils';
|
|
||||||
import type {
|
|
||||||
NumberPrefixParser,
|
|
||||||
SidebarOptions,
|
|
||||||
} from '@docusaurus/plugin-content-docs';
|
|
||||||
|
|
||||||
export type SidebarProcessorParams = {
|
|
||||||
sidebarItemsGenerator: SidebarItemsGeneratorOption;
|
|
||||||
numberPrefixParser: NumberPrefixParser;
|
|
||||||
docs: DocMetadataBase[];
|
|
||||||
version: VersionMetadata;
|
|
||||||
categoryLabelSlugger: Slugger;
|
|
||||||
sidebarOptions: SidebarOptions;
|
|
||||||
categoriesMetadata: Record<string, CategoryMetadataFile>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function toSidebarItemsGeneratorDoc(
|
function toSidebarItemsGeneratorDoc(
|
||||||
doc: DocMetadataBase,
|
doc: DocMetadataBase,
|
||||||
|
@ -66,15 +48,15 @@ function toSidebarItemsGeneratorVersion(
|
||||||
// post-processing checks
|
// post-processing checks
|
||||||
async function processSidebar(
|
async function processSidebar(
|
||||||
unprocessedSidebar: NormalizedSidebar,
|
unprocessedSidebar: NormalizedSidebar,
|
||||||
|
categoriesMetadata: Record<string, CategoryMetadataFile>,
|
||||||
params: SidebarProcessorParams,
|
params: SidebarProcessorParams,
|
||||||
): Promise<Sidebar> {
|
): Promise<ProcessedSidebar> {
|
||||||
const {
|
const {
|
||||||
sidebarItemsGenerator,
|
sidebarItemsGenerator,
|
||||||
numberPrefixParser,
|
numberPrefixParser,
|
||||||
docs,
|
docs,
|
||||||
version,
|
version,
|
||||||
sidebarOptions,
|
sidebarOptions,
|
||||||
categoriesMetadata,
|
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
// Just a minor lazy transformation optimization
|
// Just a minor lazy transformation optimization
|
||||||
|
@ -83,20 +65,9 @@ async function processSidebar(
|
||||||
version: toSidebarItemsGeneratorVersion(version),
|
version: toSidebarItemsGeneratorVersion(version),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function processCategoryItem(
|
|
||||||
item: NormalizedSidebarItemCategory,
|
|
||||||
): Promise<SidebarItemCategory> {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
items: (await Promise.all(item.items.map(processItem))).flat(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processAutoGeneratedItem(
|
async function processAutoGeneratedItem(
|
||||||
item: SidebarItemAutogenerated,
|
item: SidebarItemAutogenerated,
|
||||||
): Promise<SidebarItem[]> {
|
): Promise<ProcessedSidebarItem[]> {
|
||||||
// TODO the returned type can't be trusted in practice (generator can be
|
|
||||||
// user-provided)
|
|
||||||
const generatedItems = await sidebarItemsGenerator({
|
const generatedItems = await sidebarItemsGenerator({
|
||||||
item,
|
item,
|
||||||
numberPrefixParser,
|
numberPrefixParser,
|
||||||
|
@ -106,50 +77,23 @@ async function processSidebar(
|
||||||
options: sidebarOptions,
|
options: sidebarOptions,
|
||||||
categoriesMetadata,
|
categoriesMetadata,
|
||||||
});
|
});
|
||||||
// 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
|
// Process again... weird but sidebar item generated might generate some
|
||||||
// auto-generated items?
|
// auto-generated items?
|
||||||
return processItems(generatedItemsNormalized);
|
// TODO repeatedly process & unwrap autogenerated items until there are no
|
||||||
|
// more autogenerated items, or when loop count (e.g. 10) is reached
|
||||||
|
return processItems(generatedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processItem(
|
async function processItem(
|
||||||
item: NormalizedSidebarItem,
|
item: NormalizedSidebarItem,
|
||||||
): Promise<SidebarItem[]> {
|
): Promise<ProcessedSidebarItem[]> {
|
||||||
if (item.type === 'category') {
|
if (item.type === 'category') {
|
||||||
// If the current category doesn't have subitems, we render a normal doc link instead.
|
return [
|
||||||
if (item.items.length === 0) {
|
{
|
||||||
if (!item.link) {
|
...item,
|
||||||
throw new Error(
|
items: (await Promise.all(item.items.map(processItem))).flat(),
|
||||||
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
|
},
|
||||||
);
|
];
|
||||||
}
|
|
||||||
switch (item.link.type) {
|
|
||||||
case 'doc':
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'doc',
|
|
||||||
label: item.label,
|
|
||||||
id: item.link.id,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
case 'generated-index':
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'link',
|
|
||||||
label: item.label,
|
|
||||||
href: item.link.permalink,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
default:
|
|
||||||
throw new Error('Unexpected sidebar category link type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [await processCategoryItem(item)];
|
|
||||||
}
|
}
|
||||||
if (item.type === 'autogenerated') {
|
if (item.type === 'autogenerated') {
|
||||||
return processAutoGeneratedItem(item);
|
return processAutoGeneratedItem(item);
|
||||||
|
@ -159,32 +103,24 @@ async function processSidebar(
|
||||||
|
|
||||||
async function processItems(
|
async function processItems(
|
||||||
items: NormalizedSidebarItem[],
|
items: NormalizedSidebarItem[],
|
||||||
): Promise<SidebarItem[]> {
|
): Promise<ProcessedSidebarItem[]> {
|
||||||
return (await Promise.all(items.map(processItem))).flat();
|
return (await Promise.all(items.map(processItem))).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedSidebar = await processItems(unprocessedSidebar);
|
const processedSidebar = await processItems(unprocessedSidebar);
|
||||||
|
return processedSidebar;
|
||||||
const fixSidebarItemInconsistencies = (item: SidebarItem): SidebarItem => {
|
|
||||||
// A non-collapsible category can't be collapsed!
|
|
||||||
if (item.type === 'category' && !item.collapsible && item.collapsed) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
collapsed: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
return transformSidebarItems(processedSidebar, fixSidebarItemInconsistencies);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processSidebars(
|
export async function processSidebars(
|
||||||
unprocessedSidebars: NormalizedSidebars,
|
unprocessedSidebars: NormalizedSidebars,
|
||||||
|
categoriesMetadata: Record<string, CategoryMetadataFile>,
|
||||||
params: SidebarProcessorParams,
|
params: SidebarProcessorParams,
|
||||||
): Promise<Sidebars> {
|
): Promise<ProcessedSidebars> {
|
||||||
return combinePromises(
|
const processedSidebars = await combinePromises(
|
||||||
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
|
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
|
||||||
processSidebar(unprocessedSidebar, params),
|
processSidebar(unprocessedSidebar, categoriesMetadata, params),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
validateSidebars(processedSidebars);
|
||||||
|
return processedSidebars;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
SidebarOptions,
|
SidebarOptions,
|
||||||
CategoryIndexMatcher,
|
CategoryIndexMatcher,
|
||||||
} from '@docusaurus/plugin-content-docs';
|
} from '@docusaurus/plugin-content-docs';
|
||||||
|
import type {Slugger} from '@docusaurus/utils';
|
||||||
|
|
||||||
// Makes all properties visible when hovering over the type
|
// Makes all properties visible when hovering over the type
|
||||||
type Expand<T extends Record<string, unknown>> = {[P in keyof T]: T[P]};
|
type Expand<T extends Record<string, unknown>> = {[P in keyof T]: T[P]};
|
||||||
|
@ -107,9 +108,9 @@ export type SidebarsConfig = {
|
||||||
|
|
||||||
// Normalized but still has 'autogenerated', which will be handled in processing
|
// Normalized but still has 'autogenerated', which will be handled in processing
|
||||||
export type NormalizedSidebarItemCategory = Expand<
|
export type NormalizedSidebarItemCategory = Expand<
|
||||||
SidebarItemCategoryBase & {
|
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
|
||||||
items: NormalizedSidebarItem[];
|
items: NormalizedSidebarItem[];
|
||||||
link?: SidebarItemCategoryLink;
|
link?: SidebarItemCategoryLinkConfig;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -125,6 +126,22 @@ export type NormalizedSidebars = {
|
||||||
[sidebarId: string]: NormalizedSidebar;
|
[sidebarId: string]: NormalizedSidebar;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProcessedSidebarItemCategory = Expand<
|
||||||
|
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
|
||||||
|
items: ProcessedSidebarItem[];
|
||||||
|
link?: SidebarItemCategoryLinkConfig;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
export type ProcessedSidebarItem =
|
||||||
|
| SidebarItemDoc
|
||||||
|
| SidebarItemHtml
|
||||||
|
| SidebarItemLink
|
||||||
|
| ProcessedSidebarItemCategory;
|
||||||
|
export type ProcessedSidebar = ProcessedSidebarItem[];
|
||||||
|
export type ProcessedSidebars = {
|
||||||
|
[sidebarId: string]: ProcessedSidebar;
|
||||||
|
};
|
||||||
|
|
||||||
export type SidebarItemCategory = Expand<
|
export type SidebarItemCategory = Expand<
|
||||||
SidebarItemCategoryBase & {
|
SidebarItemCategoryBase & {
|
||||||
items: SidebarItem[];
|
items: SidebarItem[];
|
||||||
|
@ -230,9 +247,7 @@ export type SidebarItemsGeneratorArgs = {
|
||||||
};
|
};
|
||||||
export type SidebarItemsGenerator = (
|
export type SidebarItemsGenerator = (
|
||||||
generatorArgs: SidebarItemsGeneratorArgs,
|
generatorArgs: SidebarItemsGeneratorArgs,
|
||||||
) => // TODO TS issue: the generator can generate un-normalized items!
|
) => Promise<NormalizedSidebar>;
|
||||||
Promise<SidebarItem[]>;
|
|
||||||
// Promise<SidebarItemConfig[]>;
|
|
||||||
|
|
||||||
// Also inject the default generator to conveniently wrap/enhance/sort the
|
// Also inject the default generator to conveniently wrap/enhance/sort the
|
||||||
// default sidebar gen logic
|
// default sidebar gen logic
|
||||||
|
@ -242,4 +257,13 @@ export type SidebarItemsGeneratorOptionArgs = {
|
||||||
} & SidebarItemsGeneratorArgs;
|
} & SidebarItemsGeneratorArgs;
|
||||||
export type SidebarItemsGeneratorOption = (
|
export type SidebarItemsGeneratorOption = (
|
||||||
generatorArgs: SidebarItemsGeneratorOptionArgs,
|
generatorArgs: SidebarItemsGeneratorOptionArgs,
|
||||||
) => Promise<SidebarItem[]>;
|
) => Promise<NormalizedSidebarItem[]>;
|
||||||
|
|
||||||
|
export type SidebarProcessorParams = {
|
||||||
|
sidebarItemsGenerator: SidebarItemsGeneratorOption;
|
||||||
|
numberPrefixParser: NumberPrefixParser;
|
||||||
|
docs: DocMetadataBase[];
|
||||||
|
version: VersionMetadata;
|
||||||
|
categoryLabelSlugger: Slugger;
|
||||||
|
sidebarOptions: SidebarOptions;
|
||||||
|
};
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
||||||
import type {
|
import type {
|
||||||
SidebarItemConfig,
|
SidebarItemConfig,
|
||||||
SidebarCategoriesShorthand,
|
|
||||||
SidebarItemBase,
|
SidebarItemBase,
|
||||||
SidebarItemAutogenerated,
|
SidebarItemAutogenerated,
|
||||||
SidebarItemDoc,
|
SidebarItemDoc,
|
||||||
|
@ -16,12 +15,13 @@ import type {
|
||||||
SidebarItemLink,
|
SidebarItemLink,
|
||||||
SidebarItemCategoryConfig,
|
SidebarItemCategoryConfig,
|
||||||
SidebarItemCategoryLink,
|
SidebarItemCategoryLink,
|
||||||
SidebarsConfig,
|
|
||||||
SidebarItemCategoryLinkDoc,
|
SidebarItemCategoryLinkDoc,
|
||||||
SidebarItemCategoryLinkGeneratedIndex,
|
SidebarItemCategoryLinkGeneratedIndex,
|
||||||
|
NormalizedSidebars,
|
||||||
|
NormalizedSidebarItem,
|
||||||
|
NormalizedSidebarItemCategory,
|
||||||
CategoryMetadataFile,
|
CategoryMetadataFile,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {isCategoriesShorthand} from './utils';
|
|
||||||
|
|
||||||
// NOTE: we don't add any default values during validation on purpose!
|
// NOTE: we don't add any default values during validation on purpose!
|
||||||
// Config types are exposed to users for typechecking and we use the same type
|
// Config types are exposed to users for typechecking and we use the same type
|
||||||
|
@ -52,7 +52,7 @@ const sidebarItemDocSchema = sidebarItemBaseSchema.append<SidebarItemDoc>({
|
||||||
const sidebarItemHtmlSchema = sidebarItemBaseSchema.append<SidebarItemHtml>({
|
const sidebarItemHtmlSchema = sidebarItemBaseSchema.append<SidebarItemHtml>({
|
||||||
type: 'html',
|
type: 'html',
|
||||||
value: Joi.string().required(),
|
value: Joi.string().required(),
|
||||||
defaultStyle: Joi.boolean().default(false),
|
defaultStyle: Joi.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidebarItemLinkSchema = sidebarItemBaseSchema.append<SidebarItemLink>({
|
const sidebarItemLinkSchema = sidebarItemBaseSchema.append<SidebarItemLink>({
|
||||||
|
@ -88,14 +88,13 @@ const sidebarItemCategoryLinkSchema = Joi.object<SidebarItemCategoryLink>()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
is: Joi.string().required(),
|
is: Joi.required(),
|
||||||
then: Joi.forbidden().messages({
|
then: Joi.forbidden().messages({
|
||||||
'any.unknown': 'Unknown sidebar category link type "{.type}".',
|
'any.unknown': 'Unknown sidebar category link type "{.type}".',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
.id('sidebarCategoryLinkSchema');
|
|
||||||
|
|
||||||
const sidebarItemCategorySchema =
|
const sidebarItemCategorySchema =
|
||||||
sidebarItemBaseSchema.append<SidebarItemCategoryConfig>({
|
sidebarItemBaseSchema.append<SidebarItemCategoryConfig>({
|
||||||
|
@ -103,10 +102,11 @@ const sidebarItemCategorySchema =
|
||||||
label: Joi.string()
|
label: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.messages({'any.unknown': '"label" must be a string'}),
|
.messages({'any.unknown': '"label" must be a string'}),
|
||||||
// TODO: Joi doesn't allow mutual recursion. See https://github.com/sideway/joi/issues/2611
|
|
||||||
items: Joi.array()
|
items: Joi.array()
|
||||||
.required()
|
.required()
|
||||||
.messages({'any.unknown': '"items" must be an array'}), // .items(Joi.link('#sidebarItemSchema')),
|
.messages({'any.unknown': '"items" must be an array'}),
|
||||||
|
// TODO: Joi doesn't allow mutual recursion. See https://github.com/sideway/joi/issues/2611
|
||||||
|
// .items(Joi.link('#sidebarItemSchema')),
|
||||||
link: sidebarItemCategoryLinkSchema,
|
link: sidebarItemCategoryLinkSchema,
|
||||||
collapsed: Joi.boolean().messages({
|
collapsed: Joi.boolean().messages({
|
||||||
'any.unknown': '"collapsed" must be a boolean',
|
'any.unknown': '"collapsed" must be a boolean',
|
||||||
|
@ -116,56 +116,44 @@ const sidebarItemCategorySchema =
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidebarItemSchema: Joi.Schema<SidebarItemConfig> = Joi.object()
|
const sidebarItemSchema = Joi.object<SidebarItemConfig>().when('.type', {
|
||||||
.when('.type', {
|
switch: [
|
||||||
switch: [
|
{is: 'link', then: sidebarItemLinkSchema},
|
||||||
{is: 'link', then: sidebarItemLinkSchema},
|
{
|
||||||
{
|
is: Joi.string().valid('doc', 'ref').required(),
|
||||||
is: Joi.string().valid('doc', 'ref').required(),
|
then: sidebarItemDocSchema,
|
||||||
then: sidebarItemDocSchema,
|
},
|
||||||
},
|
{is: 'html', then: sidebarItemHtmlSchema},
|
||||||
{is: 'html', then: sidebarItemHtmlSchema},
|
{is: 'autogenerated', then: sidebarItemAutogeneratedSchema},
|
||||||
{is: 'autogenerated', then: sidebarItemAutogeneratedSchema},
|
{is: 'category', then: sidebarItemCategorySchema},
|
||||||
{is: 'category', then: sidebarItemCategorySchema},
|
{
|
||||||
{
|
is: Joi.any().required(),
|
||||||
is: Joi.any().required(),
|
then: Joi.forbidden().messages({
|
||||||
then: Joi.forbidden().messages({
|
'any.unknown': 'Unknown sidebar item type "{.type}".',
|
||||||
'any.unknown': 'Unknown sidebar item type "{.type}".',
|
}),
|
||||||
}),
|
},
|
||||||
},
|
],
|
||||||
],
|
});
|
||||||
})
|
// .id('sidebarItemSchema');
|
||||||
.id('sidebarItemSchema');
|
|
||||||
|
|
||||||
function validateSidebarItem(item: unknown): asserts item is SidebarItemConfig {
|
function validateSidebarItem(
|
||||||
if (typeof item === 'string') {
|
item: unknown,
|
||||||
return;
|
): asserts item is NormalizedSidebarItem {
|
||||||
}
|
|
||||||
// TODO: remove once with proper Joi support
|
// TODO: remove once with proper Joi support
|
||||||
// Because we can't use Joi to validate nested items (see above), we do it
|
// Because we can't use Joi to validate nested items (see above), we do it
|
||||||
// manually
|
// manually
|
||||||
if (isCategoriesShorthand(item as SidebarItemConfig)) {
|
Joi.assert(item, sidebarItemSchema);
|
||||||
Object.values(item as SidebarCategoriesShorthand).forEach((category) =>
|
|
||||||
category.forEach(validateSidebarItem),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Joi.assert(item, sidebarItemSchema);
|
|
||||||
|
|
||||||
if ((item as SidebarItemCategoryConfig).type === 'category') {
|
if ((item as NormalizedSidebarItemCategory).type === 'category') {
|
||||||
(item as SidebarItemCategoryConfig).items.forEach(validateSidebarItem);
|
(item as NormalizedSidebarItemCategory).items.forEach(validateSidebarItem);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateSidebars(
|
export function validateSidebars(
|
||||||
sidebars: unknown,
|
sidebars: Record<string, unknown>,
|
||||||
): asserts sidebars is SidebarsConfig {
|
): asserts sidebars is NormalizedSidebars {
|
||||||
Object.values(sidebars as SidebarsConfig).forEach((sidebar) => {
|
Object.values(sidebars as NormalizedSidebars).forEach((sidebar) => {
|
||||||
if (Array.isArray(sidebar)) {
|
sidebar.forEach(validateSidebarItem);
|
||||||
sidebar.forEach(validateSidebarItem);
|
|
||||||
} else {
|
|
||||||
validateSidebarItem(sidebar);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,15 +8,12 @@
|
||||||
/// <reference types="@docusaurus/module-type-aliases" />
|
/// <reference types="@docusaurus/module-type-aliases" />
|
||||||
|
|
||||||
import type {Sidebars} from './sidebars/types';
|
import type {Sidebars} from './sidebars/types';
|
||||||
import type {Tag, FrontMatterTag, Slugger} from '@docusaurus/utils';
|
import type {Tag, FrontMatterTag} from '@docusaurus/utils';
|
||||||
import type {
|
import type {
|
||||||
BrokenMarkdownLink as IBrokenMarkdownLink,
|
BrokenMarkdownLink as IBrokenMarkdownLink,
|
||||||
ContentPaths,
|
ContentPaths,
|
||||||
} from '@docusaurus/utils/lib/markdownLinks';
|
} from '@docusaurus/utils/lib/markdownLinks';
|
||||||
import type {
|
import type {VersionBanner} from '@docusaurus/plugin-content-docs';
|
||||||
VersionBanner,
|
|
||||||
SidebarOptions,
|
|
||||||
} from '@docusaurus/plugin-content-docs';
|
|
||||||
|
|
||||||
export type DocFile = {
|
export type DocFile = {
|
||||||
contentPath: string; // /!\ may be localized
|
contentPath: string; // /!\ may be localized
|
||||||
|
@ -41,11 +38,6 @@ export type VersionMetadata = ContentPaths & {
|
||||||
routePriority: number | undefined; // -1 for the latest docs
|
routePriority: number | undefined; // -1 for the latest docs
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NormalizeSidebarsParams = SidebarOptions & {
|
|
||||||
version: VersionMetadata;
|
|
||||||
categoryLabelSlugger: Slugger;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LastUpdateData = {
|
export type LastUpdateData = {
|
||||||
lastUpdatedAt?: number;
|
lastUpdatedAt?: number;
|
||||||
formattedLastUpdatedAt?: string;
|
formattedLastUpdatedAt?: string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue