|
1 """A simple Python template renderer, for a nano-subset of Django syntax.""" |
|
2 |
|
3 # Started from http://blog.ianbicking.org/templating-via-dict-wrappers.html |
|
4 # and http://jtauber.com/2006/05/templates.html |
|
5 # and http://code.activestate.com/recipes/496730/ |
|
6 |
|
7 import re |
|
8 |
|
9 class Templite(object): |
|
10 """A simple template renderer, for a nano-subset of Django syntax. |
|
11 |
|
12 Supported constructs are extended variable access:: |
|
13 |
|
14 {{var.modifer.modifier|filter|filter}} |
|
15 |
|
16 and loops:: |
|
17 |
|
18 {% for var in list %}...{% endfor %} |
|
19 |
|
20 Construct a Templite with the template text, then use `render` against a |
|
21 dictionary context to create a finished string. |
|
22 |
|
23 """ |
|
24 def __init__(self, text, *contexts): |
|
25 """Construct a Templite with the given `text`. |
|
26 |
|
27 `contexts` are dictionaries of values to use for future renderings. |
|
28 These are good for filters and global values. |
|
29 |
|
30 """ |
|
31 self.loops = [] |
|
32 self.text = self._prepare(text) |
|
33 self.context = {} |
|
34 for context in contexts: |
|
35 self.context.update(context) |
|
36 |
|
37 def render(self, context=None): |
|
38 """Render this template by applying it to `context`. |
|
39 |
|
40 `context` is a dictionary of values to use in this rendering. |
|
41 |
|
42 """ |
|
43 # Make the complete context we'll use. |
|
44 ctx = dict(self.context) |
|
45 if context: |
|
46 ctx.update(context) |
|
47 |
|
48 ctxaccess = _ContextAccess(ctx) |
|
49 |
|
50 # Render the loops. |
|
51 for iloop, (loopvar, listvar, loopbody) in enumerate(self.loops): |
|
52 result = "" |
|
53 for listval in ctxaccess[listvar]: |
|
54 ctx[loopvar] = listval |
|
55 result += loopbody % ctxaccess |
|
56 ctx["loop:%d" % iloop] = result |
|
57 |
|
58 # Render the final template. |
|
59 return self.text % ctxaccess |
|
60 |
|
61 def _prepare(self, text): |
|
62 """Convert Django-style data references into Python-native ones.""" |
|
63 # Pull out loops. |
|
64 text = re.sub( |
|
65 r"(?s){% for ([a-z0-9_]+) in ([a-z0-9_.|]+) %}(.*?){% endfor %}", |
|
66 self._loop_prepare, text |
|
67 ) |
|
68 # Protect actual percent signs in the text. |
|
69 text = text.replace("%", "%%") |
|
70 # Convert {{foo}} into %(foo)s |
|
71 text = re.sub(r"{{([^}]+)}}", r"%(\1)s", text) |
|
72 return text |
|
73 |
|
74 def _loop_prepare(self, match): |
|
75 """Prepare a loop body for `_prepare`.""" |
|
76 nloop = len(self.loops) |
|
77 # Append (loopvar, listvar, loopbody) to self.loops |
|
78 loopvar, listvar, loopbody = match.groups() |
|
79 loopbody = self._prepare(loopbody) |
|
80 self.loops.append((loopvar, listvar, loopbody)) |
|
81 return "{{loop:%d}}" % nloop |
|
82 |
|
83 |
|
84 class _ContextAccess(object): |
|
85 """A mediator for a context. |
|
86 |
|
87 Implements __getitem__ on a context for Templite, so that string formatting |
|
88 references can pull data from the context. |
|
89 |
|
90 """ |
|
91 def __init__(self, context): |
|
92 self.context = context |
|
93 |
|
94 def __getitem__(self, key): |
|
95 if "|" in key: |
|
96 pipes = key.split("|") |
|
97 value = self[pipes[0]] |
|
98 for func in pipes[1:]: |
|
99 value = self[func](value) |
|
100 elif "." in key: |
|
101 dots = key.split('.') |
|
102 value = self[dots[0]] |
|
103 for dot in dots[1:]: |
|
104 try: |
|
105 value = getattr(value, dot) |
|
106 except AttributeError: |
|
107 value = value[dot] |
|
108 if callable(value): |
|
109 value = value() |
|
110 else: |
|
111 value = self.context[key] |
|
112 return value |