Files
marp-tectonics/tools/marp-mermaid-engine.mjs
2026-10-08 12:29:59 +08:00

126 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 无头渲染成内联 <svg>。
* 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'),
`<!DOCTYPE html><html><body><div id="result"></div>
<script src="${mermaidJs}"></script>
<script>
mermaid.initialize(${JSON.stringify(MERMAID_CONFIG)});
mermaid.render(${JSON.stringify(id)}, ${JSON.stringify(code)})
.then(function (r) { document.getElementById('result').innerHTML = r.svg; })
.catch(function (e) { document.getElementById('result').dataset.error = String(e); });
</script></body></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('<svg')
const end = dom.lastIndexOf('</svg>')
if (start < 0 || end < 0)
throw new Error(
`mermaid 渲染失败: ${dom.match(/data-error="([^"]*)"/)?.[1] || '没有拿到 svg'}`,
)
return dom.slice(start, end + '</svg>'.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 `<div class="mermaid-diagram">${renderMermaid(token.content)}</div>\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