Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | #!/usr/bin/env node /** * build_mermaid.js — pre-renders Mermaid diagrams in HTML docs to inline SVG * * Scans every .html file under docs/ for elements that look like: * * <div class="mermaid-wrapper"> * <pre class="mermaid">flowchart LR ...</pre> * </div> * * If @mermaid-js/mermaid-cli (mmdc) is available on PATH, each diagram * is rendered to an inline SVG and the <pre> block is replaced in-place. * Pre-rendering improves first-paint speed and removes the CDN dependency. * * When mmdc is not found the script exits cleanly with an informational * message — the page.html template already includes the Mermaid CDN * script, so diagrams still render in the browser without pre-rendering. * * Usage (from repo root): * node scripts/docs/build_mermaid.js * node scripts/docs/build_mermaid.js --dry-run # report only, no writes * * Install mmdc globally (optional): * npm install -g @mermaid-js/mermaid-cli * * Or as a local devDependency: * npm install --save-dev @mermaid-js/mermaid-cli * npx node scripts/docs/build_mermaid.js */ const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); // --------------------------------------------------------------------------- // CLI flags // --------------------------------------------------------------------------- const DRY_RUN = process.argv.includes('--dry-run'); // --------------------------------------------------------------------------- // Paths // --------------------------------------------------------------------------- const REPO_ROOT = path.resolve(__dirname, '..', '..'); const DOCS_DIR = path.join(REPO_ROOT, 'docs'); // --------------------------------------------------------------------------- // Detect mmdc // --------------------------------------------------------------------------- function findMmdc() { // 1. Local devDependency (node_modules/.bin/mmdc) const localBin = path.join(REPO_ROOT, 'node_modules', '.bin', 'mmdc'); if (fs.existsSync(localBin)) return localBin; // 2. Global install on PATH try { const which = process.platform === 'win32' ? 'where mmdc' : 'which mmdc'; const result = execSync(which, { stdio: 'pipe' }).toString().trim().split('\n')[0]; if (result) return result; } catch { // not on PATH } return null; } // --------------------------------------------------------------------------- // Extract mermaid blocks from an HTML file // --------------------------------------------------------------------------- /** * Returns an array of { index, fullMatch, code } objects. * index — character offset of the match in `html` * fullMatch — the complete wrapper div string * code — the raw Mermaid diagram source */ function extractBlocks(html) { const re = /<div class="mermaid-wrapper">\s*<pre class="mermaid">([\s\S]*?)<\/pre>\s*<\/div>/g; const blocks = []; let m; while ((m = re.exec(html)) !== null) { blocks.push({ index: m.index, fullMatch: m[0], code: m[1].trim() }); } return blocks; } // --------------------------------------------------------------------------- // Render one diagram via mmdc → inline SVG // --------------------------------------------------------------------------- function renderDiagram(mmdc, code) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mermaid-')); const inFile = path.join(tmpDir, 'diagram.mmd'); const outFile = path.join(tmpDir, 'diagram.svg'); try { fs.writeFileSync(inFile, code, 'utf8'); execSync(`"${mmdc}" -i "${inFile}" -o "${outFile}" --quiet`, { stdio: 'pipe' }); const svg = fs.readFileSync(outFile, 'utf8'); // Strip XML declaration and DOCTYPE if present return svg .replace(/^<\?xml[^?]*\?>\s*/i, '') .replace(/^<!DOCTYPE[^>]*>\s*/i, '') .trim(); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } } // --------------------------------------------------------------------------- // Process one HTML file // --------------------------------------------------------------------------- function processFile(htmlPath, mmdc) { let html = fs.readFileSync(htmlPath, 'utf8'); const blocks = extractBlocks(html); if (!blocks.length) return 0; let replaced = 0; // Replace in reverse order so character offsets stay valid for (const block of blocks.slice().reverse()) { try { const svg = renderDiagram(mmdc, block.code); const wrapper = `<div class="mermaid-wrapper">\n${svg}\n</div>`; html = html.slice(0, block.index) + wrapper + html.slice(block.index + block.fullMatch.length); replaced++; } catch (err) { console.warn(`[build_mermaid] Could not render diagram: ${err.message}`); } } if (replaced > 0 && !DRY_RUN) { fs.writeFileSync(htmlPath, html, 'utf8'); } return replaced; } // --------------------------------------------------------------------------- // Collect HTML files // --------------------------------------------------------------------------- function collectHtmlFiles(dir) { const results = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { // Skip jsdoc — those are generated by JSDoc and should not be modified if (entry.name === 'jsdoc') continue; results.push(...collectHtmlFiles(path.join(dir, entry.name))); } else if (entry.name.endsWith('.html')) { results.push(path.join(dir, entry.name)); } } return results; } // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- function run() { if (DRY_RUN) console.log('[build_mermaid] --dry-run: no files will be written.'); const mmdc = findMmdc(); if (!mmdc) { console.log( '[build_mermaid] mmdc not found — skipping pre-render.\n' + ' Diagrams will be rendered client-side by the Mermaid CDN script\n' + ' already included in docs/templates/page.html.\n' + '\n' + ' To enable pre-rendering, install mermaid-cli:\n' + ' npm install --save-dev @mermaid-js/mermaid-cli' ); return; } console.log(`[build_mermaid] Using mmdc: ${mmdc}`); const files = collectHtmlFiles(DOCS_DIR); let totalDiagrams = 0; let totalFiles = 0; for (const file of files) { const count = processFile(file, mmdc); if (count > 0) { const rel = path.relative(DOCS_DIR, file); const tag = DRY_RUN ? '(dry-run)' : '✓'; console.log(`[build_mermaid] ${tag} ${rel}: ${count} diagram${count > 1 ? 's' : ''} pre-rendered`); totalDiagrams += count; totalFiles++; } } if (totalDiagrams === 0) { console.log('[build_mermaid] No mermaid diagrams found in HTML files.'); } else { console.log( `[build_mermaid] Done — ${totalDiagrams} diagram${totalDiagrams > 1 ? 's' : ''} ` + `in ${totalFiles} file${totalFiles > 1 ? 's' : ''}.` ); } } run(); |