WeKan Markdown 中的 LaTeX 数学公式渲染:Temml、MathML 与 DOMPurify 白名单的完整实现解析
发布时间:2026/9/13 3:58:44 作者:尧图编辑部 阅读量:1,286

WeKan Markdown 中的 LaTeX 数学公式渲染Temml、MathML 与 DOMPurify 白名单的完整实现解析【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekanWeKan 允许在卡片的描述、评论、清单等任意 Markdown 字段中直接书写 LaTeX 公式并通过 Temml 将其转换为浏览器原生支持的 MathML而不是在客户端打包一个重量级公式引擎。本文基于文档 LaTeX 功能说明 与 markdown 包源码完整讲解该功能的渲染架构、公式书写语法含 pandoc 约定的边界规则、依赖版本以及 DOMPurify 净化层如何精确放行 MathML 的同时阻断 MathML XSS 攻击面。功能定位哪些字段可以渲染公式WeKan 的 LaTeX 公式渲染作用于所有走 Markdown 渲染管线的字段——卡片描述、卡片评论、清单等内容只要以 Markdown 形式呈现其中的$...$与$$...$$片段都会被渲染为数学符号。渲染入口是 Blaze 模板助手markdown注册于 template-integration.js在模板中通过{{#markdown}}...{{/markdown}}包裹内容即可触发包的使用方式见 markdown 包 README。需要注意一个前置开关管理面板 Features 中的“始终以纯文本显示代码”alwaysShowCodeAsText一旦开启该助手会跳过全部渲染逻辑把原始 Markdown/HTML 源码逐字转义后显示template-integration.js。因此若发现公式“没有渲染”可先确认该安全开关的状态。渲染架构markdown-it-math 负责切词Temml 负责转换浏览器负责显示文档 How it works 一节 给出了架构结论源码逐行印证公式切词由markdown-it-math完成。它在 markdown-it 的解析链路中识别行内$...$与块级$$...$$公式并将其从普通文本流中剥离。LaTeX → MathML 转换由temmlTemml完成输出原生 MathML。现代浏览器Chrome、Safari、Firefox已原生支持 MathML 排版因此 WeKan 的客户端包中不携带任何 MathJax/KaTeX 之类的重型公式引擎公式“渲染”成本为零。净化放行Temml 生成的 MathML 标签与属性被加入 DOMPurify 的白名单见下文安全边界一节确保公式在 XSS 过滤后依然存活而不是被整段剥离。插件装配代码位于 template-integration.js// LaTeX math support. Renders $...$ (inline) and $$...$$ (block) to native // MathML using Temml, which browsers display without any client-side rendering // engine. Migrated from markdown-it-mathjax3 (which bundled all of MathJax and // had a double-render bug); MathML rendering had been silently dropped in commit // 63ce45c53 during the Meteor 3 refactor. The emitted MathML is whitelisted in // secureDOMPurify.js so DOMPurify does not strip it. // We use the markdown-it-math no-default-renderer entrypoint and call Temml // ourselves, instead of the markdown-it-math/temml entrypoint, to avoid a // top-level await import(temml) that the Meteor/rspack bundler dislikes. const renderMath (src, displayMode) { try { return temml.renderToString(src, { throwOnError: false, errorColor: #cc0000, displayMode }); } catch (e) { // Never let one malformed formula break the whole markdown render. return src; } }; Markdown.use(markdownItMath, { inlineRenderer: (src) renderMath(src, false), blockRenderer: (src) renderMath(src, true), });这段装配里有三个值得注意的工程决策刻意选用markdown-it-math/no-default-renderer入口而不是官方的markdown-it-math/temml入口。后者会引入顶层await import(temml)而 Meteor/rspack 打包器对顶层动态导入不友好改为自行调用temml.renderToString后导入变为静态的文件头部import temml from temml见 template-integration.js。throwOnError: falseerrorColor: #cc0000单个非法子公式不会让 Temml 抛异常而是把出错片段以红色#cc0000标出公式其余部分照常渲染。兜底try/catch即便 Temml 整体抛错renderMath也只做“原样返回 LaTeX 源码”这一件事——一条坏公式绝不会拖垮整张卡片的 Markdown 渲染。这与整个 markdown 助手的防御风格一致markdown 助手 在Markdown.render抛异常时同样降级为转义纯文本保证卡片永远可以打开。inlineRenderer传displayMode falseblockRenderer传displayMode true对应行内小号符号与块级大号居中排版两种模式。公式语法行内、块级与 pandoc 兼容的边界规则行内公式Inline用单个$包裹 LaTeX 源码即可行内渲染文档给出的示例$\sqrt{3x-1}(1x)^2$块级公式Block用$$包裹则进入块级渲染符号更大且整体居中。文档 Examples 一节 给出的完整块级示例是麦克斯韦方程组的array环境可直接复制到卡片描述中验证$$\begin{array}{c} \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} 4 \pi \rho \\ \nabla \times \vec{\mathbf{E}}\, \, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} \vec{\mathbf{0}} \\ \nabla \cdot \vec{\mathbf{B}} 0 \end{array}$$解析边界与 pandoc 约定保持一致文档 Syntax 一节 明确Markdown 中的数学解析刻意对齐 pandoc 的约定。规则原文如下Anything between two $ characters will be treated as TeX math. The opening $ must have a non-space character immediately to its right, while the closing $ must have a non-space character immediately to its left, and must not be followed immediately by a digit. Thus, $20,000 and $30,000 won’t parse as math. If for some reason you need to enclose text in literal $ characters, backslash-escape them and they won’t be treated as math delimiters.用开发者语言概括这三条边界规则起始$右侧必须紧跟非空白字符——$ \int x dx$不会成为公式结束$左侧必须紧跟非空白字符且其右侧不能紧跟数字——这正是$20,000 and $30,000价格区间不会被误判为公式的原因字面量$用反斜杠转义\$100会原样显示$100而不进入公式解析。由于公式渲染发生在客户端 Markdown 助手内部这些规则对卡片的存储内容没有任何要求——你写入数据库的始终是最原始的 LaTeX 源码所见即所存。安全边界MathML 如何穿过 DOMPurify 而不被误杀或滥用公式功能与安全净化是一对天然矛盾DOMPurify 的默认配置会把 MathML 节点当作未知 HTML 直接剥离。WeKan 的解法集中在 secureDOMPurify.js采用精确白名单策略。标签白名单只放行 Temml 实际会输出的元素secureDOMPurify.js 定义了 Temml 会产出的 MathML表现层标签集合并将其并入ALLOWED_TAGS与a、p、table等常规 Markdown 标签同列表const MATHML_TAGS [ math, semantics, annotation, mrow, mi, mo, mn, ms, mtext, mspace, msup, msub, msubsup, mfrac, mroot, msqrt, mover, munder, munderover, mmultiscripts, mprescripts, none, mtable, mtr, mtd, mlabeledtr, mpadded, mphantom, mstyle, merror, menclose, maction, ];其中有一处刻意的排除annotation-xml不在白名单内。源码注释说明annotation-xml可以夹带 HTML/SVG 内容是已知的 MathML XSS 攻击向量而 Temml 只会输出纯文本的annotation元素因此排除前者不影响功能只关闭攻击面。属性白名单只放行排版属性MATHML_ATTRsecureDOMPurify.js列出 MathML 用于排版/语义的约 37 个展示属性如display、displaystyle、mathvariant、mathcolor、mfrac/mtable相关的columnalign、rowlines等。注释特别指出颜色与尺寸使用专用的math*属性而非 CSSstylestyle、class、id依旧被全局钩子拦截——即公式无法借 MathML 属性注入 CSS。元素钩子style属性不删除公式本体DOMPurify 的uponSanitizeElement钩子对“携带危险属性的元素”默认整节点删除这对普通元素是合理的但对 MathML 会造成误杀。secureDOMPurify.js 通过命名空间判断做了区分const isMathML node.namespaceURI http://www.w3.org/1998/Math/MathML; const dangerousAttrs isMathML ? [onload, onerror, onclick, onmouseover, onfocus, onblur] : [style, onload, onerror, onclick, onmouseover, onfocus, onblur];含义是MathML 节点上只有关联事件处理器onload等才会导致元素被移除残留的style属性不会删掉整个公式而由uponSanitizeAttribute钩子单独剥离secureDOMPurify.js。这样既保住了 CSS 注入防护又不会出现“公式里混入一个 style 属性导致整条公式消失”的静默降级。净化在管线中的位置在 markdown 助手的正常分支里顺序是外部问题单自动链接 → WeKan 卡片 URL 重标注 →Markdown.render此时数学公式已变为 MathML 字符串→DOMPurify.sanitize(renderedMarkdown, getSecureDOMPurifyConfig())template-integration.js。公式只有在通过这最后一道净化后才进入页面因此 MathML 白名单是公式功能的必要条件——缺了它Temml 的输出会在 sanitize 后被静默清空。依赖版本与历史演进当前仓库 package.json 中固定了两个直接依赖markdown-it-math: ^6.0.0, temml: ^0.13.3,即package-lock.json锁定的 Temml 为 0.13.x 系列。历史演进在文档与源码注释中均有记录脉络如下早期方案WeKan 曾使用markdown-it-mathjax3它会把整个 MathJax 打进客户端包体且存在“渲染两次”double-render的 bug见 template-integration.js 注释。静默丢失在 Meteor 3 重构期间数学公式渲染曾在一次重构提交中被意外移除长期无人察觉。当前方案基于 Temml 的 MathML 路线已恢复上线。迁移过程也被记录在 2026 年 6 月变更日志 中migrating math rendering from markdown-it-mathjax3 ...。选择 MathML 而非 SVG/HTML 快照路线的核心收益是包体客户端零公式引擎且浏览器升级带来的 MathML 排版改进会自动惠及 WeKan。验证与延伸阅读路径功能文档本文主体依据docs/Features/Editor/LaTeX.md插件装配与错误兜底packages/markdown/src/template-integration.jsMathML 白名单与净化钩子packages/markdown/src/secureDOMPurify.js依赖版本声明package.json同目录相邻的编辑器能力文档便于对照各字段支持的富文本特性Mermaid-Diagram.md、Emoji.md、Markdown.md总结WeKan 的 LaTeX 支持用最小客户端成本换取了完整的公式能力——markdown-it-math做 pandoc 兼容的公式切词Temml 做 LaTeX 到原生 MathML 的无引擎转换DOMPurify 白名单精确放行 Temml 输出并主动关闭annotation-xml等攻击向量外层try/catch保证坏公式只降级为源码显示而不影响卡片打开。理解了这条“切词 → 转换 → 净化”的链路后无论是排查公式未渲染问题检查alwaysShowCodeAsText开关、查看浏览器控制台是否有[markdown] rendering failed日志还是评估将同款方案移植到其他 Meteor 应用都有了明确的落点。【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考