summaryrefslogtreecommitdiff
path: root/coverage/templite.py
blob: c0c1b1da34749de32a28821edda722598c79a894 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""A simple Python template renderer, for a nano-subset of Django syntax."""

import re

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.
        stack = []
        ops = []
        for tok in toks:
            if tok.startswith('{{'):
                ops.append(('var', tok[2:-2].strip()))
            elif tok.startswith('{#'):
                continue
            elif tok.startswith('{%'):
                words = tok[2:-2].strip().split()
                if words[0] == 'if':
                    if_ops = []
                    assert len(words) == 2
                    ops.append(('if', (words[1], if_ops)))
                    stack.append(ops)
                    ops = if_ops
                elif words[0] == 'for':
                    assert len(words) == 4 and words[2] == 'in'
                    for_ops = []
                    ops.append(('for', (words[1], words[3], for_ops)))
                    stack.append(ops)
                    ops = for_ops
                elif words[0].startswith('end'):
                    ops = stack.pop()
                    assert ops[-1][0] == words[0][3:]
            else:
                ops.append(('lit', tok))
        
        assert not stack
        
        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)
            
        engine = _TempliteEngine(ctx)
        engine.execute(self.ops)
        return 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 += args
            elif op == 'var':
                self.result += str(self.evaluate(args))
            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:
                self.result += "???"

    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