当前位置: 首页 > 工具软件 > LESS Engine > 使用案例 >

500 lines or less-A Template Engine 修改版代码

谷梁嘉悦
2023-12-01

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()
 类似资料: