| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- #!/usr/bin/env node
- /**
- * 文档结构校验 —— 防止 Markdown 表格静默失真
- *
- * 为什么需要这个脚本:
- * 2026-10-14 连续两轮评审都发现「表格被空行/blockquote 截断」导致渲染失真,
- * 而文件本身"改成功了"。人眼极难发现,必须机器校验。
- *
- * 检查项:
- * 1. 表格被空行或 blockquote 截断(表体中间出现空行/`>` 后仍有 `|` 行)
- * 2. 表头被段落文本紧贴(GFM 规范:表格不能打断段落,否则整表不渲染)
- * 3. 未闭合的表格行(以 `|` 开头但不以 `|` 结尾)
- * 4. 表格列数不一致(数据行列数与表头不符)
- *
- * 用法:
- * node scripts/check-doc-structure.mjs # 检查全部 docs/
- * node scripts/check-doc-structure.mjs docs/ 2026-10-14 # 限定路径
- *
- * 退出码: 0 = 全部通过;1 = 发现问题
- */
- import { readFileSync, readdirSync } from 'node:fs';
- const root = process.argv[2] || 'docs';
- const SEP = /^\s*\|[\s:|-]+\|\s*$/;
- /**
- * 数表格列数。必须先把转义竖线 `\|` 与行内代码里的 `|` 排除,
- * 否则 `| a \| b | c |` 会被误判为 4 列(实际 3 列)。
- */
- function countCols(line) {
- // 1. 去掉行内代码段(`...`),其中的 | 不算分隔
- const noCode = line.replace(/`[^`]*`/g, '§');
- // 2. 转义竖线 → 占位符
- const noEsc = noCode.replace(/\\\|/g, '§');
- // 3. 去掉首尾竖线后按 | 切分
- const t = noEsc.trim().replace(/^\|/, '').replace(/\|$/, '');
- return t.split('|').length;
- }
- /** 收集所有 .md */
- function collect(dir) {
- const out = [];
- const walk = (d) => {
- let ents;
- try { ents = readdirSync(d, { withFileTypes: true }); } catch { return; }
- for (const e of ents) {
- const p = `${d}/${e.name}`;
- if (e.isDirectory()) walk(p);
- else if (e.name.endsWith('.md')) out.push(p);
- }
- };
- walk(dir);
- return out;
- }
- const files = collect(root);
- if (!files.length) {
- console.log(`[check-doc-structure] ${root} 下无 .md 文件`);
- process.exit(0);
- }
- const problems = [];
- for (const file of files) {
- const lines = readFileSync(file, 'utf8').split('\n');
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- // ---- 检查 2:表头被段落紧贴 ----
- if (SEP.test(line) && i >= 2) {
- const hdr = lines[i - 1].trim();
- const prev = lines[i - 2].trim();
- if (hdr.startsWith('|') && prev &&
- !prev.startsWith('#') && !prev.startsWith('|') &&
- !prev.startsWith('>') && !prev.startsWith('-') &&
- !prev.startsWith('*') && !prev.startsWith('`')) {
- problems.push({
- file, line: i - 1, kind: '表头被段落紧贴',
- detail: `表格不会渲染。需在表头前加空行。前文:「${prev.slice(0, 50)}」`,
- });
- }
- }
- // ---- 检查 1:表格被空行/blockquote 截断 ----
- if (SEP.test(line) && i >= 1 && lines[i - 1].trim().startsWith('|')) {
- const cols = countCols(lines[i - 1]);
- let j = i + 1;
- while (j < lines.length) {
- const s = lines[j].trim();
- if (s.startsWith('|')) {
- // ---- 检查 4:列数一致性 ----
- const n = countCols(lines[j]);
- if (n !== cols) {
- problems.push({
- file, line: j + 1, kind: '表格列数不一致',
- detail: `表头 ${cols - 2} 列,本行 ${n - 2} 列`,
- });
- }
- j++;
- continue;
- }
- // 表体中断:空行或 blockquote
- if (s === '' || s.startsWith('>')) {
- let k = j + 1;
- while (k < lines.length && lines[k].trim() === '') k++;
- if (k < lines.length && lines[k].trim().startsWith('|')) {
- // 中断后仍是表格行 —— 但要区分两种情况:
- // a) 新表的正常开始:该行是表头,且下一行是分隔线 → 合法,不是缺陷
- // b) 原表的孤立续行:该行之后不是分隔线 → 缺陷(失去表头)
- const isNewTable = k + 1 < lines.length && SEP.test(lines[k + 1]);
- if (!isNewTable) {
- problems.push({
- file, line: k + 1, kind: '表格被中断后出现孤立行',
- detail: `第 ${j + 1} 行是${s === '' ? '空行' : 'blockquote'},表格在此终止,` +
- `但第 ${k + 1} 行仍是表格行且其后无分隔线 → 该行失去表头,渲染为裸文本`,
- });
- }
- }
- }
- break;
- }
- }
- // ---- 检查 3:未闭合表格行 ----
- const t = line.trim();
- if (t.startsWith('|') && !t.endsWith('|') && t.length > 1) {
- problems.push({
- file, line: i + 1, kind: '未闭合的表格行',
- detail: `以 | 开头但未以 | 结尾:「${t.slice(0, 60)}」`,
- });
- }
- }
- }
- if (problems.length) {
- console.error(`\n[check-doc-structure] ✗ 发现 ${problems.length} 处结构问题:\n`);
- for (const p of problems) {
- console.error(` ${p.file}:${p.line}`);
- console.error(` [${p.kind}] ${p.detail}\n`);
- }
- console.error('[check-doc-structure] 提示:Markdown 表格必须连续,说明性内容放表格之后。\n');
- process.exit(1);
- }
- console.log(`[check-doc-structure] ✓ ${files.length} 个文件,表格结构全部正常`);
|