# TemplateRender **Repository Path**: hdt3213/TemplateRender ## Basic Information - **Project Name**: TemplateRender - **Description**: jinja tenplate engine powered by python3 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2016-09-25 - **Last Updated**: 2022-06-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 基于DOM树的模板引擎, 模板采用类似jinja2的规范, 支持的标签包括: - `{{val}}` - `{%if%}{%elif%}{%else%}{%end%}` - `{%ifequal%}{%end%}` - `{%ifnotequal%}{%end%}` - `{%for%}{%end%}` - `{%include%}` - `{%block%}`, `{%extends%}` # render.py 渲染的接口和总体调度. ## `render_from_file(path, context={})` 渲染模板文件, 根目录在config.TEMPLATE_PATH中设置. `source.FileSource`将文件模拟为字符串, 使用FileSource代替字符串表示模板全文. ## `render_from_str(src, context={})` src参数为模板原文, context为渲染模板所需上下文, 返回渲染后的字符串.接口可以参考django.shortcuts.render. 该方法依次调用: node_list, include_list = get_node_list(src) render_include_nodes(node_list, include_list, context=context) node_tree, extends_list = get_node_tree(node_list) render_extends_nodes(extends_list, context=context) return node_tree.render(context=context) 核心流程: 1. 从模板全文中提取节点 1. 预处理IncludeNode 1. 按照节点列表建立DOM树结构 1. 预处理ExtendsNode 1. 从根节点开始调用各节点的render方法进行渲染 ## `render_include_nodes(include_list, context={})` 因为`render.py`引用了`nodes.py`, 所以`IncludeNode.render`无法调用`render_from_file`, 故而将渲染逻辑分离. 具体的渲染流程将在渲染IncludeNode介绍. ## `render_extends_nodes(extends_nodes, context={})` 因为循环引用的原因, 独立ExtendsNode的渲染逻辑.具体的渲染流程将在渲染ExtendsNode介绍 # parser.py parse.py从nodes中导入了各节点类,包括: - RootNode: 根节点 - TextNode: 所有无需模板解析的文本均放入TextNode中 - ValNode: {{}}显示表达式返回值 - ForNode, IfNode, ElseNode等 每个Node拥有构造器: src = '{{a}}' node = ValNode(src=src) ## `get_node_list(src)` 该方法分析模板原文, 将其解析为节点列表, 并将IncludeNode列表单独给出便于: >src = '{%for val in arr%}{%if val%}{{val}}{%else%}233{%end%}{%end%}{%include panel.html%}' >node_list, include_list = get_node_list(src) >node_list [ , , , , , , , ] >include_list [ ] `get_node_list`函数依赖工具函数: - `build_node`: 根据原文创建相应类型的节点 - `build_text_node`: 根据原文创建文本节点 该函数可以规避python字符串中出现的转义字符和模板标记的干扰, 如: {{ '}}' }} 定义: LEFT_BRACKETS = { '{{': '}}', '{#': '#}', '{%': '%}', } SHADOW = { '\'': '\'', '\"': '\"', } 执行流程: 初始化 遍历模板原文: 发现SHADOW字符且未被转义: 重置shadowed标记 发现LEFT_BRACKET: 将上一个节点结束至当前节点开始建立TextNode 发现RIGHT_BRACKET: 检查是否与栈顶匹配 提取节点内容, 交给build_node建立节点 如果是IncludeNode, 将其加入include_list中 弹栈 ## `get_node_tree(node_list)` 根据`get_node_list`返回的结果建立DOM树. 定义WRAPPER为可以包含子节点的标签, 如`{%if%}`, `{%for%}`. 定义SINGLE为不能包含子节点的标签, 如`{{}}`.为了处理方便, `{%else%}`, `{%elif%}也被作为SINGLE`处理. 执行流程: 建立root节点 初始化wrapper_nodes栈, 将root压栈 遍历node_list: 跳过注释节点 发现WRAPPER节点: 压栈(node, index) 发现SINGLE节点: 将节点加入栈顶的子节点中 发现EndNode节点: 取栈顶tmp, 弹栈 若tmp为ExtendsNode, 加入extends_list列表中 将tmp加入到新栈顶的子节点中 返回root, extends_list # nodes.py 定义各节点类. ## Node 节点基类, 定义属性: - `tag`: 使用类名作为类型标签如: 'IfNode', 'ForNode' - `child_nodes`: 子节点列表 - `src_str`: 包含模板标记的原文, 如`{% for i in arr%}` - `content`: 去除模板标记后的内容, 如`for i in arr` 构造器可以根据原文构造节点: ValNode(src=src) 所有派生类需要重写render()方法, 返回代表渲染结果的字符串,并定义了公用的方法. node.render(context=context) ## RootNode RootNode是DOM树的根节点, 其render方法只是依次调用其子节点的render方法. 包括含有子节点的节点在内, 所有节点的render方法均会返回自身的渲染结果, 从根节点开始调用render方法就可以渲染整个DOM树. ## CommentNode 模板注释类, render()返回空字符串. 类似的仅起标记作用的标签包括: - EndNode - ElseNode - ElifNode ## TextNode 文本节点, 继承了Node但没有进行重写. ## ValNode ValNode使用eval来计算python表达式: code = compile(content, '', "eval") text = str(eval(code)) eval计算需要依赖render方法的上下文, 因为locals()方法通过浅拷贝返回值, 故可以在它的返回值中动态的添加的添加变量构造上下文: for key in context.keys(): locals()[key] = context[key] ## IfNode 渲染IfNode需要几个工具函数支持: - `Node.get_condition(content, context=context, tag='if')`方法会寻找if语句中的条件表达式, 并调用`Node.eval_condition(content, context=context)`计算结果. - `IfNode.get_else_tags()`方法会在`child_nodes`中寻找`ElseNode`, `ElifNode`并记录它们的下标. - `Node.render_body(nodes, context=context)`可以按顺序渲染列表nodes中的节点, 这里用于渲染节点内部嵌套的节点块. 渲染流程: if条件成立: 渲染嵌套节点 返回 否则: 依次检查ElifNode, 若成立: 渲染嵌套节点 返回 存在ElseNode: 渲染嵌套节点 返回 ## IfEqualNode 重写`eval_condition`方法, 将求解python表达式变为检查两个值是否相等. 不支持ElifNode, 渲染时仅需判断一次条件. ## IfNotEqual 继承了IfEqualNode, 重写`get_condition`方法: def get_condition(self, content, **kwargs): return not super(IfNotEqualNode, self).get_condition(content, **kwargs) 简单地将结果取反 ## ForNode 重写`eval_condition`, 返回迭代器的名字和容器对象, 如`for i in arr`返回('i', arr) render方法需要动态建立迭代器对象, python无法使用`locals()[iter]`作为迭代器, 所以采用手动更新的方法: for _i in arr: locals()[iter] = _i text += self.render_body(self.child_nodes, context=context) # 特殊节点渲染 ## IncludeNode 因为`render.py`引用了`nodes.py`, 所以`IncludeNode.render`无法调用`render_from_file`. `IncludeNode.build`方法从自身原文中提取文件名, 写入`IncludeNode.filename`属性中. `render.render_from_str`函数调用`parser.get_node_list`方法获得`node_list`和`include_list`. `render.render_include_nodes`函数调用`render_from_file`逐个渲染`include_list`中的节点, 并将渲染结果存入`IncludeNode.content`中. `IncludeNode.render`仅需返回`IncludeNode.content`. ## BlockNode BlockNode可以被重写, 若未被重写则仅当做容器. BlockNode继承了RootNode, 渲染时自身不产生任何输出, 仅渲染子节点. `BlockNode.build`从原文中提取块名并放入`BlockNode.block_node`中. ## ExtendsNode 因为ExtendsNode和BlockNode均可以包含子节点, 所以不能像IncludeNode一样直接在node_list中处理. `get_block_by_name(root_node, block_name)`函数在`root_node`的DOM树中寻找名字为`block_name`的BlockNode并返回其引用. `parser.get_node_tree`返回`root_node`和`extends_list`. `extends_list`中每一个ExtendsNode都在`child_nodes`属性中保存着自身子节点. `render_extends_nodes`函数调用`render_extends_node`渲染`extends_list`中每一个节点并将结果存储到`ExtendsNode.content`中. `ExtendsNode.`将返回`ExtendsNode.content`. `render_extends_node(extends_node, context)`实际上在常规渲染流程中添加了更新被覆盖的BlockNode的步骤, 我们将被ExtendsNode扩展的模板文件称为基模板: src = FileSource() src.open(extends_node.filename) node_list, include_list = get_node_list(src) render_include_nodes(include_list, context=context) node_tree, extends_list = get_node_tree(node_list) # 替换被覆盖的BlockNode 遍历extends_node.child_nodes中的BlockNode: 找到node_tree中被覆盖的BlockNode 使用extends_node.child_nodes中的新节点替换被覆盖的节点 # 替换完成 渲染基模板中ExtendsNode return node_tree.render(context=context) 可以看到与正常渲染流程相比, 只是增加了一个替换步骤. 当然, 渲染基模板中ExtendsNode时形成了递归调用.