GitHub Markdown 数学公式:渲染规则与变通方案
GitHub 分两个阶段渲染 Markdown 中的数学公式:
- 服务端(
cmark-gfm+ 数学清洗器): 找到$ ... $和$$ ... $$的边界,对 ASCII 标点应用 CommonMark 反斜杠取消转义,并拒绝不允许的 LaTeX 宏,然后发出<math-renderer>标签。 - 客户端(
<math-renderer>+MathJax.tex2mml): 将 TeX 转换为 MathML 并注入页面,以便浏览器原生 MathML Core 布局。
这两个阶段都存在破坏普通 LaTeX 的 Bug。以下规则是针对这些 Bug 的变通方案。
何时使用此技能
- 在将在 GitHub 上阅读的
.md文件中编写或编辑数学公式时。 - 修复在 GitHub 上渲染为被挤压、红色或原始
$$文本的公式时。 - 对于仅通过 LaTeX、Pandoc、Jupyter、MkDocs 或 VS Code 处理的文件则不需要。如果一个文件在两个地方都被读取,下面的替换仍然是普通的 TeX/MathJax,因此它们在别处仍然可以渲染。
工作流程
- 在编写时,遵循下面的规则 1 到 4。
- 在提交之前,对更改过的 Markdown 文件运行检查脚本(参见 脚本)。不加
--check运行则就地修复。 - 使用
git diff审查结果。该脚本基于正则表达式,请将其输出视为草稿。无法在本地验证渲染效果;如有疑问,请在 GitHub 上预览。
快速参考
| 避免使用 | 改用 | 原因 |
|---|---|---|
\tag{2.1} | \qquad\text{(2.1)} | \tag 会将公式挤压成垂直条 |
\operatorname{Foo} | \mathrm{Foo} | 清洗器会拦截 operatorname |
\mathrm{Im} D_t | \mathrm{Im}\thinspace D_t | 恢复运算符间距 |
\hbox{...}, {\rm ...} | \text{...}, {\mathrm{...}} | 纯 TeX 宏会被拦截 |
\{, \} | \lbrace, \rbrace | CommonMark 将 \{ 变成 { |
单行中的 \\ | \cr | CommonMark 将 \\ 变成 \ |
\, | \thinspace | CommonMark 将 \, 变成 , |
\,, ,(连续) | , | 否则会渲染成双逗号 |
q_*(一个块中有多个 *) | q_\ast | 成对的 * 可能变成 Markdown 强调 |
1. 永远不要使用 \tag{...}(防止垂直挤压成一字符宽)
- Bug:
MathJax.tex2mml将任何包含\tag{2.1}的公式序列化为<mtable displaystyle="true"><mlabeledtr><mtd>...。浏览器 MathML Core 规范移除了<mlabeledtr>,因此 Chromium/Blink 和 Firefox 不会将其视为表格行,而是将公式挤压成一字符宽的列。 - 变通方案: 在显示公式的末尾放置
\qquad\text{(2.1)}代替。如果需要标签中的撇号,请在\text{(3.3′)}内使用 Unicode′,而不是嵌套的$\tag{3.3$'$}$。
<!-- 在 GitHub 上损坏(被挤压成垂直列) -->
$$
\partial_t \mathcal{Q} + \partial_x \mathcal{J} = 0 \tag{2.1}
$$
<!-- 在 GitHub 上正常工作 -->
$$
\partial_t \mathcal{Q} + \partial_x \mathcal{J} = 0 \qquad\text{(2.1)}
$$
2. 永远不要使用 \operatorname{...}、\hbox{...} 或 {\rm ...}(防止不允许的宏错误)
- Bug: GitHub 的服务端清洗器会拦截
\operatorname和\operatorname*,并显示提示The following macros are not allowed: operatorname。 - 变通方案:
- 将
\operatorname{Foo}替换为\mathrm{Foo}。当它后面紧跟着一个字母或反斜杠宏时(例如\operatorname{Im} D_t或\operatorname{pr} v),添加\thinspace(\mathrm{Im}\thinspace D_t)以保持运算符间距。 - 将纯 TeX 的
\hbox{...}替换为\text{...},将{\rm ...}替换为{\mathrm{...}}。
- 将
<!-- 在 GitHub 上损坏("The following macros are not allowed: operatorname") -->
$\operatorname{diag}(1, -1)$ and $\operatorname{Im} D_t$
<!-- 在 GitHub 上正常工作 -->
$\mathrm{diag}(1, -1)$ and $\mathrm{Im}\thinspace D_t$
3. 使分隔符、间距和换行符能够安全通过 CommonMark 的取消转义
- Bug: 在数学公式到达
<math-renderer>之前,GitHub 的 Markdown 解析器会在$ ... $和$$ ... $$内部对 ASCII 标点应用 CommonMark 反斜杠取消转义:\{变成{,\}变成}。这会导致\left\{出现Missing or unrecognized delimiter for \left,并静默地去除泊松括号\{F, G\}中的花括号。\\(后面没有换行符时)变成\。这会破坏单行矩阵和诸如\begin{pmatrix} 0 & 1 \\ -1 & 0 \end{pmatrix}的情况,使其变成\-1,导致红色未定义宏错误。\,,,(连续)变成,,(双逗号),\,变成,(dx之前的字面逗号)。- 数学块内成对的
*(例如U_+(z - 2q_*), \quad z = w + Dq_*)可能被解析为 Markdown<em>强调。
- 变通方案:
- 使用
\lbrace和\rbrace代替\{和\}(如果后面跟着字母,请添加空格:\lbrace F, G\rbrace)。 - 在单行矩阵和环境中使用
\cr代替\\作为换行符,或者在多行$$...$$块中,在\\后面立即放置一个换行符。 - 使用
\thinspace代替\,(在字母前添加尾随空格:dt\thinspace dx),并将\,,,替换为,。 - 当方程包含多个星号时,使用
\ast(q_\ast)代替*(q_*)。
- 使用
<!-- 在 GitHub 上损坏 -->
内联泊松括号 $\{F, G\} = 0$,集合 $\left\{ x : x > 0 \right\}$,积分 $\int dx\,dy$,矩阵 $\bigl(\begin{smallmatrix} 0 & 1 \\ -1 & 0 \end{smallmatrix}\bigr)$。
<!-- 在 GitHub 上正常工作 -->
内联泊松括号 $\lbrace F, G\rbrace = 0$,集合 $\left\lbrace x : x > 0 \right\rbrace$,积分 $\int dx\thinspace dy$,矩阵 $\bigl(\begin{smallmatrix} 0 & 1 \cr -1 & 0 \end{smallmatrix}\bigr)$。
4. 为 cmark-gfm 格式化 $$ ... $$ 块的边界
- Bug: 当一个方程包含多个
_或*字符时,cmark-gfm在三种情况下无法识别$$ ... $$块,留下原始$$文本并使下划线之间的文本变为斜体:- 顶层
$$ ... $$块前后没有空行。 - 多行
$$\begin{aligned} ... \end{aligned}$$块中$$与\begin{aligned}在同一行,而不是单独一行。 - 在列表项(
1. ...或- ...)或块引用(> ...)内缩进的多行$$...$$块。
- 顶层
- 变通方案:
-
顶层显示数学: 将开闭的
$$放在各自的行的第 0 列,前后各有一个空行:$$ \begin{aligned} a &= b + c, \\ d &= e + f. \end{aligned} \qquad\text{(1.1)} $$ -
列表项或块引用内的显示数学: 将整个方程放在 一行上,写为
$$\displaystyle ...$$(在环境中使用\cr作为换行符),周围环绕空行:1. 在编号列表项内: $$\displaystyle S_d = \int_{\mathbb{R}} dt\thinspace \mathcal{L}_d(\phi_t, \psi_t) \qquad\text{(1.2)}$$
-
脚本
scripts/fix_github_math.py 将上述规则应用于 Markdown 文件。它只重写 $...$ 和 $$...$$ 内的内容,不触碰围栏代码和内联代码,不处理没有数学公式的文件,并且是幂等的。可以从任何位置运行它:
# 检查而不修改(如果需要更改任何文件则退出码为 1;可用于 CI 或 pre-commit hook)
python3 ~/.claude/skills/github-markdown-math/scripts/fix_github_math.py --check path/to/file.md
# 就地修复
python3 ~/.claude/skills/github-markdown-math/scripts/fix_github_math.py path/to/file.md
如果此技能安装在项目中(.claude/skills/github-markdown-math/),请使用该路径。
局限性
- 它基于正则表达式,因此请审查差异。它也无法确认 GitHub 正确渲染了结果。
\operatorname{...}仅在其参数没有嵌套花括号时被转换。仅对一组固定的名称(Im、im、pr、graph、Div、ker、dim、deg、span、spec、End、Re、tr、rank)在\mathrm{...}之后添加\thinspace;对于其他名称,请手动添加。- 在星号中,只有
q_*被重写为q_\ast;请手动检查其他*(规则 3)。 - 块引用方程仅处理一层深度(
>),不处理嵌套(> >)。 - 带有两个货币符号的行(
$5 and $10)可能被读取为内联数学。除非它们之间的文本包含规则会重写的反斜杠序列,否则这是无害的。 - 这些规则描述了观察到的 GitHub 行为。如果 GitHub 修复了这些 Bug 中的某一个,相应的变通方案就变得不必要了。