Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2014-03-20) 自己动手写模板引擎 – SharkDart (5) – 标签结构的解析

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

终于到了最关键的标签结构的解析了。对于标签结构,最难的地方在于主体外面的花括号,因为花括号的个数不限,所以这个匹配是动态的。为了简单起见,我将把主体与前面分开解释。

结构一:带参数列表的标签

结构形如(不包括主体部分):

@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`,把它们组合在一起。

这里有两点需要注意:
  1. type和description都是可选的

  2. 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匹配走,只留下了/menuparamVariable

    为了解决这个问题,我再改成:

    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类的作用,是把接收到的连续字符拼成一个字符串。

    普通和纯文本主体块

    需要注意的是,主体块有两种:

  3. 普通主体块:里面可嵌入其它标签、表达式等,标签由@开头

  4. 纯文本主体块:里面所有内容不做解析,保持原样,标签由@!开头

    其实两者的逻辑还是比较相似的,不同点在于对于花括号间的内容,前者使用的解析器要多一些,后者仅仅是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()中,提供了两种情况对应的代码:

  5. 一定有参数部分,主体部分可选
    ref('tagParams').trim() & blockParser.optional()

  6. 只有主体部分
    whitespace().star().trim().map((_) => null) & blockParser

    整个模板的解析

    最后是整个模板内容的解析,就是把前面的几个主要解析规则拼在一起就行了:

    def('start', (
    ref('atAt')
    | ref('sharkTag').separatedBy(whitespace().star(), includeSeparators:false)
    | ref('sharkPlainTextTag')
    | ref('sharkExpression')
    | any()
    ).star());

到这里为止,SharkDart所有的解析规则都讲完了。正因为采用了通用的标签结构设计,解析这块的内容就比较少了。通过这两篇,应该可以看到文本解析大体上是怎么回事了。如果想了解更细节的东西,可以直接看源代码

下一篇要讲如果设计语法树了。

comments powered by Disqus