// 数学公式、Mermaid 图表、emoji 的渲染。依赖体积较大,首次用到时才加载(各自打成独立的包) import type { MermaidConfig } from 'mermaid' let katexLoad: Promise | undefined function loadKatex() { katexLoad ??= Promise.all([import('katex'), import('katex/dist/katex.min.css')]).then(([m]) => m.default) return katexLoad } /** 渲染公式;语法错误以红色显示错误信息而不是抛出 */ export async function renderMath(el: HTMLElement, tex: string, displayMode: boolean): Promise { const katex = await loadKatex() katex.render(tex, el, { displayMode, throwOnError: false, output: 'htmlAndMathml' }) } let mermaidLoad: Promise | undefined let mermaidSeq = 0 function loadMermaid() { mermaidLoad ??= import('mermaid').then(({ default: mermaid }) => mermaid) return mermaidLoad } /** 渲染 Mermaid 图表,返回 SVG 标记;语法错误时抛出带说明的 Error */ async function renderDiagram(code: string): Promise { const mermaid = await loadMermaid() const mode = document.documentElement.dataset.rmColorMode const dark = mode === 'dark' || (mode === undefined && matchMedia('(prefers-color-scheme: dark)').matches) // 配置与渲染串行进行,避免切换模式时并发图表读到另一个模式的全局配置。 const config: MermaidConfig = { startOnLoad: false, securityLevel: 'strict', theme: dark ? 'dark' : 'default' } mermaid.initialize(config) const id = `rm-mermaid-${++mermaidSeq}` try { return (await mermaid.render(id, code)).svg } catch (err) { // 出错时 mermaid 会把临时节点留在 body 中 document.getElementById('d' + id)?.remove() throw err instanceof Error ? err : new Error(String(err)) } } let renderQueue: Promise = Promise.resolve() export function renderMermaid(code: string): Promise { const result = renderQueue.then(() => renderDiagram(code)) renderQueue = result.catch(() => {}) return result } let emojiLoad: Promise> | undefined /** 按 GitHub/Typora 的名称查找 emoji,如 smile → 😄;未知名称返回 undefined */ export async function emojiFor(name: string): Promise { emojiLoad ??= import('gemoji').then(({ gemoji }) => { const map = new Map() for (const g of gemoji) for (const n of g.names) map.set(n, g.emoji) return map }) return (await emojiLoad).get(name) } /** 为渲染器输出的占位元素(表格单元格等处)填充公式与 emoji */ export function hydrate(root: HTMLElement) { for (const el of root.querySelectorAll('[data-math]')) { void renderMath(el, el.dataset.math!, el.tagName === 'DIV') } for (const el of root.querySelectorAll('[data-emoji]')) { void emojiFor(el.dataset.emoji!).then((e) => { if (e) el.textContent = e }) } }