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 # Started from http://blog.ianbicking.org/templating-via-dict-wrappers.html |
3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/ |
4 # and http://jtauber.com/2006/05/templates.html |
|
5 # and http://code.activestate.com/recipes/496730/ |
|
6 |
4 |
7 import re |
5 import re, sys |
8 |
6 |
9 class Templite(object): |
7 class Templite(object): |
10 """A simple template renderer, for a nano-subset of Django syntax. |
8 """A simple template renderer, for a nano-subset of Django syntax. |
11 |
9 |
12 Supported constructs are extended variable access:: |
10 Supported constructs are extended variable access:: |
13 |
11 |
14 {{var.modifer.modifier|filter|filter}} |
12 {{var.modifer.modifier|filter|filter}} |
15 |
13 |
16 and loops:: |
14 loops:: |
17 |
15 |
18 {% for var in list %}...{% endfor %} |
16 {% for var in list %}...{% endfor %} |
19 |
17 |
|
18 and ifs:: |
|
19 |
|
20 {% if var %}...{% endif %} |
|
21 |
|
22 Comments are within curly-hash markers:: |
|
23 |
|
24 {# This will be ignored #} |
|
25 |
20 Construct a Templite with the template text, then use `render` against a |
26 Construct a Templite with the template text, then use `render` against a |
21 dictionary context to create a finished string. |
27 dictionary context to create a finished string. |
22 |
28 |
23 """ |
29 """ |
24 def __init__(self, text, *contexts): |
30 def __init__(self, text, *contexts): |
25 """Construct a Templite with the given `text`. |
31 """Construct a Templite with the given `text`. |
26 |
32 |
27 `contexts` are dictionaries of values to use for future renderings. |
33 `contexts` are dictionaries of values to use for future renderings. |
28 These are good for filters and global values. |
34 These are good for filters and global values. |
29 |
35 |
30 """ |
36 """ |
31 self.loops = [] |
37 self.text = text |
32 self.text = self._prepare(text) |
|
33 self.context = {} |
38 self.context = {} |
34 for context in contexts: |
39 for context in contexts: |
35 self.context.update(context) |
40 self.context.update(context) |
36 |
41 |
|
42 # Split the text to form a list of tokens. |
|
43 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) |
|
44 |
|
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 = [] |
|
54 for tok in toks: |
|
55 if tok.startswith('{{'): |
|
56 # Expression: ('exp', expr) |
|
57 ops.append(('exp', tok[2:-2].strip())) |
|
58 elif tok.startswith('{#'): |
|
59 # Comment: ignore it and move on. |
|
60 continue |
|
61 elif tok.startswith('{%'): |
|
62 # Action tag: split into words and parse further. |
|
63 words = tok[2:-2].strip().split() |
|
64 if words[0] == 'if': |
|
65 # If: ('if', (expr, body_ops)) |
|
66 if_ops = [] |
|
67 assert len(words) == 2 |
|
68 ops.append(('if', (words[1], if_ops))) |
|
69 ops_stack.append(ops) |
|
70 ops = if_ops |
|
71 elif words[0] == 'for': |
|
72 # For: ('for', (varname, listexpr, body_ops)) |
|
73 assert len(words) == 4 and words[2] == 'in' |
|
74 for_ops = [] |
|
75 ops.append(('for', (words[1], words[3], for_ops))) |
|
76 ops_stack.append(ops) |
|
77 ops = for_ops |
|
78 elif words[0].startswith('end'): |
|
79 # Endsomething. Pop the ops stack |
|
80 ops = ops_stack.pop() |
|
81 assert ops[-1][0] == words[0][3:] |
|
82 else: |
|
83 raise SyntaxError("Don't understand tag %r" % words) |
|
84 else: |
|
85 ops.append(('lit', tok)) |
|
86 |
|
87 assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0] |
|
88 self.ops = ops |
|
89 |
37 def render(self, context=None): |
90 def render(self, context=None): |
38 """Render this template by applying it to `context`. |
91 """Render this template by applying it to `context`. |
39 |
92 |
40 `context` is a dictionary of values to use in this rendering. |
93 `context` is a dictionary of values to use in this rendering. |
41 |
94 |
42 """ |
95 """ |
43 # Make the complete context we'll use. |
96 # Make the complete context we'll use. |
44 ctx = dict(self.context) |
97 ctx = dict(self.context) |
45 if context: |
98 if context: |
46 ctx.update(context) |
99 ctx.update(context) |
47 |
|
48 ctxaccess = _ContextAccess(ctx) |
|
49 |
|
50 # Render the loops. |
|
51 for iloop, (loopvar, listvar, loopbody) in enumerate(self.loops): |
|
52 result = "" |
|
53 for listval in ctxaccess[listvar]: |
|
54 ctx[loopvar] = listval |
|
55 result += loopbody % ctxaccess |
|
56 ctx["loop:%d" % iloop] = result |
|
57 |
|
58 # Render the final template. |
|
59 return self.text % ctxaccess |
|
60 |
100 |
61 def _prepare(self, text): |
101 # Run it through an engine, and return the result. |
62 """Convert Django-style data references into Python-native ones.""" |
102 engine = _TempliteEngine(ctx) |
63 # Pull out loops. |
103 engine.execute(self.ops) |
64 text = re.sub( |
104 return engine.result |
65 r"(?s){% for ([a-z0-9_]+) in ([a-z0-9_.|]+) %}(.*?){% endfor %}", |
|
66 self._loop_prepare, text |
|
67 ) |
|
68 # Protect actual percent signs in the text. |
|
69 text = text.replace("%", "%%") |
|
70 # Convert {{foo}} into %(foo)s |
|
71 text = re.sub(r"{{([^}]+)}}", r"%(\1)s", text) |
|
72 return text |
|
73 |
|
74 def _loop_prepare(self, match): |
|
75 """Prepare a loop body for `_prepare`.""" |
|
76 nloop = len(self.loops) |
|
77 # Append (loopvar, listvar, loopbody) to self.loops |
|
78 loopvar, listvar, loopbody = match.groups() |
|
79 loopbody = self._prepare(loopbody) |
|
80 self.loops.append((loopvar, listvar, loopbody)) |
|
81 return "{{loop:%d}}" % nloop |
|
82 |
105 |
83 |
106 |
84 class _ContextAccess(object): |
107 class _TempliteEngine(object): |
85 """A mediator for a context. |
108 """Executes Templite objects to produce strings.""" |
86 |
|
87 Implements __getitem__ on a context for Templite, so that string formatting |
|
88 references can pull data from the context. |
|
89 |
|
90 """ |
|
91 def __init__(self, context): |
109 def __init__(self, context): |
92 self.context = context |
110 self.context = context |
|
111 self.result = "" |
93 |
112 |
94 def __getitem__(self, key): |
113 def execute(self, ops): |
95 if "|" in key: |
114 """Execute `ops` in the engine. |
96 pipes = key.split("|") |
115 |
97 value = self[pipes[0]] |
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]) |
98 for func in pipes[1:]: |
152 for func in pipes[1:]: |
99 value = self[func](value) |
153 value = self.evaluate(func)(value) |
100 elif "." in key: |
154 elif "." in expr: |
101 dots = key.split('.') |
155 dots = expr.split('.') |
102 value = self[dots[0]] |
156 value = self.evaluate(dots[0]) |
103 for dot in dots[1:]: |
157 for dot in dots[1:]: |
104 try: |
158 try: |
105 value = getattr(value, dot) |
159 value = getattr(value, dot) |
106 except AttributeError: |
160 except AttributeError: |
107 value = value[dot] |
161 value = value[dot] |
108 if hasattr(value, '__call__'): |
162 if hasattr(value, '__call__'): |
109 value = value() |
163 value = value() |
110 else: |
164 else: |
111 value = self.context[key] |
165 value = self.context[expr] |
112 return value |
166 return value |