5 # This software may be used and distributed according to the terms of the |
5 # This software may be used and distributed according to the terms of the |
6 # GNU General Public License version 2 or any later version. |
6 # GNU General Public License version 2 or any later version. |
7 |
7 |
8 from i18n import _ |
8 from i18n import _ |
9 import sys, os |
9 import sys, os |
10 import util, config, templatefilters |
10 import util, config, templatefilters, parser, error |
|
11 |
|
12 # template parsing |
|
13 |
|
14 elements = { |
|
15 "(": (20, ("group", 1, ")"), ("func", 1, ")")), |
|
16 ",": (2, None, ("list", 2)), |
|
17 "|": (5, None, ("|", 5)), |
|
18 "%": (6, None, ("%", 6)), |
|
19 ")": (0, None, None), |
|
20 "symbol": (0, ("symbol",), None), |
|
21 "string": (0, ("string",), None), |
|
22 "end": (0, None, None), |
|
23 } |
|
24 |
|
25 def tokenizer(data): |
|
26 program, start, end = data |
|
27 pos = start |
|
28 while pos < end: |
|
29 c = program[pos] |
|
30 if c.isspace(): # skip inter-token whitespace |
|
31 pass |
|
32 elif c in "(,)%|": # handle simple operators |
|
33 yield (c, None, pos) |
|
34 elif (c in '"\'' or c == 'r' and |
|
35 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings |
|
36 if c == 'r': |
|
37 pos += 1 |
|
38 c = program[pos] |
|
39 decode = lambda x: x |
|
40 else: |
|
41 decode = lambda x: x.decode('string-escape') |
|
42 pos += 1 |
|
43 s = pos |
|
44 while pos < end: # find closing quote |
|
45 d = program[pos] |
|
46 if d == '\\': # skip over escaped characters |
|
47 pos += 2 |
|
48 continue |
|
49 if d == c: |
|
50 yield ('string', decode(program[s:pos]), s) |
|
51 break |
|
52 pos += 1 |
|
53 else: |
|
54 raise error.ParseError(_("unterminated string"), s) |
|
55 elif c.isalnum() or c in '_': |
|
56 s = pos |
|
57 pos += 1 |
|
58 while pos < end: # find end of symbol |
|
59 d = program[pos] |
|
60 if not (d.isalnum() or d == "_"): |
|
61 break |
|
62 pos += 1 |
|
63 sym = program[s:pos] |
|
64 yield ('symbol', sym, s) |
|
65 pos -= 1 |
|
66 elif c == '}': |
|
67 pos += 1 |
|
68 break |
|
69 else: |
|
70 raise error.ParseError(_("syntax error"), pos) |
|
71 pos += 1 |
|
72 data[2] = pos |
|
73 yield ('end', None, pos) |
|
74 |
|
75 def compiletemplate(tmpl, context): |
|
76 parsed = [] |
|
77 pos, stop = 0, len(tmpl) |
|
78 p = parser.parser(tokenizer, elements) |
|
79 |
|
80 while pos < stop: |
|
81 n = tmpl.find('{', pos) |
|
82 if n < 0: |
|
83 parsed.append(("string", tmpl[pos:])) |
|
84 break |
|
85 if n > 0 and tmpl[n - 1] == '\\': |
|
86 # escaped |
|
87 parsed.append(("string", tmpl[pos:n - 1] + "{")) |
|
88 pos = n + 1 |
|
89 continue |
|
90 if n > pos: |
|
91 parsed.append(("string", tmpl[pos:n])) |
|
92 |
|
93 pd = [tmpl, n + 1, stop] |
|
94 parsed.append(p.parse(pd)) |
|
95 pos = pd[2] |
|
96 |
|
97 return [compileexp(e, context) for e in parsed] |
|
98 |
|
99 def compileexp(exp, context): |
|
100 t = exp[0] |
|
101 if t in methods: |
|
102 return methods[t](exp, context) |
|
103 raise error.ParseError(_("unknown method '%s'") % t) |
|
104 |
|
105 # template evaluation |
|
106 |
|
107 def getsymbol(exp): |
|
108 if exp[0] == 'symbol': |
|
109 return exp[1] |
|
110 raise error.ParseError(_("expected a symbol")) |
|
111 |
|
112 def getlist(x): |
|
113 if not x: |
|
114 return [] |
|
115 if x[0] == 'list': |
|
116 return getlist(x[1]) + [x[2]] |
|
117 return [x] |
|
118 |
|
119 def getfilter(exp, context): |
|
120 f = getsymbol(exp) |
|
121 if f not in context._filters: |
|
122 raise error.ParseError(_("unknown function '%s'") % f) |
|
123 return context._filters[f] |
|
124 |
|
125 def gettemplate(exp, context): |
|
126 if exp[0] == 'string': |
|
127 return compiletemplate(exp[1], context) |
|
128 if exp[0] == 'symbol': |
|
129 return context._load(exp[1]) |
|
130 raise error.ParseError(_("expected template specifier")) |
|
131 |
|
132 def runstring(context, mapping, data): |
|
133 return data |
|
134 |
|
135 def runsymbol(context, mapping, key): |
|
136 v = mapping.get(key) |
|
137 if v is None: |
|
138 v = context._defaults.get(key, '') |
|
139 if hasattr(v, '__call__'): |
|
140 return v(**mapping) |
|
141 return v |
|
142 |
|
143 def buildfilter(exp, context): |
|
144 func, data = compileexp(exp[1], context) |
|
145 filt = getfilter(exp[2], context) |
|
146 return (runfilter, (func, data, filt)) |
|
147 |
|
148 def runfilter(context, mapping, data): |
|
149 func, data, filt = data |
|
150 return filt(func(context, mapping, data)) |
|
151 |
|
152 def buildmap(exp, context): |
|
153 func, data = compileexp(exp[1], context) |
|
154 ctmpl = gettemplate(exp[2], context) |
|
155 return (runmap, (func, data, ctmpl)) |
|
156 |
|
157 def runmap(context, mapping, data): |
|
158 func, data, ctmpl = data |
|
159 d = func(context, mapping, data) |
|
160 lm = mapping.copy() |
|
161 |
|
162 for i in d: |
|
163 if isinstance(i, dict): |
|
164 lm.update(i) |
|
165 for f, d in ctmpl: |
|
166 yield f(context, lm, d) |
|
167 else: |
|
168 # v is not an iterable of dicts, this happen when 'key' |
|
169 # has been fully expanded already and format is useless. |
|
170 # If so, return the expanded value. |
|
171 yield i |
|
172 |
|
173 def buildfunc(exp, context): |
|
174 n = getsymbol(exp[1]) |
|
175 args = [compileexp(x, context) for x in getlist(exp[2])] |
|
176 if n in context._filters: |
|
177 if len(args) != 1: |
|
178 raise error.ParseError(_("filter %s expects one argument") % n) |
|
179 f = context._filters[n] |
|
180 return (runfilter, (args[0][0], args[0][1], f)) |
|
181 elif n in context._funcs: |
|
182 f = context._funcs[n] |
|
183 return (f, args) |
|
184 |
|
185 methods = { |
|
186 "string": lambda e, c: (runstring, e[1]), |
|
187 "symbol": lambda e, c: (runsymbol, e[1]), |
|
188 "group": lambda e, c: compileexp(e[1], c), |
|
189 # ".": buildmember, |
|
190 "|": buildfilter, |
|
191 "%": buildmap, |
|
192 "func": buildfunc, |
|
193 } |
|
194 |
|
195 # template engine |
11 |
196 |
12 path = ['templates', '../templates'] |
197 path = ['templates', '../templates'] |
13 stringify = templatefilters.stringify |
198 stringify = templatefilters.stringify |
14 |
199 |
15 def _flatten(thing): |
200 def _flatten(thing): |
64 self._loader = loader |
249 self._loader = loader |
65 self._filters = filters |
250 self._filters = filters |
66 self._defaults = defaults |
251 self._defaults = defaults |
67 self._cache = {} |
252 self._cache = {} |
68 |
253 |
|
254 def _load(self, t): |
|
255 '''load, parse, and cache a template''' |
|
256 if t not in self._cache: |
|
257 self._cache[t] = compiletemplate(self._loader(t), self) |
|
258 return self._cache[t] |
|
259 |
69 def process(self, t, mapping): |
260 def process(self, t, mapping): |
70 '''Perform expansion. t is name of map element to expand. |
261 '''Perform expansion. t is name of map element to expand. |
71 mapping contains added elements for use during expansion. Is a |
262 mapping contains added elements for use during expansion. Is a |
72 generator.''' |
263 generator.''' |
73 return _flatten(self._process(self._load(t), mapping)) |
264 return _flatten(func(self, mapping, data) for func, data in |
74 |
265 self._load(t)) |
75 def _load(self, t): |
|
76 '''load, parse, and cache a template''' |
|
77 if t not in self._cache: |
|
78 self._cache[t] = self._parse(self._loader(t)) |
|
79 return self._cache[t] |
|
80 |
|
81 def _get(self, mapping, key): |
|
82 v = mapping.get(key) |
|
83 if v is None: |
|
84 v = self._defaults.get(key, '') |
|
85 if hasattr(v, '__call__'): |
|
86 v = v(**mapping) |
|
87 return v |
|
88 |
|
89 def _filter(self, mapping, parts): |
|
90 filters, val = parts |
|
91 x = self._get(mapping, val) |
|
92 for f in filters: |
|
93 x = f(x) |
|
94 return x |
|
95 |
|
96 def _format(self, mapping, args): |
|
97 key, parsed = args |
|
98 v = self._get(mapping, key) |
|
99 if not hasattr(v, '__iter__'): |
|
100 raise SyntaxError(_("error expanding '%s%%%s'") |
|
101 % (key, parsed)) |
|
102 lm = mapping.copy() |
|
103 for i in v: |
|
104 if isinstance(i, dict): |
|
105 lm.update(i) |
|
106 yield self._process(parsed, lm) |
|
107 else: |
|
108 # v is not an iterable of dicts, this happen when 'key' |
|
109 # has been fully expanded already and format is useless. |
|
110 # If so, return the expanded value. |
|
111 yield i |
|
112 |
|
113 def _parse(self, tmpl): |
|
114 '''preparse a template''' |
|
115 parsed = [] |
|
116 pos, stop = 0, len(tmpl) |
|
117 while pos < stop: |
|
118 n = tmpl.find('{', pos) |
|
119 if n < 0: |
|
120 parsed.append((None, tmpl[pos:stop])) |
|
121 break |
|
122 if n > 0 and tmpl[n - 1] == '\\': |
|
123 # escaped |
|
124 parsed.append((None, tmpl[pos:n - 1] + "{")) |
|
125 pos = n + 1 |
|
126 continue |
|
127 if n > pos: |
|
128 parsed.append((None, tmpl[pos:n])) |
|
129 |
|
130 pos = n |
|
131 n = tmpl.find('}', pos) |
|
132 if n < 0: |
|
133 # no closing |
|
134 parsed.append((None, tmpl[pos:stop])) |
|
135 break |
|
136 |
|
137 expr = tmpl[pos + 1:n] |
|
138 pos = n + 1 |
|
139 |
|
140 if '%' in expr: |
|
141 # the keyword should be formatted with a template |
|
142 key, t = expr.split('%') |
|
143 parsed.append((self._format, (key.strip(), |
|
144 self._load(t.strip())))) |
|
145 elif '|' in expr: |
|
146 # process the keyword value with one or more filters |
|
147 parts = expr.split('|') |
|
148 val = parts[0].strip() |
|
149 try: |
|
150 filters = [self._filters[f.strip()] for f in parts[1:]] |
|
151 except KeyError, i: |
|
152 raise SyntaxError(_("unknown filter '%s'") % i[0]) |
|
153 parsed.append((self._filter, (filters, val))) |
|
154 else: |
|
155 # just get the keyword |
|
156 parsed.append((self._get, expr.strip())) |
|
157 |
|
158 return parsed |
|
159 |
|
160 def _process(self, parsed, mapping): |
|
161 '''Render a template. Returns a generator.''' |
|
162 for f, e in parsed: |
|
163 if f: |
|
164 yield f(mapping, e) |
|
165 else: |
|
166 yield e |
|
167 |
266 |
168 engines = {'default': engine} |
267 engines = {'default': engine} |
169 |
268 |
170 class templater(object): |
269 class templater(object): |
171 |
270 |