import { getLocalizedHref } from '../i18n'; import type { LotusThemeConfig, SidebarAutogenerateItem } from '../theme'; import type { DocsNavItem, DocsSidebarItem, SidebarContentEntry, } from './types'; type AutogenerateOptions = SidebarAutogenerateItem['autogenerate']; export function normalizeAutogenerateDirectory(directory: string): string { const normalizedDirectory = directory.trim().replace(/^\/+|\/+$/g, ''); return normalizedDirectory === '.' ? '' : normalizedDirectory; } function normalizeSlug(slug: string): string { return slug.trim().replace(/^\/+|\/+$/g, ''); } function escapeRegExp(value: string): string { return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&'); } function globToRegExp(pattern: string): RegExp { const normalizedPattern = normalizeSlug(pattern); let source = ''; for (let index = 0; index < normalizedPattern.length; index += 1) { const character = normalizedPattern[index]; const nextCharacter = normalizedPattern[index + 1]; if (character === '*' && nextCharacter === '*') { source += '.*'; index += 1; continue; } if (character === '*') { source += '[^/]*'; continue; } source += escapeRegExp(character); } return new RegExp(`^${source}$`); } function toExcludePatterns(exclude: AutogenerateOptions['exclude']): string[] { return Array.isArray(exclude) ? exclude : exclude ? [exclude] : []; } function isExcludedEntry( entry: SidebarContentEntry, relativeSlug: string, options: AutogenerateOptions, ): boolean { const patterns = toExcludePatterns(options.exclude); return patterns.some((pattern) => { const matcher = globToRegExp(pattern); return matcher.test(entry.slug) || matcher.test(relativeSlug); }); } function getRelativeSlug(entrySlug: string, directory: string): string { if (!directory) { return entrySlug; } return entrySlug === directory ? 'index' : entrySlug.slice(directory.length + 1); } function getRelativeDepth(relativeSlug: string): number { const segments = relativeSlug.split('/').filter(Boolean); if (segments.at(-1) === 'index') { return Math.max(segments.length - 1, 1); } return segments.length; } function sortNavItems(items: DocsNavItem[]): DocsNavItem[] { return [...items].sort((left, right) => { if (left.order !== right.order) { return left.order - right.order; } return left.title.localeCompare(right.title); }); } function entryToNavItem( config: LotusThemeConfig, entry: SidebarContentEntry, section: string | undefined, localeKey: string, ): DocsNavItem { return { title: entry.title, href: getLocalizedHref(config, entry.slug, localeKey), slug: entry.slug, section, icon: entry.icon, order: entry.order, }; } function navItemToSidebarItem(item: DocsNavItem): DocsSidebarItem { return { label: item.title, href: item.href, order: item.order, slug: item.slug, icon: item.icon, }; } export function getAutogeneratedEntries( entries: SidebarContentEntry[], options: AutogenerateOptions, ): SidebarContentEntry[] { const normalizedDirectory = normalizeAutogenerateDirectory(options.directory); const maxDepth = options.depth && options.depth > 0 ? Math.floor(options.depth) : normalizedDirectory ? Number.POSITIVE_INFINITY : 1; return entries.filter((entry) => { if (entry.hidden || entry.slug === 'index') { return false; } if (normalizedDirectory && !entry.slug.startsWith(`${normalizedDirectory}/`)) { return false; } if (!normalizedDirectory && entry.slug === 'index') { return false; } const relativeSlug = getRelativeSlug(entry.slug, normalizedDirectory); if (!relativeSlug || getRelativeDepth(relativeSlug) > maxDepth) { return false; } return !isExcludedEntry(entry, relativeSlug, options); }); } function getFolderLabel(segment: string): string { return segment .replace(/[-_]+/g, ' ') .replace(/\b\w/g, (letter) => letter.toUpperCase()); } function createTreeItems( config: LotusThemeConfig, entries: SidebarContentEntry[], directory: string, localeKey: string, ): DocsSidebarItem[] { const rootItems: DocsSidebarItem[] = []; const groups = new Map(); for (const entry of entries) { const relativeSlug = getRelativeSlug(entry.slug, directory); const segments = relativeSlug.split('/').filter(Boolean); if (segments.length <= 1) { rootItems.push(navItemToSidebarItem(entryToNavItem(config, entry, undefined, localeKey))); continue; } const parentSegments = segments.slice(0, -1); let siblings = rootItems; let currentPath = ''; for (const segment of parentSegments) { currentPath = [currentPath, segment].filter(Boolean).join('/'); let group = groups.get(currentPath); if (!group) { group = { label: getFolderLabel(segment), items: [], }; groups.set(currentPath, group); siblings.push(group); } siblings = group.items ?? []; } siblings.push(navItemToSidebarItem(entryToNavItem(config, entry, undefined, localeKey))); } return sortTreeItems(rootItems); } function sortTreeItems(items: DocsSidebarItem[]): DocsSidebarItem[] { return [...items].sort((left, right) => { const leftOrder = getSidebarItemOrder(left); const rightOrder = getSidebarItemOrder(right); if (leftOrder !== rightOrder) { return leftOrder - rightOrder; } return left.label.localeCompare(right.label); }).map((item) => ({ ...item, items: item.items ? sortTreeItems(item.items) : undefined, })); } function getSidebarItemOrder(item: DocsSidebarItem): number { if (item.order !== undefined) { return item.order; } const childOrders = item.items?.map(getSidebarItemOrder) ?? []; return Math.min(...childOrders, Number.MAX_SAFE_INTEGER); } export function generateSidebarItems( config: LotusThemeConfig, entries: SidebarContentEntry[], options: AutogenerateOptions, localeKey: string, ): DocsSidebarItem[] { const autogeneratedEntries = getAutogeneratedEntries(entries, options); const normalizedDirectory = normalizeAutogenerateDirectory(options.directory); if (options.structure === 'tree') { return createTreeItems(config, autogeneratedEntries, normalizedDirectory, localeKey); } return sortNavItems( autogeneratedEntries.map((entry) => entryToNavItem(config, entry, undefined, localeKey), ), ).map(navItemToSidebarItem); }