import { getLocalizedHref, localizeDocsHref, translateLabel, } from '../i18n'; import { type DocsNavConfig, type LotusThemeConfig, type SidebarAutogenerateItem, type SidebarGroupItem, type SidebarItemConfig, type SidebarLinkItem, } from '../theme'; import type { DocsSidebarGroupNav, DocsSidebarItem, DocsSidebarNav, ResolvedSidebars, SidebarContentEntry, SidebarIssue, } from './types'; import { generateSidebarItems, getAutogeneratedEntries, normalizeAutogenerateDirectory, } from './autogenerate'; function slugToTitle(slug: string): string { const segment = slug.split('/').filter(Boolean).at(-1) ?? slug; return segment .replace(/[-_]+/g, ' ') .replace(/\b\w/g, (letter) => letter.toUpperCase()); } export function slugifyLabel(label: string): string { return label .trim() .toLowerCase() .replace(/&/g, 'and') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); } export function getSidebarSectionSlug(sidebar: DocsNavConfig): string { return sidebar.slug ?? slugifyLabel(sidebar.label); } function normalizeSlug(slug: string): string { return slug.trim().replace(/^\/+|\/+$/g, '') || 'index'; } function isExternalLink(link: string): boolean { return /^[a-z][a-z\d+.-]*:/i.test(link) || link.startsWith('//'); } function isLinkItem(item: SidebarItemConfig): item is SidebarLinkItem { return typeof item === 'object' && item !== null && 'link' in item; } function isGroupItem(item: SidebarItemConfig): item is SidebarGroupItem { return typeof item === 'object' && item !== null && 'items' in item; } function isAutogenerateItem(item: SidebarItemConfig): item is SidebarAutogenerateItem { return typeof item === 'object' && item !== null && 'autogenerate' in item; } function getEntryMap(entries: SidebarContentEntry[]): Map { return new Map(entries.map((entry) => [entry.slug, entry])); } function getEntryForSlug( entriesBySlug: Map, slug: string, ): SidebarContentEntry | undefined { return entriesBySlug.get(normalizeSlug(slug)); } function entryToSidebarItem( config: LotusThemeConfig, entry: SidebarContentEntry, localeKey: string, ): DocsSidebarItem { return { label: entry.title, href: getLocalizedHref(config, entry.slug, localeKey), slug: entry.slug, icon: entry.icon, }; } function slugToSidebarItem( config: LotusThemeConfig, entriesBySlug: Map, slug: string, localeKey: string, sectionSlug: string, issues: SidebarIssue[], ): DocsSidebarItem { const normalizedSlug = normalizeSlug(slug); const entry = getEntryForSlug(entriesBySlug, normalizedSlug); if (entry) { return entryToSidebarItem(config, entry, localeKey); } issues.push({ type: 'missing-entry', sectionSlug, slug: normalizedSlug, }); return { label: slugToTitle(normalizedSlug), href: getLocalizedHref(config, normalizedSlug, localeKey), slug: normalizedSlug, }; } function linkToSidebarItem( config: LotusThemeConfig, item: SidebarLinkItem, localeKey: string, ): DocsSidebarItem { const external = item.external ?? isExternalLink(item.link); return { label: translateLabel(item.label, item.translations, localeKey), href: external ? item.link : localizeDocsHref(config, item.link, localeKey), external, icon: item.icon, badge: item.badge, slug: item.slug, }; } async function resolveSidebarItems(items: DocsNavConfig['items']): Promise { return await items; } async function normalizeSidebarItems( config: LotusThemeConfig, items: SidebarItemConfig[], entries: SidebarContentEntry[], entriesBySlug: Map, localeKey: string, sectionSlug: string, issues: SidebarIssue[], ): Promise { const resolvedItems = await Promise.all(items.map(async (item): Promise => { if (typeof item === 'string') { return [slugToSidebarItem(config, entriesBySlug, item, localeKey, sectionSlug, issues)]; } if (isAutogenerateItem(item)) { const autogeneratedEntries = getAutogeneratedEntries(entries, item.autogenerate); if (autogeneratedEntries.length === 0) { issues.push({ type: 'empty-autogenerate', sectionSlug, directory: normalizeAutogenerateDirectory(item.autogenerate.directory), }); } return generateSidebarItems(config, entries, item.autogenerate, localeKey); } if (isGroupItem(item)) { return [ { collapsed: item.collapsed, label: translateLabel(item.label, item.translations, localeKey), icon: item.icon, items: await normalizeSidebarItems( config, await resolveSidebarItems(item.items), entries, entriesBySlug, localeKey, sectionSlug, issues, ), }, ]; } if (isLinkItem(item)) { return [linkToSidebarItem(config, item, localeKey)]; } return []; })); return resolvedItems.flat(); } async function resolveSidebar( config: LotusThemeConfig, sidebar: DocsNavConfig, entries: SidebarContentEntry[], localeKey: string, issues: SidebarIssue[], ): Promise { const entriesBySlug = getEntryMap(entries); const sectionSlug = getSidebarSectionSlug(sidebar); const links: DocsSidebarItem[] = []; const groups: DocsSidebarGroupNav[] = []; for (const item of await resolveSidebarItems(sidebar.items)) { if (isGroupItem(item)) { groups.push({ title: translateLabel(item.label, item.translations, localeKey), items: await normalizeSidebarItems( config, await resolveSidebarItems(item.items), entries, entriesBySlug, localeKey, sectionSlug, issues, ), }); } else { links.push(...await normalizeSidebarItems( config, [item], entries, entriesBySlug, localeKey, sectionSlug, issues, )); } } return { links, groups }; } function getDuplicateSectionIssues(sidebars: DocsNavConfig[]): SidebarIssue[] { const labelsBySlug = new Map(); for (const sidebar of sidebars) { const sectionSlug = getSidebarSectionSlug(sidebar); const labels = labelsBySlug.get(sectionSlug) ?? []; labels.push(sidebar.label); labelsBySlug.set(sectionSlug, labels); } return [...labelsBySlug.entries()] .filter(([, labels]) => labels.length > 1) .map(([sectionSlug, labels]) => ({ type: 'duplicate-section', sectionSlug, labels, })); } export async function resolveSidebars( config: LotusThemeConfig, entries: SidebarContentEntry[], localeKey: string, ): Promise { const issues = getDuplicateSectionIssues(config.docsNav); const sidebars = Object.fromEntries(await Promise.all( config.docsNav.map(async (sidebar) => { const sectionSlug = getSidebarSectionSlug(sidebar); return [sectionSlug, await resolveSidebar(config, sidebar, entries, localeKey, issues)]; }), )); return { sidebars, issues }; }