@docusaurus/plugin-content-docs 2.0.0-beta.12faed89d → 2.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/.tsbuildinfo +1 -1
- package/lib/categoryGeneratedIndex.d.ts +12 -0
- package/lib/categoryGeneratedIndex.js +37 -0
- package/lib/cli.d.ts +2 -2
- package/lib/cli.js +12 -34
- package/lib/client/docsClientUtils.d.ts +0 -3
- package/lib/client/docsClientUtils.js +19 -22
- package/lib/docFrontMatter.d.ts +1 -1
- package/lib/docFrontMatter.js +7 -3
- package/lib/docs.d.ts +25 -3
- package/lib/docs.js +125 -41
- package/lib/globalData.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +100 -131
- package/lib/lastUpdate.js +8 -9
- package/lib/markdown/index.d.ts +3 -6
- package/lib/markdown/index.js +3 -3
- package/lib/markdown/linkify.js +2 -2
- package/lib/numberPrefix.d.ts +1 -1
- package/lib/options.d.ts +3 -3
- package/lib/options.js +48 -11
- package/lib/props.d.ts +7 -2
- package/lib/props.js +60 -8
- package/lib/routes.d.ts +27 -0
- package/lib/routes.js +105 -0
- package/lib/{sidebarItemsGenerator.d.ts → sidebars/generator.d.ts} +5 -2
- package/lib/sidebars/generator.js +216 -0
- package/lib/sidebars/index.d.ts +15 -0
- package/lib/sidebars/index.js +73 -0
- package/lib/sidebars/normalization.d.ts +14 -0
- package/lib/sidebars/normalization.js +77 -0
- package/lib/sidebars/processor.d.ts +18 -0
- package/lib/sidebars/processor.js +85 -0
- package/lib/sidebars/types.d.ts +127 -0
- package/lib/sidebars/types.js +8 -0
- package/lib/sidebars/utils.d.ts +35 -0
- package/lib/sidebars/utils.js +228 -0
- package/lib/sidebars/validation.d.ts +10 -0
- package/lib/sidebars/validation.js +138 -0
- package/lib/slug.d.ts +4 -3
- package/lib/slug.js +27 -15
- package/lib/tags.d.ts +8 -0
- package/lib/tags.js +20 -0
- package/lib/theme/hooks/useDocs.js +21 -21
- package/lib/translations.d.ts +2 -2
- package/lib/translations.js +71 -29
- package/lib/types.d.ts +52 -63
- package/lib/versions.d.ts +3 -3
- package/lib/versions.js +41 -22
- package/package.json +20 -20
- package/src/__tests__/__fixtures__/simple-site/docs/_partials/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/simple-site/docs/_partials/subfolder/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/simple-site/docs/_somePartial.md +3 -0
- package/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md +5 -0
- package/src/__tests__/__fixtures__/simple-site/docs/hello.md +2 -0
- package/src/__tests__/__fixtures__/simple-site/docs/rootAbsoluteSlug.md +2 -0
- package/src/__tests__/__fixtures__/simple-site/docs/rootRelativeSlug.md +2 -0
- package/src/__tests__/__fixtures__/simple-site/docs/rootResolvedSlug.md +2 -0
- package/src/__tests__/__fixtures__/simple-site/docs/rootTryToEscapeSlug.md +2 -0
- package/src/__tests__/__fixtures__/simple-site/sidebars.json +15 -1
- package/src/__tests__/__fixtures__/site-with-doc-label/docs/hello-1.md +1 -0
- package/src/__tests__/__fixtures__/versioned-site/docs/foo/bar.md +6 -0
- package/src/__tests__/__fixtures__/versioned-site/docs/hello.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.0/hello.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/_partials/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/_partials/subfolder/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/_somePartial.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/versioned_docs/version-1.0.1/hello.md +3 -0
- package/src/__tests__/__fixtures__/versioned-site/versioned_sidebars/version-1.0.1-sidebars.json +2 -2
- package/src/__tests__/__snapshots__/cli.test.ts.snap +48 -73
- package/src/__tests__/__snapshots__/docs.test.ts.snap +140 -0
- package/src/__tests__/__snapshots__/index.test.ts.snap +745 -97
- package/src/__tests__/__snapshots__/translations.test.ts.snap +45 -15
- package/src/__tests__/cli.test.ts +15 -11
- package/src/__tests__/docFrontMatter.test.ts +160 -45
- package/src/__tests__/docs.test.ts +311 -150
- package/src/__tests__/index.test.ts +108 -66
- package/src/__tests__/lastUpdate.test.ts +1 -1
- package/src/__tests__/options.test.ts +48 -3
- package/src/__tests__/props.test.ts +62 -0
- package/src/__tests__/slug.test.ts +127 -20
- package/src/__tests__/translations.test.ts +7 -1
- package/src/__tests__/versions.test.ts +73 -70
- package/src/categoryGeneratedIndex.ts +57 -0
- package/src/cli.ts +8 -41
- package/src/client/docsClientUtils.ts +14 -26
- package/{types.d.ts → src/deps.d.ts} +0 -0
- package/src/docFrontMatter.ts +9 -4
- package/src/docs.ts +158 -32
- package/src/globalData.ts +6 -1
- package/src/index.ts +125 -169
- package/src/lastUpdate.ts +10 -13
- package/src/markdown/index.ts +8 -12
- package/src/numberPrefix.ts +5 -3
- package/src/options.ts +59 -14
- package/src/plugin-content-docs.d.ts +173 -40
- package/src/props.ts +90 -15
- package/src/routes.ts +173 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-category-shorthand.js +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-category-wrong-items.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-category-wrong-label.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-category.js +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-collapsed-first-level.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-collapsed.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-doc-id-not-string.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-first-level-not-category.js +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-link-wrong-href.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-link-wrong-label.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-link.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-unknown-type.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars-wrong-field.json +0 -0
- package/src/{__tests__ → sidebars/__tests__}/__fixtures__/sidebars/sidebars.json +0 -0
- package/src/{__tests__/__snapshots__/sidebars.test.ts.snap → sidebars/__tests__/__snapshots__/index.test.ts.snap} +36 -6
- package/src/{__tests__/sidebarItemsGenerator.test.ts → sidebars/__tests__/generator.test.ts} +143 -18
- package/src/sidebars/__tests__/index.test.ts +204 -0
- package/src/sidebars/__tests__/processor.test.ts +237 -0
- package/src/sidebars/__tests__/utils.test.ts +695 -0
- package/src/sidebars/__tests__/validation.test.ts +105 -0
- package/src/sidebars/generator.ts +310 -0
- package/src/sidebars/index.ts +94 -0
- package/src/sidebars/normalization.ts +112 -0
- package/src/sidebars/processor.ts +154 -0
- package/src/sidebars/types.ts +211 -0
- package/src/sidebars/utils.ts +329 -0
- package/src/sidebars/validation.ts +168 -0
- package/src/slug.ts +32 -17
- package/src/tags.ts +19 -0
- package/src/translations.ts +103 -47
- package/src/types.ts +64 -107
- package/src/versions.ts +59 -25
- package/lib/sidebarItemsGenerator.js +0 -211
- package/lib/sidebars.d.ts +0 -43
- package/lib/sidebars.js +0 -320
- package/src/__tests__/sidebars.test.ts +0 -639
- package/src/sidebarItemsGenerator.ts +0 -307
- package/src/sidebars.ts +0 -522
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {validateSidebars, validateCategoryMetadataFile} from '../validation';
|
|
9
|
+
import {CategoryMetadataFile} from '../generator';
|
|
10
|
+
import {SidebarsConfig} from '../types';
|
|
11
|
+
|
|
12
|
+
describe('validateSidebars', () => {
|
|
13
|
+
// TODO add more tests
|
|
14
|
+
|
|
15
|
+
// TODO it seems many error cases are not validated properly
|
|
16
|
+
// and error messages are quite bad
|
|
17
|
+
test('throw for bad value', async () => {
|
|
18
|
+
expect(() => validateSidebars({sidebar: [{type: 42}]}))
|
|
19
|
+
.toThrowErrorMatchingInlineSnapshot(`
|
|
20
|
+
"{
|
|
21
|
+
\\"type\\": 42,
|
|
22
|
+
[41m\\"undefined\\"[0m[31m [1]: -- missing --[0m
|
|
23
|
+
}
|
|
24
|
+
[31m
|
|
25
|
+
[1] Unknown sidebar item type \\"42\\".[0m"
|
|
26
|
+
`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('accept empty object', async () => {
|
|
30
|
+
const sidebars: SidebarsConfig = {};
|
|
31
|
+
validateSidebars(sidebars);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('accept valid values', async () => {
|
|
35
|
+
const sidebars: SidebarsConfig = {
|
|
36
|
+
sidebar1: [
|
|
37
|
+
{type: 'doc', id: 'doc1'},
|
|
38
|
+
{type: 'doc', id: 'doc2'},
|
|
39
|
+
{
|
|
40
|
+
type: 'category',
|
|
41
|
+
label: 'Category',
|
|
42
|
+
items: [{type: 'doc', id: 'doc3'}],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
validateSidebars(sidebars);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('validateCategoryMetadataFile', () => {
|
|
51
|
+
// TODO add more tests
|
|
52
|
+
|
|
53
|
+
test('throw for bad value', async () => {
|
|
54
|
+
expect(() =>
|
|
55
|
+
validateCategoryMetadataFile(42),
|
|
56
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
57
|
+
`"\\"value\\" must be of type object"`,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('accept empty object', async () => {
|
|
62
|
+
const content: CategoryMetadataFile = {};
|
|
63
|
+
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('accept valid values', async () => {
|
|
67
|
+
const content: CategoryMetadataFile = {
|
|
68
|
+
className: 'className',
|
|
69
|
+
label: 'Category Label',
|
|
70
|
+
link: {
|
|
71
|
+
type: 'generated-index',
|
|
72
|
+
slug: 'slug',
|
|
73
|
+
title: 'title',
|
|
74
|
+
description: 'description',
|
|
75
|
+
},
|
|
76
|
+
collapsible: true,
|
|
77
|
+
collapsed: true,
|
|
78
|
+
position: 3,
|
|
79
|
+
};
|
|
80
|
+
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('rejects permalink', async () => {
|
|
84
|
+
const content: CategoryMetadataFile = {
|
|
85
|
+
className: 'className',
|
|
86
|
+
label: 'Category Label',
|
|
87
|
+
link: {
|
|
88
|
+
type: 'generated-index',
|
|
89
|
+
slug: 'slug',
|
|
90
|
+
// @ts-expect-error: rejected on purpose
|
|
91
|
+
permalink: 'somePermalink',
|
|
92
|
+
title: 'title',
|
|
93
|
+
description: 'description',
|
|
94
|
+
},
|
|
95
|
+
collapsible: true,
|
|
96
|
+
collapsed: true,
|
|
97
|
+
position: 3,
|
|
98
|
+
};
|
|
99
|
+
expect(() =>
|
|
100
|
+
validateCategoryMetadataFile(content),
|
|
101
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
102
|
+
`"\\"link.permalink\\" is not allowed"`,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
SidebarItem,
|
|
10
|
+
SidebarItemDoc,
|
|
11
|
+
SidebarItemCategory,
|
|
12
|
+
SidebarItemsGenerator,
|
|
13
|
+
SidebarItemsGeneratorDoc,
|
|
14
|
+
SidebarItemCategoryLink,
|
|
15
|
+
SidebarItemCategoryLinkConfig,
|
|
16
|
+
} from './types';
|
|
17
|
+
import {sortBy, last} from 'lodash';
|
|
18
|
+
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import fs from 'fs-extra';
|
|
22
|
+
import Yaml from 'js-yaml';
|
|
23
|
+
import {validateCategoryMetadataFile} from './validation';
|
|
24
|
+
import {createDocsByIdIndex, isConventionalDocIndex} from '../docs';
|
|
25
|
+
|
|
26
|
+
const BreadcrumbSeparator = '/';
|
|
27
|
+
// To avoid possible name clashes with a folder of the same name as the ID
|
|
28
|
+
const docIdPrefix = '$doc$/';
|
|
29
|
+
|
|
30
|
+
// Just an alias to the make code more explicit
|
|
31
|
+
function getLocalDocId(docId: string): string {
|
|
32
|
+
return last(docId.split('/'))!;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CategoryMetadataFilenameBase = '_category_';
|
|
36
|
+
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
|
|
37
|
+
|
|
38
|
+
export type CategoryMetadataFile = {
|
|
39
|
+
label?: string;
|
|
40
|
+
position?: number;
|
|
41
|
+
collapsed?: boolean;
|
|
42
|
+
collapsible?: boolean;
|
|
43
|
+
className?: string;
|
|
44
|
+
link?: SidebarItemCategoryLinkConfig;
|
|
45
|
+
|
|
46
|
+
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
|
|
47
|
+
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
|
|
48
|
+
// cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type WithPosition<T> = T & {position?: number};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A representation of the fs structure. For each object entry:
|
|
55
|
+
* If it's a folder, the key is the directory name, and value is the directory content;
|
|
56
|
+
* If it's a doc file, the key is the doc id prefixed with '$doc$/', and value is null
|
|
57
|
+
*/
|
|
58
|
+
type Dir = {
|
|
59
|
+
[item: string]: Dir | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata
|
|
63
|
+
// Example use-case being able to disable number prefix parsing at the folder level, or customize the default route path segment for an intermediate directory...
|
|
64
|
+
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it
|
|
65
|
+
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
|
|
66
|
+
async function readCategoryMetadataFile(
|
|
67
|
+
categoryDirPath: string,
|
|
68
|
+
): Promise<CategoryMetadataFile | null> {
|
|
69
|
+
async function tryReadFile(filePath: string): Promise<CategoryMetadataFile> {
|
|
70
|
+
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
|
|
71
|
+
const unsafeContent = Yaml.load(contentString);
|
|
72
|
+
try {
|
|
73
|
+
return validateCategoryMetadataFile(unsafeContent);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(
|
|
76
|
+
chalk.red(
|
|
77
|
+
`The docs sidebar category metadata file looks invalid!\nPath: ${filePath}`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
84
|
+
for (const ext of ['.json', '.yml', '.yaml']) {
|
|
85
|
+
// Simpler to use only posix paths for mocking file metadata in tests
|
|
86
|
+
const filePath = posixPath(
|
|
87
|
+
path.join(categoryDirPath, `${CategoryMetadataFilenameBase}${ext}`),
|
|
88
|
+
);
|
|
89
|
+
if (await fs.pathExists(filePath)) {
|
|
90
|
+
return tryReadFile(filePath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
|
|
97
|
+
export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|
98
|
+
numberPrefixParser,
|
|
99
|
+
docs: allDocs,
|
|
100
|
+
options,
|
|
101
|
+
item: {dirName: autogenDir},
|
|
102
|
+
version,
|
|
103
|
+
}) => {
|
|
104
|
+
const docsById = createDocsByIdIndex(allDocs);
|
|
105
|
+
const findDoc = (docId: string): SidebarItemsGeneratorDoc | undefined =>
|
|
106
|
+
docsById[docId];
|
|
107
|
+
const getDoc = (docId: string): SidebarItemsGeneratorDoc => {
|
|
108
|
+
const doc = findDoc(docId);
|
|
109
|
+
if (!doc) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Can't find any doc with id=${docId}.\nAvailable doc ids:\n- ${Object.keys(
|
|
112
|
+
docsById,
|
|
113
|
+
).join('\n- ')}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return doc;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Step 1. Extract the docs that are in the autogen dir.
|
|
121
|
+
*/
|
|
122
|
+
function getAutogenDocs(): SidebarItemsGeneratorDoc[] {
|
|
123
|
+
function isInAutogeneratedDir(doc: SidebarItemsGeneratorDoc) {
|
|
124
|
+
return (
|
|
125
|
+
// Doc at the root of the autogenerated sidebar dir
|
|
126
|
+
doc.sourceDirName === autogenDir ||
|
|
127
|
+
// autogen dir is . and doc is in subfolder
|
|
128
|
+
autogenDir === '.' ||
|
|
129
|
+
// autogen dir is not . and doc is in subfolder
|
|
130
|
+
// "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included)
|
|
131
|
+
doc.sourceDirName.startsWith(addTrailingSlash(autogenDir))
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const docs = allDocs.filter(isInAutogeneratedDir);
|
|
135
|
+
|
|
136
|
+
if (docs.length === 0) {
|
|
137
|
+
console.warn(
|
|
138
|
+
chalk.yellow(
|
|
139
|
+
`No docs found in dir ${autogenDir}: can't auto-generate a sidebar.`,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return docs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Step 2. Turn the linear file list into a tree structure.
|
|
148
|
+
*/
|
|
149
|
+
function treeify(docs: SidebarItemsGeneratorDoc[]): Dir {
|
|
150
|
+
// Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item)
|
|
151
|
+
// autogenDir=a/b and docDir=a/b/c/d => returns [c, d]
|
|
152
|
+
// autogenDir=a/b and docDir=a/b => returns []
|
|
153
|
+
// TODO: try to use path.relative()
|
|
154
|
+
function getRelativeBreadcrumb(doc: SidebarItemsGeneratorDoc): string[] {
|
|
155
|
+
return autogenDir === doc.sourceDirName
|
|
156
|
+
? []
|
|
157
|
+
: doc.sourceDirName
|
|
158
|
+
.replace(addTrailingSlash(autogenDir), '')
|
|
159
|
+
.split(BreadcrumbSeparator);
|
|
160
|
+
}
|
|
161
|
+
const treeRoot: Dir = {};
|
|
162
|
+
docs.forEach((doc) => {
|
|
163
|
+
const breadcrumb = getRelativeBreadcrumb(doc);
|
|
164
|
+
let currentDir = treeRoot; // We walk down the file's path to generate the fs structure
|
|
165
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
166
|
+
for (const dir of breadcrumb) {
|
|
167
|
+
if (typeof currentDir[dir] === 'undefined') {
|
|
168
|
+
currentDir[dir] = {}; // Create new folder.
|
|
169
|
+
}
|
|
170
|
+
currentDir = currentDir[dir]!; // Go into the subdirectory.
|
|
171
|
+
}
|
|
172
|
+
currentDir[`${docIdPrefix}${doc.id}`] = null; // We've walked through the file path. Register the file in this directory.
|
|
173
|
+
});
|
|
174
|
+
return treeRoot;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Step 3. Recursively transform the tree-like file structure to sidebar items.
|
|
179
|
+
* (From a record to an array of items, akin to normalizing shorthand)
|
|
180
|
+
*/
|
|
181
|
+
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
|
|
182
|
+
function createDocItem(id: string): WithPosition<SidebarItemDoc> {
|
|
183
|
+
const {
|
|
184
|
+
sidebarPosition: position,
|
|
185
|
+
frontMatter: {sidebar_label: label, sidebar_class_name: className},
|
|
186
|
+
} = getDoc(id);
|
|
187
|
+
return {
|
|
188
|
+
type: 'doc',
|
|
189
|
+
id,
|
|
190
|
+
position,
|
|
191
|
+
// We don't want these fields to magically appear in the generated sidebar
|
|
192
|
+
...(label !== undefined && {label}),
|
|
193
|
+
...(className !== undefined && {className}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function createCategoryItem(
|
|
197
|
+
dir: Dir,
|
|
198
|
+
fullPath: string,
|
|
199
|
+
folderName: string,
|
|
200
|
+
): Promise<WithPosition<SidebarItemCategory>> {
|
|
201
|
+
const categoryPath = path.join(version.contentPath, autogenDir, fullPath);
|
|
202
|
+
const categoryMetadata = await readCategoryMetadataFile(categoryPath);
|
|
203
|
+
const className = categoryMetadata?.className;
|
|
204
|
+
const {filename, numberPrefix} = numberPrefixParser(folderName);
|
|
205
|
+
const allItems = await Promise.all(
|
|
206
|
+
Object.entries(dir).map(([key, content]) =>
|
|
207
|
+
dirToItem(content, key, `${fullPath}/${key}`),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Try to match a doc inside the category folder,
|
|
212
|
+
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
|
|
213
|
+
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
|
|
214
|
+
return allItems.find(
|
|
215
|
+
(item) => item.type === 'doc' && getLocalDocId(item.id) === localId,
|
|
216
|
+
) as SidebarItemDoc | undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
|
|
220
|
+
return allItems.find(
|
|
221
|
+
(item) =>
|
|
222
|
+
item.type === 'doc' && isConventionalDocIndex(getDoc(item.id)),
|
|
223
|
+
) as SidebarItemDoc | undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getCategoryLinkedDocId(): string | undefined {
|
|
227
|
+
const link = categoryMetadata?.link;
|
|
228
|
+
if (link) {
|
|
229
|
+
if (link.type === 'doc') {
|
|
230
|
+
return findDocByLocalId(link.id)?.id || getDoc(link.id).id;
|
|
231
|
+
} else {
|
|
232
|
+
// We don't continue for other link types on purpose!
|
|
233
|
+
// IE if user decide to use type "generated-index", we should not pick a README.md file as the linked doc
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Apply default convention to pick index.md, README.md or <categoryName>.md as the category doc
|
|
238
|
+
return findConventionalCategoryDocLink()?.id;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const categoryLinkedDocId = getCategoryLinkedDocId();
|
|
242
|
+
|
|
243
|
+
const link: SidebarItemCategoryLink | undefined = categoryLinkedDocId
|
|
244
|
+
? {
|
|
245
|
+
type: 'doc',
|
|
246
|
+
id: categoryLinkedDocId, // We "remap" a potentially "local id" to a "qualified id"
|
|
247
|
+
}
|
|
248
|
+
: // TODO typing issue
|
|
249
|
+
(categoryMetadata?.link as SidebarItemCategoryLink | undefined);
|
|
250
|
+
|
|
251
|
+
// If a doc is linked, remove it from the category subItems
|
|
252
|
+
const items = allItems.filter(
|
|
253
|
+
(item) => !(item.type === 'doc' && item.id === categoryLinkedDocId),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
type: 'category',
|
|
258
|
+
label: categoryMetadata?.label ?? filename,
|
|
259
|
+
collapsible:
|
|
260
|
+
categoryMetadata?.collapsible ?? options.sidebarCollapsible,
|
|
261
|
+
collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
|
|
262
|
+
position: categoryMetadata?.position ?? numberPrefix,
|
|
263
|
+
...(className !== undefined && {className}),
|
|
264
|
+
items,
|
|
265
|
+
...(link && {link}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function dirToItem(
|
|
269
|
+
dir: Dir | null, // The directory item to be transformed.
|
|
270
|
+
itemKey: string, // For docs, it's the doc ID; for categories, it's used to generate the next `relativePath`.
|
|
271
|
+
fullPath: string, // `dir`'s full path relative to the autogen dir.
|
|
272
|
+
): Promise<WithPosition<SidebarItem>> {
|
|
273
|
+
return dir
|
|
274
|
+
? createCategoryItem(dir, fullPath, itemKey)
|
|
275
|
+
: createDocItem(itemKey.substring(docIdPrefix.length));
|
|
276
|
+
}
|
|
277
|
+
return Promise.all(
|
|
278
|
+
Object.entries(fsModel).map(([key, content]) =>
|
|
279
|
+
dirToItem(content, key, key),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Step 4. Recursively sort the categories/docs + remove the "position" attribute from final output.
|
|
286
|
+
* Note: the "position" is only used to sort "inside" a sidebar slice. It is not
|
|
287
|
+
* used to sort across multiple consecutive sidebar slices (ie a whole Category
|
|
288
|
+
* composed of multiple autogenerated items)
|
|
289
|
+
*/
|
|
290
|
+
function sortItems(sidebarItems: WithPosition<SidebarItem>[]): SidebarItem[] {
|
|
291
|
+
const processedSidebarItems = sidebarItems.map((item) => {
|
|
292
|
+
if (item.type === 'category') {
|
|
293
|
+
return {...item, items: sortItems(item.items)};
|
|
294
|
+
}
|
|
295
|
+
return item;
|
|
296
|
+
});
|
|
297
|
+
const sortedSidebarItems = sortBy(
|
|
298
|
+
processedSidebarItems,
|
|
299
|
+
(item) => item.position,
|
|
300
|
+
);
|
|
301
|
+
return sortedSidebarItems.map(({position, ...item}) => item);
|
|
302
|
+
}
|
|
303
|
+
// TODO: the whole code is designed for pipeline operator
|
|
304
|
+
// return getAutogenDocs() |> treeify |> await generateSidebar(^) |> sortItems;
|
|
305
|
+
const docs = getAutogenDocs();
|
|
306
|
+
const fsModel = treeify(docs);
|
|
307
|
+
const sidebarWithPosition = await generateSidebar(fsModel);
|
|
308
|
+
const sortedSidebar = sortItems(sidebarWithPosition);
|
|
309
|
+
return sortedSidebar;
|
|
310
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import importFresh from 'import-fresh';
|
|
10
|
+
import type {SidebarsConfig, Sidebars, NormalizedSidebars} from './types';
|
|
11
|
+
import type {NormalizeSidebarsParams, PluginOptions} from '../types';
|
|
12
|
+
import {validateSidebars} from './validation';
|
|
13
|
+
import {normalizeSidebars} from './normalization';
|
|
14
|
+
import {processSidebars, SidebarProcessorParams} from './processor';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import {createSlugger} from '@docusaurus/utils';
|
|
17
|
+
|
|
18
|
+
export const DefaultSidebars: SidebarsConfig = {
|
|
19
|
+
defaultSidebar: [
|
|
20
|
+
{
|
|
21
|
+
type: 'autogenerated',
|
|
22
|
+
dirName: '.',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const DisabledSidebars: SidebarsConfig = {};
|
|
28
|
+
|
|
29
|
+
// If a path is provided, make it absolute
|
|
30
|
+
// use this before loadSidebars()
|
|
31
|
+
export function resolveSidebarPathOption(
|
|
32
|
+
siteDir: string,
|
|
33
|
+
sidebarPathOption: PluginOptions['sidebarPath'],
|
|
34
|
+
): PluginOptions['sidebarPath'] {
|
|
35
|
+
return sidebarPathOption
|
|
36
|
+
? path.resolve(siteDir, sidebarPathOption)
|
|
37
|
+
: sidebarPathOption;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadSidebarsFileUnsafe(
|
|
41
|
+
sidebarFilePath: string | false | undefined,
|
|
42
|
+
): SidebarsConfig {
|
|
43
|
+
// false => no sidebars
|
|
44
|
+
if (sidebarFilePath === false) {
|
|
45
|
+
return DisabledSidebars;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// undefined => defaults to autogenerated sidebars
|
|
49
|
+
if (typeof sidebarFilePath === 'undefined') {
|
|
50
|
+
return DefaultSidebars;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Non-existent sidebars file: no sidebars
|
|
54
|
+
// Note: this edge case can happen on versioned docs, not current version
|
|
55
|
+
// We avoid creating empty versioned sidebars file with the CLI
|
|
56
|
+
if (!fs.existsSync(sidebarFilePath)) {
|
|
57
|
+
return DisabledSidebars;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// We don't want sidebars to be cached because of hot reloading.
|
|
61
|
+
return importFresh(sidebarFilePath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function loadSidebarsFile(
|
|
65
|
+
sidebarFilePath: string | false | undefined,
|
|
66
|
+
): SidebarsConfig {
|
|
67
|
+
const sidebarsConfig = loadSidebarsFileUnsafe(sidebarFilePath);
|
|
68
|
+
validateSidebars(sidebarsConfig);
|
|
69
|
+
return sidebarsConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function loadNormalizedSidebars(
|
|
73
|
+
sidebarFilePath: string | false | undefined,
|
|
74
|
+
params: NormalizeSidebarsParams,
|
|
75
|
+
): NormalizedSidebars {
|
|
76
|
+
return normalizeSidebars(loadSidebarsFile(sidebarFilePath), params);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Note: sidebarFilePath must be absolute, use resolveSidebarPathOption
|
|
80
|
+
export async function loadSidebars(
|
|
81
|
+
sidebarFilePath: string | false | undefined,
|
|
82
|
+
options: SidebarProcessorParams,
|
|
83
|
+
): Promise<Sidebars> {
|
|
84
|
+
const normalizeSidebarsParams: NormalizeSidebarsParams = {
|
|
85
|
+
...options.sidebarOptions,
|
|
86
|
+
version: options.version,
|
|
87
|
+
categoryLabelSlugger: createSlugger(),
|
|
88
|
+
};
|
|
89
|
+
const normalizedSidebars = loadNormalizedSidebars(
|
|
90
|
+
sidebarFilePath,
|
|
91
|
+
normalizeSidebarsParams,
|
|
92
|
+
);
|
|
93
|
+
return processSidebars(normalizedSidebars, options);
|
|
94
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {NormalizeSidebarsParams, SidebarOptions} from '../types';
|
|
9
|
+
import type {
|
|
10
|
+
NormalizedSidebarItem,
|
|
11
|
+
NormalizedSidebar,
|
|
12
|
+
NormalizedSidebars,
|
|
13
|
+
SidebarCategoriesShorthand,
|
|
14
|
+
SidebarItemCategoryConfig,
|
|
15
|
+
SidebarItemConfig,
|
|
16
|
+
SidebarConfig,
|
|
17
|
+
SidebarsConfig,
|
|
18
|
+
SidebarItemCategoryLink,
|
|
19
|
+
NormalizedSidebarItemCategory,
|
|
20
|
+
} from './types';
|
|
21
|
+
import {isCategoriesShorthand} from './utils';
|
|
22
|
+
import {mapValues} from 'lodash';
|
|
23
|
+
import {normalizeUrl} from '@docusaurus/utils';
|
|
24
|
+
|
|
25
|
+
function normalizeCategoryLink(
|
|
26
|
+
category: SidebarItemCategoryConfig,
|
|
27
|
+
params: NormalizeSidebarsParams,
|
|
28
|
+
): SidebarItemCategoryLink | undefined {
|
|
29
|
+
if (category.link?.type === 'generated-index') {
|
|
30
|
+
// default slug logic can be improved
|
|
31
|
+
const getDefaultSlug = () =>
|
|
32
|
+
`/category/${params.categoryLabelSlugger.slug(category.label)}`;
|
|
33
|
+
const slug = category.link.slug ?? getDefaultSlug();
|
|
34
|
+
const permalink = normalizeUrl([params.version.versionPath, slug]);
|
|
35
|
+
return {
|
|
36
|
+
...category.link,
|
|
37
|
+
slug,
|
|
38
|
+
permalink,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return category.link;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeCategoriesShorthand(
|
|
45
|
+
sidebar: SidebarCategoriesShorthand,
|
|
46
|
+
options: SidebarOptions,
|
|
47
|
+
): SidebarItemCategoryConfig[] {
|
|
48
|
+
return Object.entries(sidebar).map(([label, items]) => ({
|
|
49
|
+
type: 'category',
|
|
50
|
+
collapsed: options.sidebarCollapsed,
|
|
51
|
+
collapsible: options.sidebarCollapsible,
|
|
52
|
+
label,
|
|
53
|
+
items,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalizes recursively item and all its children. Ensures that at the end
|
|
59
|
+
* each item will be an object with the corresponding type.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeItem(
|
|
62
|
+
item: SidebarItemConfig,
|
|
63
|
+
options: NormalizeSidebarsParams,
|
|
64
|
+
): NormalizedSidebarItem[] {
|
|
65
|
+
if (typeof item === 'string') {
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
type: 'doc',
|
|
69
|
+
id: item,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
if (isCategoriesShorthand(item)) {
|
|
74
|
+
return normalizeCategoriesShorthand(item, options).flatMap((subItem) =>
|
|
75
|
+
normalizeItem(subItem, options),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (item.type === 'category') {
|
|
79
|
+
const link = normalizeCategoryLink(item, options);
|
|
80
|
+
const normalizedCategory: NormalizedSidebarItemCategory = {
|
|
81
|
+
...item,
|
|
82
|
+
link,
|
|
83
|
+
items: (item.items ?? []).flatMap((subItem) =>
|
|
84
|
+
normalizeItem(subItem, options),
|
|
85
|
+
),
|
|
86
|
+
collapsible: item.collapsible ?? options.sidebarCollapsible,
|
|
87
|
+
collapsed: item.collapsed ?? options.sidebarCollapsed,
|
|
88
|
+
};
|
|
89
|
+
return [normalizedCategory];
|
|
90
|
+
}
|
|
91
|
+
return [item];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeSidebar(
|
|
95
|
+
sidebar: SidebarConfig,
|
|
96
|
+
options: NormalizeSidebarsParams,
|
|
97
|
+
): NormalizedSidebar {
|
|
98
|
+
const normalizedSidebar = Array.isArray(sidebar)
|
|
99
|
+
? sidebar
|
|
100
|
+
: normalizeCategoriesShorthand(sidebar, options);
|
|
101
|
+
|
|
102
|
+
return normalizedSidebar.flatMap((subItem) =>
|
|
103
|
+
normalizeItem(subItem, options),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function normalizeSidebars(
|
|
108
|
+
sidebars: SidebarsConfig,
|
|
109
|
+
params: NormalizeSidebarsParams,
|
|
110
|
+
): NormalizedSidebars {
|
|
111
|
+
return mapValues(sidebars, (items) => normalizeSidebar(items, params));
|
|
112
|
+
}
|