mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-19 01:28:38 +02:00
529 lines
15 KiB
TypeScript
529 lines
15 KiB
TypeScript
/**
|
|
* 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 _ from 'lodash';
|
|
import {toMessageRelativeFilePath} from '@docusaurus/utils';
|
|
import type {
|
|
Sidebars,
|
|
Sidebar,
|
|
SidebarItem,
|
|
SidebarItemCategory,
|
|
SidebarItemLink,
|
|
SidebarItemDoc,
|
|
SidebarItemType,
|
|
SidebarCategoriesShorthand,
|
|
SidebarItemConfig,
|
|
SidebarItemCategoryWithGeneratedIndex,
|
|
SidebarNavigationItem,
|
|
} from './types';
|
|
import type {
|
|
DocMetadataBase,
|
|
PropNavigationLink,
|
|
VersionMetadata,
|
|
} from '@docusaurus/plugin-content-docs';
|
|
|
|
export function isCategoriesShorthand(
|
|
item: SidebarItemConfig,
|
|
): item is SidebarCategoriesShorthand {
|
|
return typeof item === 'object' && !item.type;
|
|
}
|
|
|
|
export function transformSidebarItems(
|
|
sidebar: Sidebar,
|
|
updateFn: (item: SidebarItem) => SidebarItem,
|
|
): Sidebar {
|
|
function transformRecursive(item: SidebarItem): SidebarItem {
|
|
if (item.type === 'category') {
|
|
return updateFn({
|
|
...item,
|
|
items: item.items.map(transformRecursive),
|
|
});
|
|
}
|
|
return updateFn(item);
|
|
}
|
|
return sidebar.map(transformRecursive);
|
|
}
|
|
|
|
/**
|
|
* Flatten sidebar items into a single flat array (containing categories/docs on
|
|
* the same level). Order matters (useful for next/prev nav), top categories
|
|
* appear before their child elements
|
|
*/
|
|
function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] {
|
|
function flattenRecursive(item: SidebarItem): SidebarItem[] {
|
|
return item.type === 'category'
|
|
? [item, ...item.items.flatMap(flattenRecursive)]
|
|
: [item];
|
|
}
|
|
return items.flatMap(flattenRecursive);
|
|
}
|
|
|
|
function collectSidebarItemsOfType<
|
|
Type extends SidebarItemType,
|
|
Item extends SidebarItem & {type: SidebarItemType},
|
|
>(type: Type, sidebar: Sidebar): Item[] {
|
|
return flattenSidebarItems(sidebar).filter(
|
|
(item) => item.type === type,
|
|
) as Item[];
|
|
}
|
|
|
|
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
|
|
return collectSidebarItemsOfType('doc', sidebar);
|
|
}
|
|
export function collectSidebarCategories(
|
|
sidebar: Sidebar,
|
|
): SidebarItemCategory[] {
|
|
return collectSidebarItemsOfType('category', sidebar);
|
|
}
|
|
export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] {
|
|
return collectSidebarItemsOfType('link', sidebar);
|
|
}
|
|
export function collectSidebarRefs(sidebar: Sidebar): SidebarItemDoc[] {
|
|
return collectSidebarItemsOfType('ref', sidebar);
|
|
}
|
|
|
|
// /!\ docId order matters for navigation!
|
|
export function collectSidebarDocIds(sidebar: Sidebar): string[] {
|
|
return flattenSidebarItems(sidebar).flatMap((item) => {
|
|
if (item.type === 'category') {
|
|
return item.link?.type === 'doc' ? [item.link.id] : [];
|
|
}
|
|
if (item.type === 'doc') {
|
|
return [item.id];
|
|
}
|
|
return [];
|
|
});
|
|
}
|
|
|
|
export function collectSidebarNavigation(
|
|
sidebar: Sidebar,
|
|
): SidebarNavigationItem[] {
|
|
return flattenSidebarItems(sidebar).flatMap((item) => {
|
|
if (item.type === 'category' && item.link) {
|
|
return [item as SidebarNavigationItem];
|
|
}
|
|
if (item.type === 'doc') {
|
|
return [item];
|
|
}
|
|
return [];
|
|
});
|
|
}
|
|
|
|
export function collectSidebarsDocIds(sidebars: Sidebars): {
|
|
[sidebarId: string]: string[];
|
|
} {
|
|
return _.mapValues(sidebars, collectSidebarDocIds);
|
|
}
|
|
|
|
export function collectSidebarsNavigations(sidebars: Sidebars): {
|
|
[sidebarId: string]: SidebarNavigationItem[];
|
|
} {
|
|
return _.mapValues(sidebars, collectSidebarNavigation);
|
|
}
|
|
|
|
export type SidebarNavigation = {
|
|
sidebarName: string | undefined;
|
|
previous: SidebarNavigationItem | undefined;
|
|
next: SidebarNavigationItem | undefined;
|
|
};
|
|
|
|
// A convenient and performant way to query the sidebars content
|
|
export type SidebarsUtils = {
|
|
sidebars: Sidebars;
|
|
getFirstDocIdOfFirstSidebar: () => string | undefined;
|
|
getSidebarNameByDocId: (docId: string) => string | undefined;
|
|
getDocNavigation: (params: {
|
|
docId: string;
|
|
displayedSidebar: string | null | undefined;
|
|
unlistedIds: Set<string>;
|
|
}) => SidebarNavigation;
|
|
getCategoryGeneratedIndexList: () => SidebarItemCategoryWithGeneratedIndex[];
|
|
getCategoryGeneratedIndexNavigation: (
|
|
categoryGeneratedIndexPermalink: string,
|
|
) => SidebarNavigation;
|
|
/**
|
|
* This function may return undefined. This is usually a user mistake, because
|
|
* it means this sidebar will never be displayed; however, we can still use
|
|
* `displayed_sidebar` to make it displayed. Pretty weird but valid use-case
|
|
*/
|
|
getFirstLink: (sidebarId: string) =>
|
|
| {
|
|
type: 'doc';
|
|
id: string;
|
|
label: string;
|
|
}
|
|
| {
|
|
type: 'generated-index';
|
|
permalink: string;
|
|
label: string;
|
|
}
|
|
| undefined;
|
|
|
|
checkLegacyVersionedSidebarNames: ({
|
|
versionMetadata,
|
|
}: {
|
|
sidebarFilePath: string;
|
|
versionMetadata: VersionMetadata;
|
|
}) => void;
|
|
|
|
checkSidebarsDocIds: ({
|
|
allDocIds,
|
|
sidebarFilePath,
|
|
versionMetadata,
|
|
}: {
|
|
allDocIds: string[];
|
|
sidebarFilePath: string;
|
|
versionMetadata: VersionMetadata;
|
|
}) => void;
|
|
};
|
|
|
|
export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
|
|
const sidebarNameToDocIds = collectSidebarsDocIds(sidebars);
|
|
const sidebarNameToNavigationItems = collectSidebarsNavigations(sidebars);
|
|
|
|
// Reverse mapping
|
|
const docIdToSidebarName = Object.fromEntries(
|
|
Object.entries(sidebarNameToDocIds).flatMap(([sidebarName, docIds]) =>
|
|
docIds.map((docId) => [docId, sidebarName]),
|
|
),
|
|
);
|
|
|
|
function getFirstDocIdOfFirstSidebar(): string | undefined {
|
|
return Object.values(sidebarNameToDocIds)[0]?.[0];
|
|
}
|
|
|
|
function getSidebarNameByDocId(docId: string): string | undefined {
|
|
return docIdToSidebarName[docId];
|
|
}
|
|
|
|
function emptySidebarNavigation(): SidebarNavigation {
|
|
return {
|
|
sidebarName: undefined,
|
|
previous: undefined,
|
|
next: undefined,
|
|
};
|
|
}
|
|
|
|
function getDocNavigation({
|
|
docId,
|
|
displayedSidebar,
|
|
unlistedIds,
|
|
}: {
|
|
docId: string;
|
|
displayedSidebar: string | null | undefined;
|
|
unlistedIds: Set<string>;
|
|
}): SidebarNavigation {
|
|
const sidebarName =
|
|
displayedSidebar === undefined
|
|
? getSidebarNameByDocId(docId)
|
|
: displayedSidebar;
|
|
|
|
if (!sidebarName) {
|
|
return emptySidebarNavigation();
|
|
}
|
|
let navigationItems = sidebarNameToNavigationItems[sidebarName];
|
|
if (!navigationItems) {
|
|
throw new Error(
|
|
`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`,
|
|
);
|
|
}
|
|
|
|
// Filter unlisted items from navigation
|
|
navigationItems = navigationItems.filter((item) => {
|
|
if (item.type === 'doc' && unlistedIds.has(item.id)) {
|
|
return false;
|
|
}
|
|
if (
|
|
item.type === 'category' &&
|
|
item.link.type === 'doc' &&
|
|
unlistedIds.has(item.link.id)
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const currentItemIndex = navigationItems.findIndex((item) => {
|
|
if (item.type === 'doc') {
|
|
return item.id === docId;
|
|
}
|
|
if (item.type === 'category' && item.link.type === 'doc') {
|
|
return item.link.id === docId;
|
|
}
|
|
return false;
|
|
});
|
|
if (currentItemIndex === -1) {
|
|
return {sidebarName, next: undefined, previous: undefined};
|
|
}
|
|
|
|
return {
|
|
sidebarName,
|
|
previous: navigationItems[currentItemIndex - 1],
|
|
next: navigationItems[currentItemIndex + 1],
|
|
};
|
|
}
|
|
|
|
function getCategoryGeneratedIndexList(): SidebarItemCategoryWithGeneratedIndex[] {
|
|
return Object.values(sidebarNameToNavigationItems)
|
|
.flat()
|
|
.flatMap((item) => {
|
|
if (item.type === 'category' && item.link.type === 'generated-index') {
|
|
return [item as SidebarItemCategoryWithGeneratedIndex];
|
|
}
|
|
return [];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* We identity the category generated index by its permalink (should be
|
|
* unique). More reliable than using object identity
|
|
*/
|
|
function getCategoryGeneratedIndexNavigation(
|
|
categoryGeneratedIndexPermalink: string,
|
|
): SidebarNavigation {
|
|
function isCurrentCategoryGeneratedIndexItem(
|
|
item: SidebarNavigationItem,
|
|
): boolean {
|
|
return (
|
|
item.type === 'category' &&
|
|
item.link.type === 'generated-index' &&
|
|
item.link.permalink === categoryGeneratedIndexPermalink
|
|
);
|
|
}
|
|
|
|
const sidebarName = Object.entries(sidebarNameToNavigationItems).find(
|
|
([, navigationItems]) =>
|
|
navigationItems.find(isCurrentCategoryGeneratedIndexItem),
|
|
)![0];
|
|
const navigationItems = sidebarNameToNavigationItems[sidebarName]!;
|
|
const currentItemIndex = navigationItems.findIndex(
|
|
isCurrentCategoryGeneratedIndexItem,
|
|
);
|
|
return {
|
|
sidebarName,
|
|
previous: navigationItems[currentItemIndex - 1],
|
|
next: navigationItems[currentItemIndex + 1],
|
|
};
|
|
}
|
|
|
|
// TODO remove in Docusaurus v4
|
|
function getLegacyVersionedPrefix(versionMetadata: VersionMetadata): string {
|
|
return `version-${versionMetadata.versionName}/`;
|
|
}
|
|
|
|
// In early v2, sidebar names used to be versioned
|
|
// example: "version-2.0.0-alpha.66/my-sidebar-name"
|
|
// In v3 it's not the case anymore and we throw an error to explain
|
|
// TODO remove in Docusaurus v4
|
|
function checkLegacyVersionedSidebarNames({
|
|
versionMetadata,
|
|
sidebarFilePath,
|
|
}: {
|
|
versionMetadata: VersionMetadata;
|
|
sidebarFilePath: string;
|
|
}): void {
|
|
const illegalPrefix = getLegacyVersionedPrefix(versionMetadata);
|
|
const legacySidebarNames = Object.keys(sidebars).filter((sidebarName) =>
|
|
sidebarName.startsWith(illegalPrefix),
|
|
);
|
|
if (legacySidebarNames.length > 0) {
|
|
throw new Error(
|
|
`Invalid sidebar file at "${toMessageRelativeFilePath(
|
|
sidebarFilePath,
|
|
)}".
|
|
These legacy versioned sidebar names are not supported anymore in Docusaurus v3:
|
|
- ${legacySidebarNames.sort().join('\n- ')}
|
|
|
|
The sidebar names you should now use are:
|
|
- ${legacySidebarNames
|
|
.sort()
|
|
.map((legacyName) => legacyName.split('/').splice(1).join('/'))
|
|
.join('\n- ')}
|
|
|
|
Please remove the "${illegalPrefix}" prefix from your versioned sidebar file.
|
|
This breaking change is documented on Docusaurus v3 release notes: https://docusaurus.io/blog/releases/3.0
|
|
`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// throw a better error message for Docusaurus v3 breaking change
|
|
// TODO this can be removed in Docusaurus v4
|
|
function handleLegacyVersionedDocIds({
|
|
invalidDocIds,
|
|
sidebarFilePath,
|
|
versionMetadata,
|
|
}: {
|
|
invalidDocIds: string[];
|
|
sidebarFilePath: string;
|
|
versionMetadata: VersionMetadata;
|
|
}) {
|
|
const illegalPrefix = getLegacyVersionedPrefix(versionMetadata);
|
|
|
|
// In older v2.0 alpha/betas, versioned docs had a legacy versioned prefix
|
|
// Example: "version-1.4/my-doc-id"
|
|
//
|
|
const legacyVersionedDocIds = invalidDocIds.filter((docId) =>
|
|
docId.startsWith(illegalPrefix),
|
|
);
|
|
if (legacyVersionedDocIds.length > 0) {
|
|
throw new Error(
|
|
`Invalid sidebar file at "${toMessageRelativeFilePath(
|
|
sidebarFilePath,
|
|
)}".
|
|
These legacy versioned document ids are not supported anymore in Docusaurus v3:
|
|
- ${legacyVersionedDocIds.sort().join('\n- ')}
|
|
|
|
The document ids you should now use are:
|
|
- ${legacyVersionedDocIds
|
|
.sort()
|
|
.map((legacyId) => legacyId.split('/').splice(1).join('/'))
|
|
.join('\n- ')}
|
|
|
|
Please remove the "${illegalPrefix}" prefix from your versioned sidebar file.
|
|
This breaking change is documented on Docusaurus v3 release notes: https://docusaurus.io/blog/releases/3.0
|
|
`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function checkSidebarsDocIds({
|
|
allDocIds,
|
|
sidebarFilePath,
|
|
versionMetadata,
|
|
}: {
|
|
allDocIds: string[];
|
|
sidebarFilePath: string;
|
|
versionMetadata: VersionMetadata;
|
|
}) {
|
|
const allSidebarDocIds = Object.values(sidebarNameToDocIds).flat();
|
|
const invalidDocIds = _.difference(allSidebarDocIds, allDocIds);
|
|
|
|
if (invalidDocIds.length > 0) {
|
|
handleLegacyVersionedDocIds({
|
|
invalidDocIds,
|
|
sidebarFilePath,
|
|
versionMetadata,
|
|
});
|
|
throw new Error(
|
|
`Invalid sidebar file at "${toMessageRelativeFilePath(
|
|
sidebarFilePath,
|
|
)}".
|
|
These sidebar document ids do not exist:
|
|
- ${invalidDocIds.sort().join('\n- ')}
|
|
|
|
Available document ids are:
|
|
- ${_.uniq(allDocIds).sort().join('\n- ')}
|
|
`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getFirstLink(sidebar: Sidebar):
|
|
| {
|
|
type: 'doc';
|
|
id: string;
|
|
label: string;
|
|
}
|
|
| {
|
|
type: 'generated-index';
|
|
permalink: string;
|
|
label: string;
|
|
}
|
|
| undefined {
|
|
for (const item of sidebar) {
|
|
if (item.type === 'doc') {
|
|
return {
|
|
type: 'doc',
|
|
id: item.id,
|
|
label: item.label ?? item.id,
|
|
};
|
|
} else if (item.type === 'category') {
|
|
if (item.link?.type === 'doc') {
|
|
return {
|
|
type: 'doc',
|
|
id: item.link.id,
|
|
label: item.label,
|
|
};
|
|
} else if (item.link?.type === 'generated-index') {
|
|
return {
|
|
type: 'generated-index',
|
|
permalink: item.link.permalink,
|
|
label: item.label,
|
|
};
|
|
}
|
|
const firstSubItem = getFirstLink(item.items);
|
|
if (firstSubItem) {
|
|
return firstSubItem;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
sidebars,
|
|
getFirstDocIdOfFirstSidebar,
|
|
getSidebarNameByDocId,
|
|
getDocNavigation,
|
|
getCategoryGeneratedIndexList,
|
|
getCategoryGeneratedIndexNavigation,
|
|
checkLegacyVersionedSidebarNames,
|
|
checkSidebarsDocIds,
|
|
getFirstLink: (id) => getFirstLink(sidebars[id]!),
|
|
};
|
|
}
|
|
|
|
export function toDocNavigationLink(
|
|
doc: DocMetadataBase,
|
|
options?: {sidebarItemLabel?: string},
|
|
): PropNavigationLink {
|
|
const {
|
|
title,
|
|
permalink,
|
|
frontMatter: {
|
|
pagination_label: paginationLabel,
|
|
sidebar_label: sidebarLabel,
|
|
},
|
|
} = doc;
|
|
return {
|
|
title:
|
|
paginationLabel ?? sidebarLabel ?? options?.sidebarItemLabel ?? title,
|
|
permalink,
|
|
};
|
|
}
|
|
|
|
export function toNavigationLink(
|
|
navigationItem: SidebarNavigationItem | undefined,
|
|
docsById: {[docId: string]: DocMetadataBase},
|
|
): PropNavigationLink | undefined {
|
|
function getDocById(docId: string) {
|
|
const doc = docsById[docId];
|
|
if (!doc) {
|
|
throw new Error(
|
|
`Can't create navigation link: no doc found with id=${docId}`,
|
|
);
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
if (!navigationItem) {
|
|
return undefined;
|
|
}
|
|
|
|
if (navigationItem.type === 'category') {
|
|
return navigationItem.link.type === 'doc'
|
|
? toDocNavigationLink(getDocById(navigationItem.link.id))
|
|
: {
|
|
title: navigationItem.label,
|
|
permalink: navigationItem.link.permalink,
|
|
};
|
|
}
|
|
return toDocNavigationLink(getDocById(navigationItem.id), {
|
|
sidebarItemLabel: navigationItem?.label,
|
|
});
|
|
}
|