美文网首页程序员GitHub上有趣的资源
基于 Parser Generator 实现的网页代码语法高亮工

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

作者: NARUTO_86 | 来源:发表于2015-09-29 16:03 被阅读652次

    问题

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

    分析

    当然,通常我们不需要实现,已经有很棒的工具了,例如刚刚在 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 这些关键字,包装为类似<span class="highlight-reserveword">if</span> 的 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 '<span class="highlight-' + classname + '">' + str + '</span>'
      }
    }
    

    就是把原始的文本给包装了一下。

    上面的规则配置文件,通过 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 的处理后为:

    <span class="highlight-callee"><span class="highlight-reserveword">new</span> Date().getTime</span>()
    

    再来看样式,其实样式的部分比较简单,扩展起来也很容易,我用 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 实现的网页代码语法高亮工

        本文链接:https://www.haomeiwen.com/subject/cigwcttx.html