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

(2014-03-19) 自己动手写模板引擎 – SharkDart (4) – @@及表达式的解析

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

在本篇将会使用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类是将在后面介绍的语法树结点类之一,用来表示匹配到了一个表达式,这里暂不用考虑。

标签结构的解析比较复杂,将放在下一篇。

comments powered by Disqus