一直对模板引擎的实现很好奇,正好看到了这篇文章,翻译一下,供大家学习、参考。原文和 GitHub 链接在文后。
我们编写一个最简单的模板引擎,并且探索一下它的底层实现。如果你想直接看代码的话,GitHub 是你的好朋友
语言设计
这里设计的模板语言非常基础。使用两种标签,变量和块。
<!-- 变量使用 `{{` 和 `}}` 作为标识--> <div>{{my_var}}</div> <!-- 块使用 `{%` 和 `%}` 作为标识--> {% each items %} <div>{{it}}</div> {% end %}
大多数的块需要使用关闭标签,关闭标签使用 {%end%}
表示。
这个模板引擎能够处理基本的循环和条件语句,而且也支持在块中使用 callable。在我看来,能够在模板中调用任意的 Python 函数非常方便。
循环
使用循环可以遍历集合或者 iterable。
{% each people %} <div>{{it.name}}</div> {% end %} {% each [1, 2, 3] %} <div>{{it}}</div> {% end %} {% each records %} <div>{{..name}}</div> {% end %}
在上面的例子里面,people 是一个集合, it
指向了当前迭代的元素。使用点分隔的路径会被解析成字典属性。使用 ..
可以访问外部上下文中的对象。
条件语句
条件语句不需要多解释。这个语言支持 if 和 else 结构,而且支持 ==
, <=
, >=
, !=
, is
, <
, >
这几个操作符。
{% ifnum 5 %} <div>more than 5</div> {% else %} <div>less than or equal to 5</div> {% end %}
调用块
Callable 可以通过模板上下文传递,并且使用普通位置参数或者具名参数调用。调用块不需要使用 end 关闭。
<!-- 使用普通参数... --> <div class='date'>{% call prettify date_created %}</div> <!-- ...使用具名参数 --> <div>{% call log 'here' verbosity='debug' %}</div>
原理
在探索引擎是如何编译和渲染模板之前,我们需要了解下在内存中如何表示一个编译好的模板。
编译器使用抽象语法树(Abstract Syntax Tree, AST)来表示计算机程序。AST 是对源代码进行词法分析(lexical analysis)的结果。AST 相对源代码来说有很多好处,比如说它不包含任何无关紧要的文本元素,比如说分隔符这种。而且,树中的节点可以使用属性来添加更多的功能,而不需要改动代码。
我们会解析并分析模板来构造这样一棵树,并用它来表示编译后的模板。渲染的时候,遍历这棵树,传给它对应的上下文,然后输出 HTML。
模板切词(tokenize)
解析的第一步是把内容分隔成不同的片段。每个片段可以是任意的 HTML 或者是一个标签。这里使用正则表达式和 split()
函数分隔文本。
VAR_TOKEN_START = '{{' VAR_TOKEN_END = '}}' BLOCK_TOKEN_START = '{%' BLOCK_TOKEN_END = '%}' TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" %( VAR_TOKEN_START, VAR_TOKEN_END, BLOCK_TOKEN_START, BLOCK_TOKEN_END))
让我们来看一下 TOKREGEX。可以看到这个正则的意思是 TOKREGEX 要么是一个变量标签,要么是一个块标签,这是为了让变量标签和块标签都能够分隔文本。表达式的最外层是一个括号,用来捕获匹配到的文本。其中的 ?
表示非贪婪的匹配。我们想让我们的正则表达式是惰性的,并且在第一次匹配到的时候停下来。
下面这个例子实际展示了一下上面的正则:
>>> TOK_REGEX.split('{% each vars %}<i>{{it}}</i>{% endeach %}') ['{% each vars %}',<i>''{{it}}', '</i>, '{% endeach %}']
把每个片段封装成 Fragment 对象。这个对象包含了片段的类型,并且可以作为编译函数的参数。片段有以下四种类型
VAR_FRAGMENT = 0 OPEN_BLOCK_FRAGMENT = 1 CLOSE_BLOCK_FRAGMENT = 2 TEXT_FRAGMENT = 3