500 lines or less中的模板引擎代码
原文
http://aosabook.org/en/500L/a-template-engine.html
翻译
https://www.jianshu.com/p/b5d4aa45e771
我在原有基础上删掉了错误处理跟优化的代码,提取出了一个 _parse_template函数,方便理解
import re
def log(*arg, **kwarg):
print('log: ', *arg, **kwarg)
class CodeBuilder:
INDENT_STEP = 4
def __init__(self, indent=0):
self.code = []
self.indent_level = indent
def add_line(self, line):
self.code.extend([
' ' * self.indent_level,
line,
'\n',
])
def indent(self):
self.indent_level += self.INDENT_STEP
def dedent(self):
self.indent_level -= self.INDENT_STEP
def add_section(self):
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section
def __str__(self):
return ''.join(str(c) for c in self.code)
def get_globals(self):
assert self.indent_level == 0
python_src = str(self)
global_namespace = {}
# log('\n', python_src)
exec(python_src, global_namespace)
return global_namespace
class Template:
def __init__(self, text, *contexts):
self.context = {}
for context in contexts:
self.context.update(context)
self.all_vars = set()
self.loop_vars = set()
code = CodeBuilder()
code.add_line('def render_function(context, do_dots):')
code.indent()
vars_code = code.add_section()
code.add_line('result = []')
self._parse_template(code, text)
for var_name in self.all_vars - self.loop_vars:
vars_code.add_line('c_{} = context[{}]'.format(var_name, repr(var_name)))
code.add_line("return ''.join(result)")
code.dedent()
self._render_function = code.get_globals()['render_function']
def _parse_template(self, code, text):
tokens = re.split(r'(?s)({{.*?}}|{%.*?%}|{#.*?#})', text)
for token in tokens:
if token.startswith('{#'):
continue
elif token.startswith('{{'):
expr = self._expr_code(token[2:-2].strip())
item = 'str({})'.format(expr)
code.add_line('result.append({})'.format(item))
elif token.startswith('{%'):
words = token[2:-2].strip().split()
if words[0] == 'if':
code.add_line('if {}:'.format(self._expr_code(words[1])))
code.indent()
elif words[0] == 'for':
code.add_line('for c_{} in {}:'.format(words[1], self._expr_code(words[3])))
self._variable(words[1], self.loop_vars)
code.indent()
elif words[0].startswith('end'):
code.dedent()
else:
if token:
code.add_line('result.append({})'.format(repr(token)))
def _expr_code(self, expr):
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_{}({})'.format(func, code)
elif '.' in expr:
dots = expr.split('.')
code = self._expr_code(dots[0])
args = ', '.join(repr(d) for d in dots[1:])
code = 'do_dots({}, {})'.format(code, args)
else:
self._variable(expr, self.all_vars)
code = 'c_{}'.format(expr)
return code
def _variable(self, name, vars_set):
vars_set.add(name)
def render(self, context=None):
render_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, _do_dots)
def _do_dots(value, *dots):
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
def test():
template = Template('''
<h1>Hello {{name|upper}}!</h1>
{% for topic in topics %}
<p>You are interested in {{topic}}.</p>
{% endfor %}
''',
{'upper': str.upper},
)
text = template.render({
'name': "Ned",
'topics': ['Python', 'Geometry', 'Juggling'],
})
log('\n', text)
if __name__ == '__main__':
test()