summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/templite.py147
-rw-r--r--test/test_templite.py59
2 files changed, 144 insertions, 62 deletions
diff --git a/coverage/templite.py b/coverage/templite.py
index 3b2efebd..c0c1b1da 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -1,10 +1,5 @@
"""A simple Python template renderer, for a nano-subset of Django syntax."""
-# Started from http://blog.ianbicking.org/templating-via-dict-wrappers.html
-# and http://jtauber.com/2006/05/templates.html
-# and http://code.activestate.com/recipes/496730/
-# Coincidentally named the same as http://code.activestate.com/recipes/496702/
-
import re
class Templite(object):
@@ -14,10 +9,14 @@ class Templite(object):
{{var.modifer.modifier|filter|filter}}
- and loops::
+ loops::
{% for var in list %}...{% endfor %}
+ and ifs::
+
+ {% if var %}...{% endif %}
+
Comments are within curly-hash markers::
{# This will be ignored #}
@@ -33,11 +32,47 @@ class Templite(object):
These are good for filters and global values.
"""
- self.loops = []
- self.text = self._prepare(text)
+ self.text = text
self.context = {}
for context in contexts:
self.context.update(context)
+
+ # Split the text to form a list of tokens.
+ toks = re.split(
+ r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text
+ )
+
+ # Parse the tokens into a nested list of operations.
+ stack = []
+ ops = []
+ for tok in toks:
+ if tok.startswith('{{'):
+ ops.append(('var', tok[2:-2].strip()))
+ elif tok.startswith('{#'):
+ continue
+ elif tok.startswith('{%'):
+ words = tok[2:-2].strip().split()
+ if words[0] == 'if':
+ if_ops = []
+ assert len(words) == 2
+ ops.append(('if', (words[1], if_ops)))
+ stack.append(ops)
+ ops = if_ops
+ elif words[0] == 'for':
+ assert len(words) == 4 and words[2] == 'in'
+ for_ops = []
+ ops.append(('for', (words[1], words[3], for_ops)))
+ stack.append(ops)
+ ops = for_ops
+ elif words[0].startswith('end'):
+ ops = stack.pop()
+ assert ops[-1][0] == words[0][3:]
+ else:
+ ops.append(('lit', tok))
+
+ assert not stack
+
+ self.ops = ops
def render(self, context=None):
"""Render this template by applying it to `context`.
@@ -50,63 +85,55 @@ class Templite(object):
if context:
ctx.update(context)
- ctxaccess = _ContextAccess(ctx)
-
- # Render the loops.
- for iloop, (loopvar, listvar, loopbody) in enumerate(self.loops):
- result = ""
- for listval in ctxaccess[listvar]:
- ctx[loopvar] = listval
- result += loopbody % ctxaccess
- ctx["loop:%d" % iloop] = result
-
- # Render the final template.
- return self.text % ctxaccess
-
- def _prepare(self, text):
- """Convert Django-style data references into Python-native ones."""
- # Remove comments.
- text = re.sub(r"(?s){#.*?#}", "", text)
- # Pull out loops.
- text = re.sub(
- r"(?s){% for ([a-z0-9_]+) in ([a-z0-9_.|]+) %}(.*?){% endfor %}",
- self._loop_prepare, text
- )
- # Protect actual percent signs in the text.
- text = text.replace("%", "%%")
- # Convert {{foo}} into %(foo)s
- text = re.sub(r"{{(.+?)}}", r"%(\1)s", text)
- return text
+ engine = _TempliteEngine(ctx)
+ engine.execute(self.ops)
+ return engine.result
- def _loop_prepare(self, match):
- """Prepare a loop body for `_prepare`."""
- nloop = len(self.loops)
- # Append (loopvar, listvar, loopbody) to self.loops
- loopvar, listvar, loopbody = match.groups()
- loopbody = self._prepare(loopbody)
- self.loops.append((loopvar, listvar, loopbody))
- return "{{loop:%d}}" % nloop
-
-class _ContextAccess(object):
- """A mediator for a context.
-
- Implements __getitem__ on a context for Templite, so that string formatting
- references can pull data from the context.
-
- """
+class _TempliteEngine(object):
+ """Executes Templite objects to produce strings."""
def __init__(self, context):
self.context = context
+ self.result = ""
- def __getitem__(self, key):
- if "|" in key:
- pipes = key.split("|")
- value = self[pipes[0]]
+ def execute(self, ops):
+ """Execute `ops` in the engine.
+
+ Called recursively for the bodies of if's and loops.
+
+ """
+ for op, args in ops:
+ if op == 'lit':
+ self.result += args
+ elif op == 'var':
+ self.result += str(self.evaluate(args))
+ elif op == 'if':
+ expr, body = args
+ if self.evaluate(expr):
+ self.execute(body)
+ elif op == 'for':
+ var, lis, body = args
+ vals = self.evaluate(lis)
+ for val in vals:
+ self.context[var] = val
+ self.execute(body)
+ else:
+ self.result += "???"
+
+ def evaluate(self, expr):
+ """Evaluate an expression.
+
+ `expr` can have pipes and dots to indicate data access and filtering.
+
+ """
+ if "|" in expr:
+ pipes = expr.split("|")
+ value = self.evaluate(pipes[0])
for func in pipes[1:]:
- value = self[func](value)
- elif "." in key:
- dots = key.split('.')
- value = self[dots[0]]
+ value = self.evaluate(func)(value)
+ elif "." in expr:
+ dots = expr.split('.')
+ value = self.evaluate(dots[0])
for dot in dots[1:]:
try:
value = getattr(value, dot)
@@ -115,5 +142,5 @@ class _ContextAccess(object):
if hasattr(value, '__call__'):
value = value()
else:
- value = self.context[key]
+ value = self.context[expr]
return value
diff --git a/test/test_templite.py b/test/test_templite.py
index 4c7545bf..8cb25865 100644
--- a/test/test_templite.py
+++ b/test/test_templite.py
@@ -130,12 +130,67 @@ class TempliteTest(unittest.TestCase):
# Single-line comments work:
self.try_render(
"Hello, {# Name goes here: #}{{name}}!",
- {'name':'Ned'}, "Hello, Ned!")
+ {'name':'Ned'}, "Hello, Ned!"
+ )
# and so do multi-line comments:
self.try_render(
"Hello, {# Name\ngoes\nhere: #}{{name}}!",
- {'name':'Ned'}, "Hello, Ned!")
+ {'name':'Ned'}, "Hello, Ned!"
+ )
+ def test_if(self):
+ self.try_render(
+ "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!",
+ {'ned': 1, 'ben': 0},
+ "Hi, NED!"
+ )
+ self.try_render(
+ "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!",
+ {'ned': 0, 'ben': 1},
+ "Hi, BEN!"
+ )
+ self.try_render(
+ "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!",
+ {'ned': 0, 'ben': 0},
+ "Hi, !"
+ )
+ self.try_render(
+ "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!",
+ {'ned': 1, 'ben': 0},
+ "Hi, NED!"
+ )
+ self.try_render(
+ "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!",
+ {'ned': 1, 'ben': 1},
+ "Hi, NEDBEN!"
+ )
+ def test_loop_if(self):
+ self.try_render(
+ "@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!",
+ {'nums': [0,1,2]},
+ "@0Z1Z2!"
+ )
+ self.try_render(
+ "X{%if nums%}@{% for n in nums %}{{n}}{% endfor %}{%endif%}!",
+ {'nums': [0,1,2]},
+ "X@012!"
+ )
+ self.try_render(
+ "X{%if nums%}@{% for n in nums %}{{n}}{% endfor %}{%endif%}!",
+ {'nums': []},
+ "X!"
+ )
+
+ def test_nested_loops(self):
+ self.try_render(
+ "@{% for n in nums %}"
+ "{% for a in abc %}{{a}}{{n}}{% endfor %}"
+ "{% endfor %}!",
+ {'nums': [0,1,2], 'abc': ['a', 'b', 'c']},
+ "@a0b0c0a1b1c1a2b2c2!"
+ )
+
+
if __name__ == '__main__':
unittest.main()