/** * marp-mermaid-engine —— 让这套 deck 离线也能渲染「原生 mermaid」 * * 背景:本机装的 marp-cli 4.5.1 自带的 marp-core 4.4.0 里没有 mermaid(二进制里搜不到这个词), * 而 marp-core v5 的 mermaid 插件需要联网装 beautiful-mermaid。为了在离线环境也能用原生 * mermaid 语法,这里把 marp-core 的 fence 渲染规则换掉:遇到 ```mermaid 代码块时, * 调用 vendor/mermaid 里那份 mermaid + Chromium 无头渲染成内联 。 * markdown 里保持原生 mermaid 写法和 fence,输出里是真的 mermaid 图。 * * 用法(marp.config.mjs): * export default { engine: './tools/marp-mermaid-engine.mjs', ... } * * 可调环境变量: * MARP_MERMAID_DIR mermaid dist 目录(默认 vendor/mermaid) * MARP_MERMAID_CHROMIUM 浏览器可执行文件(默认 /usr/bin/chromium) * * 如果以后装了 marp-core v5 的 mermaid 插件,把这个 engine 配置删掉即可切回官方渲染。 */ import { execFileSync } from 'node:child_process' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' const here = path.dirname(fileURLToPath(import.meta.url)) const CHROMIUM = process.env.MARP_MERMAID_CHROMIUM || '/usr/bin/chromium' const MERMAID_DIR = process.env.MARP_MERMAID_DIR || path.join(here, '..', 'vendor', 'mermaid') const MERMAID_JS = path.join(MERMAID_DIR, 'mermaid.min.js') /** 图的外观,跟主题主色对齐 */ const MERMAID_CONFIG = { startOnLoad: false, theme: 'base', themeVariables: { primaryColor: '#e8f1fa', primaryBorderColor: '#004e98', primaryTextColor: '#1a1a1a', lineColor: '#004e98', fontFamily: '"Noto Sans CJK SC", "Source Han Sans SC", sans-serif', fontSize: '24px', }, // wrappingWidth 给够, 免得节点里的中文被折成两行 flowchart: { curve: 'basis', wrappingWidth: 360, padding: 14, nodeSpacing: 60 }, } let seq = 0 /** 同步渲染(markdown-it 的渲染函数必须是同步的,所以用 execFileSync 调 Chromium) */ const renderMermaid = (code) => { const id = `marp-mermaid-${++seq}` const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'marp-mermaid-')) const mermaidJs = pathToFileURL(MERMAID_JS).href fs.writeFileSync( path.join(dir, 'index.html'), `
`, ) const dom = execFileSync( CHROMIUM, [ '--headless', '--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--virtual-time-budget=10000', '--dump-dom', pathToFileURL(path.join(dir, 'index.html')).href, ], { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 }, ) const start = dom.indexOf('') if (start < 0 || end < 0) throw new Error( `mermaid 渲染失败: ${dom.match(/data-error="([^"]*)"/)?.[1] || '没有拿到 svg'}`, ) return dom.slice(start, end + ''.length) } /** * marp-cli 的「函数式 engine」 * * 两个注意点(读 marp-cli 源码得出的结论): * 1. 必须是箭头函数 —— CLI 用 isClass() 判断,普通函数带 prototype 会被当成类去 new; * 2. 参数里的 marp 是 CLI 自带 marp-core 的一个**实例**(不是类)。改完它的 markdown * 渲染规则后,把这个实例返回即可。 */ const marpMermaidEngine = ({ marp }) => { const md = marp.markdown const originalFence = md.renderer.rules.fence md.renderer.rules.fence = (tokens, idx, mdOpts, env, self) => { const token = tokens[idx] const info = (token.info || '').trim().toLowerCase() if (info === 'mermaid' || info.startsWith('mermaid ')) { try { return `
${renderMermaid(token.content)}
\n` } catch (e) { process.stderr.write(`[marp-mermaid] ${e.message}\n`) process.stderr.write('[marp-mermaid] 已回退成普通代码块\n') } } return originalFence(tokens, idx, mdOpts, env, self) } return marp } export default marpMermaidEngine