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
5import re, sys
6
7class Templite(object):
8    """A simple template renderer, for a nano-subset of Django syntax.
9
10    Supported constructs are extended variable access::
11
12        {{var.modifer.modifier|filter|filter}}
13
14    loops::
15
16        {% for var in list %}...{% endfor %}
17
18    and ifs::
19
20        {% if var %}...{% endif %}
21
22    Comments are within curly-hash markers::
23
24        {# This will be ignored #}
25
26    Construct a Templite with the template text, then use `render` against a
27    dictionary context to create a finished string.
28
29    """
30    def __init__(self, text, *contexts):
31        """Construct a Templite with the given `text`.
32
33        `contexts` are dictionaries of values to use for future renderings.
34        These are good for filters and global values.
35
36        """
37        self.text = text
38        self.context = {}
39        for context in contexts:
40            self.context.update(context)
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
90    def render(self, context=None):
91        """Render this template by applying it to `context`.
92
93        `context` is a dictionary of values to use in this rendering.
94
95        """
96        # Make the complete context we'll use.
97        ctx = dict(self.context)
98        if context:
99            ctx.update(context)
100
101        # Run it through an engine, and return the result.
102        engine = _TempliteEngine(ctx)
103        engine.execute(self.ops)
104        return "".join(engine.result)
105
106
107class _TempliteEngine(object):
108    """Executes Templite objects to produce strings."""
109    def __init__(self, context):
110        self.context = context
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.append(args)
122            elif op == 'exp':
123                try:
124                    self.result.append(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
167