@docusaurus/plugin-content-docs 0.0.0-4240 → 0.0.0-4241
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.js +5 -23
- package/lib/docs.d.ts +22 -2
- package/lib/docs.js +71 -29
- package/lib/index.js +34 -62
- package/lib/options.js +2 -0
- package/lib/props.js +35 -6
- package/lib/routes.d.ts +27 -0
- package/lib/routes.js +105 -0
- package/lib/sidebars/generator.d.ts +2 -1
- package/lib/sidebars/generator.js +55 -13
- package/lib/sidebars/index.d.ts +5 -4
- package/lib/sidebars/index.js +18 -9
- package/lib/sidebars/normalization.d.ts +8 -3
- package/lib/sidebars/normalization.js +36 -17
- package/lib/sidebars/processor.d.ts +5 -3
- package/lib/sidebars/processor.js +33 -18
- package/lib/sidebars/types.d.ts +43 -2
- package/lib/sidebars/utils.d.ts +18 -6
- package/lib/sidebars/utils.js +149 -24
- package/lib/sidebars/validation.d.ts +2 -0
- package/lib/sidebars/validation.js +44 -8
- package/lib/slug.d.ts +4 -3
- package/lib/slug.js +26 -14
- package/lib/translations.js +51 -7
- package/lib/types.d.ts +18 -3
- package/package.json +8 -8
- 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 -106
- package/src/__tests__/__snapshots__/index.test.ts.snap +279 -28
- package/src/__tests__/__snapshots__/translations.test.ts.snap +45 -0
- package/src/__tests__/docs.test.ts +122 -7
- package/src/__tests__/index.test.ts +27 -1
- package/src/__tests__/options.test.ts +2 -0
- package/src/__tests__/slug.test.ts +127 -20
- package/src/__tests__/translations.test.ts +7 -0
- package/src/categoryGeneratedIndex.ts +57 -0
- package/src/cli.ts +5 -35
- package/src/docs.ts +103 -45
- package/src/index.ts +55 -93
- package/src/options.ts +4 -0
- package/src/plugin-content-docs.d.ts +71 -8
- package/src/props.ts +48 -9
- package/src/routes.ts +173 -0
- package/src/sidebars/__tests__/__snapshots__/index.test.ts.snap +21 -6
- package/src/sidebars/__tests__/generator.test.ts +105 -1
- package/src/sidebars/__tests__/index.test.ts +26 -24
- package/src/sidebars/__tests__/processor.test.ts +110 -19
- package/src/sidebars/__tests__/utils.test.ts +320 -20
- package/src/sidebars/__tests__/validation.test.ts +105 -0
- package/src/sidebars/generator.ts +82 -19
- package/src/sidebars/index.ts +23 -13
- package/src/sidebars/normalization.ts +47 -23
- package/src/sidebars/processor.ts +57 -27
- package/src/sidebars/types.ts +64 -3
- package/src/sidebars/utils.ts +217 -42
- package/src/sidebars/validation.ts +52 -8
- package/src/slug.ts +32 -17
- package/src/translations.ts +74 -8
- package/src/types.ts +22 -5
|
@@ -12,8 +12,12 @@ import {
|
|
|
12
12
|
collectSidebarLinks,
|
|
13
13
|
transformSidebarItems,
|
|
14
14
|
collectSidebarsDocIds,
|
|
15
|
+
SidebarNavigation,
|
|
16
|
+
toDocNavigationLink,
|
|
17
|
+
toNavigationLink,
|
|
15
18
|
} from '../utils';
|
|
16
19
|
import type {Sidebar, Sidebars} from '../types';
|
|
20
|
+
import {DocMetadataBase, DocNavLink} from '../../types';
|
|
17
21
|
|
|
18
22
|
describe('createSidebarsUtils', () => {
|
|
19
23
|
const sidebar1: Sidebar = [
|
|
@@ -21,13 +25,13 @@ describe('createSidebarsUtils', () => {
|
|
|
21
25
|
type: 'category',
|
|
22
26
|
collapsed: false,
|
|
23
27
|
collapsible: true,
|
|
24
|
-
label: '
|
|
28
|
+
label: 'S1 Category',
|
|
25
29
|
items: [
|
|
26
30
|
{
|
|
27
31
|
type: 'category',
|
|
28
32
|
collapsed: false,
|
|
29
33
|
collapsible: true,
|
|
30
|
-
label: 'Subcategory
|
|
34
|
+
label: 'S1 Subcategory',
|
|
31
35
|
items: [{type: 'doc', id: 'doc1'}],
|
|
32
36
|
},
|
|
33
37
|
{type: 'doc', id: 'doc2'},
|
|
@@ -40,7 +44,7 @@ describe('createSidebarsUtils', () => {
|
|
|
40
44
|
type: 'category',
|
|
41
45
|
collapsed: false,
|
|
42
46
|
collapsible: true,
|
|
43
|
-
label: '
|
|
47
|
+
label: 'S2 Category',
|
|
44
48
|
items: [
|
|
45
49
|
{type: 'doc', id: 'doc3'},
|
|
46
50
|
{type: 'doc', id: 'doc4'},
|
|
@@ -48,10 +52,58 @@ describe('createSidebarsUtils', () => {
|
|
|
48
52
|
},
|
|
49
53
|
];
|
|
50
54
|
|
|
51
|
-
const
|
|
55
|
+
const sidebar3: Sidebar = [
|
|
56
|
+
{
|
|
57
|
+
type: 'category',
|
|
58
|
+
collapsed: false,
|
|
59
|
+
collapsible: true,
|
|
60
|
+
label: 'S3 Category',
|
|
61
|
+
link: {
|
|
62
|
+
type: 'doc',
|
|
63
|
+
id: 'doc5',
|
|
64
|
+
},
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
type: 'category',
|
|
68
|
+
collapsed: false,
|
|
69
|
+
collapsible: true,
|
|
70
|
+
label: 'S3 SubCategory',
|
|
71
|
+
link: {
|
|
72
|
+
type: 'generated-index',
|
|
73
|
+
slug: '/s3-subcategory-index-slug',
|
|
74
|
+
permalink: '/s3-subcategory-index-permalink',
|
|
75
|
+
},
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
type: 'category',
|
|
79
|
+
collapsed: false,
|
|
80
|
+
collapsible: true,
|
|
81
|
+
label: 'S3 SubSubCategory',
|
|
82
|
+
link: {
|
|
83
|
+
type: 'generated-index',
|
|
84
|
+
slug: '/s3-subsubcategory-slug',
|
|
85
|
+
permalink: '/s3-subsubcategory-index-permalink',
|
|
86
|
+
},
|
|
87
|
+
items: [
|
|
88
|
+
{type: 'doc', id: 'doc6'},
|
|
89
|
+
{type: 'doc', id: 'doc7'},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const sidebars: Sidebars = {sidebar1, sidebar2, sidebar3};
|
|
52
99
|
|
|
53
|
-
const {
|
|
54
|
-
|
|
100
|
+
const {
|
|
101
|
+
getFirstDocIdOfFirstSidebar,
|
|
102
|
+
getSidebarNameByDocId,
|
|
103
|
+
getDocNavigation,
|
|
104
|
+
getCategoryGeneratedIndexNavigation,
|
|
105
|
+
getCategoryGeneratedIndexList,
|
|
106
|
+
} = createSidebarsUtils(sidebars);
|
|
55
107
|
|
|
56
108
|
test('getSidebarNameByDocId', async () => {
|
|
57
109
|
expect(getFirstDocIdOfFirstSidebar()).toEqual('doc1');
|
|
@@ -62,32 +114,117 @@ describe('createSidebarsUtils', () => {
|
|
|
62
114
|
expect(getSidebarNameByDocId('doc2')).toEqual('sidebar1');
|
|
63
115
|
expect(getSidebarNameByDocId('doc3')).toEqual('sidebar2');
|
|
64
116
|
expect(getSidebarNameByDocId('doc4')).toEqual('sidebar2');
|
|
65
|
-
expect(getSidebarNameByDocId('doc5')).toEqual(
|
|
66
|
-
expect(getSidebarNameByDocId('doc6')).toEqual(
|
|
117
|
+
expect(getSidebarNameByDocId('doc5')).toEqual('sidebar3');
|
|
118
|
+
expect(getSidebarNameByDocId('doc6')).toEqual('sidebar3');
|
|
119
|
+
expect(getSidebarNameByDocId('doc7')).toEqual('sidebar3');
|
|
120
|
+
expect(getSidebarNameByDocId('unknown_id')).toEqual(undefined);
|
|
67
121
|
});
|
|
68
122
|
|
|
69
123
|
test('getDocNavigation', async () => {
|
|
70
124
|
expect(getDocNavigation('doc1')).toEqual({
|
|
71
125
|
sidebarName: 'sidebar1',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
126
|
+
previous: undefined,
|
|
127
|
+
next: {
|
|
128
|
+
type: 'doc',
|
|
129
|
+
id: 'doc2',
|
|
130
|
+
},
|
|
131
|
+
} as SidebarNavigation);
|
|
75
132
|
expect(getDocNavigation('doc2')).toEqual({
|
|
76
133
|
sidebarName: 'sidebar1',
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
previous: {
|
|
135
|
+
type: 'doc',
|
|
136
|
+
id: 'doc1',
|
|
137
|
+
},
|
|
138
|
+
next: undefined,
|
|
139
|
+
} as SidebarNavigation);
|
|
80
140
|
|
|
81
141
|
expect(getDocNavigation('doc3')).toEqual({
|
|
82
142
|
sidebarName: 'sidebar2',
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
143
|
+
previous: undefined,
|
|
144
|
+
next: {
|
|
145
|
+
type: 'doc',
|
|
146
|
+
id: 'doc4',
|
|
147
|
+
},
|
|
148
|
+
} as SidebarNavigation);
|
|
86
149
|
expect(getDocNavigation('doc4')).toEqual({
|
|
87
150
|
sidebarName: 'sidebar2',
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
151
|
+
previous: {
|
|
152
|
+
type: 'doc',
|
|
153
|
+
id: 'doc3',
|
|
154
|
+
},
|
|
155
|
+
next: undefined,
|
|
156
|
+
} as SidebarNavigation);
|
|
157
|
+
|
|
158
|
+
expect(getDocNavigation('doc5')).toMatchObject({
|
|
159
|
+
sidebarName: 'sidebar3',
|
|
160
|
+
previous: undefined,
|
|
161
|
+
next: {
|
|
162
|
+
type: 'category',
|
|
163
|
+
label: 'S3 SubCategory',
|
|
164
|
+
},
|
|
165
|
+
} as SidebarNavigation);
|
|
166
|
+
expect(getDocNavigation('doc6')).toMatchObject({
|
|
167
|
+
sidebarName: 'sidebar3',
|
|
168
|
+
previous: {
|
|
169
|
+
type: 'category',
|
|
170
|
+
label: 'S3 SubSubCategory',
|
|
171
|
+
},
|
|
172
|
+
next: {
|
|
173
|
+
type: 'doc',
|
|
174
|
+
id: 'doc7',
|
|
175
|
+
},
|
|
176
|
+
} as SidebarNavigation);
|
|
177
|
+
expect(getDocNavigation('doc7')).toMatchObject({
|
|
178
|
+
sidebarName: 'sidebar3',
|
|
179
|
+
previous: {
|
|
180
|
+
type: 'doc',
|
|
181
|
+
id: 'doc6',
|
|
182
|
+
},
|
|
183
|
+
next: undefined,
|
|
184
|
+
} as SidebarNavigation);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('getCategoryGeneratedIndexNavigation', async () => {
|
|
188
|
+
expect(
|
|
189
|
+
getCategoryGeneratedIndexNavigation('/s3-subcategory-index-permalink'),
|
|
190
|
+
).toMatchObject({
|
|
191
|
+
sidebarName: 'sidebar3',
|
|
192
|
+
previous: {
|
|
193
|
+
type: 'category',
|
|
194
|
+
label: 'S3 Category',
|
|
195
|
+
},
|
|
196
|
+
next: {
|
|
197
|
+
type: 'category',
|
|
198
|
+
label: 'S3 SubSubCategory',
|
|
199
|
+
},
|
|
200
|
+
} as SidebarNavigation);
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
getCategoryGeneratedIndexNavigation('/s3-subsubcategory-index-permalink'),
|
|
204
|
+
).toMatchObject({
|
|
205
|
+
sidebarName: 'sidebar3',
|
|
206
|
+
previous: {
|
|
207
|
+
type: 'category',
|
|
208
|
+
label: 'S3 SubCategory',
|
|
209
|
+
},
|
|
210
|
+
next: {
|
|
211
|
+
type: 'doc',
|
|
212
|
+
id: 'doc6',
|
|
213
|
+
},
|
|
214
|
+
} as SidebarNavigation);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('getCategoryGeneratedIndexList', async () => {
|
|
218
|
+
expect(getCategoryGeneratedIndexList()).toMatchObject([
|
|
219
|
+
{
|
|
220
|
+
type: 'category',
|
|
221
|
+
label: 'S3 SubCategory',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
type: 'category',
|
|
225
|
+
label: 'S3 SubSubCategory',
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
91
228
|
});
|
|
92
229
|
});
|
|
93
230
|
|
|
@@ -393,3 +530,166 @@ describe('transformSidebarItems', () => {
|
|
|
393
530
|
]);
|
|
394
531
|
});
|
|
395
532
|
});
|
|
533
|
+
|
|
534
|
+
describe('toDocNavigationLink', () => {
|
|
535
|
+
type TestDoc = Pick<DocMetadataBase, 'permalink' | 'title' | 'frontMatter'>;
|
|
536
|
+
function testDoc(data: TestDoc) {
|
|
537
|
+
return data as DocMetadataBase;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
test('with no frontmatter', () => {
|
|
541
|
+
expect(
|
|
542
|
+
toDocNavigationLink(
|
|
543
|
+
testDoc({
|
|
544
|
+
title: 'Doc Title',
|
|
545
|
+
permalink: '/docPermalink',
|
|
546
|
+
frontMatter: {},
|
|
547
|
+
}),
|
|
548
|
+
),
|
|
549
|
+
).toEqual({
|
|
550
|
+
title: 'Doc Title',
|
|
551
|
+
permalink: '/docPermalink',
|
|
552
|
+
} as DocNavLink);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('with pagination_label frontmatter', () => {
|
|
556
|
+
expect(
|
|
557
|
+
toDocNavigationLink(
|
|
558
|
+
testDoc({
|
|
559
|
+
title: 'Doc Title',
|
|
560
|
+
permalink: '/docPermalink',
|
|
561
|
+
frontMatter: {
|
|
562
|
+
pagination_label: 'pagination_label',
|
|
563
|
+
},
|
|
564
|
+
}),
|
|
565
|
+
),
|
|
566
|
+
).toEqual({
|
|
567
|
+
title: 'pagination_label',
|
|
568
|
+
permalink: '/docPermalink',
|
|
569
|
+
} as DocNavLink);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('with sidebar_label frontmatter', () => {
|
|
573
|
+
expect(
|
|
574
|
+
toDocNavigationLink(
|
|
575
|
+
testDoc({
|
|
576
|
+
title: 'Doc Title',
|
|
577
|
+
permalink: '/docPermalink',
|
|
578
|
+
frontMatter: {
|
|
579
|
+
sidebar_label: 'sidebar_label',
|
|
580
|
+
},
|
|
581
|
+
}),
|
|
582
|
+
),
|
|
583
|
+
).toEqual({
|
|
584
|
+
title: 'sidebar_label',
|
|
585
|
+
permalink: '/docPermalink',
|
|
586
|
+
} as DocNavLink);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('with pagination_label + sidebar_label frontmatter', () => {
|
|
590
|
+
expect(
|
|
591
|
+
toDocNavigationLink(
|
|
592
|
+
testDoc({
|
|
593
|
+
title: 'Doc Title',
|
|
594
|
+
permalink: '/docPermalink',
|
|
595
|
+
frontMatter: {
|
|
596
|
+
pagination_label: 'pagination_label',
|
|
597
|
+
sidebar_label: 'sidebar_label',
|
|
598
|
+
},
|
|
599
|
+
}),
|
|
600
|
+
),
|
|
601
|
+
).toEqual({
|
|
602
|
+
title: 'pagination_label',
|
|
603
|
+
permalink: '/docPermalink',
|
|
604
|
+
} as DocNavLink);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe('toNavigationLink', () => {
|
|
609
|
+
type TestDoc = Pick<DocMetadataBase, 'permalink' | 'title'>;
|
|
610
|
+
function testDoc(data: TestDoc) {
|
|
611
|
+
return {...data, frontMatter: {}} as DocMetadataBase;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const docsById: Record<string, DocMetadataBase> = {
|
|
615
|
+
doc1: testDoc({
|
|
616
|
+
title: 'Doc 1',
|
|
617
|
+
permalink: '/doc1',
|
|
618
|
+
}),
|
|
619
|
+
doc2: testDoc({
|
|
620
|
+
title: 'Doc 1',
|
|
621
|
+
permalink: '/doc1',
|
|
622
|
+
}),
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
test('with doc items', () => {
|
|
626
|
+
expect(toNavigationLink({type: 'doc', id: 'doc1'}, docsById)).toEqual(
|
|
627
|
+
toDocNavigationLink(docsById.doc1),
|
|
628
|
+
);
|
|
629
|
+
expect(toNavigationLink({type: 'doc', id: 'doc2'}, docsById)).toEqual(
|
|
630
|
+
toDocNavigationLink(docsById.doc2),
|
|
631
|
+
);
|
|
632
|
+
expect(() =>
|
|
633
|
+
toNavigationLink({type: 'doc', id: 'doc3'}, docsById),
|
|
634
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
635
|
+
`"Can't create navigation link: no doc found with id=doc3"`,
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('with category item and doc link', () => {
|
|
640
|
+
expect(
|
|
641
|
+
toNavigationLink(
|
|
642
|
+
{
|
|
643
|
+
type: 'category',
|
|
644
|
+
label: 'Category',
|
|
645
|
+
items: [],
|
|
646
|
+
link: {
|
|
647
|
+
type: 'doc',
|
|
648
|
+
id: 'doc1',
|
|
649
|
+
},
|
|
650
|
+
collapsed: true,
|
|
651
|
+
collapsible: true,
|
|
652
|
+
},
|
|
653
|
+
docsById,
|
|
654
|
+
),
|
|
655
|
+
).toEqual(toDocNavigationLink(docsById.doc1));
|
|
656
|
+
expect(() =>
|
|
657
|
+
toNavigationLink(
|
|
658
|
+
{
|
|
659
|
+
type: 'category',
|
|
660
|
+
label: 'Category',
|
|
661
|
+
items: [],
|
|
662
|
+
link: {
|
|
663
|
+
type: 'doc',
|
|
664
|
+
id: 'doc3',
|
|
665
|
+
},
|
|
666
|
+
collapsed: true,
|
|
667
|
+
collapsible: true,
|
|
668
|
+
},
|
|
669
|
+
docsById,
|
|
670
|
+
),
|
|
671
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
672
|
+
`"Can't create navigation link: no doc found with id=doc3"`,
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('with category item and generated-index link', () => {
|
|
677
|
+
expect(
|
|
678
|
+
toNavigationLink(
|
|
679
|
+
{
|
|
680
|
+
type: 'category',
|
|
681
|
+
label: 'Category',
|
|
682
|
+
items: [],
|
|
683
|
+
link: {
|
|
684
|
+
type: 'generated-index',
|
|
685
|
+
slug: 'slug',
|
|
686
|
+
permalink: 'generated-index-permalink',
|
|
687
|
+
},
|
|
688
|
+
collapsed: true,
|
|
689
|
+
collapsible: true,
|
|
690
|
+
},
|
|
691
|
+
docsById,
|
|
692
|
+
),
|
|
693
|
+
).toEqual({title: 'Category', permalink: 'generated-index-permalink'});
|
|
694
|
+
});
|
|
695
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -11,19 +11,27 @@ import type {
|
|
|
11
11
|
SidebarItemCategory,
|
|
12
12
|
SidebarItemsGenerator,
|
|
13
13
|
SidebarItemsGeneratorDoc,
|
|
14
|
+
SidebarItemCategoryLink,
|
|
15
|
+
SidebarItemCategoryLinkConfig,
|
|
14
16
|
} from './types';
|
|
15
|
-
import {
|
|
17
|
+
import {sortBy, last} from 'lodash';
|
|
16
18
|
import {addTrailingSlash, posixPath} from '@docusaurus/utils';
|
|
17
|
-
import {Joi} from '@docusaurus/utils-validation';
|
|
18
19
|
import chalk from 'chalk';
|
|
19
20
|
import path from 'path';
|
|
20
21
|
import fs from 'fs-extra';
|
|
21
22
|
import Yaml from 'js-yaml';
|
|
23
|
+
import {validateCategoryMetadataFile} from './validation';
|
|
24
|
+
import {createDocsByIdIndex, isConventionalDocIndex} from '../docs';
|
|
22
25
|
|
|
23
26
|
const BreadcrumbSeparator = '/';
|
|
24
27
|
// To avoid possible name clashes with a folder of the same name as the ID
|
|
25
28
|
const docIdPrefix = '$doc$/';
|
|
26
29
|
|
|
30
|
+
// Just an alias to the make code more explicit
|
|
31
|
+
function getLocalDocId(docId: string): string {
|
|
32
|
+
return last(docId.split('/'))!;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export const CategoryMetadataFilenameBase = '_category_';
|
|
28
36
|
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
|
|
29
37
|
|
|
@@ -33,6 +41,7 @@ export type CategoryMetadataFile = {
|
|
|
33
41
|
collapsed?: boolean;
|
|
34
42
|
collapsible?: boolean;
|
|
35
43
|
className?: string;
|
|
44
|
+
link?: SidebarItemCategoryLinkConfig;
|
|
36
45
|
|
|
37
46
|
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
|
|
38
47
|
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
|
|
@@ -50,17 +59,9 @@ type Dir = {
|
|
|
50
59
|
[item: string]: Dir | null;
|
|
51
60
|
};
|
|
52
61
|
|
|
53
|
-
const CategoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
|
54
|
-
label: Joi.string(),
|
|
55
|
-
position: Joi.number(),
|
|
56
|
-
collapsed: Joi.boolean(),
|
|
57
|
-
collapsible: Joi.boolean(),
|
|
58
|
-
className: Joi.string(),
|
|
59
|
-
});
|
|
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
|
|
62
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...
|
|
63
|
-
// TODO later if there is `CategoryFolder/
|
|
64
|
+
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it
|
|
64
65
|
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
|
|
65
66
|
async function readCategoryMetadataFile(
|
|
66
67
|
categoryDirPath: string,
|
|
@@ -69,7 +70,7 @@ async function readCategoryMetadataFile(
|
|
|
69
70
|
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
|
|
70
71
|
const unsafeContent = Yaml.load(contentString);
|
|
71
72
|
try {
|
|
72
|
-
return
|
|
73
|
+
return validateCategoryMetadataFile(unsafeContent);
|
|
73
74
|
} catch (e) {
|
|
74
75
|
console.error(
|
|
75
76
|
chalk.red(
|
|
@@ -100,6 +101,21 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|
|
100
101
|
item: {dirName: autogenDir},
|
|
101
102
|
version,
|
|
102
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
|
+
|
|
103
119
|
/**
|
|
104
120
|
* Step 1. Extract the docs that are in the autogen dir.
|
|
105
121
|
*/
|
|
@@ -163,12 +179,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|
|
163
179
|
* (From a record to an array of items, akin to normalizing shorthand)
|
|
164
180
|
*/
|
|
165
181
|
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
|
|
166
|
-
const docsById = keyBy(allDocs, (doc) => doc.id);
|
|
167
182
|
function createDocItem(id: string): WithPosition<SidebarItemDoc> {
|
|
168
183
|
const {
|
|
169
184
|
sidebarPosition: position,
|
|
170
185
|
frontMatter: {sidebar_label: label, sidebar_class_name: className},
|
|
171
|
-
} =
|
|
186
|
+
} = getDoc(id);
|
|
172
187
|
return {
|
|
173
188
|
type: 'doc',
|
|
174
189
|
id,
|
|
@@ -187,6 +202,57 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|
|
187
202
|
const categoryMetadata = await readCategoryMetadataFile(categoryPath);
|
|
188
203
|
const className = categoryMetadata?.className;
|
|
189
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
|
+
|
|
190
256
|
return {
|
|
191
257
|
type: 'category',
|
|
192
258
|
label: categoryMetadata?.label ?? filename,
|
|
@@ -195,11 +261,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|
|
195
261
|
collapsed: categoryMetadata?.collapsed ?? options.sidebarCollapsed,
|
|
196
262
|
position: categoryMetadata?.position ?? numberPrefix,
|
|
197
263
|
...(className !== undefined && {className}),
|
|
198
|
-
items
|
|
199
|
-
|
|
200
|
-
dirToItem(content, key, `${fullPath}/${key}`),
|
|
201
|
-
),
|
|
202
|
-
),
|
|
264
|
+
items,
|
|
265
|
+
...(link && {link}),
|
|
203
266
|
};
|
|
204
267
|
}
|
|
205
268
|
async function dirToItem(
|