You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			167 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			167 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			Python
		
	
"""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
 |