17757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch"""A simple Python template renderer, for a nano-subset of Django syntax."""
27757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
37757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch# Coincidentally named the same as http://code.activestate.com/recipes/496702/
47757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
57757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochimport re, sys
67757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
77757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochclass Templite(object):
87757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """A simple template renderer, for a nano-subset of Django syntax.
97757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    Supported constructs are extended variable access::
117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        {{var.modifer.modifier|filter|filter}}
137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    loops::
157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        {% for var in list %}...{% endfor %}
177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    and ifs::
197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        {% if var %}...{% endif %}
217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    Comments are within curly-hash markers::
237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        {# This will be ignored #}
257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    Construct a Templite with the template text, then use `render` against a
277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    dictionary context to create a finished string.
287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """
307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def __init__(self, text, *contexts):
317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Construct a Templite with the given `text`.
327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        `contexts` are dictionaries of values to use for future renderings.
347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        These are good for filters and global values.
357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """
377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.text = text
387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.context = {}
397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for context in contexts:
407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.context.update(context)
417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Split the text to form a list of tokens.
437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Parse the tokens into a nested list of operations.  Each item in the
467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # list is a tuple with an opcode, and arguments.  They'll be
477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # interpreted by TempliteEngine.
487757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        #
497757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # When parsing an action tag with nested content (if, for), the current
507757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # ops list is pushed onto ops_stack, and the parsing continues in a new
517757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # ops list that is part of the arguments to the if or for op.
527757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        ops = []
537757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        ops_stack = []
547757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for tok in toks:
557757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            if tok.startswith('{{'):
567757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                # Expression: ('exp', expr)
577757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                ops.append(('exp', tok[2:-2].strip()))
587757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif tok.startswith('{#'):
597757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                # Comment: ignore it and move on.
607757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                continue
617757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif tok.startswith('{%'):
627757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                # Action tag: split into words and parse further.
637757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                words = tok[2:-2].strip().split()
647757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                if words[0] == 'if':
657757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    # If: ('if', (expr, body_ops))
667757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    if_ops = []
677757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    assert len(words) == 2
687757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops.append(('if', (words[1], if_ops)))
697757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops_stack.append(ops)
707757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops = if_ops
717757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                elif words[0] == 'for':
727757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    # For: ('for', (varname, listexpr, body_ops))
737757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    assert len(words) == 4 and words[2] == 'in'
747757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    for_ops = []
757757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops.append(('for', (words[1], words[3], for_ops)))
767757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops_stack.append(ops)
777757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops = for_ops
787757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                elif words[0].startswith('end'):
797757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    # Endsomething.  Pop the ops stack
807757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    ops = ops_stack.pop()
817757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    assert ops[-1][0] == words[0][3:]
827757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                else:
837757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    raise SyntaxError("Don't understand tag %r" % words)
847757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            else:
857757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                ops.append(('lit', tok))
867757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
877757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
887757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.ops = ops
897757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
907757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def render(self, context=None):
917757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Render this template by applying it to `context`.
927757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
937757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        `context` is a dictionary of values to use in this rendering.
947757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
957757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """
967757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Make the complete context we'll use.
977757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        ctx = dict(self.context)
987757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if context:
997757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            ctx.update(context)
1007757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1017757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Run it through an engine, and return the result.
1027757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        engine = _TempliteEngine(ctx)
1037757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        engine.execute(self.ops)
1047757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return "".join(engine.result)
1057757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1067757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1077757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochclass _TempliteEngine(object):
1087757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """Executes Templite objects to produce strings."""
1097757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def __init__(self, context):
1107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.context = context
1117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.result = []
1127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def execute(self, ops):
1147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Execute `ops` in the engine.
1157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        Called recursively for the bodies of if's and loops.
1177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """
1197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for op, args in ops:
1207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            if op == 'lit':
1217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                self.result.append(args)
1227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif op == 'exp':
1237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                try:
1247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    self.result.append(str(self.evaluate(args)))
1257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                except:
1267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    exc_class, exc, _ = sys.exc_info()
1277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    new_exc = exc_class("Couldn't evaluate {{ %s }}: %s"
1287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                                        % (args, exc))
1297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    raise new_exc
1307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif op == 'if':
1317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                expr, body = args
1327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                if self.evaluate(expr):
1337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    self.execute(body)
1347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif op == 'for':
1357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                var, lis, body = args
1367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                vals = self.evaluate(lis)
1377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                for val in vals:
1387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    self.context[var] = val
1397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    self.execute(body)
1407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            else:
1417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                raise AssertionError("TempliteEngine doesn't grok op %r" % op)
1427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def evaluate(self, expr):
1447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Evaluate an expression.
1457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        `expr` can have pipes and dots to indicate data access and filtering.
1477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1487757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """
1497757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if "|" in expr:
1507757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            pipes = expr.split("|")
1517757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            value = self.evaluate(pipes[0])
1527757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            for func in pipes[1:]:
1537757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                value = self.evaluate(func)(value)
1547757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        elif "." in expr:
1557757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            dots = expr.split('.')
1567757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            value = self.evaluate(dots[0])
1577757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            for dot in dots[1:]:
1587757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                try:
1597757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    value = getattr(value, dot)
1607757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                except AttributeError:
1617757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    value = value[dot]
1627757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                if hasattr(value, '__call__'):
1637757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    value = value()
1647757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        else:
1657757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            value = self.context[expr]
1667757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return value
167