区分正则
想要高亮正则表达式的前提是区分它。由于正则表达式以/开头,因此这和除法、单行注释、多行注释会产生混淆。那么究竟该如何辨别呢?

注释的区别最为简单,因为注释拥有最高优先级。除非注释符出现在字符串内,否则一旦开始,编译器便立刻识别//和/*。
如图所示,当向前看字符为斜线时,根据规则由初始状态0进入状态1。如果接下来读入的字符是*,则进入状态2,并成为多行注释;倘若是/,则进入状态3,变成单行注释;而其它的情况么,则是正则除法皆有可能。
我们不妨设想一下:正则和除法有哪些区别?从语境上说,原则性的区别就是除法的前面是被除数,正则的前面不是。而成为被除数的可能无非是以下3种:数字、变量、括号内的运算结果。我不知道js解析引擎是如何做的,它可能相当得严谨(也简单,因为它会忽略空白和注释)。但是在web端语法高亮的需求中,由于假设输入的代码一定是正确的,所以可以“投机取巧”般地识别二者:
//检查当前除号是否是perl风格正则表达式开头
protected function isPerlReg():Boolean {
for (var j:int = index - 3; j > -1; j--) {
//先要忽略之前的单行注释
if (j == singleCommentEnd) {
j = singleCommentStart;
continue;
}
var cc:int = code.charCodeAt(j);
//忽略空白
if (Character.isBlank(cc) || Character.isLine(cc)) {
continue;
}
//前面也是一个除号的话防止是多行结尾注释,需跳过注释段继续向前找
else if (Character.isSlash(cc) && Character.isStar(code.charCodeAt(j - 1))) {
j = code.lastIndexOf("/*", j - 1);
}
//前面一个非空白字符若是字母数字下划线则为正则,否则除号
else if (Character.isLetterOrDigit(cc) || Character.isUnderline(cc) || Character.isDollar(cc) || Character.isRightParenthese(cc)) {
return false;
}
else {
break;
}
}
return true;
}
程序开始回溯,我们要看在斜线符号之前的“东西”到底是什么——当然,空白符是不算在内的,因此循环体内首先要做的就是忽略空白。假如前面是标识符的组成或者右括号的话,那么显然它是个被除数,斜线也应该被解读为除号;而如果是其它情况的话,斜线则是正则表达式的开头。
除此外还有个很大的难点,就是斜线前面插入了一段注释,这也需要忽略(js引擎无需考虑这些,因为一开始注释就被剔除了,所以它会相对简单一些)。目前jssc 5 beta 3版仅能识别出多行注释,对于单行注释尚未解决,这是下个版本需要注意的问题。
注:新版本已从语法层级上解决,方法为设置一个布尔标量,每次处理token时切换其正确状态,上述代码在新版本中已废弃。
识别正则
区分出来之后别是识别正则,根据特点我们画出这样的DFA:

我们假设当前/已经被识别为正则表达式的开头,那么接下来的任务便是寻找另外一个结尾/。
在状态1中首先要明确对待的就是转义符,因为它会转义掉下面一个符号,所以要单独为其区分出一个状态2。而状态2上的条件就很宽松了:无论输入什么都会被转义掉,因此遇到任何字符(any)都会回到状态1。
值得注意的是,字符集([])是个特殊之处,因为它里面可以省略转义符(既可以有也可以没有),为此需要画出状态3和状态4来对待它(字符集中出现未被转义的/也是合法的,你不可能把它当作正则表达式的结束符)。
当真正出现结束符后进入状态5,此时正则并没有完全结束,因为后面还可能跟有譬如i这样的flag。在状态5上多加种条件循环会本身便可解决问题(更严谨的做法应该是flag仅允许出现一次);而其它情况才是最终进入终结状态6。
//perl正则
protected function dealPerlReg():void {
var start:int = index - 2;
var cc:int;
outer:
while (index <= code.length) {
readch();
cc = peek.charCodeAt(0);
//转义符
if (Character.isBackslash(cc)) {
readch();
//后面是换行跳出
if (Character.isLine(peek.charCodeAt(0))) {
break;
}
}
//[括号
else if (Character.isLeftbracket(cc)) {
while (index <= code.length) {
readch();
cc = peek.charCodeAt(0);
//转义符
if (Character.isBackslash(cc)) {
readch();
if (Character.isLine(peek.charCodeAt(0))) {
break outer;
}
}
//]括号
else if (Character.isRightbracket(cc)) {
continue outer;
}
}
}
//行末尾
else if (Character.isLine(cc)) {
break;
}
//正则表达式/结束
else if (Character.isSlash(cc)) {
for (var i:int = 0; i < 3 && index <= code.length; i++) {
readch();
cc = peek.charCodeAt(0);
//是img中的一个,存入flag中,不分大小写
if(cc == 103 || cc == 105 || cc == 109 || cc == 71 || cc == 73 || cc == 77) {
//
}
//其它情况跳出
else {
break outer;
}
}
break;
}
}
//高亮
result.append(HighLighter.regular(HtmlEscape.encode(code.slice(start, index - 1))));
}
其它
其它符号的处理都很简单,不做任何高亮即可,只是要注意个别需要特殊编码的字符(如&变为&)。在编译器的词法分析中,每个符号都是有语法意义的,有时符号的长度还不止一个(<=就有2个符号)。幸运的是,在做语法高亮的情况下,这些全可以被“偷懒”掉。
下面贴一下c系列语言处理符号的代码,它相当得简单,可能唯一特殊之处就是其中对花括弧的深度处理了。
//抽象类中的处理符号方法
protected function dealSign():void {
result.append(HtmlEscape.encodeChar(peek));
readch();
}
//c系列语言继承后覆盖的方法,处理符号同时要计算深度
protected override function dealSign():void {
var cc:int = peek.charCodeAt(0);
if (Character.isLeftBrace(cc)) {
depth++;
}
else if (Character.isRightBrace(cc)) {
depth--;
}
super.dealSign();
}
在下一篇中,我将讲述jssc 5是如何做到嵌套高亮(html中内嵌css和js)的。