import type { App, Page } from 'vuepress' import type { ResolvedSidebarItem, ThemeDocCollection, ThemeIcon, ThemePageData, ThemeSidebar, ThemeSidebarItem, } from '../../shared/index.js' import { deleteKey } from '@pengzhanbo/utils' import { ensureLeadingSlash, entries, isArray, isPlainObject, removeLeadingSlash, } from '@vuepress/helper' import { findCollection } from '../collections/index.js' import { getThemeConfig } from '../loadConfig/index.js' import { normalizeLink, perf, resolveContent, writeTemp } from '../utils/index.js' /** * Prepare sidebar data * * 准备侧边栏数据,处理所有语言环境的侧边栏配置并生成临时文件 */ export async function prepareSidebar(app: App): Promise { perf.mark('prepare:sidebar') const sidebar = getAllSidebar() const { resolved, autoHome } = getSidebarData(app, sidebar) sidebar.__auto__ = resolved sidebar.__home__ = autoHome as any await writeTemp(app, 'internal/sidebar.js', resolveContent(app, { name: 'sidebar', content: sidebar })) perf.log('prepare:sidebar') } function getSidebarData( app: App, locales: Record, ): { resolved: ThemeSidebar, autoHome: Record } { const autoDirList: string[] = [] const resolved: ThemeSidebar = {} entries(locales).forEach(([localePath, sidebar]) => { if (!sidebar) return if (isArray(sidebar)) { autoDirList.push(...findAutoDirList(sidebar)) } else if (isPlainObject(sidebar)) { entries(sidebar).forEach(([dirname, config]) => { const prefix = normalizeLink(localePath, removeLeadingSlash(dirname)) if (config === 'auto') { autoDirList.push(prefix) } else if (isArray(config)) { autoDirList.push(...findAutoDirList(config, prefix)) } else if (config.items === 'auto') { autoDirList.push(normalizeLink(prefix, config.prefix)) } else { autoDirList.push( ...findAutoDirList( config.items || [], normalizeLink(prefix, config.prefix), ), ) } }) } else if (sidebar === 'auto') { autoDirList.push(localePath) } }) const autoHome: Record = {} autoDirList.forEach((localePath) => { const { link, sidebar } = getAutoDirSidebar(app, localePath) resolved[localePath] = sidebar if (link) { autoHome[localePath] = link } }) return { resolved, autoHome } } const MD_RE = /\.md$/ const NUMBER_RE = /^\d+\./ function resolveTitle(dirname: string) { return dirname .replace(MD_RE, '') .replace(NUMBER_RE, '') } const RE_FILE_SORTING = /(?:(\d+)\.)?(?=[^/]+$)/ function fileSorting(filepath?: string): number | false { if (!filepath) return false const matched = filepath.match(RE_FILE_SORTING) const sorted = matched ? Number(matched[1]) : 0 if (Number.isNaN(sorted)) return Number.MAX_SAFE_INTEGER return sorted } function getAutoDirSidebar( app: App, prefix: string, ): { link: string, sidebar: ThemeSidebarItem[] } { const rootPath = removeLeadingSlash(prefix) let pages = (app.pages as Page[]) .filter(page => page.data.filePathRelative?.startsWith(rootPath)) .map((page) => { return { ...page, splitPath: page.data.filePathRelative?.split('/') || [] } }) const maxIndex = Math.max(...pages.map(page => page.splitPath.length)) let nowIndex = maxIndex - 1 while (nowIndex >= 0) { pages = pages.sort((prev, next) => { const pi = fileSorting(prev.splitPath?.[nowIndex]) const ni = fileSorting(next.splitPath?.[nowIndex]) if (pi === false || ni === false) return 0 if (pi === ni) return 0 return pi < ni ? -1 : 1 }) nowIndex-- } const RE_INDEX = ['index.md', 'README.md', 'readme.md'] const sidebar: ResolvedSidebarItem[] = [] let rootLink = '' for (const page of pages) { const { data, title, path, frontmatter } = page const paths = (data.filePathRelative || '') .slice(rootPath.replace(/^\/|\/$/g, '').length + 1) .split('/') const collection = findCollection(page) as ThemeDocCollection | undefined let index = 0 let dir: string let items = sidebar let parent: ResolvedSidebarItem | undefined // eslint-disable-next-line no-cond-assign while ((dir = paths[index])) { const text = resolveTitle(dir) const isHome = RE_INDEX.includes(dir) let current = items.find(item => item.text === text) if (!current) { current = { text, link: undefined, items: [], collapsed: collection?.sidebarCollapsed } as ResolvedSidebarItem if (!isHome) { items.push(current) } } if (dir.endsWith('.md')) { if (isHome) { if (parent) { parent.link = path } else { rootLink = path } } else { current.link = path current.text = title } } if (frontmatter.icon && dir.endsWith('.md')) { current.icon = frontmatter.icon as ThemeIcon } if (parent?.items?.length) { parent.collapsed ??= false } parent = current items = current.items as ResolvedSidebarItem[] index++ } } return { link: rootLink, sidebar: cleanSidebar(sidebar) } } function cleanSidebar(sidebar: (ThemeSidebarItem)[]) { for (const item of sidebar) { if (isPlainObject(item)) { if (isArray(item.items)) { if (item.items.length === 0) { deleteKey(item, ['items', 'collapsed']) } else { cleanSidebar(item.items as ThemeSidebarItem[]) } } else if (!('items' in item)) { deleteKey(item, 'collapsed') } } } return sidebar } function findAutoDirList(sidebar: (string | ThemeSidebarItem)[], prefix = ''): string[] { const list: string[] = [] if (!sidebar.length) return list sidebar.forEach((item) => { if (isPlainObject(item)) { const nextPrefix = normalizeLink(prefix, item.prefix || item.dir) if (item.items === 'auto') { list.push(nextPrefix) } else if (item.items?.length) { list.push(...findAutoDirList(item.items, nextPrefix)) } } }) return list } function getAllSidebar(): Record { const options = getThemeConfig() const locales: Record = {} for (const [locale, opt] of entries(options.locales || {})) { const rawCollections = locale === '/' ? (opt.collections || options.collections) : opt.collections const sidebar = locale === '/' ? (opt.sidebar || options.sidebar) : opt.sidebar locales[locale] = {} for (const [key, value] of entries(sidebar || {})) { locales[locale][ensureLeadingSlash(key)] = value } const collections = rawCollections?.filter(item => item.type === 'doc') if (collections?.length) { for (const collection of collections) { if (collection.sidebar) { locales[locale][normalizeLink(collection.linkPrefix || collection.dir)] = { items: collection.sidebar, prefix: normalizeLink(locale, removeLeadingSlash(collection.dir)), } } } } } return locales }