mirror of
https://gitea.com/gitea/docs.git
synced 2026-11-04 09:15:33 +00:00
Follow-up to #588. - rename `DocusaurusSidebarItem` -> `SidebarSourceItem`, `fromDocusaurus` -> `fromSidebarSource`, `docusaurusSidebars` -> `sidebarSources` - reword comments and READMEs that described behaviour by comparison with docusaurus; fix the stale note in `sites/docs/src/config/api.ts` - drop `.docusaurus/` from `.gitignore` No functional change. `pnpm check` has 0 errors and `pnpm build` still builds 6690 pages. <!-- cloudflare-preview --> Preview: https://pr-589.docs-gitea-com.pages.dev Reviewed-on: https://gitea.com/gitea/docs/pulls/589 Reviewed-by: silverwind <[email protected]>
231 lines
7.2 KiB
TypeScript
231 lines
7.2 KiB
TypeScript
/**
|
|
* Builds one sidebar per (product, version, language) out of the loaded pages,
|
|
* deriving it from `_category_.json` and from the
|
|
* handwritten sidebar files of the runner docs.
|
|
*/
|
|
|
|
import { defaultLocale } from './products';
|
|
|
|
export interface SidebarPage {
|
|
/** Route id of the page. */
|
|
id: string;
|
|
/** Url of the page. */
|
|
href: string;
|
|
title: string;
|
|
hidden: boolean;
|
|
meta: {
|
|
product: string;
|
|
version: string;
|
|
locale: string;
|
|
prefix: string;
|
|
dir: string;
|
|
name: string;
|
|
order?: number;
|
|
category?: boolean;
|
|
};
|
|
}
|
|
|
|
export interface SidebarLink {
|
|
type: 'link';
|
|
label: string;
|
|
href: string;
|
|
isCurrent: boolean;
|
|
badge: undefined;
|
|
attrs: Record<string, never>;
|
|
}
|
|
|
|
export interface SidebarGroup {
|
|
type: 'group';
|
|
label: string;
|
|
entries: SidebarEntry[];
|
|
collapsed: boolean;
|
|
badge: undefined;
|
|
}
|
|
|
|
export type SidebarEntry = SidebarLink | SidebarGroup;
|
|
|
|
/** A hand written sidebar file, as used by the runner docs. */
|
|
export type SidebarSourceItem =
|
|
| string
|
|
| { type: 'doc'; id: string; label?: string }
|
|
| { type: 'autogenerated'; dirName: string }
|
|
| { type: 'category'; label: string; collapsed?: boolean; items: SidebarSourceItem[] };
|
|
|
|
const last = Number.MAX_SAFE_INTEGER;
|
|
|
|
function link(page: SidebarPage, label = page.title): SidebarLink {
|
|
return { type: 'link', label, href: page.href, isCurrent: false, badge: undefined, attrs: {} };
|
|
}
|
|
|
|
function labelFromDirname(name: string): string {
|
|
return name
|
|
.split('-')
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
/**
|
|
* Builds the sidebars of every group of pages, keyed by the route prefix of the
|
|
* group. `manual` provides the hand written sidebar of the groups that ship one.
|
|
*/
|
|
export function buildSidebars(
|
|
pages: SidebarPage[],
|
|
manual: Map<string, SidebarSourceItem[]> = new Map(),
|
|
): Map<string, SidebarEntry[]> {
|
|
const groups = new Map<string, SidebarPage[]>();
|
|
for (const page of pages) {
|
|
const list = groups.get(page.meta.prefix);
|
|
if (list) list.push(page);
|
|
else groups.set(page.meta.prefix, [page]);
|
|
}
|
|
|
|
const sidebars = new Map<string, SidebarEntry[]>();
|
|
for (const [prefix, groupPages] of groups) {
|
|
const key = `${groupPages[0]!.meta.product}@${groupPages[0]!.meta.version}`;
|
|
const items = manual.get(key);
|
|
const tree = treeOf(groupPages);
|
|
sidebars.set(prefix, items ? fromSidebarSource(items, groupPages, tree) : tree.build(''));
|
|
}
|
|
return sidebars;
|
|
}
|
|
|
|
interface Tree {
|
|
/** Entries of a directory, ordered by their position. */
|
|
build(dir: string): SidebarEntry[];
|
|
/** Localized label of a directory, from its `_category_.json`. */
|
|
label(dir: string): string;
|
|
}
|
|
|
|
/** Sidebar of a directory tree, ordered by `sidebar_position`. */
|
|
function treeOf(pages: SidebarPage[]): Tree {
|
|
const categories = new Map<string, SidebarPage>();
|
|
for (const page of pages) {
|
|
if (!page.meta.category) continue;
|
|
const dir = [page.meta.dir, page.meta.name].filter(Boolean).join('/');
|
|
categories.set(dir, page);
|
|
}
|
|
|
|
const childDirs = (dir: string) => {
|
|
const prefix = dir ? `${dir}/` : '';
|
|
const found = new Set<string>();
|
|
for (const page of pages) {
|
|
if (!page.meta.dir.startsWith(prefix)) continue;
|
|
const rest = page.meta.dir.slice(prefix.length);
|
|
if (!rest) continue;
|
|
found.add(prefix + rest.split('/')[0]);
|
|
}
|
|
return [...found];
|
|
};
|
|
|
|
const build = (dir: string): SidebarEntry[] => {
|
|
const entries: { sort: [number, number, string]; entry: SidebarEntry }[] = [];
|
|
|
|
for (const child of childDirs(dir)) {
|
|
const category = categories.get(child);
|
|
const label = category?.title ?? labelFromDirname(child.split('/').at(-1)!);
|
|
entries.push({
|
|
sort: [category?.meta.order ?? last, 0, label],
|
|
entry: { type: 'group', label, entries: build(child), collapsed: true, badge: undefined },
|
|
});
|
|
}
|
|
|
|
for (const page of pages) {
|
|
if (page.meta.dir !== dir || page.meta.category || page.hidden) continue;
|
|
entries.push({ sort: [page.meta.order ?? last, 1, page.title], entry: link(page) });
|
|
}
|
|
|
|
return entries
|
|
.sort(
|
|
(a, b) =>
|
|
a.sort[0] - b.sort[0] || a.sort[1] - b.sort[1] || a.sort[2].localeCompare(b.sort[2]),
|
|
)
|
|
.map((item) => item.entry);
|
|
};
|
|
|
|
const label = (dir: string) =>
|
|
categories.get(dir)?.title ?? labelFromDirname(dir.split('/').at(-1)!);
|
|
|
|
return { build, label };
|
|
}
|
|
|
|
/** Sidebar described by a hand written sidebar file, used by the runner docs. */
|
|
function fromSidebarSource(
|
|
items: SidebarSourceItem[],
|
|
pages: SidebarPage[],
|
|
tree: Tree,
|
|
): SidebarEntry[] {
|
|
const byDocId = new Map<string, SidebarPage>();
|
|
for (const page of pages) {
|
|
byDocId.set([page.meta.dir, page.meta.name].filter(Boolean).join('/'), page);
|
|
}
|
|
|
|
const convert = (item: SidebarSourceItem): SidebarEntry | undefined => {
|
|
if (typeof item === 'string') {
|
|
const page = byDocId.get(item);
|
|
return page && link(page);
|
|
}
|
|
if (item.type === 'doc') {
|
|
const page = byDocId.get(item.id);
|
|
// the label of the sidebar file is english, the title of a translated
|
|
// page is already localized (translated from the json)
|
|
return page && link(page, page.meta.locale === defaultLocale ? (item.label ?? page.title) : page.title);
|
|
}
|
|
if (item.type === 'autogenerated') {
|
|
// only reached inside a category, the caller flattens the result
|
|
return {
|
|
type: 'group',
|
|
label: tree.label(item.dirName),
|
|
entries: tree.build(item.dirName),
|
|
collapsed: true,
|
|
badge: undefined,
|
|
};
|
|
}
|
|
|
|
// a category whose only child is an autogenerated directory keeps the
|
|
// translated label of the directory, like the sidebar files do
|
|
const single =
|
|
item.items.length === 1 && typeof item.items[0] === 'object' && 'dirName' in item.items[0]
|
|
? item.items[0]
|
|
: undefined;
|
|
if (single) {
|
|
return {
|
|
type: 'group',
|
|
label: tree.label(single.dirName),
|
|
entries: tree.build(single.dirName),
|
|
collapsed: item.collapsed ?? true,
|
|
badge: undefined,
|
|
};
|
|
}
|
|
return {
|
|
type: 'group',
|
|
label: item.label,
|
|
entries: item.items.map(convert).filter((entry): entry is SidebarEntry => Boolean(entry)),
|
|
collapsed: item.collapsed ?? true,
|
|
badge: undefined,
|
|
};
|
|
};
|
|
|
|
return items.map(convert).filter((entry): entry is SidebarEntry => Boolean(entry));
|
|
}
|
|
|
|
/** Marks the entry of the current page and expands the groups leading to it. */
|
|
export function markCurrent(entries: SidebarEntry[], pathname: string): SidebarEntry[] {
|
|
return entries.map((entry) => {
|
|
if (entry.type === 'link') {
|
|
return { ...entry, isCurrent: entry.href === pathname };
|
|
}
|
|
const children = markCurrent(entry.entries, pathname);
|
|
const contains = children.some(
|
|
(child) => (child.type === 'link' && child.isCurrent) || (child.type === 'group' && !child.collapsed),
|
|
);
|
|
return { ...entry, entries: children, collapsed: entry.collapsed && !contains };
|
|
});
|
|
}
|
|
|
|
/** Flattens the sidebar into the reading order used for the previous and next links. */
|
|
export function flattenSidebar(entries: SidebarEntry[]): SidebarLink[] {
|
|
return entries.flatMap((entry) =>
|
|
entry.type === 'link' ? [entry] : flattenSidebar(entry.entries),
|
|
);
|
|
}
|