构建 AST
一旦我们做好了分词,下一步就可以遍历每个片段并构建语法树了。我们使用 Node 类来作为树的节点的基类,然后创建对每一种节点类型创建子类。每个子类都必须提供 process_fragment
和 render
方法。 process_fragment
用来进一步解析片段的内容并且把需要的属性存到 Node
对象上。 render
方法负责使用提供的上下文转换对应的节点内容到 HTML。
子类也可以实现 enter_scope
和 exit_scope
钩子方法,这两个方法不是必须的。在编译器编译期间,会调用这两个钩子函数,他们应该负责进一步的初始化和清理工作。当一个 Node
创建了一个新的作用域(scope)的时候,会调用 enter_scope
,当退出作用域时,会调用 exit_scope。关于作用域
,下面会讲到。
Node 基类如下:
class _Node(object): def __init__(self, fragment=None): self.children = [] self.creates_scope = False self.process_fragment(fragment) def process_fragment(self, fragment): pass def enter_scope(self): pass def render(self, context): pass def exit_scope(self): pass def render_children(self, context, children=None): if children is None: children = self.children def render_child(child): child_html = child.render(context) return''if not child_html else str(child_html) return ''.join(map(render_child, children))
下面是变量节点的定义:
class _Variable(_Node): def process_fragment(self, fragment): self.name = fragment def render(self, context): return resolve_in_context(self.name, context)
为了确定 Node 的类型(并且进一步初始化正确的类),需要查看片段的类型和文本。文本和变量片段直接翻译成文本节点和变量节点。块片段需要一些额外的处理 —— 他们的类型是使用块命令来确定的。比如说:
{% each items %}
是一个 each
类型的块节点,因为块命令是 each。
一个节点也可以创建作用域。在编译时,我们记录当前的作用域,并且把新的节点作为作为当前作用域的子节点。一旦遇到一个正确的关闭标签,关闭当前作用域,并且从作用域栈中把当前作用域 pop 出来,使用栈顶作为新的作用域。
def compile(self): root = _Root() scope_stack = [root] for fragment in self.each_fragment(): if not scope_stack: raise TemplateError('nesting issues') parent_scope = scope_stack[-1] if fragment.type == CLOSE_BLOCK_FRAGMENT: parent_scope.exit_scope() scope_stack.pop() continue new_node = self.create_node(fragment) if new_node: parent_scope.children.append(new_node) if new_node.creates_scope: scope_stack.append(new_node) new_node.enter_scope() return root
渲染
管线的最后一步就是把 AST 渲染成 HTML 了。这一步访问 AST 中的所有节点并且使用传递给模板的 context 参数调用 render 方法。在渲染过程中,render 不断地解析上下文变量的值。可以使用使用 ast.literal_eval
函数,它可以安全的执行包含了 Python 代码的字符串。
def eval_expression(expr): try: return 'literal', ast.literal_eval(expr) except ValueError, SyntaxError: return 'name', expr
如果我们使用上下文变量,而不是字面量的话,需要在上下文中搜索来找到它的值。在这里需要处理包含点的变量名以及使用两个点访问外部上下文的变量。下面是 resolve 函数,也是整个难题的最后一部分了~
def resolve(name, context): if name.startswith('..'): context = context.get('..', {} name = name[2:] try: for tok in name.split('.'): context = context[tok] return context except KeyError: raise TemplateContextError(name)
结论
我希望这个小小的学术联系能够让你对模板引擎是怎样工作的有一点初步的感觉。这个生产级别的代码还差得很远,但是也可以作为你开发更好的工具的基础。
你可以在 GitHub 上找到完整的代码,你也可以进一步在 Hacker News 上讨论.