2025-02-19 | 研究与探索 | UNLOCK

在 Hexo 中写数学公式

最近又要在博客里写很多数学公式,结果又出现了我都忘了的问题:在数学块(两个 $$ 之前)里写换行需要写 \\\\ 而非 \,这个问题让我很不爽。于是我决定彻底弄懂并解决这个问题。

Hexo 默认使用 Marked 渲染 Markdown,而 Marked 本身并不支持数学公式,所以我们写的诸如 $a^2 + b^2 = c^2$ 只会被当成普通文本,实际的渲染是在浏览器上通过 MathJax 完成的。由于 Markdown 本身的特性,像 \frac 这样的命令中的 \f 会被翻译成 \f 两个字符,而且只有单词边缘的下划线会被翻译成 <em> 标签1,比如 a _em_ b 这样,而这种写法一般也不会出现,$\mathrm{\LaTeX}$ 写法里下划线一般在单词中间,如 x_i,所以一切基本可以工作。

但 CommonMark 里规定反斜线转义标点符号得到的是符号本身,比如 \, 相当于 ,,而 $\mathrm{\LaTeX}$ 里有的反斜线加标点符号组合也是命令,比如 \, 表示一种较窄的间距,其中最常用的是 \\ 表示换行,而直接写 \\ 会被翻译成单个 \,所以在公式中需要用 \\\\ 来代表 \\

这个问题该怎么解决已经有一些方案了2 3。实际上我们可以直接写扩展代码来解决:在 Hexo 的 scripts 目录下增加一个脚本文件,可以命名为 math.js,然后在其中写入以下代码修改 Marked 的行为:

1
2
3
4
5
6
7
8
9
10
11
"use strict";
const htmlencode = (text) => text.replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' })[c]);
hexo.extend.filter.register("marked:renderer", function (renderer) {
const default_paragraph = renderer.paragraph;
renderer.paragraph = function ({ text }) {
if (text.startsWith("$$") && text.endsWith("$$") && text.length > 4) {
return '<p>\n' + htmlencode(text) + '\n</p>';
}
return default_paragraph ? default_paragraph.apply(this, arguments) : false;
};
})

这样就把 $$ 之间的文本原样输出没有转义了,我们直接写 \\ 就可以让 MathJax 正常工作。

还可以使用类似 GitHub 的语法,将 `$a^2 + b^2 = c^2$` 这样的行内代码块渲染为数学公式,避免行内公式中的 Markdown 转义(注意 GitHub 用的语法是 $`a^2 + b^2 = c^2`$ 反引号和 $ 的顺序是反的):

1
2
3
4
5
6
7
const default_codespan = renderer.codespan;
renderer.codespan = function ({ text }) {
if (!raw.startsWith('``') && text.startsWith("$") && text.endsWith("$") && text.length > 2) {
return htmlencode(text);
}
return default_codespan ? default_codespan.apply(this, arguments) : false;
};

如果需要以 $ 开头结尾的行内代码块,可以使用两个反引号,如 ``$demo$``

也可以将 math 代码块渲染为数学公式:

1
2
3
4
5
6
7
const default_code = renderer.code;
renderer.code = function ({ text, lang }) {
if (lang === 'math') {
return '<p>\n$$' + htmlencode(text) + '\n$$</p>';
}
return default_code ? default_code.apply(this, arguments) : false;
};

但这样需要关掉 Hexo 内置的代码高亮功能。

未来还需要支持 mermaid 图表,也许需要完全在浏览器端渲染代码高亮?日后再议吧。

脚注功能通过 marked-footnote 实现:

1
2
3
4
5
const markedFootnote = require('marked-footnote');

hexo.extend.filter.register("marked:use", function (mdUse) {
mdUse(markedFootnote());
});

2025.02.20 更新:我发现 hexo-util 中提供了 highlight 函数来实现代码高亮,这样就可以在配置中关掉高亮,改为完全在自定义脚本中实现,另外其中也提供了 escapeHTML 转换 HTML 实体,不用自己定义了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { escapeHTML, highlight } = require("hexo-util");

// ...

const default_code = renderer.code;
renderer.code = function ({ text, lang }) {
if (lang === 'math') {
return '<p>\n$$' + escapeHTML(text) + '\n$$</p>';
}
return highlight(text, {
lang,
gutter: hexo.config.highlight.line_number,
});
};

目前我只需要 gutter 来显示行号,其他配置也可按需开启。

示例代码:

1
2
3
4
5
6
```math
\begin{aligned}
\frac{1}{2} + \frac{1}{3} &= \frac{5}{6} \\
\frac{1}{2} - \frac{1}{3} &= \frac{1}{6}
\end{aligned}
```

效果:

$$\begin{aligned} \frac{1}{2} + \frac{1}{3} &= \frac{5}{6} \\ \frac{1}{2} - \frac{1}{3} &= \frac{1}{6} \end{aligned} $$

稍作修改,也可以使用 mermaid 代码块表示 Mermaid 图表了:

1
2
3
4
5
6
7
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```

效果:

graph TD; A-->B; A-->C; B-->D; C-->D;

Footnotes

  1. https://spec.commonmark.org/0.31.2/#backslash-escapes

  2. https://kexue.fm/archives/10332

  3. https://www.lizhechen.com/2017/03/08/Hexo%E4%B8%8EMathjax%E7%9A%84%E5%86%B2%E7%AA%81%E5%8F%8A%EF%BC%88%E9%83%A8%E5%88%86%EF%BC%89%E8%A7%A3%E5%86%B3/