基于 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 这些关键字,包装为类似<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 文件确实比较大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容