import { defineConfig } from 'vitepress' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) /** * 侧边栏直接读**项目根**的原稿,不读 docs/ 里的副本 —— 保证目录永远跟着正文走。 * (docs/ 里的 index.md 由 scripts/sync-docs.mjs 在 predev / prebuild 时拷进来, * 拷贝的原因见那个脚本顶部的注释。) */ const DOC = path.resolve(__dirname, '../../需求文档.md') const PLAN = path.resolve(__dirname, '../../固件开发计划.md') /** * 与 VitePress 内部(@mdit-vue/shared)一致的 slugify。 * 复制一份是因为 @mdit-vue/shared 打包在 vitepress 里,外部 import 不到。 * 生成的锚点必须和正文里

的 id 完全一致,改动前先跑 `node scripts/check-anchors.mjs`。 */ function slugify(str: string): string { return str .normalize('NFKD') .replace(/[\u0300-\u036F]/g, '') .trim() .toLowerCase() .replace(/[\u0000-\u001f]/g, '') .replace(/[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g, '-') .replace(/-{2,}/g, '-') .replace(/^-+|-+$/g, '') .replace(/^(\d)/, '_$1') } const DIAGRAM_DIR = path.resolve(__dirname, '../diagrams') /** * 把 ```diagram=<名字> 代码块换成 diagrams/<名字>.svg 的内联 SVG。 * * 为什么不直接把 SVG 写进 markdown:GitHub 与编辑器会把内联 HTML 吃掉或原样显示, * 原稿就没法看了。所以**源文件里保留 ASCII 框图**(既是可读的降级,也是 alt 内容), * 只在站点构建时替换 —— 和 ⚠️ 告警块那套是同一个思路。 * * ⚠️ **代价:ASCII 与 SVG 是两份。** 改了框图内容两边都要改。 * 找不到对应 svg 时**退回渲染成普通代码块**,站点不会因此崩掉。 */ function inlineDiagram(name: string): string | null { const f = path.join(DIAGRAM_DIR, `${name}.svg`) if (!fs.existsSync(f)) return null const svg = fs.readFileSync(f, 'utf8').replace(/<\?xml[^>]*\?>\s*/, '') return `
${svg}
` } /** 侧边栏标签:去掉 markdown 强调、反引号,以及破折号后面的补充说明 */ function label(text: string): string { return text .replace(/`/g, '') .replace(/\*\*/g, '') .split(/\s*——\s*/)[0] .trim() } /** 从源稿里现读 h2,侧边栏永远跟正文同步 */ function chapters(file: string, base: string): { text: string; link: string }[] { const out: { text: string; link: string }[] = [] let inFence = false for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) { if (/^\s*```/.test(line)) { inFence = !inFence continue } if (inFence) continue const m = /^##\s+(.+?)\s*$/.exec(line) if (!m) continue const raw = m[1].replace(/\s*#+\s*$/, '') out.push({ text: label(raw), link: `${base}#${slugify(raw)}` }) } return out } const CH = chapters(DOC, '/') const CH_PLAN = chapters(PLAN, '/plan') const pick = (prefix: string) => CH.find((c) => c.text.startsWith(prefix))! export default defineConfig({ // docs/index.md = 项目根 需求文档.md 的同步副本。需求文档本身就是首页, // 不另做一个空洞的 landing page。 srcDir: 'docs', lang: 'zh-CN', title: 'PhoneRail', titleTemplate: ':title · PhoneRail 项目文档', description: '五路手机直供电源板 —— 项目需求文档', cleanUrls: true, lastUpdated: false, metaChunk: true, head: [ [ 'link', { rel: 'icon', href: 'data:image/svg+xml,' + encodeURIComponent( '' + '' + '' + '' ) } ], ['meta', { name: 'theme-color', content: '#0f766e' }] ], themeConfig: { siteTitle: 'PhoneRail 需求文档', nav: [ { text: '需求文档', link: '/' }, { text: '固件开发计划', link: '/plan' }, { text: '快速定位', items: [ { text: pick('4.').text, link: pick('4.').link }, { text: pick('5.').text, link: pick('5.').link }, { text: pick('6.').text, link: pick('6.').link }, { text: pick('7.').text, link: pick('7.').link }, { text: pick('9.').text, link: pick('9.').link }, { text: pick('10.').text, link: pick('10.').link }, { text: pick('12.').text, link: pick('12.').link } ] }, { text: '2026-09-06', link: '/#_1-背景与目标' } ], sidebar: [ { text: '需求文档', items: CH, collapsed: false }, { text: '固件开发计划', items: CH_PLAN, collapsed: true } ], outline: { level: [2, 3], label: '本页目录' }, aside: true, docFooter: { prev: false, next: false }, returnToTopLabel: '回到顶部', sidebarMenuLabel: '目录', darkModeSwitchLabel: '主题', lightModeSwitchTitle: '切换到浅色', darkModeSwitchTitle: '切换到深色', search: { provider: 'local', options: { translations: { button: { buttonText: '搜索', buttonAriaLabel: '搜索文档' }, modal: { displayDetails: '显示详情', resetButtonTitle: '清除', backButtonTitle: '返回', noResultsText: '没有找到', footer: { selectText: '选择', navigateText: '切换', closeText: '关闭' } } }, miniSearch: { // 默认分词按非单词字符切,中文会整段变成一个 token,搜不到。 // 用 Intl.Segmenter 按词切一遍,再退回默认切分处理英文 / 型号。 options: { tokenize: (text: string) => { const seg = new Intl.Segmenter('zh-CN', { granularity: 'word' }) const out: string[] = [] for (const { segment, isWordLike } of seg.segment(text)) { if (isWordLike) out.push(segment) } return out }, processTerm: (term: string) => term.toLowerCase() }, searchOptions: { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2 } } } } }, footer: { message: '本文只回答「要做成什么样」;「怎么做到」在 五路直供电源板设计.md 里。', copyright: 'PhoneRail · 五路手机直供电源板' } }, markdown: { lineNumbers: false, theme: { light: 'github-light', dark: 'github-dark' }, config(md) { // ```diagram=xxx → 内联 diagrams/xxx.svg(找不到就照常渲染成代码块) const fence = md.renderer.rules.fence! md.renderer.rules.fence = (tokens, idx, opts, env, self) => { const m = /^diagram=([a-z0-9-]+)$/i.exec(tokens[idx].info.trim()) if (m) { const svg = inlineDiagram(m[1]) if (svg) return svg console.warn(`[diagram] 找不到 diagrams/${m[1]}.svg,退回代码块`) } return fence(tokens, idx, opts, env, self) } // 正文里以 ⚠️ / ❌ 开头的引用块渲染成警告卡片,以 ✅ 开头的渲染成通过卡片。 // 用插件而不是 ::: warning,是为了让 markdown 源文件在 GitHub / 编辑器里也正常。 md.core.ruler.push('callout_blockquote', (state) => { const t = state.tokens for (let i = 0; i < t.length; i++) { if (t[i].type !== 'blockquote_open') continue let text = '' for (let j = i + 1; j < t.length; j++) { if (t[j].type === 'blockquote_close') break if (t[j].type === 'inline') { text = t[j].content break } } if (/^\s*[⚠❗❌]/.test(text)) t[i].attrJoin('class', 'callout callout-warn') else if (/^\s*✅/.test(text)) t[i].attrJoin('class', 'callout callout-ok') } return true }) } } })