如何在Hexo中使用Latex公式是大部分Hexo用户都曾面临过的棘手问题。即便已是2024年,网上仍然没有一篇文章能够给出一个完美的解决方案(也可能是我没找到)。本文将介绍一个可行且少有资料的方案,但其同样不完美

目录

  1. 当前的问题
  2. 目前的方案
    1. 优缺点
    2. 注意事项
    3. 缺点的应对之道
  3. 实际效果
  4. 转换脚本
  5. 参考

当前的问题

首先谈谈现有解决方案存在的一些问题:

1、目前很多博客中介绍的配置方法都是基于Next主题的,其他主题并不适用。(Next毫无疑问是一个优秀且维护良好的主题,但实在用的人太多。本人略有反骨,不太想随大流)。

在查资料的过程中,不得不感概好多博客的内容质量低下。一方面是内容过时,但由于点击量高仍然会被搜索引擎优先推荐;另一方面有些博客过于简略,缺少对于前置条件的描述,使得读者很难判断是否适合自己。加之,部分文章东拼西凑,过度抄袭。想要在如此庞杂的信息中找到一个适合的解决方案,着实不易。

2、依赖的插件已经不维护了,如hexo-renderer-kramedhexo-renderer-mathjax

hexo-renderer-pandochexo-filter-mathjax一直都在维护,不过本人从来没用过,不知体验如何。

3、Latex的转义规则与Markdown自身语法冲突。如,Hexo会将_转为<em>,影响了后续数学公式的渲染。

4、Mathjax的CDN不稳定,公式加载慢,有时甚至会失败。

目前的方案

本文介绍的方案是hexo-math,它是Hexo官方推出的一个标签插件,可以将KaTeX和MathJax嵌入到Hexo页面中。不知道为什么感觉国内用的不多。

优缺点

优点:

  • 配置简单,根据README配置即可。
  • 支持KaTeX和MathJax,并且可以在front-matter中控制是否启用数学公式。
  • 除特殊需求,无需再_config.yml中配置。
  • 公式使用svg格式显示,无需担心公式加载速度。
  • 目前没有遇见语法冲突问题。可能因为是官方插件,已经很好地处理了与hexo-renderer-marked的冲突。

缺点:

  • 在markdown中使用相对繁琐。
  • 缺少对于行内公式和行间公式的区分,其默认均为行内公式。行间公式需要手动进行空行和居中操作。

正常在Markdown中,只需要使用$或者$$符号包含数学公式即可。然而,hexo-math的用法却是这样:

1
2
3
{% mathjax '{options}' %}
content
{% endmathjax %}
1
2
3
{% katex '{options}' %}
content
{% endkatex %}

这不免增加了打字的工作量,减缓了打字的速度。并且这一格式使得Markdown没有那么优雅,尤其是行内公式需要包含在这么长的上下文中。

PS: 如果hexo-math之后可以支持$或者$$,那将是一个完美的解决方案,期望官方开发者可以考虑这一提案。

注意事项

  1. 需要使用hexo-renderer-marked渲染引擎。
  2. 安装前要卸载其他与数学公式相关的插件,如hexo-renderer-mathjax,以免冲突。
  3. 无需在根目录或者主题目录下的_config.yml进行配置(那些与数学公式相关的乱七八糟的配置都可以删掉啦)。如有特殊需要,请参考Global Options

缺点的应对之道

上面提到的缺点带来了两点不便之处:

  1. 其一,如果想要迁移过去的博客,则需要对所有数学公式依赖的$$$符号进行修改,工作量巨大且繁琐。
  2. 撰写新博客时,添加数学公式并不方便,不符合之前的习惯。

为此,我的应对之道便是:

  • 仍然使用之前的格式,即$$$
  • 在撰写之后通过脚本自动将$$$转换为{\% engine \%}{\% endengine \%} (请忽略%前的转义符号\)。

文末附上了我目前使用的Python转换脚本并不完善,仅供参考。该脚本支持:

  • 针对单个.md文件的处理
  • 针对指定文件下所有.md文件的处理,方便迁移

具体思路如下:

  1. 首先从在全文中定位$$,并要求识别的个数为偶数个。将第奇数个$$替换为<div align=center>{\% mathjax \%},将第偶数个$$替换为{\% endmathjax \%}</div>。这样可以实现行间公式的居中。
  2. 然后逐行扫描$,同样要求个数为偶数个。将第奇数个$替换为{\% mathjax \%},将第偶数个$替换为{\% endmathjax \%}。这里假设一个行内公式的$环境内不会出现换行。
  3. 特别地,对于每行只有一个$符号的文本,不进行处理。因为,我习惯将单个$作为shell命令的提示符。

实际效果

下面给出几个例子,展示以下效果:

Demo 1:

Demo 2:

Demo 3:

Demo 4:

Demo 5:

转换脚本

更新日期:2024-01-07

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# Python
import glob
import argparse

inline_symbol = {
"start_symbol": '{% mathjax %}',
"end_symbol": '{% endmathjax %}'
}

block_symbol = {
"start_symbol": '<div align=center>{% mathjax %}',
"end_symbol": '{% endmathjax %}</div>'
}

def get_all_indices(text, substring):
indices = []
index = -1

while True:
index = text.find(substring, index + 1)
if index == -1:
break
indices.append(index)

return indices


def render_formula(content, indices, origin_symbol='$', start_symbol='$', end_symbol='$'):

if len(indices) == 0:
return content

new_content = ''
last_idx = 0
for i, idx in enumerate(indices):
if i%2 == 0:
new_content += content[last_idx:idx] + start_symbol
else:
new_content += content[last_idx:idx] + end_symbol

last_idx = idx + len(origin_symbol)

new_content += content[last_idx:]

return new_content


def preprocess(file_path):
with open(file_path, 'r') as f:
content = f.read()

indices = get_all_indices(content, '$$')
assert (len(indices) % 2 == 0)
new_content = render_formula(content, indices, origin_symbol='$$', **block_symbol)

lines = new_content.split('\n')
new_lines = []
for line in lines:
indices = get_all_indices(line, '$')
if len(indices) == 1:
new_lines.append(line)
else:
assert (len(indices) % 2 == 0)
new_lines.append(render_formula(line, indices, origin_symbol='$', **inline_symbol))

with open(file_path, 'w') as f:
f.writelines(list(map(lambda x:x+'\n', new_lines[:-1])) + [new_lines[-1]])


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--file', dest='file', type=str)
parser.add_argument('-d', '--dir', dest='dir', type=str)

args = parser.parse_args()

if args.dir:
markdown_files = glob.glob(f"{args.dir}/*.md")
for file_path in markdown_files:
print(f'processing {file_path}')
preprocess(file_path)
elif args.file:
print(f'processing {args.file}')
preprocess(args.file)
else:
print("Please specify a .md file or a directory containing .md files")

参考