Ace Editor是一款非常有名的基于Web的源代码编辑器,具有丰富的可扩展性,而且性能也很棒,处理上百万行代码不在话下。据我所知,维基百科、可汗学院、Overleaf、CodeCombat等知名网站都在使用这款编辑器。不过,它也有一个大问题就是文档写得非常简陋。
Ace Editor用来支持语法高亮的资源文件称作language mode(语言模式),Ace提供了扩展自定义语言mode的方法1,但根据这个文档去看很多现有的mode都会一头雾水,因为有很多用法在文档里一个字都没有写。不过在本文中,我会把我阅读Ace代码了解到的东西都分享给大家,这样大家就不用再读一遍代码了。
还是从文档里已经介绍的基本用法讲起。每个mode都会把它包含的所有规则放在this.$rules之中。这些规则每个用一个数组表示。虽然格式相同,但是有一个地位特殊,那就是start这条规则,它是整个规则集的入口。
xxxxxxxxxxthis.$rules = { start: [ { token: t1, regex: r1, next: n1 }, { token: t2, regex: r2, next: n2 } ], t1: [ ], t2: [ ],};每条规则的数组就类似于PEG中的有序选择,排在前面的规则优先,而且前面匹配成功就不会检查后面的规则。token一般是一个数组,在经过regex匹配过后所有的捕获会按照顺序对应上token里面的每一项。next表示下一个状态(规则),如果不指定next的话,则会返回start规则。
我们可以简单地利用基本用法来识别一下MediaWiki中的标题
xxxxxxxxxx{ start: [ 'heading' ], heading: [{ token: [ "punctuation.definition.heading", "entity.name.section", "punctuation.definition.heading" ], regex: /(={1,6})(.+?)(\1)(?!=)/ }],};几乎大部分我们接下来要介绍的用法都是在text_highlight_rules.js这个mode中实现的。这个mode应该算是一种meta mode,并非针对一种语言。而是几乎所有语言都理所当然地要继承这个mode。更准确地,是通过this.normalizeRules这个函数来实现的。
可以发现很多现有的mode都使用了这个特性。那么它是做什么的呢?从名字我们就可以大概猜出来,应该是实现了一个stack。那么什么情况下会用到这个特性呢?通常在用来保证语法的开始与结束能够配对的时候需要用。就比如在MediaWiki中模板定义文法中的模板参数的fallback链,就需要用到。
xxxxxxxxxx{{{ 123 | test{{{456}}}xasd }}}为了能让内部参数456的结尾}}}不会被误认为结束外部参数123,我们这里就要在每次开启参数的时候push进去一个state,每次关闭参数的时候pop一下,这样就不会造成内部关闭外部的情形了。实际写出来大概就是这样:
xxxxxxxxxx{ start: [ 'argument' ], argument: [{ stateName: 'openArg', token: [ "variable.parameter", "text", "variable.other", "text", "keyword.operator" ], regex: /({{{)(\s*)(\w+)(\s*)((?:\|)?)/, push: [{ token: "variable.parameter", regex: /}}}/, next: "pop" }, 'start'] }]}在Ace中push是用一个数组来表示的,比较反直观,而且push实际上也隐含next的意味。上面定义的规则的意思是说,我这个openArg啊,它要push到stack里,同时它的下一条状态就是push里面这个数组(因为在Ace里面,就是用数组来表示rule)。
那好啦,到了下一个状态的时候,它就会按照顺序,先匹配}}}这条规则,一旦匹配上,那就会调用pop,也就意味着要跳出内部这个argument了。那么为什么还要加一个start呢?这个的作用是说,我argument内部是要按照start规则来匹配的,放到MediaWiki里意思就是说,我模板参数的fallback可以是任何wikitext,当然也能包含另一个模板参数啦。
include指令:用来引用$rules中定义好的规则,但我更推荐直接用规则的名字引用,如上面push里面的start。很多从tmLanguage文件会包含一个include: '$self'的写法,这个写法经我验证在Ace中无效,应当改为对start的引用
defaultToken指令:意思就是不管匹配到啥都按这个token处理,等价于
xxxxxxxxxx{ token: 'defaultToken', regex: /.+/ }caseInsensitive指令,用来让regex互略大小写区别
onMatch指令,这个指令是一个函数,其返回值将作为token使用。onMatch接收的参数分别是value, currentState, stack, line,它的value参数能够提供正则表达式匹配结果中的每个捕获。事实上这个指令本来是原来token传函数的情况,但由于作者修改代码的时候忽视了文档这部分,于是将原有功能改为用onMatch实现。2
rules指令所做的事是这样的:对于它内部的规则,如果全局$rules没这条规则,那么就把它赋给全局;如果没有呢,就用里面的push函数刷一遍全局的$rules。说实话我也没搞懂具体是用来做什么的。这个指令使用得很少,目前只有ruby、sh和crystal三个mode在用。
在弄清Ace Editor的文档未写清的特性之后,Ace Editor就变为了一个容易扩展语言的一个编辑器,并且性能还不错。在扩展语言mode的方式方面,其他JS代码编辑器(如CodeMirror)也大同小异。
这篇文章我想要达到的作用就是,把这些重要的参数介绍给大家,让大家至少能够看懂已经有的这些mode是在做什么。因为我发现在我弄懂,写这篇文章之前,我真的看不懂它那些push、pop是什么意思,到底为啥rule是写成数组形式等等。当然这也反映出写好文档的重要性。∎