500行Python代码实现模板引擎

发表于:2018-1-09 09:49

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Aaaaaaaaaaayou    来源:稀土掘金

分享:
  现在真正的解析开始了。我们使用正则表达式将模板文本拆分为多个 token。这是我们的正则表达式:
  tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
  split 函数将使用正则表达式拆分一个字符串。我们的模式是圆括号,因此匹配将用于分割字符串,也将作为分隔列表中的片段返回。
  (?s) 为单行模式,意味着一个点应该匹配换行符。接下来是匹配表达式/控制结构/注释,都为非贪婪匹配。
  拆分的结果是字符串列表。例如,该模板文本:
  <p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>
  会被分隔为:
[
'<p>Topics for ',               # literal
'{{name}}',                     # expression
': ',                           # literal
'{% for t in topics %}',        # tag
'',                             # literal (empty)
'{{t}}',                        # expression
', ',                           # literal
'{% endfor %}',                 # tag
'</p>'                          # literal
]
  将文本拆分为这样的 tokens 之后,我们可以对这些 tokens 进行循环,并依次处理它们。根据他们的类型划分,我们可以分别处理每种类型。
  编译代码是对这些 tokens 的循环:
for token in tokens:
# 注释直接忽略
if token.startswith('{#'):
# Comment: ignore it and move on.
continue
# 表达式:提取出内容交给 _expr_code 进行处理,然后生成一行代码
elif token.startswith('{{'):
# An expression to evaluate.
expr = self._expr_code(token[2:-2].strip())
buffered.append("to_str(%s)" % expr)
# 控制语句
elif token.startswith('{%'):
# Action tag: split into words and parse further.
# 先将前面生成的代码刷新到编译函数之中
flush_output()
words = token[2:-2].strip().split()
if words[0] == 'if':
# An if statement: evaluate the expression to determine if.
# if语句只能有两个单词
if len(words) != 2:
self._syntax_error("Don't understand if", token)
# if 入栈
ops_stack.append('if')
# 生成代码
code.add_line("if %s:" % self._expr_code(words[1]))
# 增加下一条语句的缩进级别
code.indent()
elif words[0] == 'for':
# A loop: iterate over expression result.
# 语法检查
if len(words) != 4 or words[2] != 'in':
self._syntax_error("Don't understand for", token)
# for 入栈
ops_stack.append('for')
# 记录循环体中的局部变量
self._variable(words[1], self.loop_vars)
# 生成代码
code.add_line(
"for c_%s in %s:" % (
words[1],
self._expr_code(words[3])
)
)
# 增加下一条语句的缩进级别
code.indent()
elif words[0].startswith('end'):
# Endsomething.  Pop the ops stack.
# 语法检查
if len(words) != 1:
self._syntax_error("Don't understand end", token)
end_what = words[0][3:]
# end 语句多了
if not ops_stack:
self._syntax_error("Too many ends", token)
# 对比栈顶元素
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
# 循环体结束,缩进减少缩进级别
code.dedent()
else:
self._syntax_error("Don't understand tag", words[0])
else:
# Literal content.  If it isn't empty, output it.
# 纯文本内容
if token:
buffered.append(repr(token))
  有几点需要注意:
  使用 repr 来给文本加上引号,否则生成的代码会像这样:
  extend_result([
  <h1>Hello , to_str(c_upper(c_name)), !</h1>
  ])
  使用 if token: 来去掉空字符串,避免生成不必要的空行代码
  循环结束后,需要检查 ops_stack 是否为空,不为空说明控制语句格式有问题:
  if ops_stack:
  self._syntax_error("Unmatched action tag", ops_stack[-1])
  flush_output()
  前面我们通过 vars_code = code.add_section() 创建了一个 section,它的作用是将传入的上下文解构为渲染函数的局部变量。
  循环完后,我们收集到了所有的变量,现在可以添加这一部分的代码了,以下面的模板为例:
  <p>Welcome, {{user_name}}!</p>
  <p>Products:</p>
  <ul>
  {% for product in product_list %}
  <li>{{ product.name }}:
  {{ product.price|format_price }}</li>
  {% endfor %}
  </ul>
  这里有三个变量 user_name product_list product。 all_vars 集合会包含它们,因为它们被用在表达式和控制语句之中。
  但是,最后只有 user_name product_list 会被解构成局部变量,因为 product 是循环体内的局部变量:
  for var_name in self.all_vars - self.loop_vars:
  vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
  到此,我们代码就都加入到 result 中了,最后将他们连接成字符串就大功告成了:
  code.add_line("return ''.join(result)")
  code.dedent()
  通过 get_globals  我们可以得到所创建的渲染函数,并将它保存到 _render_function 上:
  self._render_function = code.get_globals()['render_function']
  表达式
  现在让我们来仔细的分析下表达式的编译过程。
  我们的表达式可以简单到只有一个变量名:
  {{user_name}}
  也可以很复杂:
  {{user.name.localized|upper|escape}}
  这些情况, _expr_code 都会进行处理。同其他语言中的表达式一样,我们的表达式是递归构建的:大表达式由更小的表达式组成。一个完整的表达式是由管道分隔的,其中第一个部分是由逗号分开的,等等。所以我们的函数自然是递归的形式:
  def _expr_code(self, expr):
  """Generate a Python expression for `expr`."""
  第一种情形是表达式中有 |。
  这种情况会以 | 做为分隔符进行分隔,并将第一部分传给 _expr_code 继续求值。
  剩下的每一部分都是一个函数,我们可以迭代求值,即前面函数的结果作为后面函数的输入。同样,这里要收集函数变量名以便后面进行解构。
  if "|" in expr:
  pipes = expr.split("|")
  code = self._expr_code(pipes[0])
  for func in pipes[1:]:
  self._variable(func, self.all_vars)
  code = "c_%s(%s)" % (func, code)
  我们的渲染函数中的变量都加了c_前缀,下同
  第二种情况是表达式中没有 |,但是有 .。
  则以 . 作为分隔符分隔,第一部分传给 _expr_code 求值,所得结果作为 do_dots 的第一个参数。
  剩下的部分都作为 do_dots 的不定参数。
  elif "." in expr:
  dots = expr.split(".")
  code = self._expr_code(dots[0])
  args = ", ".join(repr(d) for d in dots[1:])
  code = "do_dots(%s, %s)" % (code, args)
  比如, x.y.z 会被解析成函数调用 do_dots(x, 'y', 'z')
  最后一种情况是什么都不包含。这种比较简单,直接返回带前缀的变量:
  else:
  self._variable(expr, self.all_vars)
  code = "c_%s" % expr
  return code
  工具函数
  错误处理
  def _syntax_error(self, msg, thing):
  """Raise a syntax error using `msg`, and showing `thing`."""
  raise TempliteSyntaxError("%s: %r" % (msg, thing))
  变量收集
  def _variable(self, name, vars_set):
  """Track that `name` is used as a variable.
  Adds the name to `vars_set`, a set of variable names.
  Raises an syntax error if `name` is not a valid name.
  """
  if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
  self._syntax_error("Not a valid name", name)
  vars_set.add(name)
  渲染
  前面我们已经将模板编译成了 python 代码,渲染过程就很简单了。我们要做的就是得到上下文,调用编译后的函数:
  def render(self, context=None):
  """Render this template by applying it to `context`.
  `context` is a dictionary of values to use in this rendering.
  """
  # Make the complete context we'll use.
  render_context = dict(self.context)
  if context:
  render_context.update(context)
  return self._render_function(render_context, self._do_dots)
  render 函数首先将初始传入的数据和参数进行合并得到最后的上下文数据,最后通过调用 _render_function 来得到最后的结果。
  最后,再来分析一下 _do_dots:
def _do_dots(self, value, *dots):
"""Evaluate dotted expressions at runtime."""
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
  前面说过,表达式 x.y.z 会被编译成 do_dots(x, 'y', 'z')。 下面以此为例:
  首先,将 y 作为对象 x 的一个属性尝试求值。如果失败,则将其作为一个键求值。最后,如果 y 是可调用的,则进行调用。
  然后,以得到的 value 作为对象继续进行后面的相同操作。
  TODO
  为了保持代码的精简,我们还有很多功能有待实现:
  · 模板继承和包含
  · 自定义标签
  · 自动转义
  · 过滤器参数
  · 复杂的控制逻辑如 else 和 elif
  · 超过一个循环变量的循环体
  · 空格控制

上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号