用 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 下添加了 enable
、 per_page
、 cdn
的选项,便于自定义 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
的时候,所有的问题都解决了。