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

(2014-03-13) 自己动手写模板引擎 – SharkDart (2) – 语法设计

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

首先不考虑实现,从用户的角度想想,这个模板应该有什么样的语法。

我们可能已经用过或见过很多模板引擎,大多数都是提供了一些特别的标记,可以让我们在HTML或其它文本中,嵌入我们自己的标签,以实现如数据展示、逻辑控制等功能。每一种都有自己独特的标记和语法定义,但实际使用起来,多数都差不多的。每种模板都提供了一些常用的标签,比如for, if, 变量显示等,都是居家必备的。

SharkDart也不例外。

主要特点

SharkDart语法上的主要特点有几下几点:

  1. 采用字母@作为特殊字符
  2. 提供通用tag结构,而不为具体的tag自定义语法
  3. 使用字母!表示取用原始文本

Magic @

SharkDart采用了字母@作为特殊的标识符,用于标示可由SharkDart处理的标签。这种做法参考了Rythm Template EnginePlay2的模板,而它们又参考了.net的Razor.

不过也仅此而已,其它的部分,SharkDart都有自己的特色,很快就能看到。

通用型标签结构

很多模板都为某些内置标签提供了量身定做的语法。比如for,在Play2中是:

@for(order <- orders) {
}


这模仿了scala中的for语法。

而if-else则是:

@if(items.isEmpty) {
  <h1>Nothing to display</h1>
} else {
  <h1>@items.size items!</h1>
}


它跟前面的for完全不同,无法使用同一种解析规则。所以在模板的解析阶段,必须为每一个标签提供相应的解析规则。

这样做的好处是,每个标签的语法都可定制,相当灵活,缺点则是,一旦想提供自定义的标签,可能需要修改解析规则的源代码,工作量挺大的,对于扩展也不利。

在SharkDart之前我还做过一个叫RythmDart的模板引擎,里面就采用了上面的方式,为每一个标签都提供了相应的解析规则。这一次我想换种方式。

通用规则的难点在于,要想出一个结构,可以满足大部分内置标签的需要,比如如何把上面的for和if-else统一成一种格式。

经过几天的思考与推敲,我最后定义了两种标签结构(一种实在搞不定):
  1. 可接受多个参数的标签
    @tagname(paramType paramVar1: paramDesc, paramType paramVar2: paramDesc) {

    }
    其中paramType和paramDesc都是可选项。即也可以为:
    @tagname(paramVar1, paramVar2) {}

  2. 可接受一段代码作为参数的标签
    @tagname(some code here) {

    ...
    

    }

    使用这两种结构,基本上可满足我想到的各内置标签的功能。比如:

      @for(name: names) {
    • Hello @name !
    • }

    @params(String name: 'Shark') {

    <h1>It's all about @name</h1>
    

    }

    @extends(anotherTemplate, name: 'Freewind') {

    <div>Custom content</div>
    

    }

    这些都使用了第一种结构。(注意上面的@name,表示显示变量name的值)

    再比如:

    @if(name=='Shark') {

    Enter the ocean!
    

    } @elseif(name=='Bird') {

    Fly!
    

    } @else {

    Sleep ~~
    

    }

    使用了第二种结构。

    实际上,我设计出的各内置标签(数量不多),都可以用它们实现。

    另外,为了跟后面的“表达式”结构作为区分,标签必须至少有参数部分()或主体部分{}之一,哪怕是空的也行。

    表达式

    表达式是指在页面中显示一个变量或者一些调用代码的值,也是必不可少的功能。为了一致,它也使用了@

    简单表达式,直接在前面加@,如:

    @name
    @age

    复杂表达式,可用@{}包起来:

    @{name.toLowerCase()}
    @{users.map((u)=>“[$u]“).join(', ')}

    当然单个变量也可以包:

    @{name}

    纯@字符

    @字符有了特殊的意义,那我想显示一个单纯的@怎么办?为了解决这个问题,规定@@会被解析为单个@。比如:

    @@name

    会被解析为纯文本:

    @name

    @!

    有的时候,我希望标签的主体就是一段纯文本,里面的所有内容都原封不动的取出。为了实现这个功能,我定义了一个@!,即在@字符后加一个!,表示主体将会被当作纯文本看待。

    它不跟任何一个具体的标签相关,即可以添加到任何标签前。而标签的其它部分都跟以前一样。比如,我想定义一段Dart代码:

    @!dart {

    print("Hello, Shark!");
    

    }

    或者一段普通文本:

    @!plainText {

     I'm plainText, @@ is @@.
    

    }

    里面的@@还是@@,而不会被解析为@。但如果去掉那个!,就不一样了。

    多重括号 {{{ … }}}

    仔细看前一段,很快会发现一个问题:如果纯文本中有一个},会怎样?

    @!dart {

    var s = "}";
    

    }

    回答:dart标签的主体会提前结束,我们为dart标签取得的内容,实际上为:

    var s = "

    为了解决这个问题,我对标签主体两边的花括号进行了调整,允许使用多个花括号,以保证内容不会被意外截断:

    @!dart {{{

    var s = "}";
    

    }}}

    多长都行:

    @!dart {{{{{{{{{{

    // oh my God, I can't breath
    

    }}}}}}}}}}

    不想要{}

    有些标签主体外面的花括号有点烦人,比如说@params@extends。它们两个一般放在页面的最顶端,用来指定整个页面接受的参数,以及继承了哪个页面。如果有花括号,会写成:

    @params(String name) {

    <html>
      <head>...</head>
      <body>...</body>
    </html>
    

    }

    及:

    @extends(anotherTemp) {

    <div>...</div>
    <div>...</div>
    <div>...</div>
    

    }

    如果去掉花括号,则可以写成:

    @params(String name)




    及:

    @extends(anotherTemp)

    ...
    ...
    ...

这样看起来标签就像是页面顶上的一个注释,不会对主要内容产生干扰,还能省下一个不必要的缩进。

这种需求实际上对语法的解析不产生影响,因为它并不跟前面列出的规则矛盾。只是在后面将语法树翻译成dart代码时,需要考虑如何让标签决定它后面跟着的内容是标签的一部分(主体),还是排在它后面的普通内容。

语法搞定

经过以上几个简单的规则,SharkDart的语法就定义好了!

看起来很简单,但让我想了好几天,多次推翻才定下这种方案。中间过程不提,只能说,这真是一项特别费脑子的事情。

今天到此为止,下一篇将会尝试使用这里定义的标签通用结构设计一些常用标签,看看是否真的可行。

comments powered by Disqus