修复 marked 配合 prism 进行语法高亮时,渲染代码块的格式错误

Posted on Sun 10 September 2017 in 遗迹

当前这个博客就是使用了这个渲染代码的方案,这也是我的亲身经历。

我相信与我遇到相同问题的人还有不少,下面来说说遇到的问题是什么,如何解决。

缘起

我一直觉得有一个地方不协调,那就是markdown引用代码的时候,我这里渲染出来代码没有背景颜色。之前我以为是默认样式本来如此,结果上篇文章加了很多代码发现与正文区分不开,很难看。于是就去看了 prism.js 官网之后发现默认样式是有背景色的,而我的没有,这很明显就出了偏差。

既然出了偏差,那就是要负责的。

那我们先来观察一下 http://prismjs.com 里面代码是怎么引用的:

<pre data-src="prism.js" class=" language-javascript">
    <code class=" language-javascript">
        ...
    </code>
</pre>

看上去不错,那我们渲染出来的 html 呢?

<pre>
    <code class="lang-javascript">
        ...
    </code>
</pre>

呃,其实我也不知道为什么 lang- 的前缀在我这里也是可以高亮的,大概是有什么神秘力量吧。

可以很明显看出差别了,对吧?经过实测发现,如果 <pre> 没有class,那么就没有背景(本文遇到的情况);如果 <code> class 不对,那么代码相对于背景的 padding 不正常(这是本文随后又遇到的情况)。

再随便找几个 prism 的主题看一下: https://github.com/PrismJS/prism-themes/blob/master/themes/prism-darcula.css

code[class*="language-"],
pre[class*="language-"] {
    ...
}

基本所有的 css 定义都是冲着 <code><pre> 去的。代码和实测的结论完全符合,下面着手解决问题。

调查

我们先来复习一下 marked 到 prism 的关联是如何完成的:

marked.setOptions({
    renderer: new marked.Renderer(),
    sanitize: true,
    highlight: function (code, lang) {
        if (lang) {
            let stdlang = lang.toLowerCase();
            if (Prism.languages[stdlang]) {
                return Prism.highlight(code, Prism.languages[stdlang]);
            }
        }
    }
})

这里看到是在 options 里面插入了一个函数来做这件事情,这函数直接返回渲染好的 html

那问题来了,是 prism 搞错了吗?并不是,我看了代码之后发现是 marked 弄错了。摘抄一段 marked 源码:

Renderer.prototype.code = function(code, lang, escaped) {
  if (this.options.highlight) {
    var out = this.options.highlight(code, lang);
    if (out != null && out !== code) {
      escaped = true;
      code = out;
    }
  }

  if (!lang) {
    return '<pre><code>'
      + (escaped ? code : escape(code, true))
      + '\n</code></pre>';
  }

  return '<pre><code class="'
    + this.options.langPrefix
    + escape(lang, true)
    + '">'
    + (escaped ? code : escape(code, true))
    + '\n</code></pre>\n';
};

这段代码有一系列问题……我们先只说与类型相关的部分。

我们看到这里进行了 var out = this.options.highlight(code, lang); 这样一个调用。

如果没有返回值就 return '<pre><code>' + ... 如果有的话就拼装一下

 return '<pre><code class="'
    + this.options.langPrefix
    + escape(lang, true)
    + '">'

实际上拼好就是 <pre><code class="lang-xxx">langPrefix,一个没有出现在文档中的值,实际上影响了拼装出的 class 的前缀,默认值是 'lang-'

解决

已经弄清楚了问题,下面就进行处理吧。翻阅 marked 文档得知可以手动重载 renderer 的渲染函数,这里也不卖关子,直接贴出最终的解决方案:

let renderer = new marked.Renderer();

renderer.code = function(code, lang, escaped) {
    if (this.options.highlight) {
        var out = this.options.highlight(code, lang);
        //if (out != null && out !== code) {  [*3]
        if (out != null) {
            escaped = true;
            code = out;
        }
    }

    if (!lang) {
        return `<pre class="${this.options.langPrefix}PLACEHOLDER"><code>`
            // + (escaped ? code : escape(code, true)) [1]
            + code
            + '\n</code></pre>';
    }

    let langText = this.options.langPrefix + escape(lang, true);
    return `<pre class="${langText}"><code class="${langText}">` // [2]
        + (escaped ? code : escape(code, true))
        + '\n</code></pre>\n';
};

// 后面 setOptions 的代码
marked.setOptions({
    renderer: renderer,
    sanitize: true,
    langPrefix: 'language-',
    highlight: function (code, lang) {
        if (lang) {
            let stdlang = lang.toLowerCase();
            if (Prism.languages[stdlang]) {
                return Prism.highlight(code, Prism.languages[stdlang]);
            }
        }
    }
});

关于改动:

[1] highlight 没有返回时,也就是没有代码高亮,这里我出于个人习惯还是给了 <pre> 一个 class,让背景能够渲染出来,因为我经常用 ``` ``` 来显示一些执行结果,很显然这是带不上高亮的,但没有背景又很难看。而里面的 escape 注释掉是因为外面已经有 <pre> 元素了,如果再 escape 会显示出 %20 等等玩意,对读者来说就是乱码。

[2] 改写了原版写法,给 <pre> 加上了 class

还有问题

改完了之后发现仍有代码渲染不正常,是这个样子的:

goaccess%20access.log%20-o%20report.html%20--real-time-html

但我们想要的是这个样子:

goaccess access.log -o report.html --real-time-html

经过排查,问题出在这里,也就是 [3] 修改的部分。

renderer.code = function(code, lang, escaped) {
    if (this.options.highlight) {
        var out = this.options.highlight(code, lang);
        //if (out != null && out !== code) {  [*3]
        if (out != null) {
            escaped = true;
            code = out;
        }
    }

[3] 对部分简单代码来说 out == code 是完全可能的,就比如上面那个例子用 bash 语法高亮,渲染前后内容完全一致。那么如果说 escaped 不被置为 true,那在最后明明已经格式化好的代码又要被 escape 一次,于是就出现了乱码。

完结

写这篇文章花了一个小时多一点,实际上比调试花了更多时间。在版本库中对应这部分:

https://github.com/fy0/storynote/blob/master/page/src/main.js#L109-L179