文件
phonerail/site/scripts/check-anchors.mjs
T
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

86 行
3.3 KiB
JavaScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// 校验侧边栏 / 导航里生成的锚点,在构建出来的 HTML 里真的存在。
//
// 侧边栏是 config.mts 用一份**自己实现的** slugify 从源稿的 h2 现算出来的
// (@mdit-vue/shared 打包在 vitepress 内部,外面 import 不到)。
// 只要那份实现和 VitePress 的哪怕差一个字符,链接就会静默跳空 —— 页面照样打开,
// 只是不滚动,肉眼极难发现。所以每次 build 之后自动比一遍。
//
// 多页之后还多了一类错:锚点对了但**页面指错了**(/#xxx 写成 /plan#xxx)。
// 所以这里按「路由 → 该页的 id 集合」建表,逐条链接校验它落在正确的页上。
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const here = path.dirname(fileURLToPath(import.meta.url))
const DIST = path.resolve(here, '../.vitepress/dist')
if (!fs.existsSync(DIST)) {
console.error(`[check-anchors] 没有构建产物:${DIST}`)
process.exit(1)
}
// VitePress 自己注入的锚点,不是正文标题
const BUILTIN = new Set(['VPContent', 'VPSidebarNav'])
/** dist 下的全部 html(不含 404) */
function pages(dir, out = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name)
if (e.isDirectory()) pages(p, out)
else if (e.name.endsWith('.html') && e.name !== '404.html') out.push(p)
}
return out
}
/** 文件路径 → cleanUrls 下的路由:index.html -> '/',plan.html -> '/plan' */
function routeOf(file) {
const rel = path.relative(DIST, file).split(path.sep).join('/').replace(/\.html$/, '')
return rel === 'index' ? '/' : '/' + rel.replace(/\/index$/, '')
}
const idsByRoute = new Map()
const files = pages(DIST)
for (const f of files) {
const html = fs.readFileSync(f, 'utf8')
const ids = new Set([...html.matchAll(/<h[1-6][^>]*\sid="([^"]+)"/g)].map((m) => m[1]))
idsByRoute.set(routeOf(f), ids)
}
let checked = 0
const bad = []
for (const f of files) {
const from = routeOf(f)
const html = fs.readFileSync(f, 'utf8')
for (const m of html.matchAll(/href="([^"]*#[^"]+)"/g)) {
const href = m[1]
const hash = decodeURIComponent(href.slice(href.indexOf('#') + 1))
if (BUILTIN.has(hash)) continue
const rawPath = href.slice(0, href.indexOf('#'))
if (rawPath && !rawPath.startsWith('/')) continue // 外链
const route = rawPath === '' ? from : rawPath === '/' ? '/' : rawPath.replace(/\/$/, '')
checked++
const ids = idsByRoute.get(route)
if (!ids) bad.push(`${from}:链接指向不存在的页面 ${route}`)
else if (!ids.has(hash)) bad.push(`${from}:${route}#${hash} —— 该页没有这个标题`)
}
}
if (bad.length) {
console.error(`\n[check-anchors] ✗ ${bad.length} 条锚点对不上:`)
for (const b of [...new Set(bad)]) console.error(` ${b}`)
console.error(`\n 多半是 config.mts 里的 slugify() 与 VitePress 不一致,或侧边栏的 base 写错了页。`)
for (const [route, ids] of idsByRoute) {
console.error(` ${route} 的标题 id:`)
for (const id of ids) console.error(` #${id}`)
}
process.exit(1)
}
const total = [...idsByRoute.values()].reduce((n, s) => n + s.size, 0)
console.log(
`[check-anchors] ✓ ${checked} 条锚点全部对得上` +
`(${idsByRoute.size} 个页面,共 ${total} 个标题)`
)