Skip to content

Instantly share code, notes, and snippets.

@rhinoceros
Last active January 23, 2026 08:51
Show Gist options
  • Select an option

  • Save rhinoceros/d14a833b7ddcc7b3e740097e03fa4546 to your computer and use it in GitHub Desktop.

Select an option

Save rhinoceros/d14a833b7ddcc7b3e740097e03fa4546 to your computer and use it in GitHub Desktop.

scripts/check-mermaid.js

#!/usr/bin/env node
/**
 * Mermaid 语法检测脚本
 * 用法: node scripts/check-mermaid.js
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 查找所有 md 文件
function findMdFiles(dir, files = []) {
  const items = fs.readdirSync(dir);
  for (const item of items) {
    const fullPath = path.join(dir, item);
    const stat = fs.statSync(fullPath);
    if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
      findMdFiles(fullPath, files);
    } else if (item.endsWith('.md')) {
      files.push(fullPath);
    }
  }
  return files;
}

// 提取 mermaid 代码块
function extractMermaidBlocks(content, filePath) {
  const blocks = [];
  const regex = /```mermaid\n([\s\S]*?)```/g;
  let match;
  let lineNum = 1;
  let lastIndex = 0;
  
  while ((match = regex.exec(content)) !== null) {
    // 计算行号
    const beforeMatch = content.substring(lastIndex, match.index);
    lineNum += (beforeMatch.match(/\n/g) || []).length;
    
    blocks.push({
      content: match[1],
      file: filePath,
      line: lineNum
    });
    
    lastIndex = match.index;
  }
  return blocks;
}

// 检查常见的 mermaid 语法问题
function checkMermaidSyntax(block) {
  const errors = [];
  const content = block.content;
  const lines = content.split('\n');
  
  lines.forEach((line, idx) => {
    // 检查 @ 符号(mermaid 中的特殊字符)
    if (line.includes('@') && !line.includes('"')) {
      errors.push({
        ...block,
        lineInBlock: idx + 1,
        issue: `包含未转义的 @ 符号: "${line.trim()}"`,
        suggestion: '用引号包裹含 @ 的文本,如 ["@xxx"]'
      });
    }
    
    // 检查 < > 符号
    if ((line.includes('<') || line.includes('>')) && !line.includes('-->') && !line.includes('---') && !line.match(/\[.*<.*>.*\]/)) {
      if (!line.includes('"') && line.match(/\[.*[<>].*\]/)) {
        errors.push({
          ...block,
          lineInBlock: idx + 1,
          issue: `包含未转义的 < 或 > 符号: "${line.trim()}"`,
          suggestion: '用引号包裹含 <> 的文本'
        });
      }
    }
    
    // 检查括号不匹配
    const openBrackets = (line.match(/\[/g) || []).length;
    const closeBrackets = (line.match(/\]/g) || []).length;
    if (openBrackets !== closeBrackets) {
      errors.push({
        ...block,
        lineInBlock: idx + 1,
        issue: `括号不匹配: "${line.trim()}"`,
        suggestion: '检查 [ ] 是否成对'
      });
    }
    
    // 检查圆括号不匹配
    const openParens = (line.match(/\(/g) || []).length;
    const closeParens = (line.match(/\)/g) || []).length;
    if (openParens !== closeParens && !line.includes('-->')) {
      errors.push({
        ...block,
        lineInBlock: idx + 1,
        issue: `圆括号不匹配: "${line.trim()}"`,
        suggestion: '检查 ( ) 是否成对'
      });
    }
    
    // 检查引号不匹配
    const quotes = (line.match(/"/g) || []).length;
    if (quotes % 2 !== 0) {
      errors.push({
        ...block,
        lineInBlock: idx + 1,
        issue: `引号不匹配: "${line.trim()}"`,
        suggestion: '检查 " 是否成对'
      });
    }
    
    // 检查特殊字符 # { } & 等
    if (line.match(/\[[^\]]*[#{}|&][^\]]*\]/) && !line.includes('"')) {
      errors.push({
        ...block,
        lineInBlock: idx + 1,
        issue: `包含特殊字符 #{}|&: "${line.trim()}"`,
        suggestion: '用引号包裹含特殊字符的文本'
      });
    }
  });
  
  return errors;
}

// 主函数
function main() {
  const rootDir = process.cwd();
  console.log('🔍 扫描 Mermaid 语法问题...\n');
  
  const mdFiles = findMdFiles(rootDir);
  console.log(`📁 找到 ${mdFiles.length} 个 Markdown 文件\n`);
  
  let totalBlocks = 0;
  let totalErrors = 0;
  const allErrors = [];
  
  for (const file of mdFiles) {
    const content = fs.readFileSync(file, 'utf-8');
    const blocks = extractMermaidBlocks(content, file);
    totalBlocks += blocks.length;
    
    for (const block of blocks) {
      const errors = checkMermaidSyntax(block);
      if (errors.length > 0) {
        allErrors.push(...errors);
        totalErrors += errors.length;
      }
    }
  }
  
  console.log(`📊 共扫描 ${totalBlocks} 个 Mermaid 代码块\n`);
  
  if (allErrors.length === 0) {
    console.log('✅ 未发现明显的语法问题!\n');
  } else {
    console.log(`⚠️  发现 ${totalErrors} 个潜在问题:\n`);
    console.log('─'.repeat(80));
    
    for (const error of allErrors) {
      const relPath = path.relative(rootDir, error.file);
      console.log(`\n📄 ${relPath}:${error.line}`);
      console.log(`   ❌ ${error.issue}`);
      console.log(`   💡 ${error.suggestion}`);
    }
    
    console.log('\n' + '─'.repeat(80));
    console.log(`\n📋 总结: ${allErrors.length} 个问题需要修复\n`);
  }
}

main();

scripts/fix-mermaid.js

#!/usr/bin/env node
/**
 * Mermaid 语法自动修复脚本
 * 用法: node scripts/fix-mermaid.js
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 查找所有 md 文件
function findMdFiles(dir, files = []) {
  const items = fs.readdirSync(dir);
  for (const item of items) {
    const fullPath = path.join(dir, item);
    const stat = fs.statSync(fullPath);
    if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
      findMdFiles(fullPath, files);
    } else if (item.endsWith('.md')) {
      files.push(fullPath);
    }
  }
  return files;
}

// 修复 mermaid 代码块中的语法问题
function fixMermaidBlock(content) {
  const lines = content.split('\n');
  const fixedLines = lines.map(line => {
    // 跳过空行和图表类型声明行
    if (!line.trim() || line.match(/^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|journey|gitGraph|mindmap|timeline|quadrantChart|sankey|xychart)/)) {
      return line;
    }
    
    let fixed = line;
    
    // 1. 修复时序图消息中的 @ 符号: A->>B : 调用@xxx -> A->>B : "调用@xxx"
    fixed = fixed.replace(/(->>|-->>|->|-->)\s*([^:]+)\s*:\s*([^"\n]*@[^"\n]*)/g, (match, arrow, target, message) => {
      return `${arrow} ${target.trim()} : "${message.trim()}"`;
    });
    
    // 2. 修复类图中的属性/方法 @ 符号: +@observable xxx -> +"@observable xxx"
    fixed = fixed.replace(/^(\s*)([+\-#~])(@[a-zA-Z]+.*)/gm, (match, indent, modifier, rest) => {
      if (rest.includes('"')) return match;
      return `${indent}${modifier}"${rest}"`;
    });
    
    // 3. 修复节点标签中的 @ 符号: A[@xxx] -> A["@xxx"]
    fixed = fixed.replace(/\[([^\]"]*@[^\]"]*)](?!")/g, (match, inner) => {
      return `["${inner}"]`;
    });
    
    // 4. 修复节点标签中的 # { } | & 特殊字符
    fixed = fixed.replace(/\[([^\]"]*[#{}|&][^\]"]*)](?!")/g, (match, inner) => {
      return `["${inner}"]`;
    });
    
    // 5. 修复边标签中的 @ 符号: |@Action注解| -> |"@Action注解"|
    fixed = fixed.replace(/\|([^|"]*@[^|"]*)]\|/g, (match, inner) => {
      return `|"${inner}"|`;
    });
    
    // 6. 修复菱形节点: {A@xxx} -> {"A@xxx"}
    fixed = fixed.replace(/\{([^{}"]*@[^{}"]*)}(?!")/g, (match, inner) => {
      return `{"${inner}"}`;
    });
    
    // 7. 修复类图中单独一行的注解: @BillPlugin -> "@BillPlugin"
    if (fixed.trim().match(/^@[a-zA-Z]+/) && !fixed.includes('"')) {
      fixed = fixed.replace(/^(\s*)(@[^\s"]+.*)$/gm, (match, indent, annotation) => {
        return `${indent}"${annotation}"`;
      });
    }
    
    return fixed;
  });
  
  return fixedLines.join('\n');
}

// 处理单个文件
function processFile(filePath) {
  const content = fs.readFileSync(filePath, 'utf-8');
  
  // 匹配并替换所有 mermaid 代码块
  const mermaidRegex = /(```mermaid\n)([\s\S]*?)(```)/g;
  let modified = false;
  
  const newContent = content.replace(mermaidRegex, (match, start, mermaidContent, end) => {
    const fixed = fixMermaidBlock(mermaidContent);
    if (fixed !== mermaidContent) {
      modified = true;
    }
    return start + fixed + end;
  });
  
  if (modified) {
    fs.writeFileSync(filePath, newContent, 'utf-8');
    return true;
  }
  return false;
}

// 主函数
function main() {
  const rootDir = process.cwd();
  console.log('🔧 自动修复 Mermaid 语法问题...\n');
  
  const mdFiles = findMdFiles(rootDir);
  console.log(`📁 找到 ${mdFiles.length} 个 Markdown 文件\n`);
  
  let fixedCount = 0;
  const fixedFiles = [];
  
  for (const file of mdFiles) {
    if (processFile(file)) {
      fixedCount++;
      fixedFiles.push(path.relative(rootDir, file));
    }
  }
  
  if (fixedCount === 0) {
    console.log('✅ 没有需要修复的文件\n');
  } else {
    console.log(`✅ 已修复 ${fixedCount} 个文件:\n`);
    for (const file of fixedFiles) {
      console.log(`   📄 ${file}`);
    }
    console.log('\n💡 建议运行 npm run check:mermaid 验证修复结果\n');
  }
}

package.json

{
  "name": "xyz-docs",
  "version": "1.0.0",
  "description": "xyz-docs",
  "type": "module",
  "scripts": {
    "dev": "vitepress dev",
    "build": "vitepress build",
    "preview": "vitepress preview",
    "check:mermaid": "node scripts/check-mermaid.js"
  },
  "devDependencies": {
    "markdown-it-container": "^4.0.0",
    "mermaid": "^11.12.2",
    "vitepress": "^1.0.0",
    "vitepress-sidebar": "^1.33.1"
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment