Hexo + Next:一个字面 code 标签引发的 unexpected end of file
背景与现象
在完善一篇《Markdown 语法说明》时,主题开启了 Mermaid 与 FancyBox,并在文中加入了流程图 / 时序图。随后执行构建:
1 | npx hexo clean && npx hexo g |
稳定报错:
1 | Template render error: (unknown path) |
报错来自 Nunjucks(Hexo 的模板引擎),含义是某个块标签没有闭合。但通读全文,所有 note / centerquote 等标签明明都是成对出现的,肉眼完全看不出问题。
更具迷惑性的是:同一段 Mermaid 时序图单独放进一篇新文章却能正常构建,一度让人以为是「中文参与者」「箭头语法」或 Mermaid 本身的问题。
环境信息
1 | 操作系统:Linux |
排查过程
1. 先排除「想当然」的原因
网上同类报错大多建议「用 raw 标签包住代码块里的花括号」「卸载多余的 Mermaid 插件」「MathJax 插件没注册」。逐条核对本仓库后均不成立:
package.json中没有hexo-filter-mermaid-diagrams,也没有hexo-renderer-marked,不存在插件冲突;markdown-it-mathjax3已安装,数学公式的 SVG 在页面中正常生成;- Hexo 8 在模板扫描前,会先把三反引号围栏代码块整体替换成占位符,围栏里的标签并不会被执行,所以给围栏套 raw 标签没有意义。
结论:不能靠猜,需要拿到「传给 Nunjucks 的最终输入」做确定性复现。
2. 拿到最终模板输入并二分定位
在 Hexo 的渲染管线里给临时插桩,把交给 Nunjucks 之前的完整 HTML 落盘,随后在这份文件上做只读二分:
- 以每个
endnote为安全边界切前缀:直到「Mermaid」一节末尾的 note 都还正常; - 再逐行向后延长,失败在加入后面「HTML 内联」一节里一句普通段落时出现;
- 把该段落单独渲染 → 正常;把它与前面那个 Mermaid note 组合 → 必现失败。
这说明问题不在某一行,而在于前面某段内容让一个保护正则「跨过了」块标签的结束标记。
3. 锁定到 note 体内的字面 code 标签
对那个 note 的正文逐句拆解,最终最小复现是:note 正文里用行内代码展示了一个只有开标签、没有闭标签的字面 HTML:
1 | {% note info %} |
注意:出问题的不是行内代码本身,而是代码里写的那个字符串——一个字面的开标签 code,附近并没有对应的闭标签。
根因
Hexo 在把文章交给 Nunjucks 之前,会用一个正则保护行内代码里的花括号,避免其中的 {% %} 被当成模板标签(见 node_modules/hexo/dist/extend/tag.js):
1 | const rCodeTag = /<code[^<>]*>[\s\S]+?<\/code>/g; |
这里有两个致命的叠加条件:
- 正则是非贪婪且跨任意字符的:从一个字面
<code ...>开始,一直匹配到后面遇到的第一个</code>。note 体里那个只开不合的<code class="mermaid">,就一路跨过了中间的{% endnote %},配到了后文第一个正常行内代码的</code>。 - 此刻是「混合内容」:note 块标签的正文此刻还是原始 Markdown(行内反引号尚未转义,里面的
<code ...>是字面字符),而 note 之外的正文已经是渲染好的 HTML(含真实的</code>)。于是这一整段匹配中的花括号被转义,{% endnote %}被改写成了普通字符,Nunjucks 再也看不到 note 的闭合,最终抛出unexpected end of file。
一句话:在 note 等会被二次渲染的标签体内,用行内代码写了一个只开不合的字面 code 开标签,触发了 Hexo 行内代码保护正则的跨段匹配,把后面的块标签结束标记一起转义掉了。
这也解释了所有矛盾现象:
- 该 note 单独渲染正常——后面没有
</code>可供错配; - 放进全文才崩——后文存在正常的行内代码闭标签;
- 换成字面的
pre、span标签不崩——保护正则只认code; - 时序图、Mermaid、MathJax 全都无辜。
解决方案
不在 note(或任何会被二次渲染的标签体)里写「只开不合」的字面 HTML 标签。把那句改写为不含字面尖括号的等价表述即可:
1 | {% note info %} |
如果确实需要展示标签文本,优先用下面两种安全方式:
- 放进三反引号围栏代码块(围栏会被 Hexo 提前整体保护,不进入模板扫描);
- 或直接用文字 / HTML 实体描述,避免出现孤立的字面
<code ...>。
配套问题:Mermaid 围栏不渲染
修复崩溃后又发现:写成语言为 mermaid 的三反引号围栏,图表只显示为高亮源码、并不出图。原因是 NexT v8 只注册了 mermaid 块标签,并不会自动转换 mermaid 围栏:
- 标签插件输出前端需要的结构:
<pre><code class="mermaid">…,前端脚本只选择pre > .mermaid的元素来画图; - 三反引号围栏则被高亮器渲染成普通代码块(
class="highlight plaintext"),没有.mermaid,Mermaid 库即使加载了也找不到目标。
因此图要改用标签写法(起始标记 mermaid、结束标记 endmermaid):
1 | {% mermaid %} |
标签体会被原样保护并做 HTML 转义,所以流程图里的花括号 B{判断条件}、时序图里的 -->> 都能保留,前端正常绘制。
另一种做法是写一个高优先级的
before_post_render过滤器,在代码高亮前把 mermaid 围栏批量转换成标签。本站只有少量图,为减少全局脚本、保持文章自包含,最终选择直接在文章里使用标签。
验证
1 | npx hexo clean && npx hexo g # 退出码 0 |
构建产物中确认:
- 不再报
unexpected end of file,所有文件正常生成; - 两张图都输出为
<code class="mermaid">(流程图、时序图各一),且B{判断条件}的花括号原样保留; - 演示标签语法的围栏里,标签以转义后的纯文本展示,不会被执行;
- Mermaid 与 FancyBox 的 JS / CSS 资源均在页面中加载(FancyBox 由前端脚本动态包裹正文图片,静态 HTML 中没有包裹标签属于正常现象)。
经验小结
- 区分现象与根因:报错点在 Nunjucks,根因却在更前面的一个字符串保护正则;被怀疑的时序图其实无辜。
- 警惕「非贪婪 + 跨任意字符」的全局正则:在混合内容(部分已渲染、部分未渲染)上,孤立的开标签可能让匹配跨过结构边界。
- 标签插件的正文是特殊区域:note 等会对内容做二次 Markdown 渲染,其中放原始 HTML 标签要格外小心;展示源码优先用围栏。
- 二分要用「安全边界」:沿
endnote之类保证成对完整的位置切,再逐行延长,能快速把范围收敛到最小复现;在围栏中途截断会产生大量假失败。 - 每次改动后执行
npx hexo clean && npx hexo g,用退出码和产物做确定性验证。