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:
Joshua Chen 2022-02-04 09:46:25 +08:00 committed by GitHub
parent d6bdf7e804
commit e3fd3e74ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 750 additions and 615 deletions

View file

@ -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;
}