1 """A simple Python template renderer, for a nano-subset of Django syntax."""
3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
7 from coverage
.backward
import set # pylint: disable=W0622
10 class CodeBuilder(object):
11 """Build source code conveniently."""
13 def __init__(self
, indent
=0):
15 self
.indent_amount
= indent
17 def add_line(self
, line
):
18 """Add a line of source to the code.
20 Don't include indentations or newlines.
23 self
.code
.append(" " * self
.indent_amount
)
24 self
.code
.append(line
)
25 self
.code
.append("\n")
27 def add_section(self
):
28 """Add a section, a sub-CodeBuilder."""
29 sect
= CodeBuilder(self
.indent_amount
)
30 self
.code
.append(sect
)
34 """Increase the current indent for following lines."""
35 self
.indent_amount
+= 4
38 """Decrease the current indent for following lines."""
39 self
.indent_amount
-= 4
42 return "".join([str(c
) for c
in self
.code
])
44 def get_function(self
, fn_name
):
45 """Compile the code, and return the function `fn_name`."""
46 assert self
.indent_amount
== 0
53 class Templite(object):
54 """A simple template renderer, for a nano-subset of Django syntax.
56 Supported constructs are extended variable access::
58 {{var.modifer.modifier|filter|filter}}
62 {% for var in list %}...{% endfor %}
66 {% if var %}...{% endif %}
68 Comments are within curly-hash markers::
70 {# This will be ignored #}
72 Construct a Templite with the template text, then use `render` against a
73 dictionary context to create a finished string.
76 def __init__(self
, text
, *contexts
):
77 """Construct a Templite with the given `text`.
79 `contexts` are dictionaries of values to use for future renderings.
80 These are good for filters and global values.
85 for context
in contexts
:
86 self
.context
.update(context
)
88 # We construct a function in source form, then compile it and hold onto
89 # it, and execute it to render the template.
92 code
.add_line("def render(ctx, dot):")
94 vars_code
= code
.add_section()
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")
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
))
111 # Split the text to form a list of tokens.
112 toks
= re
.split(r
"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text
)
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.
122 elif tok
.startswith('{%'):
123 # Action tag: split into words and parse further.
125 words
= tok
[2:-2].strip().split()
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]))
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])
138 "for c_%s in %s:" % (
140 self
.expr_code(words
[3])
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
)
152 raise SyntaxError("Don't understand tag: %r" % words
[0])
154 # Literal content. If it isn't empty, output it.
156 buffered
.append("%r" % tok
)
159 for var_name
in self
.all_vars
- self
.loop_vars
:
160 vars_code
.add_line("c_%s = ctx[%r]" % (var_name
, var_name
))
163 raise SyntaxError("Unmatched action tag: %r" % ops_stack
[-1])
165 code
.add_line("return ''.join(result)")
167 self
.render_function
= code
.get_function('render')
169 def expr_code(self
, expr
):
170 """Generate a Python expression for `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
)
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
))
183 self
.all_vars
.add(expr
)
187 def render(self
, context
=None):
188 """Render this template by applying it to `context`.
190 `context` is a dictionary of values to use in this rendering.
193 # Make the complete context we'll use.
194 ctx
= dict(self
.context
)
197 return self
.render_function(ctx
, self
.do_dots
)
199 def do_dots(self
, value
, *dots
):
200 """Evaluate dotted expressions at runtime."""
203 value
= getattr(value
, dot
)
204 except AttributeError:
206 if hasattr(value
, '__call__'):