使用 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

以上代码有两点需要注意:

  1. 类似 JS 中的字符串模版功能,Ruby 中也有类似的语法,在上面最终输出 HTML 内容中有使用,但是必须使用双引号包裹。
  2. 转义标签內的代码内容使用了h()函数,还记得开头引入的 StandardFilters 嘛,h()是里面escape()同名函数。这个函数接受输入流(这里就是代码块内容),返回解析后的 HTML 字符串。

总结

深刻感觉到 Jekyll 用的人真的不多了,很多插件都处于无人维护的状态。