1 """A simple Python template renderer, for a nano-subset of Django syntax.""" |
1 """A simple Python template renderer, for a nano-subset of Django syntax.""" |
2 |
2 |
3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/ |
3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/ |
4 |
4 |
5 import re, sys |
5 import re |
|
6 |
|
7 from .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 |
6 |
52 |
7 class Templite(object): |
53 class Templite(object): |
8 """A simple template renderer, for a nano-subset of Django syntax. |
54 """A simple template renderer, for a nano-subset of Django syntax. |
9 |
55 |
10 Supported constructs are extended variable access:: |
56 Supported constructs are extended variable access:: |
37 self.text = text |
83 self.text = text |
38 self.context = {} |
84 self.context = {} |
39 for context in contexts: |
85 for context in contexts: |
40 self.context.update(context) |
86 self.context.update(context) |
41 |
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 |
42 # Split the text to form a list of tokens. |
111 # Split the text to form a list of tokens. |
43 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) |
112 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) |
44 |
113 |
45 # Parse the tokens into a nested list of operations. Each item in the |
|
46 # list is a tuple with an opcode, and arguments. They'll be |
|
47 # interpreted by TempliteEngine. |
|
48 # |
|
49 # When parsing an action tag with nested content (if, for), the current |
|
50 # ops list is pushed onto ops_stack, and the parsing continues in a new |
|
51 # ops list that is part of the arguments to the if or for op. |
|
52 ops = [] |
|
53 ops_stack = [] |
114 ops_stack = [] |
54 for tok in toks: |
115 for tok in toks: |
55 if tok.startswith('{{'): |
116 if tok.startswith('{{'): |
56 # Expression: ('exp', expr) |
117 # An expression to evaluate. |
57 ops.append(('exp', tok[2:-2].strip())) |
118 buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip())) |
58 elif tok.startswith('{#'): |
119 elif tok.startswith('{#'): |
59 # Comment: ignore it and move on. |
120 # Comment: ignore it and move on. |
60 continue |
121 continue |
61 elif tok.startswith('{%'): |
122 elif tok.startswith('{%'): |
62 # Action tag: split into words and parse further. |
123 # Action tag: split into words and parse further. |
|
124 flush_output() |
63 words = tok[2:-2].strip().split() |
125 words = tok[2:-2].strip().split() |
64 if words[0] == 'if': |
126 if words[0] == 'if': |
65 # If: ('if', (expr, body_ops)) |
127 # An if statement: evaluate the expression to determine if. |
66 if_ops = [] |
|
67 assert len(words) == 2 |
128 assert len(words) == 2 |
68 ops.append(('if', (words[1], if_ops))) |
129 ops_stack.append('if') |
69 ops_stack.append(ops) |
130 code.add_line("if %s:" % self.expr_code(words[1])) |
70 ops = if_ops |
131 code.indent() |
71 elif words[0] == 'for': |
132 elif words[0] == 'for': |
72 # For: ('for', (varname, listexpr, body_ops)) |
133 # A loop: iterate over expression result. |
73 assert len(words) == 4 and words[2] == 'in' |
134 assert len(words) == 4 and words[2] == 'in' |
74 for_ops = [] |
135 ops_stack.append('for') |
75 ops.append(('for', (words[1], words[3], for_ops))) |
136 self.loop_vars.add(words[1]) |
76 ops_stack.append(ops) |
137 code.add_line( |
77 ops = for_ops |
138 "for c_%s in %s:" % ( |
|
139 words[1], |
|
140 self.expr_code(words[3]) |
|
141 ) |
|
142 ) |
|
143 code.indent() |
78 elif words[0].startswith('end'): |
144 elif words[0].startswith('end'): |
79 # Endsomething. Pop the ops stack |
145 # Endsomething. Pop the ops stack |
80 ops = ops_stack.pop() |
146 end_what = words[0][3:] |
81 assert ops[-1][0] == 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() |
82 else: |
151 else: |
83 raise SyntaxError("Don't understand tag %r" % words) |
152 raise SyntaxError("Don't understand tag: %r" % words[0]) |
84 else: |
153 else: |
85 ops.append(('lit', tok)) |
154 # Literal content. If it isn't empty, output it. |
86 |
155 if tok: |
87 assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0] |
156 buffered.append("%r" % tok) |
88 self.ops = ops |
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 |
89 |
186 |
90 def render(self, context=None): |
187 def render(self, context=None): |
91 """Render this template by applying it to `context`. |
188 """Render this template by applying it to `context`. |
92 |
189 |
93 `context` is a dictionary of values to use in this rendering. |
190 `context` is a dictionary of values to use in this rendering. |
95 """ |
192 """ |
96 # Make the complete context we'll use. |
193 # Make the complete context we'll use. |
97 ctx = dict(self.context) |
194 ctx = dict(self.context) |
98 if context: |
195 if context: |
99 ctx.update(context) |
196 ctx.update(context) |
100 |
197 return self.render_function(ctx, self.do_dots) |
101 # Run it through an engine, and return the result. |
198 |
102 engine = _TempliteEngine(ctx) |
199 def do_dots(self, value, *dots): |
103 engine.execute(self.ops) |
200 """Evaluate dotted expressions at runtime.""" |
104 return engine.result |
201 for dot in dots: |
105 |
202 try: |
106 |
203 value = getattr(value, dot) |
107 class _TempliteEngine(object): |
204 except AttributeError: |
108 """Executes Templite objects to produce strings.""" |
205 value = value[dot] |
109 def __init__(self, context): |
206 if hasattr(value, '__call__'): |
110 self.context = context |
207 value = value() |
111 self.result = "" |
|
112 |
|
113 def execute(self, ops): |
|
114 """Execute `ops` in the engine. |
|
115 |
|
116 Called recursively for the bodies of if's and loops. |
|
117 |
|
118 """ |
|
119 for op, args in ops: |
|
120 if op == 'lit': |
|
121 self.result += args |
|
122 elif op == 'exp': |
|
123 try: |
|
124 self.result += str(self.evaluate(args)) |
|
125 except: |
|
126 exc_class, exc, _ = sys.exc_info() |
|
127 new_exc = exc_class("Couldn't evaluate {{ %s }}: %s" |
|
128 % (args, exc)) |
|
129 raise new_exc |
|
130 elif op == 'if': |
|
131 expr, body = args |
|
132 if self.evaluate(expr): |
|
133 self.execute(body) |
|
134 elif op == 'for': |
|
135 var, lis, body = args |
|
136 vals = self.evaluate(lis) |
|
137 for val in vals: |
|
138 self.context[var] = val |
|
139 self.execute(body) |
|
140 else: |
|
141 raise AssertionError("TempliteEngine doesn't grok op %r" % op) |
|
142 |
|
143 def evaluate(self, expr): |
|
144 """Evaluate an expression. |
|
145 |
|
146 `expr` can have pipes and dots to indicate data access and filtering. |
|
147 |
|
148 """ |
|
149 if "|" in expr: |
|
150 pipes = expr.split("|") |
|
151 value = self.evaluate(pipes[0]) |
|
152 for func in pipes[1:]: |
|
153 value = self.evaluate(func)(value) |
|
154 elif "." in expr: |
|
155 dots = expr.split('.') |
|
156 value = self.evaluate(dots[0]) |
|
157 for dot in dots[1:]: |
|
158 try: |
|
159 value = getattr(value, dot) |
|
160 except AttributeError: |
|
161 value = value[dot] |
|
162 if hasattr(value, '__call__'): |
|
163 value = value() |
|
164 else: |
|
165 value = self.context[expr] |
|
166 return value |
208 return value |