Hexo + Next:一个字面 code 标签引发的 unexpected end of file

本文由 AI 辅助生成并经作者审校,仅供参考。

背景与现象

在完善一篇《Markdown 语法说明》时,主题开启了 Mermaid 与 FancyBox,并在文中加入了流程图 / 时序图。随后执行构建:

1
npx hexo clean && npx hexo g

稳定报错:

1
2
Template render error: (unknown path)
unexpected end of file

报错来自 Nunjucks(Hexo 的模板引擎),含义是某个块标签没有闭合。但通读全文,所有 note / centerquote 等标签明明都是成对出现的,肉眼完全看不出问题。

更具迷惑性的是:同一段 Mermaid 时序图单独放进一篇新文章却能正常构建,一度让人以为是「中文参与者」「箭头语法」或 Mermaid 本身的问题。

环境信息

1
2
3
4
5
6
操作系统:Linux
Hexo:8.1.1(hexo-cli 4.3.2,Node 24.21.0)
主题:hexo-theme-next 8.29.1
渲染器:hexo-renderer-markdown-it 7.1.1
Mermaid(前端 CDN):11.10.1
FancyBox(前端 CDN):@fancyapps/ui 6.1.14

排查过程

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
2
3
4
5
{% note info %}
图表代码先以 `<code class="mermaid">` 写入页面。
{% endnote %}

<p>后文某处还有一个正常的 <code>行内代码</code>。</p>

注意:出问题的不是行内代码本身,而是代码里写的那个字符串——一个字面的开标签 code,附近并没有对应的闭标签。

根因

Hexo 在把文章交给 Nunjucks 之前,会用一个正则保护行内代码里的花括号,避免其中的 {% %} 被当成模板标签(见 node_modules/hexo/dist/extend/tag.js):

1
2
3
4
const rCodeTag = /<code[^<>]*>[\s\S]+?<\/code>/g;
const escapeSwigTag = (str) => str.replace(/{/g, '&#123;').replace(/}/g, '&#125;');

str.replace(rCodeTag, s => s.match(rSwigRawFullBlock) ? s : escapeSwigTag(s));

这里有两个致命的叠加条件:

  1. 正则是非贪婪且跨任意字符的:从一个字面 <code ...> 开始,一直匹配到后面遇到的第一个 </code>。note 体里那个只开不合的 <code class="mermaid">,就一路跨过了中间的 {% endnote %},配到了后文第一个正常行内代码的 </code>。
  2. 此刻是「混合内容」: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
2
3
{% note info %}
图表代码先以一个带 mermaid 类名的 code 元素写入页面。
{% endnote %}

如果确实需要展示标签文本,优先用下面两种安全方式:

  • 放进三反引号围栏代码块(围栏会被 Hexo 提前整体保护,不进入模板扫描);
  • 或直接用文字 / HTML 实体描述,避免出现孤立的字面 <code ...>。

配套问题:Mermaid 围栏不渲染

修复崩溃后又发现:写成语言为 mermaid 的三反引号围栏,图表只显示为高亮源码、并不出图。原因是 NexT v8 只注册了 mermaid 块标签,并不会自动转换 mermaid 围栏:

  • 标签插件输出前端需要的结构:<pre><code class="mermaid">…,前端脚本只选择 pre > .mermaid 的元素来画图;
  • 三反引号围栏则被高亮器渲染成普通代码块(class="highlight plaintext"),没有 .mermaid,Mermaid 库即使加载了也找不到目标。

因此图要改用标签写法(起始标记 mermaid、结束标记 endmermaid):

1
2
3
4
5
6
7
{% mermaid %}
graph LR
A[开始] --> B{判断条件}
B -->|满足| C[执行处理]
B -->|不满足| D[结束]
C --> D
{% endmermaid %}

标签体会被原样保护并做 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,用退出码和产物做确定性验证。

参考