Glow 项目 KaTeX 公式渲染 bug 排查 — 根号 / 化学箭头为什么只占位不显示
问题现象
在 Glow 项目 (一个 LLM 辅助学习的知识图谱应用) 里, 用户报告: 节点详情页里 LaTeX 公式的根号 显示时, 只看到 , 根号完全消失了. 同样的问题:
- → 根号不见, 只看到
- → 长箭头不见, 只剩文字
- → 化学箭头不见, 公式散架
- → 向量箭头不见
但奇怪的是, 这些基础符号都正常.
打开浏览器 DevTools, 检查 <span class="katex"> 元素: 存在. .katex-html 子元素: 存在. 但里面的 <svg> 完全不存在. 这说明问题不是 "KaTeX 没渲染 SVG", 而是 "KaTeX 渲染了 SVG, 但被某个东西 strip 掉了".
排查过程
假说 1: 系统字体缺字形 (最先想到)
直觉上, "看不见" 可能是浏览器找不到 √ → 等字符的字形, 留出宽度但空白.
加 Noto Sans + Noto Sans Math + Noto Sans Symbols 2 web font (用 next/font 自托管).
结果: 部分 raw 字符 (如文本里直接出现的 √ ∛ ⁿ) 终于能看到了. 但 LaTeX 公式里的 还是不行.
排除: 不是字体问题, 是渲染层问题.
假说 2: rehype-sanitize strip 了 MathML 属性
KaTeX 输出含 MathML (<math>, <mrow>, <mo> 等). 看了 rehype-sanitize 默认 schema:
// 节选自 rehype-sanitize
defaultSchema.tagNames = ['a', 'abbr', 'address', 'blockquote', 'br', 'code', 'dd', 'del', 'details', 'div', ...]
defaultSchema.attributes = { 'a': [...], 'abbr': [...], ... }
<math> 不在 tagNames 里. 我们的 rehypePlugins 配置:
rehypePlugins={[rehypeKatex, [rehypeSanitize, mathSanitizeSchema]]}
跑顺序是: 先 KaTeX 渲染, 后 sanitize 清理. KaTeX 输出的 <math> 没在白名单 → 被 strip. 所以数学公式整个被吃掉.
修复 (M85): 扩展 mathSanitizeSchema, 把 MathML 标签 (math, mrow, mi, mo, mn, mfrac, msqrt 等) 和它们需要的属性 (如 mo stretchy="true" 让 \xrightarrow 拉长) 都加进白名单.
结果: \xrightarrow 类的 lspace/rspace 属性被保留了. 但是 还是不显示根号.
线索: 根号是 SVG 画的, 不是 MathML. MathML 修对了, SVG 没修.
假说 3: CSS 链断裂 (Tailwind 覆盖 KaTeX)
KaTeX 用 <svg> 画根号, CSS 规则:
.katex svg { height: inherit; position: absolute; width: 100%; }
height: inherit 需要父元素有显式高度. 怀疑 Tailwind 的 @layer base 把 height: auto 套到 svg/img 上覆盖了.
修复 (M85.1): 强制给 .sqrt-sign, .op-limits, .accent 容器加 height: 100% !important. .katex svg { height: inherit } 也明确写一遍.
结果: CSS 看起来正确, 但根号还是不显示.
假说 4 (用户提议): CSS 冲突
用户在另一台电脑上分析, 提到可能 Tailwind 把 height: auto 套到 svg 上. 我看了 Tailwind preflight:
img, video { max-width: 100%; height: auto; }
只对 img, video, 不对 svg. 所以这条不是直接原因.
但用户提到的另一条线索很关键: KaTeX 用 SVG 画复杂符号. 如果 SVG 在某个步骤被 strip 了, 根号就消失, CSS 修了也没用 (CSS 选不存在的元素, 等于没写).
实际验证: KaTeX 输出含 SVG 吗?
跑了一个简单的 Node 测试, 直接看 KaTeX 输出:
const katex = require('katex');
require('katex/contrib/mhchem');
const html = katex.renderToString('\\sqrt{x^2 + y^2}', {output: 'html'});
console.log('has <svg>:', html.includes('<svg')); // true
console.log('has <path>:', html.includes('<path')); // true
KaTeX 确实输出 <svg> 和 <path>. 那它们去哪了?
又测了几个表达式:
\sqrt{x^2 + y^2}→ svg=1, path=1\xrightarrow{光照} A→ svg=1, path=1\ce{H2 + Cl2 -> 2HCl}→ svg=1, path=1\vec{x}→ svg=1, path=1\sum_{i=1}^n i→ svg=0, path=0 (这个用纯 HTML, 不需要 SVG!)
这解释了为什么 一直正常 — 它本来就不用 SVG.
结论: KaTeX 输出含 SVG, 后续某步骤把它们删了.
真正根因: 插件顺序错
回到我们的 markdown.tsx:
rehypePlugins={[rehypeKatex, [rehypeSanitize, mathSanitizeSchema]]}
顺序是: 先 KaTeX, 后 sanitize.
跑一遍实际 pipeline (在容器里 simulate 完整 unified 链):
rehypeKatex跑 → 产出 HTML 含<svg>,<path>,<math>等rehypeSanitize跑 → 用defaultSchema清理- 查
defaultSchema.tagNames: 只有a, abbr, address, blockquote, br, code, dd, del, details, div, ...(51 个 HTML 标签), 没有svg <svg>不在白名单 → 被 strip<path>也不在 → 被 strip- 剩下的就是空的
<span>, 占着位置但内容没了
之前 M85.1 的 CSS .katex svg { height: inherit } 看起来正确, 但 SVG 元素本身已经被 sanitize 删了, CSS 找不到目标, 等于没写.
M85 (MathML) 解决了 <math> 等的 strip 问题, 但 KaTeX 输出的 SVG 部分还是被 strip 了.
修复 (M86.3)
把插件顺序反过来:
rehypePlugins={[
[rehypeSanitize, mathSanitizeSchema], // 先: 清理用户输入 (XSS 防护)
[rehypeKatex, {
strict: 'ignore', // 抑制 unicodeTextInMathMode 等 warning
throwOnError: false, // KaTeX 错误不抛, 整页 markdown 不会挂
}],
]}
原理:
- sanitize 跑在前面, 清理用户输入 — 这才是它真正要做的事 (防止用户在数学公式里塞
<script>之类) - KaTeX 跑在后面, 渲染 sanitize 后的 AST 为 HTML
- KaTeX 输出信任, 不再过 sanitize,
<svg><path>完整保留
mathSanitizeSchema 保留 (仍负责 XSS 防护), 不需要 把 svg / path / g / defs / use 等 SVG 标签加到白名单.
验证
build 后看 minified JS:
rehypePlugins:[[j.A,l],[h.A,{strict:"ignore",throwOnError:!1}]]
j.A (sanitize) 在前, h.A (katex) 在后 — 新顺序生效.
刷新浏览器:
| 表达式 | 结果 |
|---|---|
| ✓ 根号 | |
| ✓ 化学式 + 箭头 | |
| ✓ 长箭头 | |
| ✓ 向量箭头 | |
| ✓ (一直正常, 无 SVG) | |
| console warning | ✓ 静默 |
关键教训
1. 当 sanitize 和 math 渲染一起用, sanitize 必须先跑
这是 rehype 官方推荐做法, 但容易记反. 直觉是 "KaTeX 输出的 HTML 也得 sanitize 防 XSS" — 但 KaTeX 输出是 trusted code, 不用 sanitize. sanitize 的职责是清理用户输入, 不是清理库输出.
2. 不要假设 sanitize 默认 schema 全
defaultSchema.tagNames 只有 51 个 HTML 标签, 不含 svg, math, path 等. KaTeX 输出大量依赖这些. 必须明确加白名单 OR 改插件顺序 (推荐后者).
3. CSS 修复看起来正确但没生效时, 检查 DOM 里元素是否存在
我看 .katex svg 选了它, DevTools 里根本没有这个元素, CSS 必然无效. 应该先 document.querySelector('.katex svg') 验证元素存在, 再调样式.
4. 每个修复单独验证
| Commit | 改动 | 修了什么 | 没修什么 |
|---|---|---|---|
| M85 | 扩 mathSanitizeSchema 加 MathML attr |
\xrightarrow 等的 lspace/rspace/stretchy 属性 |
SVG 元素本身 |
| M85.1 | 加 .katex .sqrt-sign { height: 100% !important } CSS |
假设 CSS 链断裂 (其实 SVG 已被 strip) | 仍未显示 |
| M86.1 | 加 import 'katex/contrib/mhchem' |
启用 \ce 化学式 (没参与这个 bug) |
- |
| M86.3 | 改 rehype 插件顺序 + KaTeX options | 真正修根因 | - |
每个 commit 单独看起来都正确, 但每个只解决了问题的子集. 只有最终 commit 才解决了根因.
一些技术细节
KaTeX 输出结构
KaTeX 对大部分数学用 HTML + CSS 排版, 但对一些复杂符号 (根号 / 积分号 / 大箭头 / 矢量箭头) 用 SVG 画. SVG 在 .katex-html 里:
<span class="katex">
<span class="katex-html" aria-hidden="true">
<span class="base">
<span class="strut" style="height: 1.24em;"></span>
<span class="mord sqrt">
<span class="vlist-t">
<span class="vlist-r">
<span class="vlist" style="height: 0.9578em;">
<span class="svg-align" style="top: -3.2em;">
<!-- 这里有 <svg> 画根号 -->
</span>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
rehype-sanitize 默认 schema
// 节选
defaultSchema.tagNames = ['a', 'abbr', 'address', 'blockquote', 'br', 'code', 'dd', 'del', 'details', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'li', 'ol', 'p', 'picture', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'source', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var']
// 注意: 没有 svg, math, path, g, defs, use, mrow, mo 等
defaultSchema.attributes 也类似, 没有 svg / path 等 SVG 专用 attr (viewBox, d, fill, stroke 等).
经验总结
这次 bug 修复用了 4 个 commit, 走了 6 天才真正修好. 如果一开始就先验证 KaTeX 实际输出含什么元素, 再看 sanitize 怎么处理它, 可能 1 个 commit 就够了.
教训:
- 遇到 "看不见" bug, 第一步是 DevTools 看 DOM 元素是否存在 — 比看代码猜原因快得多
- 每加一个 fix, 立刻验证它真的修了什么 — 不要等所有 fix 都做完再测
- 插件顺序 / 数据流方向 这类问题, 画个图比读代码清楚
- 官方文档 + GitHub issues 是这类底层问题的最快答案 (rehype-katex 文档就明确推荐 sanitize 在前)
相关资源
- KaTeX 官方文档
- rehype-katex GitHub
- rehype-sanitize GitHub
- remark-math GitHub
- react-markdown 文档
- 项目 commit: M85, M85.1, M86.1, M86.3 (gitcode.com/Zengtudor/Glow)