修复 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