问题
如何实现一个网页代码语法高亮的工具?
分析
当然,通常我们不需要实现,已经有很棒的工具了,例如刚刚在 Github 上按 highlight 搜索找到的 highlight.js。
新的问题
如果自己写一个呢?
背景
说实话,我觉着自己搞不出来,感觉是很复杂的东西。
不过,之前在知乎上看到一个回答(貌似没有点赞,所以找不到地址了),其中提到做代码高亮采用了 正则表达式 匹配的方式。这应该是很自然的解决方案吧,通过写精(you)巧(chou)灵(you)活(chang)的正在表达式,匹配出源代码文本中的各种“要素”,然后进行包装,再通过关联的样式进行美化。
不过,我想起曾经看到过的一些资料,依稀还记得,对于这样的文本匹配的问题,可以采用大概叫做“parser”的工具来做。实际上,我还了解过一个 JavaScript 实现的类似的工具,叫做 PEG.js。看看它的介绍,这一类的工具应该叫做 Parser Generator。
我的理解
事后分析,我觉得使用这样的工具,只需要告诉工具如果来解析文本,工具按照指定给定的规则解析文本,返回结果。这个规则,貌似可以称为 语法 吧?
也就是说,Parser Generator 按照给定的规则生成一个 Parser,通过这个 Parser,就可以用来对特定类型的文本进行解析,得到想要的结果了。而这,貌似正可以用来帮我实现代码高亮的工具!
设计思路
[Parser Generator] + [语法规则] => [Parser]
[Parser] + [待解析的文本] => [解析结果]
对应到我的实践里面就是:
[PEG.js] + [JS 语法规则配置] => [JS Parser]
[JS Parser] + [JS 代码文本] => [按语法规则添加高亮标记后的文本]
然后:
[按语法规则添加高亮标记后的文本] + [CSS] => [代码高亮效果实现!]
以上就是实现思路了,实现的结果在这里:https://github.com/luobotang/js-highlight,感兴趣可以打开链接去瞅瞅。
好了,要说的已经说完,下面是啰嗦的实现过程。
实现过程
首先,得搭建一个环境。涉及的工具包括:Node.js、PEG.js、Grunt、LESS。这个就略过了,已安装 Node.js,后面的工具通过 npm 安装就行了。
JavaScript 的语法规则配置文件?
自己写的话....估计就没戏了。不过,还好,PEG.js 的示例中就有:pegjs\examples\javascript.pegjs。
如何解析、渲染?
简单,在 HTML 页面拿到 JS 源码的文本,通过 PEG.js 生成的 Parser 解析一下,得到一个结果,再输出到指定的位置就行了:
[JS text] + [Parser] => [highlighted text]
不过,PEG.js 中的 javascript.pegjs 解析后的结果是一个超级复杂的对象,代表了整个程序,里面有语句、变量什么之类的东西,而这些,我用不到。我要得到的结果应该是字符串,只不过在解析的过程中对于必要的部分添加了样式标记,例如 if、else、for 这些关键字,包装为类似if
的 HTML 片段,然后替换原来的文本,这样最终得到的结果就是渲染好的 HTML 文本。
所以,在初步的试验成功后,我就把整个语法配置文件给“翻译”了一遍,工作量还是挺大的。不过还好,由于每修改一点,都可以通过前面提到的工具进行处理、测试,反馈比较及时,所以比较有成就感,也就这么做完了,前后差不多一天时间吧。
来看看成果(一部分):
FunctionDeclaration
= func:FunctionToken s1:__ id:Identifier s2:__
"(" s3:__ params:(p:FormalParameterList s:__ {return p + s})? ")" s4:__
"{" s5:__ body:FunctionBody s6:__ "}"
{
return highlight('reserveword', func) +
s1 + id + s2 +
'(' + s3 + (params || '') + ')' + s4 +
'{' + s5 + body + s6 + '}'
}
这是函数声明的解析规则,可以看到,对于识别出的文本片段,我对特定的部分(这里是 function 这个关键字)添加了高亮的标记,然后拼接其他部分的文本返回。
再来看一个例子:
Comment "comment"
= MultiLineComment { return highlight('comment', text()) }
/ SingleLineComment { return highlight('comment', text()) }
这里对于注释的处理比较简单粗暴,直接把匹配的文本(通过 text()
获取)进行标记。
来看下 highlight()
的定义:
{
// output highlighted html fragment
function highlight(classname, str) {
return '' + str + ''
}
}
就是把原始的文本给包装了一下。
上面的规则配置文件,通过 PEG.js 进行处理得到一个 Parser,我这里需要将得到的 Parser 输出,所以在 PEG.js 处理时做了配置。这一部分的处理我写在 Gruntfile.js 文件中,通过 Grunt 进行调用,所以注册为一个 Grunt 任务:
var fs = require('fs')
var peg = require('pegjs')
module.exports = function (grunt) {
grunt.registerMultiTask('pegbuild', 'build peg', function () {
this.files.forEach(function(filePair) {
filePair.src.forEach(function(src) {
var data = grunt.file.read(src, { encoding: 'UTF-8' })
try {
var parser = peg.buildParser(data, {output: 'source'})
} catch (e) {
console.error(e)
throw e
}
grunt.file.write(filePair.dest, 'module.exports = ' + parser)
grunt.log.ok(filePair.dest + ' builded')
})
})
})
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
pegbuild: {
build: {
src: 'lib/syntax',
dest: 'lib/parser.js'
}
},
// ....
这里是通过 PEG.js 将语法规则配置文件(lib/syntax)进行处理,得到一个 Parser 并输出。
来看一个例子:
new Date().getTime()
上例的 JS 代码经过 Parser 的处理后为:
new Date().getTime()
再来看样式,其实样式的部分比较简单,扩展起来也很容易,我用 LESS 写的,看下编译后的 CSS 吧:
.highlight-comment {
color: green;
}
.highlight-string {
color: red;
}
.highlight-reserveword {
color: blue;
}
.highlight-number,
.highlight-regex {
color: orange;
}
.highlight-callee,
.highlight-operator {
color: #33c;
}
这里就是给特别标记的文本设置了颜色,来看下效果:
总结
相对于比较容易想到的正则表达式,通过 Parser Generator 来解决文本匹配的问题,看似复杂,其实反而比较容易。你看,我就通过 PEG.js 的帮助实现了代码高亮。工作量虽然有点大,但其实并不复杂,学习下如何配置语法规则文件就行。
OK,我翻译了 JavaScript 的语法,然后实现了 JavaScript 代码的高亮,你如果有兴趣,可以搞搞其他类型的嘛。
PS:通过 PEG.js 生成的 Parser 文件确实比较大。