在本篇将会使用PetitParser来实现解析器。
关于PetitParser的基础知识,在这里不多做介绍,因为会另写一个系列。可以在这里看入门教程,或自行到官网了解更多信息。
我们将使用PetitParser,由简单到难分别实现以下元素的解析:
由于@
在Shark模板中有特殊作用,所以如果我们想要输出一个单纯的@
,需要多做一点事:写成@@
。所以我们在解析的过程中,如果看到@@
,就应该把它变成@
.
PetitParser代码如下:
string('@@').map((each) => '@')
它的意思是,如果遇到了`@@`,就返回一个`@`。
对于要完全匹配给定的字符串,PetitParser提供了`string()`这个函数,`string('@@')`的意思非常直白,就是要从模板中匹配到`@@`这个字符串。
在SharkDart里,这句话拆成了两部分,如下:
class SharkParser extends CompositeParser {
@override
void initialize() {
grammar();
parser();
}
grammar() {
def('atAt', string('@@'));
}
parser() {
action('atAt', (_) => '@');
}
}
我们人为把代码分成了两部分,`grammar()`中只定义解析规则,`parser()`中只负责对匹配的内容进行转换,这样看起来更加清晰一些。
另外需要了解的是,`SharkParser`继承了`CompositeParser`这个由PetitParser提供的基类。它里面提供了一些如`def()`和`ref()`这样的方法,可以让我们在任意地方定义和引用另一个parser,哪怕它可能还没有来得及定义。在一个复杂的文法中,经常会出现循环引用的情况,所以PetitParser提供了多种解决方案,这里的CompositeParser是一种。另外还有先定义一个`undefined()`的占位符,先用后更新,也是一种常用的做法,但这里没用上,就不多说。
## 简单表达式 @expr
这里表达式变量的定义应该跟dart语言一致:它可以包含字母、数字、下划线和$,但数字不能出现有首位。
为了方便复用,我先把首位定义出来:
def('variableHead', pattern(r'a-zA-Z_$'));
其中`pattern()`也是PetitParser提供的一个parser。PetitParser是不支持正则表达式的,但有时候正则中的一些写法,比如`a-z`,`0-9`等又的确方便,所以它便提供了`pattern()`这个函数,有限的支持这种写法。
对于非首位的字符,使用以下定义:
ref('variableHead') | digit()
即在`variableHead`的基础上,增加了数字的支持。
把它们合并在一起,就是整个变量:
def('variable', (ref('variableHead') & (ref('variableHead') | digit()).star()).flatten());
即一个首位字符,再加上任意多个非首位字符。看到那个奇怪的`&`和`|`操作符了吗?在Dart中允许重载操作符,而`&`可看作是另一个方法`seq(...)`的别名,用来表示两个parser是相邻的,`|`可看作是`or(...)`的别名,表示一个不行试另一个。
最后的`flatten()`用来把整个匹配结果合成一个单独的字符串,不然到时候拿到的就是一个由字符和列表组成的列表了。
完整的定义如下:
def('simpleExpression', char('@') & ref('variable'));
## 复杂表达式
复杂表达式里会包含调用或者参数传递,用`@{}`括起来,比如:
@{hello("world")}
@{names.map((name)=>name.toUpperCase())}
如果我们细心考虑,会发现我们不能简单的用`@{`和`}`作为首尾来匹配,因为有这样的情况存在:
@{hello("wor}}}}}ld")}
或者
@{names.map((name){return name.toUpperCase()})}
中间的`}`会让我们的匹配提前结束,所以我们必须把“字符串”和“花括号对”挑出来,不让它们干扰真正的匹配。
### 字符串
在Dart中,可以使用单引号和双引号来括字符串,就像javascript一样。其实还有更复杂的情况,如三个连接双引号或单引号,在字符串中嵌入表达式等,但我们不予考虑,太复杂了。
另外,如果用单引号括起来的字符串里还有单引号,则需要在前面加'\',对于双引号来说也一样。
def('singleString', _sharkString("'").flatten());
def('doubleString', _sharkString('"').flatten());
因为两者的逻辑很像,所以提供了一个`_sharkString()`的函数:
Parser _sharkString(String boundChar) {
return (
char(boundChar)
& (string(r"\" + boundChar) | char(boundChar).neg()).star()
& char(boundChar)
);
}
以单引号举例,这段代码的意思是,首先要有一个单引号,然后把`\'`及非单引号字符尽可能匹配掉,直到最后遇到另一个单引号。
其中`neg()`表示negative,比如`digit()`表示匹配数字,则`digit().neg()`就表示匹配“非数字”。`r"\"`这个字符串前面有一个前缀`r`,它表示字符串里的内容不需要转义,其值就是`\`。
### 花括号对
再然后就是“花括号对”了。由于复杂表达式自己就带了一对花括号,所以我们可以巧妙的利用上它,即对自己迭代:
def('complexExpression', char('@') & ref('complexExpressionBody'));
def('complexExpressionBody', (
char('{')
& (
ref('complexExpressionBody')
| ref('singleString')
| ref('doubleString')
| char('}').neg()
).star().flatten()
& char('}')
).pick(1));
最后的`pick(1)`表示结果列表中的第2个元素。因为`complexExpressionBody`的内容可分为三部分,`{`、中间内容、结尾的`}`,它们用`&`连接在一起,匹配的结果将会是一个由三个元素组成的列表,而我们只对中间内容感兴趣,所以`pick(1)`,在这里处理一下可提前丢掉不要的内容,方便后面的处理。
上面这段代码不多讲解,请自己体会,关键就在于自己在内部调用自己,可以匹配内部嵌套的花括号对。
## 表达式action
前面分别定义了简单表达式和复杂表达式。为了方便处理,我们把它们合在一起:
def('sharkExpression', ref('simpleExpression') | ref('complexExpression'));
然后在`parser()`方法中添加下面的action:
action('sharkExpression', (each) {
var expr = each[1];
return new SharkExpression(expr);
});
注意each将会是一个长度为2的数组,each[0]
是@
,each[1]
是@{
与}
中间的内容,是我们需要的。最后的SharkExpression
类是将在后面介绍的语法树结点类之一,用来表示匹配到了一个表达式,这里暂不用考虑。
标签结构的解析比较复杂,将放在下一篇。