Update 20180704:负向零宽断言我手边只有 Chrome 支持,所以目前还是不可用的,还好 PrismJS
提供了一种 Hack 的方法可以支持这个特性的很有限的部分:当传入 lookbehind
属性时,会删掉 match
数组的第零项,这样第一个括号的内容就成了匹配到的东西……我已经给 PrismJS
提了 Pull Request(地址在 这里),但尚未被合并,仅供参考。
背景
作为一个强迫症,我对博客里的代码高亮一向是要求很高的,一个好的代码高亮库,需要满足如下几点:屏幕大、电量足……啊走错了,应该是配色好看、识别准确、插件生态丰富。之前从 highlight.js
换到 PrismJS
,就是因为前者的插件不够多。关于配色,我一直很喜欢 Monokai
,自己的 CSS 功底也不差,于是就照着改了一份 CSS 文件。PrismJS
的插件超级多,也基本满足了我的需求。但唯一一点不爽的是,highlight.js
可以将函数的参数高亮出来,但是 PrismJS
截止到目前还没有这个功能,于是我就自己试着改。
打开了 PrismJS
的 GitHub 仓库,发现里面有好多语言,于是决定还是从最常用的 JavaScript 开始改起。
先附上成果吧!修改的部分在这儿:Revisions · JavaScript file highlight for Prism.js (support function arguments)。
PrismJS 的原理
看了一圈代码,PrismJS
的原理大概是按照特定的规则将代码文件 Split 成多个部分,并为它们打个标记(用一个 <span>
包住),然后对它们再使用一些规则 Split 成更小的部分并打标记(包住),大概是这样:
+------+-------------+---------------------------------+------+
| Step | Class | String | Next |
|------+-------------+---------------------------------+------|
| 0 | tag | <UpdateJudge element={element}> | 1 |
|------+-------------+---------------------------------+------|
| 1 | tag | <UpdateJudge | 2 |
| | attr-name | element | |
| | script | ={element} | 3 |
| | punctuation | > | |
|------+-------------+---------------------------------+------|
| 2 | punctuation | < | |
|------+-------------+---------------------------------+------|
| 3 | punctuation | = | |
| | punctuation | { | |
| | punctuation | } | |
+------+-------------+---------------------------------+------+
上面第一列是解析的阶段,第二列可以确定 <span>
的 class
,第三列是新解析出来的要被 <span>
包住的内容。没有被打标记的文字的样式就是从父元素继承。
原理就是这么简单,所以它对语言的识别有多厉害,就是看规则有多强大。
PrismJS 的规则
看了看 PrismJS
的规则,用法确实不难,毕竟官方是提供了 文档 的,难点是在规则的设计上。就以比较简单的 CSS 规则 为例吧:
Prism.languages.css = {
comment: /\/\*[\s\S]*?\*\//,
atrule: {
pattern: /@[\w-]+?.*?(?:;|(?=\s*\{))/i,
inside: {
rule: /@[\w-]+/
}
},
url: /url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
// ....
// several lines hidden
// ....
punctuation: /[(){};:]/
};
PrismJS
将会对代码字符串自上而下的使用正则来 Split,也就是说,首先先使用 comment
对应的正则将代码分割开,再对分割出来的每一个小块使用 atrule
对应的正则分割……以此类推,最后用 punctuation
对应的正则做最后一步分割。如果某个规则有 inside
属性,那么对于使用这个规则分割开的区域,只会用 inside
里面提供的规则继续分割。
大概可以想到这样设计比较合理——例如你把 punctuation
的规则放到了最上面,那么在刚开始的时候,整个代码就被分割成了一片片纯英文区域,是不可能再用其它正则来分割的。
对于函数参数的规则设计
以我对 JavaScript 的了解,函数参数可能会在如下的几种情况出现(这篇文章可以当成是对我代码的一个测试了):
// normal function
function test1(a, [b]) {}
// anonymous function
function () {}
// arrow function
(a, [b]) => {}
({ a, b = 0 }) => {}
// class method
class A {
test2(a, [b]) {}
test3({ a, b = 0 }) {}
}
但是与此同时,不能匹配到这种情况:
// if | while | for | switch
if (a || b(c)) {}
// function call
test4(a)
箭头函数没有歧义,特别好识别,所以就不说了;对于 if
之类的情况,只需要加一个零宽断言判断它前面是否是这些关键字即可;需要注意的是空格可能到处都有,所以不能忘了加足了 \s*
……总之就是一系列智力游戏。
由于我没有把整个项目下下来,所以没有单元测试,我只能自己写一个 HTML 来测试(差不多相当于把上面那两个代码块写进去)。最终的成果就是文章开头的那个链接啦!只需要再在 CSS 中添加 argument
的样式(Monokai 是橙色)就搞定了。
这不禁让我想起了 Regex Golf 这个游戏,于是也顺便给大家安利一下它吧!可带劲啦!
与 highlight.js 和 VSCode 的对比
highlight.js
highlight.js
使用的是另一种方式:使用正则表达式的 lastIndex
特性。首先定义好语法规则(规则的格式在 这里),然后将这些规则用 |
符号拼成一个超大的正则(代码中叫 terminators
),不断使用这个正则来匹配原字符串,将匹配到的部分截取出来,递归使用该规则的子规则匹配这个子串。通俗一点来讲,就是“外层是 while (true)
,内层是递归处理”。
(话说一开始看到代码中有个变量叫 lexeme
,我还以为要去复习编译原理了……)
之前我在使用的时候,发现它可以匹配到函数参数列表。于是仔细看了一下 JavaScript 语言的规则规则,发现对于一些情况还是没有做处理:
- 参数列表有复杂语法(解构、赋值等)的函数,比如前文的 test1
- ES6 Class 中的函数,比如前文的 test2、test3
虽然其实也可以加上这些处理,但规则比 PrismJS
复杂好多,不太想去研究了;以及这个库都快一年没更新了,JavaScript 的语法规则快两年没更新了,不敢用……
VSCode
VSCode 对于各种高亮做的都很好,不愧是微软爸爸的东西。翻了翻代码,发现 VSCode 和它的参照品 Atom、我以前用的 Sublime Text 一样,都使用了一种 .tmLanguage
格式的文件,上网查了一下,是 TextMate 的“Language Grammer”文件,语法规则在 这里。至于具体是如何解析的,我发现 VSCode 使用了自家的一个叫 vscode-textmate 的库来解析这种文件。翻了一下,发现有点麻烦,还调用了一个叫 oniguruma
的正则库,并不适合放到网页里加载,只能放到离线的编辑器里。
发现跟 Match 相关的代码在 这里,看起来逻辑很简单,只要不断调用 matchRule
和 matchInjections
就行了,但这儿简单就说明别的地方麻烦。但由于时间有限,没去做深入研究,因此不知道 Rule 和 Injections 是个什么东西、是怎么构造出来的。
又去看了一眼 .tmLanguage
文件,发现对于正常的网页来说大得离谱,看来是能考虑到的情况都考虑到了……甚至对于这种奇葩代码也能比较好的处理(当然,本文中的这段代码高亮会有问题):
render() {
return (
<Tag
// a piece of comment
/* another comment */
props1={value1}
props2={value2}
/>
);
}
然而 PrismJS
是没法处理这种情况的,因为前者一开始就被注释给分割开了;highlight.js
应该是可以的,只要允许 Tag 中可以包含注释就行了。
基于语法树的代码高亮
这个思路基本能保证绝对的准确,但我目前还没看到有开源的实现(可能确实有但我没找到),我觉得可能的原因是太麻烦,毕竟这跟编译器之间就差一个转换的功能了,对于一般的页面展示来讲,没有必要做到这一步。
不过 VS 和 IDEA 应该是用的这种方式,毕竟参数与其它变量颜色都不同,这可不是只靠正则就能区分开的。