markdown与hexo:下划线冲突与终极方案
以下内容完全由gemini 3.1pro生成(因为我大半夜的搏斗了两个小时快累死了)
如果你经常在博客里敲写复杂的公式(比如带海量下标的矩阵和行列式推导),并且使用的是 Hexo 博客框架,那么你大概率经历过这样的绝望时刻:
在本地 Typora 里,克莱姆法则(Cramer’s rule)的推导公式优雅完美:
$$A_i = [\mathbf{a}_1, \dots, \mathbf{a}_{i-1},\mathbf{b},\mathbf{a}_{i+1},\dots,\mathbf{a}_n]$$
满心欢喜地 hexo deploy 推送到网站后,打开网页一看,下划线全没了,排版炸成一团乱码,甚至公式里凭空出现了刺眼的明黄色斜体字。
这篇文章记录了我与 Hexo 7.x 底层渲染器长达数天的“生死搏斗”,并为你提供一个无需更换引擎、无需安装第三方废弃插件的终极物理隔离方案。
🕵️♂️ 案发现场还原:谁吃了我的下划线?
在 Hexo 的世界里,数学公式需要经过两道关卡:
- 后端的 Markdown 引擎(通常是默认的
marked):负责把 Markdown 语法转成 HTML。 - 前端的主题脚本(如 Fluid 内置的 MathJax/KaTeX):负责把 HTML 里的 LaTeX 代码渲染成漂亮的数学符号。
灾难就发生在这第一道关卡。
Markdown 语法中,成对的下划线 _文本_ 会被转换成 HTML 的斜体标签 <em>文本</em>。
虽然现在的 Markdown 引擎有所谓的“词内豁免权”(比如 x_1 不会被转义),但一旦你的下标碰到了标点符号或大括号 {},比如 \mathbf{a}_{i-1},豁免权立刻失效!
底层引擎会像推土机一样,把 _{i-1} 前面的下划线和 _{i+1} 前面的下划线强行配对,把它们中间包裹的所有内容全部变成斜体 <em>。
等这堆千疮百孔的残缺代码送到网页前端时,MathJax 发现公式里混进了 HTML 标签,直接宣告罢工。
💣 避坑指南:为什么网上的老教程都失效了?
在查阅资料时,你会搜到无数教程,但它们在最新的 Hexo 7.x 环境下几乎全部失效:
- 安装
hexo-filter-mathjax插件?
失效。在 Hexo 7.x 搭配的新版marked引擎面前,这个旧时代插件的生命周期完全被打乱,它根本拦截不住新引擎激进的正则匹配。 - 更换
kramed引擎?
失效。这是一个早已年久失修的老古董,对现代 Node.js 环境兼容极差,换上后往往会导致公式“连锅端”全军覆没。 - 更换
markdown-it引擎?
半失效。虽然它底层基于 AST(抽象语法树),能完美保护公式,但它生成了极其复杂的私有 HTML 标签。如果你的主题(如 Fluid)前端没有精确匹配它的 CSS 样式表,公式依然会“裸奔”。
🚀 终极杀招:编写原生“渲染前拦截器”
既然第三方插件靠不住,Markdown 引擎又是个没长眼睛的“破坏王”,那我们就不和它讲道理了,直接进行降维打击(物理隔离)。
利用 Hexo 强大的原生 scripts 扩展功能,我们可以写一个拦截脚本:在 Markdown 引擎触碰文章之前,把所有的公式挖出来藏进保险箱,原地留下一个安全的占位符;等引擎把普通文字渲染完,我们再把公式完好无损地放回原地。
实施步骤:
- 在你的 Hexo 博客根目录(与
source、themes同级)下,新建一个文件夹,命名为scripts(注意全小写)。 - 在该文件夹内,新建一个文件名为
protect-math.js。 - 将以下代码完整复制进去并保存:
/* scripts/protect-math.js */
// 1. 在 Markdown 渲染之前执行拦截:把公式藏起来
hexo.extend.filter.register('before_post_render', function(data) {
data.mathBlocks = [];
// 提取并保护块级公式 $$...$$
data.content = data.content.replace(/\$\$([\s\S]*?)\$\$/g, function(match) {
data.mathBlocks.push(match);
return '@@MATHBLOCK_' + (data.mathBlocks.length - 1) + '@@';
});
// 提取并保护行内公式 $...$
data.content = data.content.replace(/\$([^$\n]+?)\$/g, function(match) {
data.mathBlocks.push(match);
return '@@MATHINLINE_' + (data.mathBlocks.length - 1) + '@@';
});
return data;
});
// 2. 在 Markdown 渲染完成之后执行归还:把公式放回原地
hexo.extend.filter.register('after_post_render', function(data) {
if (!data.mathBlocks) return data;
// 还原块级公式
data.content = data.content.replace(/@@MATHBLOCK_(\d+)@@/g, function(match, i) {
return data.mathBlocks[i];
});
// 还原行内公式
data.content = data.content.replace(/@@MATHINLINE_(\d+)@@/g, function(match, i) {
return data.mathBlocks[i];
});
return data;
});