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