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

(2014-03-22) 自己动手写模板引擎 – SharkDart (7) – 自定义标签

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

在前面我们定义了一些常用标签,本篇将会看看如何实现它们。

常用标签

基础类

为了能够让用户自定义标签处理器,并让它可以被SharkDart的编译器识别,我们需要定义一些基础类和约定。

首先我们会有一个全局的标签处理器仓库,可以让我们把自定义的处理器注册上去,同时也可以让编译器来查询。

var tagRepository = new TagRepository();

class TagRepository {
  var store = new Map<String, TagHandler>();
  TagHandler find(String tagName) => store[tagName];
  void register(String tagName, TagHandler handler) => store[tagName] = handler;
}


然后是处理器的基类和返回结果类:

abstract class TagHandler {
  TagHandleResult handle(SharkTag tag, List nodesAfterTag);
}

class TagHandleResult {
  List<CompilableElement> elements;
  List tail;

  TagHandleResult(this.elements, this.tail);
}


我们自定义的处理器需要继承自`TagHandler`,并实现其`handle(...)`方法。

另外,在模板中经常需要生成一些不重复的变量名,所以我们再提供一个全局的id生成函数:

int idForTags = 0;
nextId() => idForTags++;


由于Dart代码只能在单线程中运行,所以不用担心拿到重复的值。

最后需要提供一个函数,把SharkDart内置的处理器注册上去:

registerBuiltInTags() {
  tagRepository
    ..register('xxx', new XxxTagHandler())
    ..register('yyy', new YxxTagHandler())
}
var builtInTags = registerBuiltInTags();


后面一句定义的`builtInTags`变量实际上没有用,只是dart语法不允许在library的全局域里直接调用一个函数,真正起作用的是后面的`registerBuiltInTags()`

有了这些基础设施,我们就可以来实现自己的标签了。

## if

`if`的实现比较简单,基本上就是在代码中写上dart的`if`语句即可:

class IfTagHandler extends TagHandler {
  TagHandleResult handle(SharkTag tag, List nodesAfterTag) {
    var condition = tag.tagParams.first.paramVariable;
    return new TagHandleResult([
      stmt('if ($condition) {'),
      tag.body.toCompilable(),
      stmt('}')
    ], nodesAfterTag);
  }
}


注意`if(...){`和`}`都是`stmt`,表示它们将会以代码形式原样输出在最终代码里。另外它把`nodesAfterTag`原封不动的传了回去,因为它不需要处理后续结点。

## else

`else`的实现也很简单,连参数都不需要:

class ElseTagHandler extends TagHandler {
  TagHandleResult handle(SharkTag tag, List nodesAfterTag) {
    return new TagHandleResult([
      stmt('else {'),
      tag.body.toCompilable(),
      stmt('}')
    ], nodesAfterTag);
  }
}


跟`if`一样,不需要多讲。

## elseif

`elseif`跟前面差不多,只是它要输出为`else if`:

class ElseIfTagHandler extends TagHandler {
  TagHandleResult handle(SharkTag tag, List nodesAfterTag) {
    var condition = tag.tagParams.first.paramVariable;
    return new TagHandleResult([
      stmt('else if ($condition) {'),
      tag.body.toCompilable(),
      stmt('}')
    ], nodesAfterTag);
  }
}


### 模板实例

将前面三个标签结合起来,写一个SharkDart的模板:

if_elseif_else1.shark

@params(int num)

Hello,
@if(num == 1) {
  air!
} @elseif(num == 2) {
  sky!
} @else {
  world!
}


### 生成dart代码

将会编译成以下代码:

library shark.views.tags.if_elseif_else1;

String render({int num, String implicitBody_()}) {
  if (implicitBody_ == null) {
    implicitBody_ = () => '';
  }
  var _sb_ = new StringBuffer();
  _sb_.writeln('Hello,');
  _sb_.write('');
  if (num == 1) {
    _sb_.writeln('');
    _sb_.writeln('  air!');
    _sb_.write('');
  }
  else if (num == 2) {
    _sb_.writeln('');
    _sb_.writeln('  sky!');
    _sb_.write('');
  }
  else {
    _sb_.writeln('');
    _sb_.writeln('  world!');
    _sb_.write('');
  }
  _sb_.writeln('');
  _sb_.write('');
  return _sb_.toString();
}


## for

`for`标签要编译为dart中的`for(var item in list)`,但为了能让用户使用更方便,它额外提供了一些变量供使用,如`index`, `isFirst`等。同时还可以给定一个分隔符,自动放在两个元素之间,在生成json类似的数据时很有用。

它的定义如下:

class ForTagHandler extends TagHandler {
  TagHandleResult handle(SharkTag tag, List nodesAfterTag) {
    var separator = tag.getParamAsString('separator', '');

    var main = tag.tagParams.first;
    var type = (main.paramType == null ? 'var' : main.paramType);
    var variable = main.paramVariable;
    var collections = (main.paramDescription as SharkExpression).expression;
    var indexVar = 'index_${nextId()}';
    var countVar = 'total_${nextId()}';
    return new TagHandleResult([
      stmt('if ($collections != null) {'),
      stmt('  int ${indexVar} = 0;'),
      stmt('  int ${countVar} = ${collections}.length;'),
      stmt('  for (var $variable in $collections) {'),
      stmt('    int ${variable}_index = ${indexVar};'),
      stmt('    bool ${variable}_isFirst = ${indexVar} == 0;'),
      stmt('    bool ${variable}_isLast = ${indexVar} == ${countVar} - 1;'),
      stmt('    bool ${variable}_isOdd = ${indexVar} % 2 == 1;'),
      stmt('    bool ${variable}_isEven = ${indexVar} % 2 == 0;'),
      stmt('    if(!${variable}_isFirst) {'),
      text(separator),
      stmt('    }'),
      stmt('    ${indexVar}++;'),
      tag.body.toCompilable(),
      stmt('  }'),
      stmt('}')
    ], nodesAfterTag

    );
  }
}


有这几点需要注意:
  1. 使用了很多stmt来插入多行dart代码,以实现前面所说的额外变量的功能

  2. nextId()是一个全局函数,每次调用都会生成一个增1的数字,以保证变量名不相同

  3. 增加了if(collection!=null)的判断,是为了让for后面可加上else标签

    模板实例

    for3.shark

    @params(List users)

    @for(user: users, separator: '***') {

    index: @user_index

    isFirst: @user_isFirst

    isLast: @user_isLast

    isOdd: @user_isOdd

    isEven: @user_isEven

    Hello, @user


    } @else {
    There is no user found
    }

    生成dart代码

    library shark.views.tags.for3;

    String render({List users, String implicitBody()}) {
    if (implicitBody
    == null) {

    implicitBody_ = () => '';
    

    }
    var sb = new StringBuffer();
    if (users != null) {

    int index_2 = 0;
    int total_3 = users.length;
    for (var user in users) {
      int user_index = index_2;
      bool user_isFirst = index_2 == 0;
      bool user_isLast = index_2 == total_3 - 1;
      bool user_isOdd = index_2 % 2 == 1;
      bool user_isEven = index_2 % 2 == 0;
      if (!user_isFirst) {
        _sb_.write('***');
      }
      index_2++;
      _sb_.writeln('');
      _sb_.write('  # index: ');
      _sb_.write(user_index);
      _sb_.writeln('');
      _sb_.write('  # isFirst: ');
      _sb_.write(user_isFirst);
      _sb_.writeln('');
      _sb_.write('  # isLast: ');
      _sb_.write(user_isLast);
      _sb_.writeln('');
      _sb_.write('  # isOdd: ');
      _sb_.write(user_isOdd);
      _sb_.writeln('');
      _sb_.write('  # isEven: ');
      _sb_.write(user_isEven);
      _sb_.writeln('');
      _sb_.write('  Hello, ');
      _sb_.write(user);
      _sb_.writeln('');
      _sb_.writeln('  ------------');
      _sb_.write('');
    }
    

    }
    else {

    _sb_.writeln('');
    _sb_.writeln('  There is no user found');
    _sb_.write('');
    

    }
    sb.writeln('');
    sb.write('');
    return sb.toString();
    }

    dart

    dart标签可以让我们在模板中嵌入一段dart代码,它的实现也比较简单:

    class DartTagHandler extends TagHandler {
    TagHandleResult handle(SharkTag tag, List nodesAfterTag) {

    var content = '';
    if (!tag.body.isEmpty) {
      var trim = tag.getParamAsBool('trim');
      if (trim) {
        tag.body.trim();
      }
      content = tag.body.elements.elements.first.toString();
    }
    return new TagHandleResult([
      stmt(content),
    ], nodesAfterTag);
    

    }
    }

    注意它可以接受一个trim参数,用于把代码段前后的空白去掉。

    模板实例

    dart1.shark

    @!dart {{{
    hello(who) => 'Hello, $who!';
    }}}

    @{hello('SharkDart')}

    生成dart代码

    library shark.views.tags.dart1;

    String render({String implicitBody()}) {
    if (implicitBody
    == null) {

    implicitBody_ = () => '';
    

    }
    var sb = new StringBuffer();

    hello(who) => 'Hello, $who!';

    sb.writeln('');
    sb.writeln('');
    sb.write('

    ');
    sb.write(hello('SharkDart'));
    sb.writeln('
    ');
    sb.write('');
    return sb.toString();
    }

    plainText

    plainTextdart标签很相似,不同的是它会把内容当作文本输出。

    class PlainTextTagHandler extends TagHandler {
    TagHandleResult handle(SharkTag tag, List nodesAfterTag) {

    var content = '';
    if (tag.body != null) {
      var trim = tag.getParamAsBool('trim', false);
      if (trim) {
        tag.body.trim();
      }
      content = tag.body.elements.elements.first.toString();
    }
    return new TagHandleResult([
      text(content)
    ], nodesAfterTag);
    

    }
    }

    它也同样可以接受一个trim参数,用于去除文本两端的空白。

    模板实例

    @!plainText {{{ @hello() {} }}}

    生成dart代码

    library shark.views.tags.plainText1;

    String render({String implicitBody()}) {
    if (implicitBody
    == null) {

    implicitBody_ = () => '';
    

    }
    var sb = new StringBuffer();
    sb.writeln('

    ');
    sb.write('');
    sb.writeln('');
    sb.writeln(' @hello() {}');
    sb.writeln('');
    sb.write('');
    sb.writeln('');
    sb.writeln('
    ');
    sb.write('');
    return sb.toString();
    }

    params

    params可以用来给生成的render()入口函数指定参数。

    class ParamsTagHandler extends TagHandler {
    TagHandleResult handle(SharkTag tag, List nodesAfterTag) {

    var params = "";
    if (tag.tagParams.isNotEmpty) {
      params = tag.tagParams.map((param) => param.toString()).join(', ');
    }
    if (tag.hasNoBody) {
      return new TagHandleResult([
        functionParams(params),
        new SharkNodeList(nodesAfterTag).toCompilable(),
      ], []);
    } else {
      return new TagHandleResult([
        functionParams(params),
        tag.body.toCompilable(),
      ], nodesAfterTag);
    }
    

    }
    }

    需要注意的是,如果params标签没有提供花括号括起来的主体,它会把后续结点当作主体,所以在处理时需要判断一下。

    模板实例

    params1.shark

    @params(String user) {

    hello, @user!

    }

    生成dart代码

    library shark.views.tags.params1;

    String render({String user, String implicitBody()}) {
    if (implicitBody
    == null) {

    implicitBody_ = () => '';
    

    }
    var sb = new StringBuffer();
    sb.writeln('');
    sb.write('

    hello, ');
    sb.write(user);
    sb.writeln('!
    ');
    sb.write('');
    sb.writeln('');
    sb.write('');
    return sb.toString();
    }

    extends

    extends的实现比较复杂,因为它需要把当前模板的内容以某种方式传递给另一个模板,再把最终的结果渲染出来。有这几个地方需要注意:

  4. extendsrender标签共用了同一个处理器,不同的是,当没有提供标签主体时,extends会把后续结点当主体,而render不会。当然这个默认行为也可以在标签参数中改变:传入enableImplicitBody:true/false

  5. 当继承另一个模板时,如果模板路径以.开头,如extends(./another),则会直接导入,否则会当作相对于模板根目录的相对路径。对于后者,我们需要先算出它的绝对路径,再计算出与当前模板的相对路径,然后导入它。

  6. 继承另一个模板,实际上会调用另一个模板生成的render()函数,然后把自己的内容以another.render(implicitBody: ()=> { current content }的方式得到结果。

    下面的代码有些长,但应该可以快速理解它的意思。

    class RenderTagHandler extends TagHandler {
    bool enableImplicitBodyByDefault;

    RenderTagHandler(this.enableImplicitBodyByDefault);

    TagHandleResult handle(SharkTag tag, List nodesAfterTag) {

    return new _RenderTagInternalHandler(tag, nodesAfterTag, this.enableImplicitBodyByDefault).handle();
    

    }

    }

    class RenderTagInternalHandler {
    final targetTemplateVar = '
    shark_render_${nextId()}';
    bool enableImplicitBody;
    List<_ParamForRenderTag> renderParams;
    SharkTag tag;
    List nodesAfterTag;

    _RenderTagInternalHandler(this.tag, this.nodesAfterTag, enableImplicitBodyByDefault) {

    this.enableImplicitBody = this.tag.getParamAsBool('enableImplicitBody', enableImplicitBodyByDefault);
    this.renderParams = _compileRenderParams();
    

    }

    TagHandleResult handle() {

    var compilableItems = [ buildImportPathStmt() ];
    for (var p in renderParams) {
      compilableItems.addAll(_preCalculateParamValue(p));
    }
    compilableItems.addAll(_renderTargetTemplate());
    
    if (tag.hasNoBody && enableImplicitBody) {
      return new TagHandleResult(compilableItems, []);
    } else {
      return new TagHandleResult(compilableItems, nodesAfterTag);
    }
    

    }

    _ImportPath buildImportPathStmt() {

    var targetTemplateName = tag.tagParams.first.paramVariable;
    if (targetTemplateName.startsWith('.')) {
      return importStmt('import \'${targetTemplateName}.dart\' as $targetTemplateVar;');
    } else {
      return importStmtFromTemplate((CompilableTemplate template) {
        var templateParentPath = new File(path.join(template.templateRootDir.path, template.relativePath)).parent.path;
        var targetTemplateFilePath = path.join(template.templateRootDir.path, targetTemplateName);
        var relative = path.relative(targetTemplateFilePath, from:templateParentPath);
        return 'import \'${relative}.dart\' as $targetTemplateVar;';
      });
    }
    

    }

    List<_ParamForRenderTag> _compileRenderParams() {

    return tag.tagParams.sublist(1).map((param) => new _ParamForRenderTag(
      param.paramVariable,
      '${param.paramVariable}_${nextId()}',
      param.paramDescription.toCompilable()
    )).toList();
    

    }

    List preCalculateParamValue(ParamForRenderTag p) {

    return [
      stmt('var ${p.paramGeneratedVariable} = () {'),
      stmt('  var _sb_ = new StringBuffer();'),
      p.paramValue,
      stmt('  return _sb_.toString();'),
      stmt('}();')
    ];
    

    }

    List _renderTargetTemplate() {

    var items = [];
    var paramStr = renderParams.map((p) => "${p.paramName}: ${p.paramGeneratedVariable}").join(', ');
    items.add(stmt('_sb_.write(${targetTemplateVar}.render($paramStr, implicitBody_ : () {'));
    items.add(stmt('  var _sb_ = new StringBuffer();'));
    if (tag.hasNoBody && enableImplicitBody) {
      items.add(new SharkNodeList(nodesAfterTag).toCompilable());
    } else if (tag.body != null) {
      items.add(tag.body.toCompilable());
    }
    items.add(stmt('  return _sb_.toString();'));
    items.add(stmt('}));'));
    return items;
    

    }

    }

    class _ParamForRenderTag {
    String paramName;
    String paramGeneratedVariable;
    CompilableElement paramValue;

    _ParamForRenderTag(this.paramName, this.paramGeneratedVariable, this.paramValue);

    toString() => '$paramName : $paramGeneratedVariable : $paramValue';
    }

    class _ImportPath {
    String targetFilePath;
    String asName;
    }

    模板实例

    layout1.shark

    @params(String user)

    Hello, @user!
    @renderBody()

    extends1.shark

    @params(String user)

    @extends(./layout1, user: user)

    This is inner page with @user!

    生成dart代码

    将会生成以下dart代码:

    layout1.dart

    library shark.views.tags.layout1;

    String render({String user, String implicitBody()}) {
    if (implicitBody
    == null) {

    implicitBody_ = () => '';
    

    }
    var sb = new StringBuffer();
    sb.write('

    Hello, ');
    sb.write(user);
    sb.writeln('!
    ');
    sb.write('
    ');
    sb.write(implicitBody());
    sb.writeln('
    ');
    sb.write('');
    return
    sb_.toString();
    }

    extends1.dart

    library shark.views.tags.extends1;

    import './layout1.dart' as _shark_render_0;

    String render({String user, String implicitBody()}) {
    if (implicitBody
    == null) {

    implicitBody_ = () => '';
    

    }
    var sb = new StringBuffer();
    var user_1 = () {

    var _sb_ = new StringBuffer();
    _sb_.write(user);
    return _sb_.toString();
    

    }();
    sb.write(shark_render_0.render(user: user_1, implicitBody : () {

    var _sb_ = new StringBuffer();
    _sb_.write('This is inner page with ');
    _sb_.write(user);
    _sb_.writeln('!');
    _sb_.write('');
    return _sb_.toString();
    

    }));
    return sb.toString();
    }

    render

    render标签与上面的extends标签使用的是同一个标签处理器,因为它们的逻辑实际上非常相似,但通过不同的命名可以更准确的表达出使用者的意愿。

    不同的地方在于,当标签没有提供主体时,extends默认会使用后续节点当主体,而render不会。

    通过查看两者注册到tagReposity的代码就能看出:

    tagRepository.register('extends', new RenderTagHandler(true));
    tagRepository.register('render', new RenderTagHandler(false));

    后面的true/false是指是否enableImplicitBody的功能。

    renderBody

    renderBody的内容比较简单,它直接输出调用implicitBody_()的内容。可与extendsrender标签使用,处理它们通过标签主体传过来的内容。

    class RenderBodyTagHandler extends TagHandler {
    TagHandleResult handle(SharkTag tag, List nodesAfterTag) {

    return new TagHandleResult([
      expr('implicitBody_()')
    ], nodesAfterTag);
    

    }
    }

    注册到标签仓库

    下面我们把上面定义的各标签处理器注册到全局的标签仓库中:

    var builtInTags = initializeBuiltInTags();
    initializeBuiltInTags() {
    tagRepository.register('params', new ParamsTagHandler());
    tagRepository.register('if', new IfTagHandler());
    tagRepository.register('else', new ElseTagHandler());
    tagRepository.register('elseif', new ElseIfTagHandler());
    tagRepository.register('for', new ForTagHandler());
    tagRepository.register('extends', new RenderTagHandler(true));
    tagRepository.register('render', new RenderTagHandler(false));
    tagRepository.register('renderBody', new RenderBodyTagHandler());
    tagRepository.register('dart', new DartTagHandler());
    tagRepository.register('plainText', new PlainTextTagHandler());
    }

看到这里大家已经明白如何实现自己的标签了吧。再结合前几篇的内容,相信大家对模板引擎应该有一些感性的认识了吧。

下一篇将介绍如何利用第三方工具实现自动监视模板目录,自动编译修改过的模板文件,以及对SharkDart的一些总结。

comments powered by Disqus