126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
/**
|
||
* 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
|
||
|