260 lines
7.5 KiB
TypeScript

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<void> {
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<string, ThemeSidebar>,
): { resolved: ThemeSidebar, autoHome: Record<string, string> } {
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<string, string> = {}
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<ThemePageData>[])
.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<string, ThemeSidebar> {
const options = getThemeConfig()
const locales: Record<string, ThemeSidebar> = {}
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)] = isPlainObject(value) && 'items' in value
? { ...value, prefix: value.prefix?.startsWith('/') ? value.prefix : normalizeLink(locale, removeLeadingSlash(key)) }
: {
items: value,
prefix: normalizeLink(locale, removeLeadingSlash(key)),
}
}
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
}