[fix] Preserve index trailing slash

This commit is contained in:
He
2026-03-20 20:06:01 +08:00
parent bdea1783fe
commit 8faf684391
2 changed files with 88 additions and 49 deletions

View File

@@ -49,6 +49,13 @@ type HeadingBucket = {
entries: OrderedSidebarItem[] entries: OrderedSidebarItem[]
} }
type FrontmatterMeta = {
order: number
heading?: string
subheading?: string
subOrder?: number
}
function sortByOrderAndText<T extends { order: number; text: string }>(a: T, b: T) { function sortByOrderAndText<T extends { order: number; text: string }>(a: T, b: T) {
if (a.order === b.order) return a.text.localeCompare(b.text) if (a.order === b.order) return a.text.localeCompare(b.text)
return a.order - b.order return a.order - b.order
@@ -93,6 +100,80 @@ function toLink(relativePath: string) {
return normalizeLink(relativePath ? `/${relativePath}` : '/') return normalizeLink(relativePath ? `/${relativePath}` : '/')
} }
function toDirectoryLink(relativePath: string) {
if (!relativePath) return '/'
return normalizeLink(`/${relativePath}/`)
}
function toDocMeta(absDir: string, relativeDir: string, fileName: string): SidebarDocMeta {
const full = path.join(absDir, fileName)
const { order, title } = readFrontmatterAndTitle(full)
const baseName = path.basename(fileName, '.md')
const rel = relativeDir ? `${relativeDir}/${baseName}` : baseName
// index.md -> 目录根路径(保持尾斜杠,确保 VitePress active/prev-next 正常匹配)
const link = baseName === 'index' ? toDirectoryLink(relativeDir) : toLink(rel)
return {
order,
text: title,
link
}
}
function toOrderedSidebarItem(doc: SidebarDocMeta): OrderedSidebarItem {
return {
order: doc.order,
text: doc.text,
item: { text: doc.text, link: doc.link },
source: 'doc'
}
}
function toOrderedSidebarItems(docs: SidebarDocMeta[]): OrderedSidebarItem[] {
return docs.map(toOrderedSidebarItem)
}
function createSubheadingEntry(
label: string,
order: number,
subOrder: number | undefined,
items: SidebarItem[]
): OrderedSidebarItem {
return {
order,
text: label,
subOrder,
source: 'subheading',
item: {
text: label,
collapsed: true,
items
}
}
}
function pushEntryToContainer(
entry: OrderedSidebarItem,
rootEntries: OrderedSidebarItem[],
headingBuckets: Map<string, HeadingBucket>,
targetHeadingKey?: string
) {
if (targetHeadingKey && headingBuckets.has(targetHeadingKey)) {
headingBuckets.get(targetHeadingKey)!.entries.push(entry)
return
}
rootEntries.push(entry)
}
function readIndexMeta(dirAbsPath: string): FrontmatterMeta | undefined {
const indexPath = path.join(dirAbsPath, 'index.md')
if (!fs.existsSync(indexPath)) return undefined
const { order, heading, subheading, subOrder } = readFrontmatterAndTitle(indexPath)
return { order, heading, subheading, subOrder }
}
function readFrontmatterAndTitle(filePath: string) { function readFrontmatterAndTitle(filePath: string) {
const raw = fs.readFileSync(filePath, 'utf8') const raw = fs.readFileSync(filePath, 'utf8')
const fmMatch = raw.match(/^---\s*([\s\S]*?)\s*---/) const fmMatch = raw.match(/^---\s*([\s\S]*?)\s*---/)
@@ -132,21 +213,7 @@ function listMarkdownFiles(absDir: string, includeIndex: boolean) {
function listMarkdownItems(absDir: string, relativeDir: string, includeIndex: boolean) { function listMarkdownItems(absDir: string, relativeDir: string, includeIndex: boolean) {
return listMarkdownFiles(absDir, includeIndex) return listMarkdownFiles(absDir, includeIndex)
.map((name) => { .map((name) => toDocMeta(absDir, relativeDir, name))
const full = path.join(absDir, name)
const { order, title } = readFrontmatterAndTitle(full)
const baseName = path.basename(name, '.md')
const rel = relativeDir ? `${relativeDir}/${baseName}` : baseName
// index.md -> 目录根路径
const link = baseName === 'index' ? toLink(relativeDir) : toLink(rel)
return {
order,
text: title,
link
}
})
.sort(sortByOrderAndText) .sort(sortByOrderAndText)
} }
@@ -154,15 +221,6 @@ function toSidebarLinkItems(items: SidebarDocMeta[]): SidebarItem[] {
return items.map(({ text, link }): SidebarItem => ({ text, link })) return items.map(({ text, link }): SidebarItem => ({ text, link }))
} }
function toOrderedSidebarItems(items: SidebarDocMeta[]): OrderedSidebarItem[] {
return items.map((doc) => ({
order: doc.order,
text: doc.text,
item: { text: doc.text, link: doc.link },
source: 'doc'
}))
}
function sortBySubOrderAndText<T extends { subOrder?: number; text: string }>(a: T, b: T) { function sortBySubOrderAndText<T extends { subOrder?: number; text: string }>(a: T, b: T) {
const aOrder = a.subOrder ?? Number.POSITIVE_INFINITY const aOrder = a.subOrder ?? Number.POSITIVE_INFINITY
const bOrder = b.subOrder ?? Number.POSITIVE_INFINITY const bOrder = b.subOrder ?? Number.POSITIVE_INFINITY
@@ -236,24 +294,8 @@ function generateSidebarGroups(entry: SidebarAutoItem): OrderedGroup[] {
subOrder: number | undefined, subOrder: number | undefined,
items: SidebarItem[] items: SidebarItem[]
) => { ) => {
const entry: OrderedSidebarItem = { const entry = createSubheadingEntry(label, order, subOrder, items)
order, pushEntryToContainer(entry, rootEntries, headingBuckets, targetHeadingKey)
text: label,
subOrder,
source: 'subheading',
item: {
text: label,
collapsed: true,
items
}
}
if (targetHeadingKey && headingBuckets.has(targetHeadingKey)) {
headingBuckets.get(targetHeadingKey)!.entries.push(entry)
return
}
rootEntries.push(entry)
} }
const walkDirectories = (parentAbsDir: string, parentRelativeDir: string, activeHeadingKey?: string) => { const walkDirectories = (parentAbsDir: string, parentRelativeDir: string, activeHeadingKey?: string) => {
@@ -264,10 +306,7 @@ function generateSidebarGroups(entry: SidebarAutoItem): OrderedGroup[] {
const childAbsDir = path.join(parentAbsDir, dirent.name) const childAbsDir = path.join(parentAbsDir, dirent.name)
const childRelativeDir = parentRelativeDir ? `${parentRelativeDir}/${dirent.name}` : dirent.name const childRelativeDir = parentRelativeDir ? `${parentRelativeDir}/${dirent.name}` : dirent.name
const childItems = listMarkdownItems(childAbsDir, childRelativeDir, true) const childItems = listMarkdownItems(childAbsDir, childRelativeDir, true)
const indexPath = path.join(childAbsDir, 'index.md') const meta = readIndexMeta(childAbsDir)
const meta = fs.existsSync(indexPath)
? readFrontmatterAndTitle(indexPath)
: undefined
let nextActiveHeadingKey = activeHeadingKey let nextActiveHeadingKey = activeHeadingKey

View File

@@ -1,8 +1,8 @@
{ {
"scripts": { "scripts": {
"docs:dev": "vitepress dev", "dev": "vitepress dev",
"docs:build": "vitepress build", "build": "vitepress build",
"docs:preview": "vitepress preview" "preview": "vitepress preview"
}, },
"dependencies": { "dependencies": {
"fs": "^0.0.1-security", "fs": "^0.0.1-security",