基于 Parser Generator 实现的网页代码语法高亮工具

问题

如何实现一个网页代码语法高亮的工具?

分析

当然,通常我们不需要实现,已经有很棒的工具了,例如刚刚在 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;
}

这里就是给特别标记的文本设置了颜色,来看下效果:

expression.png

总结

相对于比较容易想到的正则表达式,通过 Parser Generator 来解决文本匹配的问题,看似复杂,其实反而比较容易。你看,我就通过 PEG.js 的帮助实现了代码高亮。工作量虽然有点大,但其实并不复杂,学习下如何配置语法规则文件就行。

OK,我翻译了 JavaScript 的语法,然后实现了 JavaScript 代码的高亮,你如果有兴趣,可以搞搞其他类型的嘛。

PS:通过 PEG.js 生成的 Parser 文件确实比较大。

你可能感兴趣的:(基于 Parser Generator 实现的网页代码语法高亮工具)