init
This commit is contained in:
commit
0285b2a5d1
12 files changed
+4243
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user