Hexo+NexT使用MathJax问题

NexT主题内部已经集成了MathJax(其实还有KaTeX),相对而言,配置起来已经是很方便了。但在一些细节方面上还是存在各种小问题,这里记录一下解决方式。

版本信息:

Node.js: 11.10.1

Hexo: 3.8.0

NexT: 7.0.1

1. 启用MathJax

编辑themes/next/_config.yml,找到math配置项,配置为enable: true即可启用。

至于其余配置项:

  • per_page默认为true,表示每篇文章需要额外地单独启用MathJax。也就是说,还需要在其文章头部添加mathjax: true,否则其中的MathJax公式依然不会被解析。
  • engine默认使用的是mathjax,相比katex功能更强大(当然也慢了一点)

2. 渲染引擎问题

启用完成后,应该就已经能在文章中写一些简单的MathJax了。

但是!

Hexo默认的渲染引擎hexo-renderer-marked对MathJax的支持很不好,会出现各种莫名其妙的问题。NexT主题的官方文档也推荐换用其他渲染引擎。

推荐换用hexo-renderer-kramed

npm un hexo-renderer-marked --save
npm i hexo-renderer-kramed --save

3. MathJax符号与Markdown符号冲突问题

hexo-renderer-kramed也不是尽善尽美,它还是有一点问题:行内公式中MathJax符号与Markdown符号冲突。

比如下面这几个行内公式:

$ \alpha\beta $

$ \alpha_\beta $

$ \alpha_\beta = \gamma_\delta $

会发现前两个都正常,成功地渲染出了α(正常β)α(下标β),但是第三个公式,结果却并没有解析成公式,显示是这样的:

$ \alpha\beta = \gamma\delta $

检查元素源码发现是这样的:

<p>$ \alpha<em>\beta = \gamma</em>\delta $</p>

原来这里两个下划线_并没有被识别为公式中的下标符号,而是被识别为Markdown的斜体符号去了,最后也就生成了<em></em>

治标不治本的解决方法是:放弃行内公式,一律使用块公式。这种方法的弊端就不说了。

更好地解决方法是直接修改hexo-renderer-kramed相关源码,位于文件node_modules/kramed/lib/rules/inline.js

将其中的

var inline = {
escape: /^\\([\\`*{}\[\]()#$+\-.!_>])/,
// ...
em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
// ...
};

修改为

var inline = {
escape: /^\\([`*\[\]()#$+\-.!_>])/,
// ...
em: /^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
// ...
};

偷懒的话用下面这条sed命令可以直接完成上述修改

sed -i -e 's|escape: /^\\\\(\[\\\\`\*{}\\\[\\\]()#$+\\-.!_>\])/|escape: /^\\\\(\[`\*\\\[\\\]()#$+\\-.!_>\])/|' -e 's|em: /^\\b_((?:__\|\[\\s\\S])+?)_\\b\|^\\\*((?:\\\*\\\*\|\[\\s\\S\])+?)\\\*(?!\\\*)/|em: /^\\\*((?:\\\*\\\*\|\[\\s\\S\])+?)\\\*(?!\\\*)/|' node_modules/kramed/lib/rules/inline.js

重新生成一次,问题解决。

4. 长公式超出范围问题

如果使用的是2019年3月12日后新版本的NexT,这一节内容可以不用看了,因为这些问题已经被我提交PR修正过了。参考pull request #669

NexT主题对公式的样式配置存在一些小问题,导致当公式太长的时候,显示内容会超出文章区域。

比如考虑下面这一段代码中的MathJax公式代码,其中第一段定义了一些宏,第二段是一个行内公式,第三段是一个长公式。

$$
\def \lowercase {\alpha \beta \gamma \delta \epsilon \zeta \eta \theta \iota \kappa \lambda \mu \nu \omicron \pi \rho \sigma \tau \upsilon \phi \chi \psi \omega}
\def \uppercase {A B \Gamma \Delta E Z E \Theta I K \Lambda M N O \Pi R \Sigma T \Upsilon \Phi X \Psi \Omega}
\def \long {\uppercase \Leftrightarrow \lowercase}
$$
$ \text{long inline mathjax:}\quad \long \quad \long \quad \long $
$$
\text{<- long displayed mathjax example ->} \\
\begin{split}
\long \quad &\long \\
&\long \quad \long
\end{split}
$$

然后来看显示效果,这两张展示图中,上半部是NexT默认配置的显示效果,下部是修改后的效果展示。两幅图的差别在于使用了不同的MathJax渲染设置,效果也不一样。

效果展示

效果展示

可以发现,如果使用默认的配置,行内公式超长时总会超出文章显示区域,而块公式时候会超出区域则取决于选择的渲染方式(HTML-CSS不会超出区域,其他情况会)。

进行了修改后,总是会在公式超出区域时生成滚动条,效果更佳理想。

究其原因还是要看NexT源码中的layout/_third-party/math/mathjax.swig文件,重点是尾部的一些代码。

<script type="text/x-mathjax-config">
MathJax.Hub.Queue(function() {
var all = MathJax.Hub.getAllJax(), i;
for (i = 0; i < all.length; i += 1) {
all[i].SourceElement().parentNode.className += ' has-jax';
}
});
</script>

<style>
.MathJax_Display {
overflow-x: scroll;
overflow-y: hidden;
}
</style>

上面JS代码为每个MathJax公式的源节点(对应markdown中的公式,也对应生成的html中的<script type="math/tex;"></script>节点)的父节点添加has-jax类。但是这有两个问题:其一,这个has-jax类在整个主题中根本没有用到过,也就说没有用处了;其二,从其本意来说,MathJax源节点的父节点不一定也是MathJax显示节点(所有MathJax图形都在其中绘制)的父节点。如下所示对于块公式的情况,MathJax会为显示节点生成一个额外的包裹容器。

graph TB
ip(行内公式父节点) --> if(显示节点)
ip --> is(源节点$...$)
dp(块公式父节点) --> dc(显示节点容器)
dc --> df(显示节点)
dp --> ds(源节点$$...$$)

然后看最下面的CSS代码,它配置了MathJax_Display类样式,使之在超出区域时生成滚动条,然而,MathJax_Display仅仅对应了使用特定MathJax渲染方式的块公式,也就无怪乎为何行公式总是会超出区域。

因此,解决方法如下:

@@ -33,15 +33,16 @@
MathJax.Hub.Queue(function() {
var all = MathJax.Hub.getAllJax(), i;
for (i = 0; i < all.length; i += 1) {
- all[i].SourceElement().parentNode.className += ' has-jax';
+ document.getElementById(all[i].inputID + '-Frame').parentNode.classList.add('has-jax');
}
});
</script>
<script src="{{ theme.math.mathjax.cdn }}"></script>

<style>
-.MathJax_Display {
- overflow-x: scroll;
+
+.has-jax {
+ overflow-x: auto;
overflow-y: hidden;
}
</style>

三处修改:

  1. 利用显示节点的id正好是源节点id后面加上’-Frame’,找到显示节点再获得它的父节点,为其添加has-jax类标记
  2. 将CSS中的选择符改为.has-jax,匹配所有jax-jax类的元素
  3. scroll改为auto,效果更好点,后者会避免在没有超出区域时也显示一个空白滚动条

5. overflow属性导致列表前缀符丢失问题

这是由于上一步修改导致的次生问题,使用未经修改的NexT主题也不会有这个问题。

而且与其说这是个配置问题,更像是浏览器的bug,参考这个stackoverflow问题

表现形式为,当一个(有序/无序)列表项中的内部元素或者其本身具有overflow属性时,列表项前缀的项目符号不能正常显示(鬼知道为什么会这样)。比如考虑如下的MarkDown代码:

- $\alpha$
- $\beta$

如果采纳了上一步的提到的修改,为了避免超出区域为公式显示节点的父节点添加了overflow属性,显示效果将会是这样的:

公式倒是没有问题,但会发现列表项前面的小点没了!

具体参考#669#745中的讨论,总之目前没有比较好的解决办法。

一个很难看的解决思路是:强制将列表的前缀符号位置设置到元素内部,css如下:

li {
list-style-position: inside !important;
}

但是这种方法并不是很好,因为副作用比较大,会导致在一些情况下前缀符和列表中文字内容断成两行,显示效果就比较难看了。