#!/usr/bin/env node const fs = require('fs'); const { execSync } = require('child_process'); const path = require('path'); const os = require('os'); // 获取命令行传入的 Markdown 文件路径 const fileArg = process.argv[2]; if (!fileArg) { console.error('❌ 请提供 Markdown 文件路径。用法: node check-mermaid.js '); process.exit(1); } const filePath = path.resolve(fileArg); const fileName = path.basename(filePath); if (!fs.existsSync(filePath)) { console.error(`❌ 文件不存在: ${filePath}`); process.exit(1); } const content = fs.readFileSync(filePath, 'utf-8'); // 正则匹配 ```mermaid ... ``` 代码块,兼容 Windows(\r\n) 和 Linux(\n) const mermaidRegex = /```mermaid\r?\n([\s\S]*?)```/g; let match; let blockCount = 0; let errorCount = 0; // 用于收集所有格式化后的错误报告块 const errorReports = []; while ((match = mermaidRegex.exec(content)) !== null) { blockCount++; const fullMatch = match[0]; const code = match[1]; // 1. 计算在 Markdown 文件中的绝对行号 const textBeforeMatch = content.substring(0, match.index); const startLine = textBeforeMatch.split(/\r?\n/).length; const endLine = startLine + fullMatch.split(/\r?\n/).length - 1; // 创建临时文件存放单个 Mermaid 代码 const tmpFile = path.join(os.tmpdir(), `mermaid-check-${Date.now()}-${blockCount}.mmd`); fs.writeFileSync(tmpFile, code.trim()); try { // 调用 mmdc 进行静默渲染检查 execSync(`npx mmdc -i "${tmpFile}" -o "${tmpFile}.svg" -q`, { stdio: 'pipe' }); } catch (error) { errorCount++; // 2. 净化报错信息:剔除底层执行堆栈 const stderr = error.stderr ? error.stderr.toString() : error.message; const errorLines = stderr.split(/\r?\n/); const cleanErrorLines = []; for (const line of errorLines) { if (line.trim().startsWith('at ') || line.includes('Parser3.parseError') || line.includes('fromText')) { break; } cleanErrorLines.push(line); } const cleanErrorOutput = cleanErrorLines.join('\n').trim(); // 3. 截断代码内容:最少3行,最多6行 const codeLines = code.split(/\r?\n/).filter(l => l.trim() !== ''); let displayCode = ''; if (codeLines.length <= 6) { displayCode = codeLines.join('\n'); } else { const head = codeLines.slice(0, 3).join('\n'); const tail = codeLines.slice(-3).join('\n'); displayCode = `${head}\n ...\n ... (中间省略 ${codeLines.length - 6} 行) ...\n ...\n${tail}`; } // 4. 组装单个错误的 Markdown 块 errorReports.push(`### ❌ 错误 #${errorCount} (代码块 #${blockCount}) - **文档位置:** 第 \`${startLine}\` 行至第 \`${endLine}\` 行 #### 核心错误详情 \`\`\`text ${cleanErrorOutput} \`\`\` #### 代码内容片段 \`\`\`text ${displayCode} \`\`\``); } finally { // 清理临时文件 if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); if (fs.existsSync(`${tmpFile}.svg`)) fs.unlinkSync(`${tmpFile}.svg`); } } // ------------------------------------------------------------------ // 生成最终的 Markdown 报告输出 // ------------------------------------------------------------------ if (errorCount > 0) { // 存在错误的情况 console.log(`## 🚨 Mermaid 语法检查报告\n`); console.log(`**检查文件:** \`${fileName}\``); console.log(`**检查结果:** ❌ 发现 ${errorCount} 处语法错误 (共检测到 ${blockCount} 个代码块)\n`); console.log(`---\n`); console.log(errorReports.join('\n\n---\n\n')); // 返回非零状态码,确保在 CI/CD 或 Git Hook 中能够阻断流程 process.exit(1); } else { // 全部正确或没有代码块的情况 console.log(`## 🎉 Mermaid 语法检查报告\n`); console.log(`**检查文件:** \`${fileName}\``); if (blockCount === 0) { console.log(`**检查结果:** ⚠️ 未检测到 Mermaid 代码块`); } else { console.log(`**检查结果:** ✅ 全部通过 (共检测到 ${blockCount} 个代码块)`); } process.exit(0); }