This commit is contained in:
pluv committed 2026-10-08 12:29:59 +08:00
commit 0285b2a5d1
12 files changed
+4243

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
/**
* 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