1 问题引入
Hexo的标签(Tag)系统一直有一点「格格不入」的感觉。注意这里的标签系统是指使用了
1 | {% ... %} |
的标签插件。这里我用的代码块的方式给出标签插件的代码形式,而不是使用的更加简洁的行间代码块,为什么呢?因为直接在Hexo的markdown博文里面写
1 | `{% ... %}` |
会导致解析失败。这是何等的卧槽。但是其实如果了解解析失败的原因,我们可以用一些复杂的办法来把这个行间代码打出来:{% raw %}{% %}{% endraw %}
。 具体的写法是
1 | `{% raw %}{% %}{% endraw %}` |
那么为什么行间公式渲染会出现这个问题,但是块公式渲染又不会出现这个问题呢?另外,为什么使用上面的这个写法就能够让{% raw %}{% %}{% endraw %}
被正确的渲染呢?要搞清楚这些问题,我们首先要大致梳理一下Hexo的标签系统的实现过程。
2 实现分析
2.1 cacheObj
这里我分析的Hexo版本为3.8.0。尽管不是最新的Hexo,但是目前来看Hexo并没有修改标签插件系统的的实现方法,所以我们这里的分析还是适用的。关键的代码部分位于Hexo项目下的lib/hexo/post.js
文件中的函数Post.prototype.render
,这个函数负责进行文章内容的渲染。函数里的关键内容如下,注释部分是我的分析:
1 | Post.prototype.render = function(source, data = {}, callback) { |
上面的函数中,ctx.render
会将我们的markdown内容交给对应的渲染器进行渲染。注意在渲染内容之前,先用cacheObj.escapeContent
和cacheObj.escapeAllSwigTags
进行了处理。而在渲染完成之后的回调中,又用cacheObj.loadContent
对渲染结果进行了处理,并再次使用tag.render
进行了二次渲染。那么cacheObj
具体做了什么呢?我们来看下面的代码:
1 | const _escapeContent = (cache, str) => { |
我们注意到在escapeAllSwigTags
中,会将待处理文档中的{% raw %}/\{%[\s\S]*?%\}/g{% endraw %}
这种模式的字符串,也就是形如{% raw %}{% ... %}{% endraw %}
的内容,会被替换成`<!--${placeholder}${cache.push(str) - 1}-->`
的形式,距离而言,
1 | 这是 {% 一段 %} 文字 |
在经过 escapeAllSwigTags
的处理之后会变成
1 | 这是 <!--1--> 文字 |
注意
\uFFFC
这个Unicode字符是OBJECT REPLACEMENT CHARACTER,很多字体都不支持显示。Unicode对这个字符的定义是used as placeholder in text for an otherwise unspecified object
而被替换的内容则会被cacheObj
缓存起来。被替换的内容<!--1-->
中的数字会自动随着缓存数量的增长而增长,从而实现了从缓存列表到占位内容之间的映射。被替换的内容在HTML语义上是注释内容,因此一般的markdown渲染器都会无视这些内容。
在完成渲染后,cacheobj.loadContent
会把缓存的内容,插入回对应占位符的位置。
cacheObj
对于渲染内容的处理,确保了标签插件内容不会被系统默认的markdown渲染器处理。
2.2 tag.render
经过cacheObj
处理的文本内容随后交给tag.render
来处理。这里tag
对象的实现位于文件lib/extend/tag.js
。而在运行时中,这里的tag
就是我们自己实现标签插件的时候调用的hexo.extend.tag
对象。我们实现的标签插件的函数,就被注册到这个对象中。这个对象的关键实现代码如下:
1 | function Tag() { |
这里我们看到,Tag
系统其实是对nunjucks的封装,而对于{% raw %}{% ... %}{% endraw %}
这类内容的处理,也是交给nunjucks来处理的。在nunjucks中{% raw %}{% ... %}{% endraw %}
就是对应的自定义标签的使用。
Nunjucks 是 jinja2 的 javascript 的实现,所以如果此文档有什么缺失,你可以直接查看 jinja2 的文档,不过两者之间还存在一些差异。
值得注意一点的是,在渲染过程中tag
特别处理了/<pre><code.*>[\s\S]*?<\/code><\/pre>/gm
对应的内容,而这部分内容,也恰好是块代码渲染出来的内容。
至此我们可以解释为什么在行内代码渲染会出问题,而块代码会出问题。行内代码的渲染内容是用<code></code>
包裹的,之部分内容会直接被nunjucks解析,因此内部的{% raw %}{% ... %}{% endraw %}
会被当成nunjucks的自定义标签解析,导致识别错误。而块代码会被<pre><code></code></pre>
包裹,这部分内容会被tag.render
提取出来避免被nunjucks处理。
注意从浏览器中浏览实际渲染出来的内容你可能无法找到<pre><code></code></pre>
的渲染结构,这是因为代码高亮模块对这部分内容进行了处理。
2.3 从nunjucks角度解决问题
这里我们可以解释为什么{% raw %}{% raw %}{% %}{% endraw %}{% endraw %}
可以解决开头的问题了。在nunjucks中,raw
标签可以允许输出一些Nunjucks的特殊符号。
上面的第一个行内代码是用{% raw %}{% raw %}{% raw %}{% %}{% endraw %}{% endraw %}{% endraw %}
才渲染出来。
2.4 小结
从前面的分析来看,Hexo的所谓标签插件系统其实是完全复用的nunjucks这个模板引擎的自定义标签系统,Hexo做的主要工作是使这个标签系统能够同Hexo的各种渲染器协同工作。
3 讨论
- 除了
{% raw %}{% ... %}{% endraw %}
这些Nunjucks的特殊符号以外,还有{% raw %}{{ ... }}{% endraw %}
和{% raw %}{# ... #}{% endraw %}
也会存在类似的问题。 - 之前我为
hexo-renderer-pandoc
提交了一个PR。因为这个渲染器只注册了异步渲染函数,没有注册同步,会导致标签插件的markdown内容无法被渲染。这是当时的Issue - nunjucks渲染问题除了带来一些问题,利用得到还是能够实现一些更加fancy的功能的。