撰写博文时,请谨慎使用 HTML 语句

Coast23

遇错

在编辑某篇文章时,我在文中使用了如下语句:

<font size = 5><b><font color = '#66CCFF'>请输入文本</font></b></font>

并在该文章中启用了 mathjax(我使用的是 hexo-filter-mathjax 插件)。

然后,hexo s 时,终端报错:

FATAL Something's wrong. Maybe you can find the solution here: https://hexo.io/docs/troubleshooting.html
Error: Can't find handler for document
at HandlerList.handlesDocument (...\node_modules\mathjax-full\js\core\HandlerList.js:60:15)
at HandlerList.document (...\node_modules\mathjax-full\js\core\HandlerList.js:64:21)
at Object.document (...\node_modules\mathjax-full\js\mathjax.js:11:41)
at ...\node_modules\hexo-filter-mathjax\lib\filter.js:42:26
at Hexo.<anonymous> (...\node_modules\hexo-filter-mathjax\index.js:20:18)
at Hexo.tryCatcher (...\node_modules\bluebird\js\release\util.js:16:23)
at Hexo.<anonymous> (...\node_modules\bluebird\js\release\method.js:15:34)
at ...\node_modules\hexo\dist\extend\filter.js:58:67
at tryCatcher (...\node_modules\bluebird\js\release\util.js:16:23)
at Object.gotValue (...\node_modules\bluebird\js\release\reduce.js:166:18)
at Object.gotAccum (...\node_modules\bluebird\js\release\reduce.js:155:25)
at Object.tryCatcher (...\node_modules\bluebird\js\release\util.js:16:23)
at Promise._settlePromiseFromHandler (...\node_modules\bluebird\js\release\promise.js:547:31)
at Promise._settlePromise (...\node_modules\bluebird\js\release\promise.js:604:18)
at Promise._settlePromise0 (...\node_modules\bluebird\js\release\promise.js:649:10)
at Promise._settlePromises (...\node_modules\bluebird\js\release\promise.js:729:18)
at _drainQueueStep (...\node_modules\bluebird\js\release\async.js:93:12)
at _drainQueue (...\node_modules\bluebird\js\release\async.js:86:9)
at Async._drainQueues (...\node_modules\bluebird\js\release\async.js:102:5)
at Async.drainQueues [as _onImmediate] (...\node_modules\bluebird\js\release\async.js:15:14)
at process.processImmediate (node:internal/timers:491:21)

排错

把写到一半的文章移出 _posts 文件夹,重新执行 hexo s,报错依旧(大概率是缓存导致的)。

这使我认为不是文章的问题,而是 mathjax-full 这个模块的问题(最后发现根因就是它)。后续清空 _posts 文件夹,依然会报一些奇怪的错(估计又是缓存导致的)。

然后就在错误的方向上越走越远,甚至把 node_module 全清了然后重新装一遍,但问题依旧(由于我之前对一些模块做过修改,并且是在重装完才意识到这一点,这个过激的举措导致我彻底丢失了原有的博客环境)。用 Agent 分析了半天,也没能正确定位出问题(Agent 一口咬定是 hexo-blog-encrypt 插件导致的冲突,但之前都没报过这个错,显然不是它的问题)。

折腾了半天都没搞好,我才想到,既然之前用着都没问题,那只能是最新的那篇文章的问题了。

再次把这篇文章移出 _posts 并执行 hexo s,这次就没有报错了!太神秘了。

hexo-filter-mathjax 的 GitHub Issues 发现其早有前车之鉴,把问题范围缩小到文章中的 HTML 语块,逐个排查,最终定位到了文章开头提到的语句上。

分析

为什么 mathjax.js 无法处理这个语句?翻阅代码发现 mathjax-full/js/adaptors/lite/Parser.js 对 HTML 的切分用的是正则,且非常 tricky:

// Definition of PATTERNS:
// Parser.js:59-71
PATTERNS.TAGNAME = '[a-z][^\\s\\n>]*';
PATTERNS.ATTNAME = '[a-z][^\\s\\n>=]*';
PATTERNS.VALUE = "(?:'[^']*'|\"[^\"]*\"|[^\\s\\n]+)";
PATTERNS.VALUESPLIT = "(?:'([^']*)'|\"([^\"]*)\"|([^\\s\\n]+))";
PATTERNS.SPACE = '(?:\\s|\\n)+';
PATTERNS.OPTIONALSPACE = '(?:\\s|\\n)*';
PATTERNS.ATTRIBUTE = PATTERNS.ATTNAME + '(?:' + PATTERNS.OPTIONALSPACE + '=' + PATTERNS.OPTIONALSPACE + PATTERNS.VALUE + ')?';
PATTERNS.ATTRIBUTESPLIT = '(' + PATTERNS.ATTNAME + ')(?:' + PATTERNS.OPTIONALSPACE + '=' + PATTERNS.OPTIONALSPACE + PATTERNS.VALUESPLIT + ')?';
PATTERNS.TAG = '(<(?:' + PATTERNS.TAGNAME + '(?:' + PATTERNS.SPACE + PATTERNS.ATTRIBUTE + ')*'
+ PATTERNS.OPTIONALSPACE + '/?|/' + PATTERNS.TAGNAME + '|!--[^]*?--|![^]*?)(?:>|$))';
PATTERNS.tag = new RegExp(PATTERNS.TAG, 'i');
PATTERNS.attr = new RegExp(PATTERNS.ATTRIBUTE, 'i');
PATTERNS.attrsplit = new RegExp(PATTERNS.ATTRIBUTESPLIT, 'i');

// Parser.js:117-136
LiteParser.prototype.openTag = function (adaptor, node, tag, parts) {
var PCDATA = this.constructor.PCDATA;
var SELF_CLOSING = this.constructor.SELF_CLOSING;
var kind = tag.match(/<(.*?)[\s\n>\/]/)[1].toLowerCase();
var child = adaptor.node(kind);
var attributes = tag.replace(/^<.*?[\s\n>]/, '').split(PATTERNS.attrsplit);
if (attributes.pop().match(/>$/) || attributes.length < 5) {
this.addAttributes(adaptor, child, attributes);
adaptor.append(node, child);
if (!SELF_CLOSING[kind] && !tag.match(/\/>$/)) {
if (PCDATA[kind]) {
this.handlePCDATA(adaptor, child, kind, parts);
}
else {
node = child;
}
}
}
return node;
};

写一个测试脚本,看看 tag 到底被处理成了啥。

在博客根目录创建一个 test.js

// test.js
const { PATTERNS } = require('mathjax-full/js/adaptors/lite/Parser.js');

const attrsplit = new RegExp(PATTERNS.ATTRIBUTESPLIT, 'i');

console.log('PATTERNS.ATTRIBUTESPLIT =', PATTERNS.ATTRIBUTESPLIT);
console.log('');

function test(tag){
// 去掉 <tagname
const stripped = tag.replace(/^<.*?[\s\n>]/, '');
// 用 attrsplit 切分属性
const parts = stripped.split(attrsplit);
// pop 掉末尾元素,检查是否是 '>'
const last = parts.pop();

console.log('tag =', tag);
console.log('stripped =', JSON.stringify(stripped));
console.log('split =', JSON.stringify(parts));
console.log('pop() =', JSON.stringify(last));
console.log('');

// if 检查 tag 是否能解析
if(last.match(/>$/) || parts.length < 5) console.log('YES');
else console.log('NO');
console.log('-------------------------------');
}

test('<font size = 5>');
test('<font size = "5">');
test('<p>');

运行(命令为 node test.js),得到如下输出:

PATTERNS.ATTRIBUTESPLIT = ([a-z][^\s\n>=]*)(?:(?:\s|\n)*=(?:\s|\n)*(?:'([^']*)'|"([^"]*)"|([^\s\n]+)))?

tag = <font size = 5>
stripped = "size = 5>"
split = ["","size",null,null,"5>"]
pop() = ""

NO
-------------------------------
tag = <font size = "5">
stripped = "size = \"5\">"
split = ["","size",null,"5",null]
pop() = ">"

YES
-------------------------------
tag = <p>
stripped = ""
split = []
pop() = ""

YES
-------------------------------

<font size = 5> 被切成了 ["","size",null,null,"5>"]!?

分析正则式,发现无引号的 5 会被 ([^\s\n]+) 贪婪匹配,吞掉了末尾的 >,导致节点未被解析成 openTag,后续 closeTag 找不到匹配的节点,就会抛出异常。

最简单的解决方法在 test.js 里已有体现,给 5 加个引号即可。

总结

<font size = 5> 是否是合法的 openTag?根据 HTML 规范,当属性值仅由字母、数字、连字符(-)、句点(.)等有效字符组成时,引号是可选的,所以这个写法是没有问题的。尽管 <font> 标签在 HTML5 已经 Deprecated,但相比现代化的 <span style="...">,我觉得前者写起来会更舒服,读起来也更符合直觉,因此至今都在用 <font> 标签。

Parser 的 bug 不应由用户来迁就。但问题在于,用户不可能事先知道解析器的 bug,且面对一堆指代不明的报错,排错往往要花费大量的时间(哪怕有 Agent 的协助,也可能如此)。所以本次的经验教训就是,在使用 HTML 或类似的文本标记语言时,一定要谨慎,因为可能一不小心,就把开发者纸糊的 Parser 给 hack 了。

  • 标题: 撰写博文时,请谨慎使用 HTML 语句
  • 作者: Coast23
  • 创建于 : 2026-07-22 22:53:58
  • 更新于 : 2026-07-23 01:24:05
  • 链接: https://coast23.github.io/2026/07/22/撰写博文时,请谨慎使用-HTML-语句/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
撰写博文时,请谨慎使用 HTML 语句