OLD | NEW |
(Empty) | |
| 1 """A simple Python template renderer, for a nano-subset of Django syntax.""" |
| 2 |
| 3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/ |
| 4 |
| 5 import re |
| 6 |
| 7 from coverage.backward import set # pylint: disable=W0622 |
| 8 |
| 9 |
| 10 class CodeBuilder(object): |
| 11 """Build source code conveniently.""" |
| 12 |
| 13 def __init__(self, indent=0): |
| 14 self.code = [] |
| 15 self.indent_amount = indent |
| 16 |
| 17 def add_line(self, line): |
| 18 """Add a line of source to the code. |
| 19 |
| 20 Don't include indentations or newlines. |
| 21 |
| 22 """ |
| 23 self.code.append(" " * self.indent_amount) |
| 24 self.code.append(line) |
| 25 self.code.append("\n") |
| 26 |
| 27 def add_section(self): |
| 28 """Add a section, a sub-CodeBuilder.""" |
| 29 sect = CodeBuilder(self.indent_amount) |
| 30 self.code.append(sect) |
| 31 return sect |
| 32 |
| 33 def indent(self): |
| 34 """Increase the current indent for following lines.""" |
| 35 self.indent_amount += 4 |
| 36 |
| 37 def dedent(self): |
| 38 """Decrease the current indent for following lines.""" |
| 39 self.indent_amount -= 4 |
| 40 |
| 41 def __str__(self): |
| 42 return "".join([str(c) for c in self.code]) |
| 43 |
| 44 def get_function(self, fn_name): |
| 45 """Compile the code, and return the function `fn_name`.""" |
| 46 assert self.indent_amount == 0 |
| 47 g = {} |
| 48 code_text = str(self) |
| 49 exec(code_text, g) |
| 50 return g[fn_name] |
| 51 |
| 52 |
| 53 class Templite(object): |
| 54 """A simple template renderer, for a nano-subset of Django syntax. |
| 55 |
| 56 Supported constructs are extended variable access:: |
| 57 |
| 58 {{var.modifer.modifier|filter|filter}} |
| 59 |
| 60 loops:: |
| 61 |
| 62 {% for var in list %}...{% endfor %} |
| 63 |
| 64 and ifs:: |
| 65 |
| 66 {% if var %}...{% endif %} |
| 67 |
| 68 Comments are within curly-hash markers:: |
| 69 |
| 70 {# This will be ignored #} |
| 71 |
| 72 Construct a Templite with the template text, then use `render` against a |
| 73 dictionary context to create a finished string. |
| 74 |
| 75 """ |
| 76 def __init__(self, text, *contexts): |
| 77 """Construct a Templite with the given `text`. |
| 78 |
| 79 `contexts` are dictionaries of values to use for future renderings. |
| 80 These are good for filters and global values. |
| 81 |
| 82 """ |
| 83 self.text = text |
| 84 self.context = {} |
| 85 for context in contexts: |
| 86 self.context.update(context) |
| 87 |
| 88 # We construct a function in source form, then compile it and hold onto |
| 89 # it, and execute it to render the template. |
| 90 code = CodeBuilder() |
| 91 |
| 92 code.add_line("def render(ctx, dot):") |
| 93 code.indent() |
| 94 vars_code = code.add_section() |
| 95 self.all_vars = set() |
| 96 self.loop_vars = set() |
| 97 code.add_line("result = []") |
| 98 code.add_line("a = result.append") |
| 99 code.add_line("e = result.extend") |
| 100 code.add_line("s = str") |
| 101 |
| 102 buffered = [] |
| 103 def flush_output(): |
| 104 """Force `buffered` to the code builder.""" |
| 105 if len(buffered) == 1: |
| 106 code.add_line("a(%s)" % buffered[0]) |
| 107 elif len(buffered) > 1: |
| 108 code.add_line("e([%s])" % ",".join(buffered)) |
| 109 del buffered[:] |
| 110 |
| 111 # Split the text to form a list of tokens. |
| 112 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) |
| 113 |
| 114 ops_stack = [] |
| 115 for tok in toks: |
| 116 if tok.startswith('{{'): |
| 117 # An expression to evaluate. |
| 118 buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip())) |
| 119 elif tok.startswith('{#'): |
| 120 # Comment: ignore it and move on. |
| 121 continue |
| 122 elif tok.startswith('{%'): |
| 123 # Action tag: split into words and parse further. |
| 124 flush_output() |
| 125 words = tok[2:-2].strip().split() |
| 126 if words[0] == 'if': |
| 127 # An if statement: evaluate the expression to determine if. |
| 128 assert len(words) == 2 |
| 129 ops_stack.append('if') |
| 130 code.add_line("if %s:" % self.expr_code(words[1])) |
| 131 code.indent() |
| 132 elif words[0] == 'for': |
| 133 # A loop: iterate over expression result. |
| 134 assert len(words) == 4 and words[2] == 'in' |
| 135 ops_stack.append('for') |
| 136 self.loop_vars.add(words[1]) |
| 137 code.add_line( |
| 138 "for c_%s in %s:" % ( |
| 139 words[1], |
| 140 self.expr_code(words[3]) |
| 141 ) |
| 142 ) |
| 143 code.indent() |
| 144 elif words[0].startswith('end'): |
| 145 # Endsomething. Pop the ops stack |
| 146 end_what = words[0][3:] |
| 147 if ops_stack[-1] != end_what: |
| 148 raise SyntaxError("Mismatched end tag: %r" % end_what) |
| 149 ops_stack.pop() |
| 150 code.dedent() |
| 151 else: |
| 152 raise SyntaxError("Don't understand tag: %r" % words[0]) |
| 153 else: |
| 154 # Literal content. If it isn't empty, output it. |
| 155 if tok: |
| 156 buffered.append("%r" % tok) |
| 157 flush_output() |
| 158 |
| 159 for var_name in self.all_vars - self.loop_vars: |
| 160 vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name)) |
| 161 |
| 162 if ops_stack: |
| 163 raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1]) |
| 164 |
| 165 code.add_line("return ''.join(result)") |
| 166 code.dedent() |
| 167 self.render_function = code.get_function('render') |
| 168 |
| 169 def expr_code(self, expr): |
| 170 """Generate a Python expression for `expr`.""" |
| 171 if "|" in expr: |
| 172 pipes = expr.split("|") |
| 173 code = self.expr_code(pipes[0]) |
| 174 for func in pipes[1:]: |
| 175 self.all_vars.add(func) |
| 176 code = "c_%s(%s)" % (func, code) |
| 177 elif "." in expr: |
| 178 dots = expr.split(".") |
| 179 code = self.expr_code(dots[0]) |
| 180 args = [repr(d) for d in dots[1:]] |
| 181 code = "dot(%s, %s)" % (code, ", ".join(args)) |
| 182 else: |
| 183 self.all_vars.add(expr) |
| 184 code = "c_%s" % expr |
| 185 return code |
| 186 |
| 187 def render(self, context=None): |
| 188 """Render this template by applying it to `context`. |
| 189 |
| 190 `context` is a dictionary of values to use in this rendering. |
| 191 |
| 192 """ |
| 193 # Make the complete context we'll use. |
| 194 ctx = dict(self.context) |
| 195 if context: |
| 196 ctx.update(context) |
| 197 return self.render_function(ctx, self.do_dots) |
| 198 |
| 199 def do_dots(self, value, *dots): |
| 200 """Evaluate dotted expressions at runtime.""" |
| 201 for dot in dots: |
| 202 try: |
| 203 value = getattr(value, dot) |
| 204 except AttributeError: |
| 205 value = value[dot] |
| 206 if hasattr(value, '__call__'): |
| 207 value = value() |
| 208 return value |
OLD | NEW |