终于到了最关键的标签结构的解析了。对于标签结构,最难的地方在于主体外面的花括号,因为花括号的个数不限,所以这个匹配是动态的。为了简单起见,我将把主体与前面分开解释。
结构形如(不包括主体部分):
@tagname(type1 var1: desc1, type2 var2: desc2, ...)
可以看出每个参数都有三部分组成:类型、变量名、描述信息。提供这三个部分,使得它既可以用作参数的声明,又可以当作调用,总之你在编译过程中可以拿到这三个信息,想怎么用都可以。
### tagname
标签名的定义跟在前一篇中表达式中的变量名是一样的,所以可以直接引用:
def('tagName', ref('variable'));
### type
Dart中的类型支持泛型,所以它的定义如下:
def('paramType', pattern(r'a-zA-Z_$<>').plus().flatten());
### var
变量名就相对宽松一些,因为除了普通的变量名以外,我还希望它可以接受路径等格式,所以它的定义如下:
def('paramVariable', pattern(r'0-9a-zA-Z_$./').plus().flatten());
### description
描述信息更加宽松,它可以接受多种形式的内容,比如数字,字符串,普通变量,甚至一大块可嵌入更多标签的文本。
所以它的定义如下:
def('paramDescription', ref('variableExpression')
| ref('numberExpression')
| ref('singleString')
| ref('doubleString')
| ref('normalBlock'));
其中的`singleString`和`doubleString`在前一篇已经已经讲过,这里不重复了。而`numberExpression`的定义如下:
def('numberExpression', (digit() | char('.')).plus().flatten());
另一个`normalBlock`的规则其实跟标签主体的规则一样,所以这里就直接引用了。具体定义要放在后面讲。
### 组合起来
上面定义了三部分,我们可以把它们当成一个整体来看待,所以新定义一个`tagParam`,把它们组合在一起。
这里有两点需要注意:
type和description都是可选的
type与var的解析规则有重复
这两点导致这个规则要比预想的难。
最开始我写的规则如下:
def('tagParam', ref('paramType').trim().optional()
& ref('paramVariable').trim()
& (char(':') & ref('paramDescription').trim()).optional());
简洁明了,可惜行不通。主要原因是,在PetitParser中,一旦某个parser成功匹配到了内容,则它后面的optional()
就被忽略。
例如,当参数形如abc
(即没有type和desc)的时候,我期待它将会被paramVariable
匹配,但paramType
也能匹配上,所以它的optional()
尾巴就忽略了,将会报paramVariable
无法匹配成功的错误。
为了解决这个问题,我把代码写得复杂一些:
def('tagParam', (
(
(ref('paramType').flatten().trim() & ref('paramVariable').trim())
| ref('paramVariable').trim()
)
& (char(':') & ref('paramDescription').trim()).optional()
));
这样竟然有时候也会报错,比如:
@extends(tags/menu, items: items)
我期待tags/menu
整体被paramVariable
匹配,可惜的是paramType
会把tags
匹配走,只留下了/menu
给paramVariable
。
为了解决这个问题,我再改成:
def('tagParam', (
(
(ref('paramType').seq(whitespace().and()).flatten().trim() & ref('paramVariable').trim() )
| (epsilon() & ref('paramVariable').trim())
)
& (char(':') & ref('paramDescription').trim()).optional()
));
主要是增加了.seq(whitespace().and())
,它的作用是,判断后面跟着的是不是一个空格,这样才成功解决这个问题。and()
的作用是判断parser是否可匹配,但不消耗内容,常用来它检查一个parser的边界。
另外还添加了一个epsilon()
的函数,它其实就是alwaysMatch的意思,不知道为什么起了这样一个奇怪的名字。加上它是为了让两行匹配结果的个数能匹配上,都是两个元素的数组,后面好处理。
上面已经定义了单个参数的格式paramType
,如果是多个以逗号分隔的参数列表,则应该定义成下面这种形式:
def('multiParamArray', (
char('(')
& ref('tagParam').separatedBy(char(','), includeSeparators:false).optional([])
& char(')')
).pick(1));
其中的separatedBy
正是为这种情况定义的,我们提供一个相应的分隔符(在这里是char(',')
),就可以了。如果想在最后匹配的内容中丢掉匹配到的分隔符,则可传入参数includeSeparators:false
。
另外,参数列表也可以空,即()
也是可以的,所以我们要在最后加上optional()
及参数[]
,它的意思是,当中间部分完全没有匹配到时,返回[]
,以方便后面的处理。
最后的pick(1)
是说我们只需要()
之间的内容即可。
如果参数不满足第一种结构,我们将会把它当作一块代码看待。需要注意的是,如果代码中含有)
,可能会让我们的匹配提前结束,比如:
@if(name.toLowerCase()=='shark')
所以我们不能单线的匹配()
。这里跟前一篇中的复杂表达式匹配非常相似,所以代码也很相似:
def('codeParam', (
char('(') & (
ref('codeParam')
| ref('singleString')
| ref('doubleString')
| char(')').neg()
).star().flatten() & char(')')
).pick(1));
标签主体两边的花括号的个数是不限定的,只要能对应即可。这个规定是为了方便包含大量纯文本或代码时,能否有唯一的边界。比如你在文本中最多有5个连续的}
,那我直接把主体花括号写成6个就行了。
这也给我们的解析带来一个难题:匹配规则是动态的。我只有知道起始花括号的个数后,才能确定结束花括号应该有几个。
如果我们利用PetitParser提供的parser也能做,但会非常麻烦,因为要动态修改结束花括号的定义,并且要支持嵌套,必须有类似堆栈这样的结构去处理嵌套时结束花括号的变化,并且小心处理任一parser匹配或没匹配时会结束花括号的影响,非常复杂且不健壮。
所以我们将要自定义一个parser来处理,逻辑其实也比较简单:
我们首先用char('{').plus()
去匹配起始花括号,成功后将得到相应的字符串,并计算出结束花括号是什么样的。然后继续向下匹配,直到匹配到相应的结束花括号为止。
final blockStartDelimiter = char('{').plus().flatten();
final blockEndDelimiterBound = char('}').not();
class BlockParser extends Parser {
final Parser contentParser;
BlockParser(this.contentParser);
@override
Result parseOn(Context context) {
var result = blockStartDelimiter.parseOn(context);
if (result.isFailure) return result;
var endParser = _createEndParser(result.value);
var body = new CompressList();
while (true) {
final entry = result;
result = endParser.parseOn(entry);
if (result.isSuccess) {
var elements = convertStringToTextNode(body.compress());
return result.success(new SharkBlock(elements));
}
result = contentParser.parseOn(entry);
if (result.isSuccess) {
body.add(result.value);
continue;
}
return result.success(new SharkBlock(result.value));
}
}
Parser createEndParser(String start) {
return string(toEndString(start)).seq(char('}').not());
}
String _toEndString(String capturedStart) {
var sb = new StringBuffer();
for (var i = 0;i < capturedStart.length;i++) {
sb.write('}');
}
return sb.toString();
}
}
其中的CompressList
类的作用,是把接收到的连续字符拼成一个字符串。
需要注意的是,主体块有两种:
普通主体块:里面可嵌入其它标签、表达式等,标签由@
开头
纯文本主体块:里面所有内容不做解析,保持原样,标签由@!
开头
其实两者的逻辑还是比较相似的,不同点在于对于花括号间的内容,前者使用的解析器要多一些,后者仅仅是any()
就行了。
在我的代码里,分别给它们定义为:
def('normalBlock', new BlockParser(
ref('atAt') | ref('sharkTag') | ref('sharkPlainTextTag') | ref('sharkExpression') | any())
);
def('plainTextBlock', new BlockParser(any()));
可以看到前者要传入更多的parser.
为了让代码方便处理,我定义了两个规则,分别对应普通标签和纯文本标签:
def('sharkTag', block(char('@'), ref('normalBlock')));
def('sharkPlainTextTag', block(string('@!'), ref('plainTextBlock')));
Parser _block(Parser startMarkParser, Parser blockParser) {
return startMarkParser & ref('tagName') & (
(ref('tagParams').trim() & blockParser.optional())
| (whitespace().star().trim().map((_) => null) & blockParser)
);
}
由于标签的参数部分和主体部分,两者至少要提供一个,所以我在_block()
中,提供了两种情况对应的代码:
一定有参数部分,主体部分可选
ref('tagParams').trim() & blockParser.optional()
只有主体部分
whitespace().star().trim().map((_) => null) & blockParser
最后是整个模板内容的解析,就是把前面的几个主要解析规则拼在一起就行了:
def('start', (
ref('atAt')
| ref('sharkTag').separatedBy(whitespace().star(), includeSeparators:false)
| ref('sharkPlainTextTag')
| ref('sharkExpression')
| any()
).star());
到这里为止,SharkDart所有的解析规则都讲完了。正因为采用了通用的标签结构设计,解析这块的内容就比较少了。通过这两篇,应该可以看到文本解析大体上是怎么回事了。如果想了解更细节的东西,可以直接看源代码
下一篇要讲如果设计语法树了。