使用 Prism 配合插件实现
代码高亮是一个技术博客的重要特性,Jekyll 中默认使用Rouge进行词法分析生成 DOM 结构,搭配自定义样式。使用 Python 编写的Pygments也是一个可选方案。
博客作者可以使用如下 markdown 语法插入代码块,其中linenos
表示显示行号:
{% highlight ruby linenos %}
def foo
puts 'foo'
end
{% endhighlight %}
本来我是采用默认的 Rouge,但是发现样式有点丑,尤其是行号部分。而且有时我需要高亮显示若干行代码,例如这样:
{% highlight ruby linenos=1-2 %}
def foo
puts 'foo'
end
{% endhighlight %}
此时 Rouge 就做不到了,而且 Rouge 和 Pygments 似乎很久没有更新了,想找一个漂亮的样式也不容易。
我在网上搜了一下,发现了Prism这样一个库。MDN,Smashing magazine 很多技术网站都在使用,难怪样式看着有点眼熟。那么如何在 jekyll 中使用呢?
编写 jekyll 插件
参考 Prism 文档,在项目中引入定制后的 JS 和 CSS 文件都很简单。值得一提的是之前提过的高亮特定行数的代码,可以通过Prism 插件实现。所以我们只需要关注如何通过 jekyll 插件将 markdown 代码块转换成对应的 HTML 代码即可。
之前介绍过如何在Github Pages 中使用第三方插件。由于我的博客在本地进行编译,所以只需要将插件放在_plugins
文件夹下即可。
我搜索到一个Jekyll 插件,已经很久没有维护了,ISSUE 也很久没有回复。看了代码后决定在此基础上进行修改,顺便学习一下自定义插件的相关知识。
Liquid 模板引擎
Liquid 是使用 Ruby 编写的模版引擎。Jekyll 使用它进行 markdown 语法的解析。通过继承 Liquid 内部封装的类,可以自定义我们的语法块。
以下是声明和注册代码,继承 Block而非 Tag 的原因很简单,我们需要使用闭合标签,类似{% endprism %}
,否则一旦解析到闭合标签就会报错了。
module Jekyll
class PrismBlock < Liquid::Block
include Liquid::StandardFilters
def initialize(tag_name, markup, tokens)
super
end
def render(context)
end
end
Liquid::Template.register_tag('prism', Jekyll::PrismBlock)
代码中还引入了StandardFilters
,这个后续在输出 HTML 时会使用。
module Jekyll
class PrismBlock < Liquid::Block
include Liquid::StandardFilters
end
Liquid::Template.register_tag('prism', Jekyll::PrismBlock)
真正的处理逻辑将在两个方法:构造函数initialize()
和render()
中完成。
解析行号
通过方法签名initialize(tag_name, markup, tokens)
可以看出,markup
包含了代码语言和行号的声明。解析工作交给正则完成:
OPTIONS_SYNTAX = %r{^([a-zA-Z0-9.+#-]+)((\s+\w+(=[0-9,-]+)?)*)$}
markup.strip =~ OPTIONS_SYNTAX
所以对于{% prism ruby linenos=2,11-13 %}
这样的代码块声明,我们能够得到语言ruby
,行号linenos=2,11-13
。这部分基本不需要做修改,相关代码就不贴了。
输出 HTML
render()
函数十分简单,我们按照 Prism 接受的 HTML 结构输出即可,这里我根据linenos
决定是否展示全部行号,另外通过data-line
配合 Prism 插件实现高亮特定行:
# 转义内容
code = h(super).strip
linenos = ''
linenos_content = @options["linenos"]
if !linenos_content.nil?
linenos = "class='line-numbers' data-line='#{linenos_content}'"
end
# 返回 HTML 内容
<<-HTML
<div>
<pre #{linenos}><code class='language-#{@lang}'>#{code}</code></pre>
</div>
HTML
以上代码有两点需要注意:
- 类似 JS 中的字符串模版功能,Ruby 中也有类似的语法,在上面最终输出 HTML 内容中有使用,但是必须使用双引号包裹。
- 转义标签內的代码内容使用了
h()
函数,还记得开头引入的 StandardFilters 嘛,h()
是里面escape()
的同名函数。这个函数接受输入流(这里就是代码块内容),返回解析后的 HTML 字符串。
总结
深刻感觉到 Jekyll 用的人真的不多了,很多插件都处于无人维护的状态。