SilverLining's Blog

Hexo 下渲染 Mathjax 问题

用 Hexo 写博客已经一年多了,对 Nginx+Hexo+Markdown 的组合很满意。然而在文章中书写数学公式一直是一个头疼的问题。 Hexo 对 Mathjax 的 LaTeX 渲染原生支持并不好,因此需要做一些调整。

Hexo 文档:https://hexo.io/
主题 Hexo-Next:http://theme-next.iissnan.com/
主题 repository:https://github.com/iissnan/hexo-theme-next

Mathjax 与 Markdown 冲突

问题重现

先重现一下这个问题。在写文章中写了一个比较简单的公式:

$ H_{y'}(y)=-\sum_{i}{y'}_{i}{\cdot}log(y_i) $

渲染出来却变成了:

$\$ H_{y'}(y)=-\sum_{i}{y'}_{i}{\cdot}log(y_i) \$\$

研究了一下,MathJax 支持已经打开了。因为在 Markdown 语法中,两个下划线之间的文本会被转换为斜体,所以这个错误是由于 Markdown 本身没有支持 Latex,Markdown 文本先交由 marked.js(Hexo 默认渲染器)对文本进行渲染时,将_替换成了<em> 标签,然后才被 Mathjax 交由 mathjax.js 进行渲染,导致无法正确识别公式。同样的问题也发生在\经过转义后变成\,MathJax 渲染时不能正确识别换行符。

解决过程

pandoc

理解了这个问题的本质是 Markdown 与 LaTeX 语法冲突后,我们来理一理解决问题的思路。最根本的解决方法当然是从 Markdown 语法本身入手,换用有着更 strong 的语法的标记语言来避免冲突,比如 pandoc 。 pandoc 大法固然好,但是为了保持博客的轻量级(当初就是为了这个从 Wordpress 转到了 Markdown+Hexo),暂时还不打算动用 pandoc 这个核武器。感兴趣的小伙伴可以去了解一下 pandoc,与之对应的 Hexo 插件 hexo-renderer-pandoc

保护公式块

第二个思路就是利用 Markdown 特有的 rawblock 标签保护 LaTeX 代码块。这是较为安全的一种方法,但缺点也是显而易见的:需要改动原文本,并且如果有大段公式,修改起来很麻烦。

修改渲染规则

第三个思路是修改 Hexo 的渲染引擎,针对<em> 标签和换行符的渲染进行修改。
修改 nodes_modules/marked/lib/marked.js

// 去掉\\换行转义
escape: /^\\([\\`*{}\[\]()# +\-.!_>])/,  ->  escape: /^\\([`*{}\[\]()# +\-.!_>])/,
// 去掉_斜体转义
em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,  ->  em:/^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,

这一方法通用性较高,因为我们并没有修改原文本的内容,而且这一渲染规则也可以适用别的渲染引擎。

替换 Markdown 引擎

除了手动修改渲染规则以外,我们还可以直接替换 Markdown 引擎。 Segmentfault 中的一个 thread 给出的解决方案是使用 hexo-renderer-markdown-it 引擎进行渲染。在 hexo-renderer-markdown-it 的文档中可以看到操作相当简单:

$ npm uninstall hexo-renderer-marked --save
$ npm install hexo-renderer-markdown-it --save

两行命令即可完成,先卸载 Hexo 自带的 Markdown 解析器 hexo-renderer-marked 再安装 hexo-renderer-markdown-it 就可以了。安装完以后,hexo clean && hexo g 重新生成静态网页,这回公式能正常显示了。
但是又出现了一个新的问题,使用 hexo-renderer-markdown-it 渲染之后,文章中的 TOC 里的链接都失效了,而且侧边栏的快速导航链接也都失效了。

继续查找更优的解决方案,Hexo 有没有其他的 Markdown 渲染插件呢?在 Hexo 主站的插件页搜索关键字 Markdown,发现了 hexo-renderer-kramed 这个插件,该项目是对 hexo-renderer-marked 的 fork,并且只针对 MathJax 支持进行了改进,这正是我们需要的。替换如下:

npm uninstall hexo-renderer-marked --save
npm install hexo-renderer-kramed --save

这下,不仅能正常使用 TOC,也能完美地支持 MathJax 渲染了。

kramed 行内公式的坑

以上解决了 MathJax 对行间公式的渲染,然而 kramed 对行内公式的渲染还是有些问题。 diff 一下 kramed 引擎对 marked 的修改,可以发现 kramed 只是提高了 LaTeX 公式渲染的优先级,使类似'formula'的语法不会被 Markdown 引擎替换,从而可以正确的被渲染成 LaTeX 公式。

但在下列情况下,渲染行间公式仍然存在问题:

首先,我们希望行内公式可以用两个 $符标识;
而当我们使用的行内代码中出现两个 $符时,它们之间的内容不应被转义为 Latex 公式,而是应该按原来的内容展示;
同理,在代码块中出现两个 $符时,我们也不希望它们之间的内容不应被转义为 Latex 公式。

行内公式:$ R{m \times n} = U{m \times m} S{m \times n} V'{n \times n} $

包含两个 $的行内代码:$ R{m \times n} = U{m \times m} S{m \times n} V'{n \times n} $

行间公式:

$$ R_{m \times n} = U_{m \times m} S_{m \times n} V'_{n \times n} $$

包含两个 $的代码块:

$R_{m \times n} = U_{m \times m} S_{m \times n} V'_{n \times n}'$

这里可以看到行内公式和行内代码的渲染确实有问题,这是因为 kramed 对行内公式的实现就是基于行内代码来做的,也就是说,对于 kramed 而言,出现'符以后的两个 $符之间的部分才会被 kramed 认为是行内公式。

注意,即使没有用两个'符括起来也会被匹配成行内公式,这相当容易引起 bug 。

要解决这个问题需要修改 kramed 的渲染机制。而在代码块中这种情况是没问题的,因为 kramed 里面对行内公式和行间公式的实现机制不同。

Hexo Next 主题下 CDN 的配置

之前的主题版本为 5.0,一直没升级,最近 git pull 升级了一下主题,发现 MathJax 无法渲染公式了。查看网页中公式部分的源代码:

<script type="math/tex; mode=display">H_{y'}(y)=-\sum_{i}{y'}_{i}{\cdot}log(y_i)</script>

可以看到,这里的公式渲染是正确的,并没有出现错误。那么为什么页面不显示呢?F12 查看一下 network,发现原来是 MathJax 的 js 没有正确引用。

可以看到 Next 主题的源码中 layout/_scripts/third-party/mathjax.swig

{% if theme.mathjax.enable %}
  {% if not theme.mathjax.per_page or (page.total or page.mathjax) %}
    <script type="text/x-mathjax-config">
      MathJax.Hub.Config({
        tex2jax: {
          inlineMath: [ ['',''], ["\\(","\\)"]  ],
          processEscapes: true,
          skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
        }
      });
    </script>

    <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>
    <script type="text/javascript" src="{{ theme.mathjax.cdn }}"></script>
  {% endif %}
{% endif %}

在之前的版本,主题中对 MathJax 的开启配置方式是 mathjax: true,在新的版本(5.1)之后,作者在 mathjax 下添加了 enableper_pagecdn 的选项,便于自定义 CDN 地址。而之前的配置方式可能由于默认 CDN 地址失效,导致不能正确引用 js 。

找到了问题之后就很好解决了。只需要修改一下主题的配置文件即可。这里的 CDN 地址我们使用 MathJax 文档给出的地址。考虑到 HTTPS 安全协议,我们省略协议头,让其自动请求对应的资源。

# MathJax support
mathjax:
    enable: true
    cdn: //cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML

刷新页面,看到公式已经可以正常显示了。

MathJax 会稍稍拖慢页面加载的速度,大约 1s 左右。如果由于网络问题不能正常加载,也可以尝试更换 bootstrap CDN七牛云 CDN 等其他地址。

npm 的内存占用

最近把 VPS 从 Ubuntu 14.04 升级到了 16.04,hexo g 总是莫名其妙的就被 kill,尝试重新安装 Hexo,连 npm 也会莫名奇妙的卡住。加上--debug 标签也得不到有效的错误信息。

最后想到,可能是内存不够的原因。由于 VPS 上的是 512MB 的内存,安装过程用到一些底层的 npm 包,可能会编译一些代码,导致了内存不够用。

查看内存日志:

$ dmesg -T
Out of memory: Kill process 4093 (npm) score 526 or sacrifice child
Killed process 4093 (npm) total-vm:1457024kB, anon-rss:269708kB, file-rss:0kB

果然是因为内存不足导致的进程 kill,因此我们解决的思路就是利用 swap file 实现虚拟内存。

首先查看系统是否配置了交换区域:

sudo swapon -s

如果没有的话,便直接开始创建一个新的 1G 的 swap file:

sudo fallocate -l 1G /swapfile

确认文件创建成功及文件大小:

ls -lh /swapfile

创建成功之后我们要启用 Swap 文件:

# 调整文件的权限
sudo chmod 600 /swapfile
# 设置交换区域
sudo mkswap /swapfile
# 启用 swap file
sudo swapon /swapfile

现在基本的步骤已经完成了,可以使用最初的命令验证 swap file 是否正确使用:

$ sudo swapon -s
Filename     Type     Size     Used    Priority
/swapfile    file    1048572     0       -1

交换分区已经成功设置,系统会在必要的时候使用它。

注意,虽然现在已经启用了 swap 文件,但当我们重启的时候,系统不会自动地启用该 swap 文件。可以通过修改 fstab 文件实现开机使用 swap 文件:

sudo vim /etc/fstab

在文件的最后,添加自动使用 swap 文件的设置:

/swapfile   none    swap    sw    0   0

之后再运行 npm install 的时候,所有的问题都解决了。