# 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时形成了递归调用.