mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-19 17:49:19 +02:00
feat(content-docs): sidebar category linking to document or auto-generated index page (#5830)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com> Co-authored-by: Armano <armano2@users.noreply.github.com> Co-authored-by: Alexey Pyltsyn <lex61rus@gmail.com>
This commit is contained in:
parent
95f911efef
commit
cfae5d0933
105 changed files with 3904 additions and 816 deletions
|
@ -16,8 +16,15 @@ import type {
|
|||
SidebarCategoriesShorthand,
|
||||
SidebarItemConfig,
|
||||
} from './types';
|
||||
import {mapValues, difference} from 'lodash';
|
||||
|
||||
import {mapValues, difference, uniq} from 'lodash';
|
||||
import {getElementsAround, toMessageRelativeFilePath} from '@docusaurus/utils';
|
||||
import {DocMetadataBase, DocNavLink} from '../types';
|
||||
import {
|
||||
SidebarItemCategoryWithGeneratedIndex,
|
||||
SidebarItemCategoryWithLink,
|
||||
SidebarNavigationItem,
|
||||
} from './types';
|
||||
|
||||
export function isCategoriesShorthand(
|
||||
item: SidebarItemConfig,
|
||||
|
@ -41,21 +48,24 @@ export function transformSidebarItems(
|
|||
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[] {
|
||||
function collectRecursive(item: SidebarItem): Item[] {
|
||||
const currentItemsCollected: Item[] =
|
||||
item.type === type ? [item as Item] : [];
|
||||
|
||||
const childItemsCollected: Item[] =
|
||||
item.type === 'category' ? item.items.flatMap(collectRecursive) : [];
|
||||
|
||||
return [...currentItemsCollected, ...childItemsCollected];
|
||||
}
|
||||
|
||||
return sidebar.flatMap(collectRecursive);
|
||||
return flattenSidebarItems(sidebar).filter(
|
||||
(item) => item.type === type,
|
||||
) as Item[];
|
||||
}
|
||||
|
||||
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
|
||||
|
@ -70,25 +80,72 @@ export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] {
|
|||
return collectSidebarItemsOfType('link', 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,
|
||||
): Record<string, string[]> {
|
||||
return mapValues(sidebars, (sidebar) =>
|
||||
collectSidebarDocItems(sidebar).map((docItem) => docItem.id),
|
||||
);
|
||||
return mapValues(sidebars, collectSidebarDocIds);
|
||||
}
|
||||
|
||||
export function createSidebarsUtils(sidebars: Sidebars): {
|
||||
export function collectSidebarsNavigations(
|
||||
sidebars: Sidebars,
|
||||
): Record<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: (docId: string) => {
|
||||
sidebarName: string | undefined;
|
||||
previousId: string | undefined;
|
||||
nextId: string | undefined;
|
||||
};
|
||||
getDocNavigation: (
|
||||
unversionedId: string,
|
||||
versionedId: string,
|
||||
) => SidebarNavigation;
|
||||
getCategoryGeneratedIndexList: () => SidebarItemCategoryWithGeneratedIndex[];
|
||||
getCategoryGeneratedIndexNavigation: (
|
||||
categoryGeneratedIndexPermalink: string,
|
||||
) => SidebarNavigation;
|
||||
|
||||
checkSidebarsDocIds: (validDocIds: string[], sidebarFilePath: string) => 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]) =>
|
||||
|
@ -104,27 +161,91 @@ export function createSidebarsUtils(sidebars: Sidebars): {
|
|||
return docIdToSidebarName[docId];
|
||||
}
|
||||
|
||||
function getDocNavigation(docId: string): {
|
||||
sidebarName: string | undefined;
|
||||
previousId: string | undefined;
|
||||
nextId: string | undefined;
|
||||
} {
|
||||
const sidebarName = getSidebarNameByDocId(docId);
|
||||
function emptySidebarNavigation(): SidebarNavigation {
|
||||
return {
|
||||
sidebarName: undefined,
|
||||
previous: undefined,
|
||||
next: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getDocNavigation(
|
||||
unversionedId: string,
|
||||
versionedId: string,
|
||||
): SidebarNavigation {
|
||||
// TODO legacy id retro-compatibility!
|
||||
let docId = unversionedId;
|
||||
let sidebarName = getSidebarNameByDocId(docId);
|
||||
if (!sidebarName) {
|
||||
docId = versionedId;
|
||||
sidebarName = getSidebarNameByDocId(docId);
|
||||
}
|
||||
|
||||
if (sidebarName) {
|
||||
const docIds = sidebarNameToDocIds[sidebarName];
|
||||
const currentIndex = docIds.indexOf(docId);
|
||||
const {previous, next} = getElementsAround(docIds, currentIndex);
|
||||
return {
|
||||
sidebarName,
|
||||
previousId: previous,
|
||||
nextId: next,
|
||||
};
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
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;
|
||||
});
|
||||
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
} else {
|
||||
return {
|
||||
sidebarName: undefined,
|
||||
previousId: undefined,
|
||||
nextId: undefined,
|
||||
};
|
||||
return emptySidebarNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
if (sidebarName) {
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex(
|
||||
isCurrentCategoryGeneratedIndexItem,
|
||||
);
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
} else {
|
||||
return emptySidebarNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,15 +261,69 @@ These sidebar document ids do not exist:
|
|||
- ${invalidSidebarDocIds.sort().join('\n- ')}
|
||||
|
||||
Available document ids are:
|
||||
- ${validDocIds.sort().join('\n- ')}`,
|
||||
- ${uniq(validDocIds).sort().join('\n- ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sidebars,
|
||||
getFirstDocIdOfFirstSidebar,
|
||||
getSidebarNameByDocId,
|
||||
getDocNavigation,
|
||||
getCategoryGeneratedIndexList,
|
||||
getCategoryGeneratedIndexNavigation,
|
||||
checkSidebarsDocIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function toDocNavigationLink(doc: DocMetadataBase): DocNavLink {
|
||||
const {
|
||||
title,
|
||||
permalink,
|
||||
frontMatter: {
|
||||
pagination_label: paginationLabel,
|
||||
sidebar_label: sidebarLabel,
|
||||
},
|
||||
} = doc;
|
||||
return {title: paginationLabel ?? sidebarLabel ?? title, permalink};
|
||||
}
|
||||
|
||||
export function toNavigationLink(
|
||||
navigationItem: SidebarNavigationItem | undefined,
|
||||
docsById: Record<string, DocMetadataBase>,
|
||||
): DocNavLink | 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;
|
||||
}
|
||||
|
||||
function handleCategory(category: SidebarItemCategoryWithLink): DocNavLink {
|
||||
if (category.link.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(category.link.id));
|
||||
} else if (category.link.type === 'generated-index') {
|
||||
return {
|
||||
title: category.label,
|
||||
permalink: category.link.permalink,
|
||||
};
|
||||
} else {
|
||||
throw new Error('unexpected category link type');
|
||||
}
|
||||
}
|
||||
if (!navigationItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (navigationItem.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(navigationItem.id));
|
||||
} else if (navigationItem.type === 'category') {
|
||||
return handleCategory(navigationItem);
|
||||
} else {
|
||||
throw new Error('unexpected navigation item');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue