| """A simple Python template renderer, for a nano-subset of Django syntax.""" | 
 |  | 
 | # Coincidentally named the same as http://code.activestate.com/recipes/496702/ | 
 |  | 
 | import re, sys | 
 |  | 
 | class Templite(object): | 
 |     """A simple template renderer, for a nano-subset of Django syntax. | 
 |  | 
 |     Supported constructs are extended variable access:: | 
 |  | 
 |         {{var.modifer.modifier|filter|filter}} | 
 |  | 
 |     loops:: | 
 |  | 
 |         {% for var in list %}...{% endfor %} | 
 |  | 
 |     and ifs:: | 
 |  | 
 |         {% if var %}...{% endif %} | 
 |  | 
 |     Comments are within curly-hash markers:: | 
 |  | 
 |         {# This will be ignored #} | 
 |  | 
 |     Construct a Templite with the template text, then use `render` against a | 
 |     dictionary context to create a finished string. | 
 |  | 
 |     """ | 
 |     def __init__(self, text, *contexts): | 
 |         """Construct a Templite with the given `text`. | 
 |  | 
 |         `contexts` are dictionaries of values to use for future renderings. | 
 |         These are good for filters and global values. | 
 |  | 
 |         """ | 
 |         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.  Each item in the | 
 |         # list is a tuple with an opcode, and arguments.  They'll be | 
 |         # interpreted by TempliteEngine. | 
 |         # | 
 |         # When parsing an action tag with nested content (if, for), the current | 
 |         # ops list is pushed onto ops_stack, and the parsing continues in a new | 
 |         # ops list that is part of the arguments to the if or for op. | 
 |         ops = [] | 
 |         ops_stack = [] | 
 |         for tok in toks: | 
 |             if tok.startswith('{{'): | 
 |                 # Expression: ('exp', expr) | 
 |                 ops.append(('exp', tok[2:-2].strip())) | 
 |             elif tok.startswith('{#'): | 
 |                 # Comment: ignore it and move on. | 
 |                 continue | 
 |             elif tok.startswith('{%'): | 
 |                 # Action tag: split into words and parse further. | 
 |                 words = tok[2:-2].strip().split() | 
 |                 if words[0] == 'if': | 
 |                     # If: ('if', (expr, body_ops)) | 
 |                     if_ops = [] | 
 |                     assert len(words) == 2 | 
 |                     ops.append(('if', (words[1], if_ops))) | 
 |                     ops_stack.append(ops) | 
 |                     ops = if_ops | 
 |                 elif words[0] == 'for': | 
 |                     # For: ('for', (varname, listexpr, body_ops)) | 
 |                     assert len(words) == 4 and words[2] == 'in' | 
 |                     for_ops = [] | 
 |                     ops.append(('for', (words[1], words[3], for_ops))) | 
 |                     ops_stack.append(ops) | 
 |                     ops = for_ops | 
 |                 elif words[0].startswith('end'): | 
 |                     # Endsomething.  Pop the ops stack | 
 |                     ops = ops_stack.pop() | 
 |                     assert ops[-1][0] == words[0][3:] | 
 |                 else: | 
 |                     raise SyntaxError("Don't understand tag %r" % words) | 
 |             else: | 
 |                 ops.append(('lit', tok)) | 
 |  | 
 |         assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0] | 
 |         self.ops = ops | 
 |  | 
 |     def render(self, context=None): | 
 |         """Render this template by applying it to `context`. | 
 |  | 
 |         `context` is a dictionary of values to use in this rendering. | 
 |  | 
 |         """ | 
 |         # Make the complete context we'll use. | 
 |         ctx = dict(self.context) | 
 |         if context: | 
 |             ctx.update(context) | 
 |  | 
 |         # Run it through an engine, and return the result. | 
 |         engine = _TempliteEngine(ctx) | 
 |         engine.execute(self.ops) | 
 |         return "".join(engine.result) | 
 |  | 
 |  | 
 | class _TempliteEngine(object): | 
 |     """Executes Templite objects to produce strings.""" | 
 |     def __init__(self, context): | 
 |         self.context = context | 
 |         self.result = [] | 
 |  | 
 |     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.append(args) | 
 |             elif op == 'exp': | 
 |                 try: | 
 |                     self.result.append(str(self.evaluate(args))) | 
 |                 except: | 
 |                     exc_class, exc, _ = sys.exc_info() | 
 |                     new_exc = exc_class("Couldn't evaluate {{ %s }}: %s" | 
 |                                         % (args, exc)) | 
 |                     raise new_exc | 
 |             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: | 
 |                 raise AssertionError("TempliteEngine doesn't grok op %r" % op) | 
 |  | 
 |     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.evaluate(func)(value) | 
 |         elif "." in expr: | 
 |             dots = expr.split('.') | 
 |             value = self.evaluate(dots[0]) | 
 |             for dot in dots[1:]: | 
 |                 try: | 
 |                     value = getattr(value, dot) | 
 |                 except AttributeError: | 
 |                     value = value[dot] | 
 |                 if hasattr(value, '__call__'): | 
 |                     value = value() | 
 |         else: | 
 |             value = self.context[expr] | 
 |         return value |