mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-09 23:27:28 +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\\": [
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Test\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -791,8 +789,8 @@ Object {
|
|||
\\"docId\\": \\"foo/baz\\"
|
||||
}
|
||||
],
|
||||
\\"collapsible\\": true,
|
||||
\\"collapsed\\": true
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
|
@ -823,8 +821,8 @@ Object {
|
|||
\\"docId\\": \\"rootTryToEscapeSlug\\"
|
||||
}
|
||||
],
|
||||
\\"collapsible\\": true,
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"href\\": \\"/docs/category/slugs\\"
|
||||
},
|
||||
{
|
||||
|
@ -844,12 +842,12 @@ Object {
|
|||
\\"href\\": \\"/docs/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Guides\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -858,7 +856,9 @@ Object {
|
|||
\\"href\\": \\"/docs/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3188,8 +3188,6 @@ Object {
|
|||
\\"version-1.0.0/docs\\": [
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Test\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3204,12 +3202,12 @@ Object {
|
|||
\\"href\\": \\"/docs/1.0.0/foo/baz\\",
|
||||
\\"docId\\": \\"foo/baz\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Guides\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3218,7 +3216,9 @@ Object {
|
|||
\\"href\\": \\"/docs/1.0.0/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3255,8 +3255,6 @@ Object {
|
|||
\\"VersionedSideBarNameDoesNotMatter/docs\\": [
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Test\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3265,12 +3263,12 @@ Object {
|
|||
\\"href\\": \\"/docs/foo/bar\\",
|
||||
\\"docId\\": \\"foo/bar\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Guides\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3279,7 +3277,9 @@ Object {
|
|||
\\"href\\": \\"/docs/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3310,8 +3310,6 @@ Object {
|
|||
\\"docs\\": [
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Test\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3320,12 +3318,12 @@ Object {
|
|||
\\"href\\": \\"/docs/next/foo/barSlug\\",
|
||||
\\"docId\\": \\"foo/bar\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
},
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Guides\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3334,7 +3332,9 @@ Object {
|
|||
\\"href\\": \\"/docs/next/\\",
|
||||
\\"docId\\": \\"hello\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3385,8 +3385,6 @@ Object {
|
|||
\\"version-1.0.1/docs\\": [
|
||||
{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true,
|
||||
\\"label\\": \\"Test\\",
|
||||
\\"items\\": [
|
||||
{
|
||||
|
@ -3395,7 +3393,9 @@ Object {
|
|||
\\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
|
||||
\\"docId\\": \\"rootAbsoluteSlug\\"
|
||||
}
|
||||
]
|
||||
],
|
||||
\\"collapsed\\": true,
|
||||
\\"collapsible\\": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
|||
PathOptions,
|
||||
SidebarOptions,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import {loadSidebarsFile, resolveSidebarPathOption} from './sidebars';
|
||||
import {loadSidebarsFileUnsafe, resolveSidebarPathOption} from './sidebars';
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
|
||||
import logger from '@docusaurus/logger';
|
||||
|
||||
|
@ -34,7 +34,8 @@ async function createVersionedSidebarFile({
|
|||
// 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
|
||||
// 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
|
||||
// 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
|
||||
|
||||
exports[`loadNormalizedSidebars sidebars link 1`] = `
|
||||
exports[`loadSidebars sidebars link 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
|
@ -21,7 +21,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`loadNormalizedSidebars sidebars with category.collapsed property 1`] = `
|
||||
exports[`loadSidebars sidebars with category.collapsed property 1`] = `
|
||||
Object {
|
||||
"docs": Array [
|
||||
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 {
|
||||
"docs": Array [
|
||||
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 {
|
||||
"docs": Array [
|
||||
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 {
|
||||
"docs": Array [
|
||||
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 {
|
||||
"docs": Array [
|
||||
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',
|
||||
label: 'Tutorials',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'tutorials-index',
|
||||
|
@ -228,19 +226,16 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'Guides',
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'guides-index',
|
||||
},
|
||||
collapsed: false,
|
||||
items: [
|
||||
{type: 'doc', id: 'guide1', className: 'foo'},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'SubGuides (metadata file label)',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
items: [{type: 'doc', id: 'nested-guide'}],
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
|
@ -279,8 +274,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
'subfolder/subsubfolder/subsubsubfolder3': {
|
||||
position: 1,
|
||||
label: 'subsubsubfolder3 (_category_.json label)',
|
||||
collapsible: false,
|
||||
collapsed: false,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'doc1', // This is a "fully-qualified" ID that can't be found locally
|
||||
|
@ -355,8 +348,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'subsubsubfolder3 (_category_.json label)',
|
||||
collapsed: false,
|
||||
collapsible: false,
|
||||
link: {
|
||||
id: 'doc1',
|
||||
type: 'doc',
|
||||
|
@ -369,8 +360,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'subsubsubfolder2 (_category_.yml label)',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
className: 'bar',
|
||||
items: [{type: 'doc', id: 'doc6'}],
|
||||
},
|
||||
|
@ -379,8 +368,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'subsubsubfolder',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
items: [{type: 'doc', id: 'doc5'}],
|
||||
},
|
||||
] as Sidebar);
|
||||
|
@ -458,8 +445,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'Category label',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
link: {
|
||||
id: 'parent/doc3',
|
||||
type: 'doc',
|
||||
|
@ -478,8 +463,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'Category 2 label',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
items: [
|
||||
{
|
||||
id: 'parent/doc4',
|
||||
|
@ -583,8 +566,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'Tutorials',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'tutorials-index',
|
||||
|
@ -597,8 +578,6 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
{
|
||||
type: 'category',
|
||||
label: 'Guides',
|
||||
collapsed: true,
|
||||
collapsible: true,
|
||||
items: [
|
||||
{type: 'doc', id: 'guide1', className: 'foo'},
|
||||
{type: 'doc', id: 'guide2'},
|
||||
|
|
|
@ -6,32 +6,36 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {
|
||||
loadNormalizedSidebars,
|
||||
DefaultSidebars,
|
||||
DisabledSidebars,
|
||||
} from '../index';
|
||||
import type {NormalizeSidebarsParams, VersionMetadata} from '../../types';
|
||||
import {loadSidebars, DisabledSidebars} from '../index';
|
||||
import type {SidebarProcessorParams} from '../types';
|
||||
import {DefaultSidebarItemsGenerator} from '../generator';
|
||||
|
||||
describe('loadNormalizedSidebars', () => {
|
||||
describe('loadSidebars', () => {
|
||||
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
||||
const options: NormalizeSidebarsParams = {
|
||||
sidebarCollapsed: true,
|
||||
sidebarCollapsible: true,
|
||||
version: {
|
||||
versionName: 'version',
|
||||
versionPath: 'versionPath',
|
||||
} as VersionMetadata,
|
||||
const params: SidebarProcessorParams = {
|
||||
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
numberPrefixParser: (filename) => ({filename}),
|
||||
docs: [
|
||||
{
|
||||
source: '@site/docs/foo/bar.md',
|
||||
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 () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
||||
const result = await loadSidebars(sidebarPath, params);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars with deep level of category', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
||||
const result = await loadSidebars(sidebarPath, params);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -41,8 +45,8 @@ describe('loadNormalizedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-shorthand.js',
|
||||
);
|
||||
const sidebar1 = await loadNormalizedSidebars(sidebarPath1, options);
|
||||
const sidebar2 = await loadNormalizedSidebars(sidebarPath2, options);
|
||||
const sidebar1 = await loadSidebars(sidebarPath1, params);
|
||||
const sidebar2 = await loadSidebars(sidebarPath2, params);
|
||||
expect(sidebar1).toEqual(sidebar2);
|
||||
});
|
||||
|
||||
|
@ -51,53 +55,11 @@ describe('loadNormalizedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-category-wrong-items.json',
|
||||
);
|
||||
await expect(() => loadNormalizedSidebars(sidebarPath, options)).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
\\"type\\": \\"category\\",
|
||||
\\"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(() =>
|
||||
loadSidebars(sidebarPath, params),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid category {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}: items must be an array"`,
|
||||
);
|
||||
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 () => {
|
||||
|
@ -105,95 +67,35 @@ describe('loadNormalizedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-first-level-not-category.js',
|
||||
);
|
||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
||||
const result = await loadSidebars(sidebarPath, params);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('sidebars link', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-link.json');
|
||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
||||
const result = await loadSidebars(sidebarPath, params);
|
||||
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 () => {
|
||||
await expect(loadNormalizedSidebars('badpath', options)).resolves.toEqual(
|
||||
await expect(loadSidebars('badpath', params)).resolves.toEqual(
|
||||
DisabledSidebars,
|
||||
);
|
||||
});
|
||||
|
||||
test('undefined path', async () => {
|
||||
await expect(loadNormalizedSidebars(undefined, options)).resolves.toEqual(
|
||||
DefaultSidebars,
|
||||
);
|
||||
await expect(loadSidebars(undefined, params)).resolves.toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('literal false path', async () => {
|
||||
await expect(loadNormalizedSidebars(false, options)).resolves.toEqual(
|
||||
await expect(loadSidebars(false, params)).resolves.toEqual(
|
||||
DisabledSidebars,
|
||||
);
|
||||
});
|
||||
|
||||
test('sidebars with category.collapsed property', async () => {
|
||||
const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json');
|
||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
||||
const result = await loadSidebars(sidebarPath, params);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -202,7 +104,7 @@ describe('loadNormalizedSidebars', () => {
|
|||
fixtureDir,
|
||||
'sidebars-collapsed-first-level.json',
|
||||
);
|
||||
const result = await loadNormalizedSidebars(sidebarPath, options);
|
||||
const result = await loadSidebars(sidebarPath, params);
|
||||
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.
|
||||
*/
|
||||
|
||||
import {processSidebars, type SidebarProcessorParams} from '../processor';
|
||||
import {processSidebars} from '../processor';
|
||||
import type {
|
||||
SidebarItem,
|
||||
SidebarItemsGenerator,
|
||||
Sidebars,
|
||||
NormalizedSidebar,
|
||||
NormalizedSidebars,
|
||||
SidebarProcessorParams,
|
||||
CategoryMetadataFile,
|
||||
ProcessedSidebars,
|
||||
} from '../types';
|
||||
import {DefaultSidebarItemsGenerator} from '../generator';
|
||||
import {createSlugger} from '@docusaurus/utils';
|
||||
|
@ -25,7 +28,7 @@ describe('processSidebars', () => {
|
|||
return jest.fn(async () => sidebarSlice);
|
||||
}
|
||||
|
||||
const StaticGeneratedSidebarSlice: SidebarItem[] = [
|
||||
const StaticGeneratedSidebarSlice: NormalizedSidebar = [
|
||||
{type: 'doc', id: 'doc-generated-id-1'},
|
||||
{type: 'doc', id: 'doc-generated-id-2'},
|
||||
];
|
||||
|
@ -53,9 +56,10 @@ describe('processSidebars', () => {
|
|||
|
||||
async function testProcessSidebars(
|
||||
unprocessedSidebars: NormalizedSidebars,
|
||||
categoriesMetadata: Record<string, CategoryMetadataFile> = {},
|
||||
paramsOverrides: Partial<SidebarProcessorParams> = {},
|
||||
) {
|
||||
return processSidebars(unprocessedSidebars, {
|
||||
return processSidebars(unprocessedSidebars, categoriesMetadata, {
|
||||
...params,
|
||||
...paramsOverrides,
|
||||
});
|
||||
|
@ -101,10 +105,7 @@ describe('processSidebars', () => {
|
|||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'category-generated-index-slug',
|
||||
permalink: 'category-generated-index-permalink',
|
||||
},
|
||||
collapsed: true, // A suspicious bad config that will be normalized
|
||||
collapsible: false,
|
||||
items: [
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{type: 'autogenerated', dirName: 'dir1'},
|
||||
|
@ -131,6 +132,7 @@ describe('processSidebars', () => {
|
|||
|
||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledTimes(3);
|
||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||
categoriesMetadata: {},
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
item: {type: 'autogenerated', dirName: 'dir1'},
|
||||
docs: params.docs,
|
||||
|
@ -143,6 +145,7 @@ describe('processSidebars', () => {
|
|||
});
|
||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
categoriesMetadata: {},
|
||||
item: {type: 'autogenerated', dirName: 'dir2'},
|
||||
docs: params.docs,
|
||||
version: {
|
||||
|
@ -154,6 +157,7 @@ describe('processSidebars', () => {
|
|||
});
|
||||
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
|
||||
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
|
||||
categoriesMetadata: {},
|
||||
item: {type: 'autogenerated', dirName: 'dir3'},
|
||||
docs: params.docs,
|
||||
version: {
|
||||
|
@ -173,10 +177,7 @@ describe('processSidebars', () => {
|
|||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'category-generated-index-slug',
|
||||
permalink: 'category-generated-index-permalink',
|
||||
},
|
||||
collapsed: false,
|
||||
collapsible: false,
|
||||
items: [{type: 'doc', id: 'doc2'}, ...StaticGeneratedSidebarSlice],
|
||||
},
|
||||
{type: 'link', href: 'https://facebook.com', label: 'FB'},
|
||||
|
@ -194,20 +195,17 @@ describe('processSidebars', () => {
|
|||
items: [{type: 'doc', id: 'doc4'}],
|
||||
},
|
||||
],
|
||||
} as Sidebars);
|
||||
} as ProcessedSidebars);
|
||||
});
|
||||
|
||||
test('ensure generated items are normalized', async () => {
|
||||
const sidebarSliceContainingCategoryGeneratedIndex: SidebarItem[] = [
|
||||
const sidebarSliceContainingCategoryGeneratedIndex: NormalizedSidebar = [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Generated category',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'generated-cat-index-slug',
|
||||
// @ts-expect-error: TODO undefined should be allowed here,
|
||||
// typing error needing refactor
|
||||
permalink: undefined,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
|
@ -218,15 +216,19 @@ describe('processSidebars', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const unprocessedSidebars: NormalizedSidebars = {
|
||||
const unprocessedSidebars = {
|
||||
someSidebar: [{type: 'autogenerated', dirName: 'dir2'}],
|
||||
};
|
||||
|
||||
const processedSidebar = await testProcessSidebars(unprocessedSidebars, {
|
||||
sidebarItemsGenerator: createStaticSidebarItemGenerator(
|
||||
sidebarSliceContainingCategoryGeneratedIndex,
|
||||
),
|
||||
});
|
||||
const processedSidebar = await testProcessSidebars(
|
||||
unprocessedSidebars,
|
||||
{},
|
||||
{
|
||||
sidebarItemsGenerator: createStaticSidebarItemGenerator(
|
||||
sidebarSliceContainingCategoryGeneratedIndex,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
expect(processedSidebar).toEqual({
|
||||
someSidebar: [
|
||||
|
@ -236,7 +238,6 @@ describe('processSidebars', () => {
|
|||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'generated-cat-index-slug',
|
||||
permalink: '/docs/1.0.0/generated-cat-index-slug',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
|
@ -244,67 +245,8 @@ describe('processSidebars', () => {
|
|||
id: 'foo',
|
||||
},
|
||||
],
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
},
|
||||
],
|
||||
} as Sidebars);
|
||||
});
|
||||
|
||||
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."`,
|
||||
);
|
||||
} as ProcessedSidebars);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,9 @@
|
|||
*/
|
||||
|
||||
import {validateSidebars, validateCategoryMetadataFile} from '../validation';
|
||||
import type {CategoryMetadataFile} from '../generator';
|
||||
import type {SidebarsConfig} from '../types';
|
||||
import type {SidebarsConfig, CategoryMetadataFile} from '../types';
|
||||
|
||||
describe('validateSidebars', () => {
|
||||
// TODO add more tests
|
||||
|
||||
// TODO it seems many error cases are not validated properly
|
||||
// and error messages are quite bad
|
||||
test('throw for bad value', async () => {
|
||||
expect(() => validateSidebars({sidebar: [{type: 42}]}))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
|
@ -45,10 +40,193 @@ describe('validateSidebars', () => {
|
|||
};
|
||||
validateSidebars(sidebars);
|
||||
});
|
||||
});
|
||||
|
||||
describe('html item type', () => {
|
||||
test('requires a value', () => {
|
||||
test('sidebar category wrong label', () => {
|
||||
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 = {
|
||||
sidebar1: [
|
||||
{
|
||||
|
@ -68,7 +246,7 @@ describe('html item type', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('accepts valid values', () => {
|
||||
test('HTML type accepts valid values', () => {
|
||||
const sidebars: SidebarsConfig = {
|
||||
sidebar1: [
|
||||
{
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
SidebarItem,
|
||||
SidebarItemDoc,
|
||||
SidebarItemCategory,
|
||||
SidebarItemsGenerator,
|
||||
SidebarItemsGeneratorDoc,
|
||||
SidebarItemCategoryLink,
|
||||
NormalizedSidebarItemCategory,
|
||||
NormalizedSidebarItem,
|
||||
SidebarItemCategoryLinkConfig,
|
||||
} from './types';
|
||||
import {sortBy, last} from 'lodash';
|
||||
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
|
||||
|
@ -48,7 +48,6 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
numberPrefixParser,
|
||||
isCategoryIndex,
|
||||
docs: allDocs,
|
||||
options,
|
||||
item: {dirName: autogenDir},
|
||||
categoriesMetadata,
|
||||
}) => {
|
||||
|
@ -125,7 +124,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
* Step 3. Recursively transform the tree-like structure to sidebar items.
|
||||
* (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> {
|
||||
const {
|
||||
sidebarPosition: position,
|
||||
|
@ -145,7 +146,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
dir: Dir,
|
||||
fullPath: string,
|
||||
folderName: string,
|
||||
): Promise<WithPosition<SidebarItemCategory>> {
|
||||
): Promise<WithPosition<NormalizedSidebarItemCategory>> {
|
||||
const categoryMetadata =
|
||||
categoriesMetadata[posixPath(path.join(autogenDir, fullPath))];
|
||||
const className = categoryMetadata?.className;
|
||||
|
@ -160,18 +161,19 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
|
||||
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
|
||||
return allItems.find(
|
||||
(item) => item.type === 'doc' && getLocalDocId(item.id) === localId,
|
||||
) as SidebarItemDoc | undefined;
|
||||
(item): item is SidebarItemDoc =>
|
||||
item.type === 'doc' && getLocalDocId(item.id) === localId,
|
||||
);
|
||||
}
|
||||
|
||||
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
|
||||
return allItems.find((item) => {
|
||||
return allItems.find((item): item is SidebarItemDoc => {
|
||||
if (item.type !== 'doc') {
|
||||
return false;
|
||||
}
|
||||
const doc = getDoc(item.id);
|
||||
return isCategoryIndex(toCategoryIndexMatcherParam(doc));
|
||||
}) as SidebarItemDoc | undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryLinkedDocId(): string | undefined {
|
||||
|
@ -190,13 +192,13 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
|
||||
const categoryLinkedDocId = getCategoryLinkedDocId();
|
||||
|
||||
const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId
|
||||
? {
|
||||
type: 'doc',
|
||||
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
|
||||
}
|
||||
: // TODO typing issue
|
||||
(categoryMetadata?.link as SidebarItemCategoryLink | undefined);
|
||||
const link: SidebarItemCategoryLinkConfig | null | undefined =
|
||||
categoryLinkedDocId
|
||||
? {
|
||||
type: 'doc',
|
||||
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
|
||||
}
|
||||
: categoryMetadata?.link;
|
||||
|
||||
// If a doc is linked, remove it from the category subItems
|
||||
const items = allItems.filter(
|
||||
|
@ -206,9 +208,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
return {
|
||||
type: 'category',
|
||||
label: categoryMetadata?.label ?? filename,
|
||||
collapsible:
|
||||
categoryMetadata?.collapsible ?? options.sidebarCollapsible,
|
||||
collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
|
||||
collapsible: categoryMetadata?.collapsible,
|
||||
collapsed: categoryMetadata?.collapsed,
|
||||
position: categoryMetadata?.position ?? numberPrefix,
|
||||
...(className !== undefined && {className}),
|
||||
items,
|
||||
|
@ -219,7 +220,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
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`.
|
||||
fullPath: string, // `dir`'s full path relative to the autogen dir.
|
||||
): Promise<WithPosition<SidebarItem>> {
|
||||
): Promise<WithPosition<NormalizedSidebarItem>> {
|
||||
return dir
|
||||
? createCategoryItem(dir, fullPath, itemKey)
|
||||
: 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
|
||||
* autogenerated items)
|
||||
*/
|
||||
function sortItems(sidebarItems: WithPosition<SidebarItem>[]): SidebarItem[] {
|
||||
function sortItems(
|
||||
sidebarItems: WithPosition<NormalizedSidebarItem>[],
|
||||
): NormalizedSidebarItem[] {
|
||||
const processedSidebarItems = sidebarItems.map((item) => {
|
||||
if (item.type === 'category') {
|
||||
return {...item, items: sortItems(item.items)};
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import fs from 'fs-extra';
|
||||
import importFresh from 'import-fresh';
|
||||
import type {SidebarsConfig, Sidebars, NormalizedSidebars} from './types';
|
||||
import type {NormalizeSidebarsParams} from '../types';
|
||||
import type {SidebarsConfig, Sidebars, SidebarProcessorParams} from './types';
|
||||
import {validateSidebars, validateCategoryMetadataFile} from './validation';
|
||||
import {normalizeSidebars} from './normalization';
|
||||
import {processSidebars, type SidebarProcessorParams} from './processor';
|
||||
import {processSidebars} from './processor';
|
||||
import {postProcessSidebars} from './postProcessor';
|
||||
import path from 'path';
|
||||
import {createSlugger, Globby} from '@docusaurus/utils';
|
||||
import {Globby} from '@docusaurus/utils';
|
||||
import logger from '@docusaurus/logger';
|
||||
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
|
||||
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,
|
||||
): Promise<SidebarsConfig> {
|
||||
// false => no sidebars
|
||||
|
@ -93,37 +93,21 @@ async function loadSidebarsFileUnsafe(
|
|||
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
|
||||
export async function loadSidebars(
|
||||
sidebarFilePath: string | false | undefined,
|
||||
options: Omit<SidebarProcessorParams, 'categoriesMetadata'>,
|
||||
options: SidebarProcessorParams,
|
||||
): Promise<Sidebars> {
|
||||
const normalizeSidebarsParams: NormalizeSidebarsParams = {
|
||||
...options.sidebarOptions,
|
||||
version: options.version,
|
||||
categoryLabelSlugger: createSlugger(),
|
||||
};
|
||||
const normalizedSidebars = await loadNormalizedSidebars(
|
||||
sidebarFilePath,
|
||||
normalizeSidebarsParams,
|
||||
);
|
||||
const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath);
|
||||
const normalizedSidebars = normalizeSidebars(sidebarsConfig);
|
||||
validateSidebars(normalizedSidebars);
|
||||
const categoriesMetadata = await readCategoriesMetadata(
|
||||
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.
|
||||
*/
|
||||
|
||||
import type {NormalizeSidebarsParams} from '../types';
|
||||
import type {
|
||||
NormalizedSidebarItem,
|
||||
NormalizedSidebar,
|
||||
|
@ -15,41 +14,16 @@ import type {
|
|||
SidebarItemConfig,
|
||||
SidebarConfig,
|
||||
SidebarsConfig,
|
||||
SidebarItemCategoryLink,
|
||||
NormalizedSidebarItemCategory,
|
||||
} from './types';
|
||||
import {isCategoriesShorthand} from './utils';
|
||||
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(
|
||||
sidebar: SidebarCategoriesShorthand,
|
||||
options: SidebarOptions,
|
||||
): SidebarItemCategoryConfig[] {
|
||||
return Object.entries(sidebar).map(([label, items]) => ({
|
||||
type: 'category',
|
||||
collapsed: options.sidebarCollapsed,
|
||||
collapsible: options.sidebarCollapsible,
|
||||
label,
|
||||
items,
|
||||
}));
|
||||
|
@ -61,7 +35,6 @@ function normalizeCategoriesShorthand(
|
|||
*/
|
||||
export function normalizeItem(
|
||||
item: SidebarItemConfig,
|
||||
options: NormalizeSidebarsParams,
|
||||
): NormalizedSidebarItem[] {
|
||||
if (typeof item === 'string') {
|
||||
return [
|
||||
|
@ -72,42 +45,35 @@ export function normalizeItem(
|
|||
];
|
||||
}
|
||||
if (isCategoriesShorthand(item)) {
|
||||
return normalizeCategoriesShorthand(item, options).flatMap((subItem) =>
|
||||
normalizeItem(subItem, options),
|
||||
return normalizeCategoriesShorthand(item).flatMap((subItem) =>
|
||||
normalizeItem(subItem),
|
||||
);
|
||||
}
|
||||
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 = {
|
||||
...item,
|
||||
link,
|
||||
items: (item.items ?? []).flatMap((subItem) =>
|
||||
normalizeItem(subItem, options),
|
||||
),
|
||||
collapsible: item.collapsible ?? options.sidebarCollapsible,
|
||||
collapsed: item.collapsed ?? options.sidebarCollapsed,
|
||||
items: item.items.flatMap((subItem) => normalizeItem(subItem)),
|
||||
};
|
||||
return [normalizedCategory];
|
||||
}
|
||||
return [item];
|
||||
}
|
||||
|
||||
function normalizeSidebar(
|
||||
sidebar: SidebarConfig,
|
||||
options: NormalizeSidebarsParams,
|
||||
): NormalizedSidebar {
|
||||
function normalizeSidebar(sidebar: SidebarConfig): NormalizedSidebar {
|
||||
const normalizedSidebar = Array.isArray(sidebar)
|
||||
? sidebar
|
||||
: normalizeCategoriesShorthand(sidebar, options);
|
||||
: normalizeCategoriesShorthand(sidebar);
|
||||
|
||||
return normalizedSidebar.flatMap((subItem) =>
|
||||
normalizeItem(subItem, options),
|
||||
);
|
||||
return normalizedSidebar.flatMap((subItem) => normalizeItem(subItem));
|
||||
}
|
||||
|
||||
export function normalizeSidebars(
|
||||
sidebars: SidebarsConfig,
|
||||
params: NormalizeSidebarsParams,
|
||||
): 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 {
|
||||
Sidebars,
|
||||
Sidebar,
|
||||
SidebarItem,
|
||||
NormalizedSidebarItem,
|
||||
NormalizedSidebar,
|
||||
NormalizedSidebars,
|
||||
SidebarItemsGeneratorOption,
|
||||
SidebarItemsGeneratorDoc,
|
||||
SidebarItemsGeneratorVersion,
|
||||
NormalizedSidebarItemCategory,
|
||||
SidebarItemCategory,
|
||||
SidebarItemAutogenerated,
|
||||
ProcessedSidebarItem,
|
||||
ProcessedSidebar,
|
||||
ProcessedSidebars,
|
||||
SidebarProcessorParams,
|
||||
CategoryMetadataFile,
|
||||
} from './types';
|
||||
import {transformSidebarItems} from './utils';
|
||||
import {DefaultSidebarItemsGenerator} from './generator';
|
||||
import {validateSidebars} from './validation';
|
||||
import {mapValues, memoize, pick} from 'lodash';
|
||||
import combinePromises from 'combine-promises';
|
||||
import {normalizeItem} from './normalization';
|
||||
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(
|
||||
doc: DocMetadataBase,
|
||||
|
@ -66,15 +48,15 @@ function toSidebarItemsGeneratorVersion(
|
|||
// post-processing checks
|
||||
async function processSidebar(
|
||||
unprocessedSidebar: NormalizedSidebar,
|
||||
categoriesMetadata: Record<string, CategoryMetadataFile>,
|
||||
params: SidebarProcessorParams,
|
||||
): Promise<Sidebar> {
|
||||
): Promise<ProcessedSidebar> {
|
||||
const {
|
||||
sidebarItemsGenerator,
|
||||
numberPrefixParser,
|
||||
docs,
|
||||
version,
|
||||
sidebarOptions,
|
||||
categoriesMetadata,
|
||||
} = params;
|
||||
|
||||
// Just a minor lazy transformation optimization
|
||||
|
@ -83,20 +65,9 @@ async function processSidebar(
|
|||
version: toSidebarItemsGeneratorVersion(version),
|
||||
}));
|
||||
|
||||
async function processCategoryItem(
|
||||
item: NormalizedSidebarItemCategory,
|
||||
): Promise<SidebarItemCategory> {
|
||||
return {
|
||||
...item,
|
||||
items: (await Promise.all(item.items.map(processItem))).flat(),
|
||||
};
|
||||
}
|
||||
|
||||
async function processAutoGeneratedItem(
|
||||
item: SidebarItemAutogenerated,
|
||||
): Promise<SidebarItem[]> {
|
||||
// TODO the returned type can't be trusted in practice (generator can be
|
||||
// user-provided)
|
||||
): Promise<ProcessedSidebarItem[]> {
|
||||
const generatedItems = await sidebarItemsGenerator({
|
||||
item,
|
||||
numberPrefixParser,
|
||||
|
@ -106,50 +77,23 @@ async function processSidebar(
|
|||
options: sidebarOptions,
|
||||
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
|
||||
// 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(
|
||||
item: NormalizedSidebarItem,
|
||||
): Promise<SidebarItem[]> {
|
||||
): Promise<ProcessedSidebarItem[]> {
|
||||
if (item.type === 'category') {
|
||||
// If the current category doesn't have subitems, we render a normal doc link instead.
|
||||
if (item.items.length === 0) {
|
||||
if (!item.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 (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)];
|
||||
return [
|
||||
{
|
||||
...item,
|
||||
items: (await Promise.all(item.items.map(processItem))).flat(),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (item.type === 'autogenerated') {
|
||||
return processAutoGeneratedItem(item);
|
||||
|
@ -159,32 +103,24 @@ async function processSidebar(
|
|||
|
||||
async function processItems(
|
||||
items: NormalizedSidebarItem[],
|
||||
): Promise<SidebarItem[]> {
|
||||
): Promise<ProcessedSidebarItem[]> {
|
||||
return (await Promise.all(items.map(processItem))).flat();
|
||||
}
|
||||
|
||||
const processedSidebar = await processItems(unprocessedSidebar);
|
||||
|
||||
const fixSidebarItemInconsistencies = (item: SidebarItem): SidebarItem => {
|
||||
// A non-collapsible category can't be collapsed!
|
||||
if (item.type === 'category' && !item.collapsible && item.collapsed) {
|
||||
return {
|
||||
...item,
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
};
|
||||
return transformSidebarItems(processedSidebar, fixSidebarItemInconsistencies);
|
||||
return processedSidebar;
|
||||
}
|
||||
|
||||
export async function processSidebars(
|
||||
unprocessedSidebars: NormalizedSidebars,
|
||||
categoriesMetadata: Record<string, CategoryMetadataFile>,
|
||||
params: SidebarProcessorParams,
|
||||
): Promise<Sidebars> {
|
||||
return combinePromises(
|
||||
): Promise<ProcessedSidebars> {
|
||||
const processedSidebars = await combinePromises(
|
||||
mapValues(unprocessedSidebars, (unprocessedSidebar) =>
|
||||
processSidebar(unprocessedSidebar, params),
|
||||
processSidebar(unprocessedSidebar, categoriesMetadata, params),
|
||||
),
|
||||
);
|
||||
validateSidebars(processedSidebars);
|
||||
return processedSidebars;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
SidebarOptions,
|
||||
CategoryIndexMatcher,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import type {Slugger} from '@docusaurus/utils';
|
||||
|
||||
// Makes all properties visible when hovering over the type
|
||||
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
|
||||
export type NormalizedSidebarItemCategory = Expand<
|
||||
SidebarItemCategoryBase & {
|
||||
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
|
||||
items: NormalizedSidebarItem[];
|
||||
link?: SidebarItemCategoryLink;
|
||||
link?: SidebarItemCategoryLinkConfig;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -125,6 +126,22 @@ export type NormalizedSidebars = {
|
|||
[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<
|
||||
SidebarItemCategoryBase & {
|
||||
items: SidebarItem[];
|
||||
|
@ -230,9 +247,7 @@ export type SidebarItemsGeneratorArgs = {
|
|||
};
|
||||
export type SidebarItemsGenerator = (
|
||||
generatorArgs: SidebarItemsGeneratorArgs,
|
||||
) => // TODO TS issue: the generator can generate un-normalized items!
|
||||
Promise<SidebarItem[]>;
|
||||
// Promise<SidebarItemConfig[]>;
|
||||
) => Promise<NormalizedSidebar>;
|
||||
|
||||
// Also inject the default generator to conveniently wrap/enhance/sort the
|
||||
// default sidebar gen logic
|
||||
|
@ -242,4 +257,13 @@ export type SidebarItemsGeneratorOptionArgs = {
|
|||
} & SidebarItemsGeneratorArgs;
|
||||
export type SidebarItemsGeneratorOption = (
|
||||
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 type {
|
||||
SidebarItemConfig,
|
||||
SidebarCategoriesShorthand,
|
||||
SidebarItemBase,
|
||||
SidebarItemAutogenerated,
|
||||
SidebarItemDoc,
|
||||
|
@ -16,12 +15,13 @@ import type {
|
|||
SidebarItemLink,
|
||||
SidebarItemCategoryConfig,
|
||||
SidebarItemCategoryLink,
|
||||
SidebarsConfig,
|
||||
SidebarItemCategoryLinkDoc,
|
||||
SidebarItemCategoryLinkGeneratedIndex,
|
||||
NormalizedSidebars,
|
||||
NormalizedSidebarItem,
|
||||
NormalizedSidebarItemCategory,
|
||||
CategoryMetadataFile,
|
||||
} from './types';
|
||||
import {isCategoriesShorthand} from './utils';
|
||||
|
||||
// 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
|
||||
|
@ -52,7 +52,7 @@ const sidebarItemDocSchema = sidebarItemBaseSchema.append<SidebarItemDoc>({
|
|||
const sidebarItemHtmlSchema = sidebarItemBaseSchema.append<SidebarItemHtml>({
|
||||
type: 'html',
|
||||
value: Joi.string().required(),
|
||||
defaultStyle: Joi.boolean().default(false),
|
||||
defaultStyle: Joi.boolean(),
|
||||
});
|
||||
|
||||
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({
|
||||
'any.unknown': 'Unknown sidebar category link type "{.type}".',
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
.id('sidebarCategoryLinkSchema');
|
||||
});
|
||||
|
||||
const sidebarItemCategorySchema =
|
||||
sidebarItemBaseSchema.append<SidebarItemCategoryConfig>({
|
||||
|
@ -103,10 +102,11 @@ const sidebarItemCategorySchema =
|
|||
label: Joi.string()
|
||||
.required()
|
||||
.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()
|
||||
.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,
|
||||
collapsed: Joi.boolean().messages({
|
||||
'any.unknown': '"collapsed" must be a boolean',
|
||||
|
@ -116,56 +116,44 @@ const sidebarItemCategorySchema =
|
|||
}),
|
||||
});
|
||||
|
||||
const sidebarItemSchema: Joi.Schema<SidebarItemConfig> = Joi.object()
|
||||
.when('.type', {
|
||||
switch: [
|
||||
{is: 'link', then: sidebarItemLinkSchema},
|
||||
{
|
||||
is: Joi.string().valid('doc', 'ref').required(),
|
||||
then: sidebarItemDocSchema,
|
||||
},
|
||||
{is: 'html', then: sidebarItemHtmlSchema},
|
||||
{is: 'autogenerated', then: sidebarItemAutogeneratedSchema},
|
||||
{is: 'category', then: sidebarItemCategorySchema},
|
||||
{
|
||||
is: Joi.any().required(),
|
||||
then: Joi.forbidden().messages({
|
||||
'any.unknown': 'Unknown sidebar item type "{.type}".',
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
.id('sidebarItemSchema');
|
||||
const sidebarItemSchema = Joi.object<SidebarItemConfig>().when('.type', {
|
||||
switch: [
|
||||
{is: 'link', then: sidebarItemLinkSchema},
|
||||
{
|
||||
is: Joi.string().valid('doc', 'ref').required(),
|
||||
then: sidebarItemDocSchema,
|
||||
},
|
||||
{is: 'html', then: sidebarItemHtmlSchema},
|
||||
{is: 'autogenerated', then: sidebarItemAutogeneratedSchema},
|
||||
{is: 'category', then: sidebarItemCategorySchema},
|
||||
{
|
||||
is: Joi.any().required(),
|
||||
then: Joi.forbidden().messages({
|
||||
'any.unknown': 'Unknown sidebar item type "{.type}".',
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
// .id('sidebarItemSchema');
|
||||
|
||||
function validateSidebarItem(item: unknown): asserts item is SidebarItemConfig {
|
||||
if (typeof item === 'string') {
|
||||
return;
|
||||
}
|
||||
function validateSidebarItem(
|
||||
item: unknown,
|
||||
): asserts item is NormalizedSidebarItem {
|
||||
// TODO: remove once with proper Joi support
|
||||
// Because we can't use Joi to validate nested items (see above), we do it
|
||||
// manually
|
||||
if (isCategoriesShorthand(item as SidebarItemConfig)) {
|
||||
Object.values(item as SidebarCategoriesShorthand).forEach((category) =>
|
||||
category.forEach(validateSidebarItem),
|
||||
);
|
||||
} else {
|
||||
Joi.assert(item, sidebarItemSchema);
|
||||
Joi.assert(item, sidebarItemSchema);
|
||||
|
||||
if ((item as SidebarItemCategoryConfig).type === 'category') {
|
||||
(item as SidebarItemCategoryConfig).items.forEach(validateSidebarItem);
|
||||
}
|
||||
if ((item as NormalizedSidebarItemCategory).type === 'category') {
|
||||
(item as NormalizedSidebarItemCategory).items.forEach(validateSidebarItem);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSidebars(
|
||||
sidebars: unknown,
|
||||
): asserts sidebars is SidebarsConfig {
|
||||
Object.values(sidebars as SidebarsConfig).forEach((sidebar) => {
|
||||
if (Array.isArray(sidebar)) {
|
||||
sidebar.forEach(validateSidebarItem);
|
||||
} else {
|
||||
validateSidebarItem(sidebar);
|
||||
}
|
||||
sidebars: Record<string, unknown>,
|
||||
): asserts sidebars is NormalizedSidebars {
|
||||
Object.values(sidebars as NormalizedSidebars).forEach((sidebar) => {
|
||||
sidebar.forEach(validateSidebarItem);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,15 +8,12 @@
|
|||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
|
||||
import type {Sidebars} from './sidebars/types';
|
||||
import type {Tag, FrontMatterTag, Slugger} from '@docusaurus/utils';
|
||||
import type {Tag, FrontMatterTag} from '@docusaurus/utils';
|
||||
import type {
|
||||
BrokenMarkdownLink as IBrokenMarkdownLink,
|
||||
ContentPaths,
|
||||
} from '@docusaurus/utils/lib/markdownLinks';
|
||||
import type {
|
||||
VersionBanner,
|
||||
SidebarOptions,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import type {VersionBanner} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export type DocFile = {
|
||||
contentPath: string; // /!\ may be localized
|
||||
|
@ -41,11 +38,6 @@ export type VersionMetadata = ContentPaths & {
|
|||
routePriority: number | undefined; // -1 for the latest docs
|
||||
};
|
||||
|
||||
export type NormalizeSidebarsParams = SidebarOptions & {
|
||||
version: VersionMetadata;
|
||||
categoryLabelSlugger: Slugger;
|
||||
};
|
||||
|
||||
export type LastUpdateData = {
|
||||
lastUpdatedAt?: number;
|
||||
formattedLastUpdatedAt?: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue