##// END OF EJS Templates
templater: add if/ifeq conditionals
Matt Mackall -
r17636:ce18dbcd default
parent child Browse files
Show More
@@ -1,443 +1,470 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import sys, os, re
10 10 import util, config, templatefilters, parser, error
11 11
12 12 # template parsing
13 13
14 14 elements = {
15 15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 16 ",": (2, None, ("list", 2)),
17 17 "|": (5, None, ("|", 5)),
18 18 "%": (6, None, ("%", 6)),
19 19 ")": (0, None, None),
20 20 "symbol": (0, ("symbol",), None),
21 21 "string": (0, ("string",), None),
22 22 "end": (0, None, None),
23 23 }
24 24
25 25 def tokenizer(data):
26 26 program, start, end = data
27 27 pos = start
28 28 while pos < end:
29 29 c = program[pos]
30 30 if c.isspace(): # skip inter-token whitespace
31 31 pass
32 32 elif c in "(,)%|": # handle simple operators
33 33 yield (c, None, pos)
34 34 elif (c in '"\'' or c == 'r' and
35 35 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
36 36 if c == 'r':
37 37 pos += 1
38 38 c = program[pos]
39 39 decode = False
40 40 else:
41 41 decode = True
42 42 pos += 1
43 43 s = pos
44 44 while pos < end: # find closing quote
45 45 d = program[pos]
46 46 if decode and d == '\\': # skip over escaped characters
47 47 pos += 2
48 48 continue
49 49 if d == c:
50 50 if not decode:
51 51 yield ('string', program[s:pos].replace('\\', r'\\'), s)
52 52 break
53 53 yield ('string', program[s:pos].decode('string-escape'), s)
54 54 break
55 55 pos += 1
56 56 else:
57 57 raise error.ParseError(_("unterminated string"), s)
58 58 elif c.isalnum() or c in '_':
59 59 s = pos
60 60 pos += 1
61 61 while pos < end: # find end of symbol
62 62 d = program[pos]
63 63 if not (d.isalnum() or d == "_"):
64 64 break
65 65 pos += 1
66 66 sym = program[s:pos]
67 67 yield ('symbol', sym, s)
68 68 pos -= 1
69 69 elif c == '}':
70 70 pos += 1
71 71 break
72 72 else:
73 73 raise error.ParseError(_("syntax error"), pos)
74 74 pos += 1
75 75 yield ('end', None, pos)
76 76
77 77 def compiletemplate(tmpl, context):
78 78 parsed = []
79 79 pos, stop = 0, len(tmpl)
80 80 p = parser.parser(tokenizer, elements)
81 81
82 82 while pos < stop:
83 83 n = tmpl.find('{', pos)
84 84 if n < 0:
85 85 parsed.append(("string", tmpl[pos:]))
86 86 break
87 87 if n > 0 and tmpl[n - 1] == '\\':
88 88 # escaped
89 89 parsed.append(("string", tmpl[pos:n - 1] + "{"))
90 90 pos = n + 1
91 91 continue
92 92 if n > pos:
93 93 parsed.append(("string", tmpl[pos:n]))
94 94
95 95 pd = [tmpl, n + 1, stop]
96 96 parseres, pos = p.parse(pd)
97 97 parsed.append(parseres)
98 98
99 99 return [compileexp(e, context) for e in parsed]
100 100
101 101 def compileexp(exp, context):
102 102 t = exp[0]
103 103 if t in methods:
104 104 return methods[t](exp, context)
105 105 raise error.ParseError(_("unknown method '%s'") % t)
106 106
107 107 # template evaluation
108 108
109 109 def getsymbol(exp):
110 110 if exp[0] == 'symbol':
111 111 return exp[1]
112 112 raise error.ParseError(_("expected a symbol"))
113 113
114 114 def getlist(x):
115 115 if not x:
116 116 return []
117 117 if x[0] == 'list':
118 118 return getlist(x[1]) + [x[2]]
119 119 return [x]
120 120
121 121 def getfilter(exp, context):
122 122 f = getsymbol(exp)
123 123 if f not in context._filters:
124 124 raise error.ParseError(_("unknown function '%s'") % f)
125 125 return context._filters[f]
126 126
127 127 def gettemplate(exp, context):
128 128 if exp[0] == 'string':
129 129 return compiletemplate(exp[1], context)
130 130 if exp[0] == 'symbol':
131 131 return context._load(exp[1])
132 132 raise error.ParseError(_("expected template specifier"))
133 133
134 134 def runstring(context, mapping, data):
135 135 return data
136 136
137 137 def runsymbol(context, mapping, key):
138 138 v = mapping.get(key)
139 139 if v is None:
140 140 v = context._defaults.get(key, '')
141 141 if util.safehasattr(v, '__call__'):
142 142 return v(**mapping)
143 143 return v
144 144
145 145 def buildfilter(exp, context):
146 146 func, data = compileexp(exp[1], context)
147 147 filt = getfilter(exp[2], context)
148 148 return (runfilter, (func, data, filt))
149 149
150 150 def runfilter(context, mapping, data):
151 151 func, data, filt = data
152 152 try:
153 153 return filt(func(context, mapping, data))
154 154 except (ValueError, AttributeError, TypeError):
155 155 if isinstance(data, tuple):
156 156 dt = data[1]
157 157 else:
158 158 dt = data
159 159 raise util.Abort(_("template filter '%s' is not compatible with "
160 160 "keyword '%s'") % (filt.func_name, dt))
161 161
162 162 def buildmap(exp, context):
163 163 func, data = compileexp(exp[1], context)
164 164 ctmpl = gettemplate(exp[2], context)
165 165 return (runmap, (func, data, ctmpl))
166 166
167 167 def runtemplate(context, mapping, template):
168 168 for func, data in template:
169 169 yield func(context, mapping, data)
170 170
171 171 def runmap(context, mapping, data):
172 172 func, data, ctmpl = data
173 173 d = func(context, mapping, data)
174 174 if util.safehasattr(d, '__call__'):
175 175 d = d()
176 176
177 177 lm = mapping.copy()
178 178
179 179 for i in d:
180 180 if isinstance(i, dict):
181 181 lm.update(i)
182 182 yield runtemplate(context, lm, ctmpl)
183 183 else:
184 184 # v is not an iterable of dicts, this happen when 'key'
185 185 # has been fully expanded already and format is useless.
186 186 # If so, return the expanded value.
187 187 yield i
188 188
189 189 def buildfunc(exp, context):
190 190 n = getsymbol(exp[1])
191 191 args = [compileexp(x, context) for x in getlist(exp[2])]
192 192 if n in funcs:
193 193 f = funcs[n]
194 194 return (f, args)
195 195 if n in context._filters:
196 196 if len(args) != 1:
197 197 raise error.ParseError(_("filter %s expects one argument") % n)
198 198 f = context._filters[n]
199 199 return (runfilter, (args[0][0], args[0][1], f))
200 200
201 201 def join(context, mapping, args):
202 202 if not (1 <= len(args) <= 2):
203 203 raise error.ParseError(_("join expects one or two arguments"))
204 204
205 205 joinset = args[0][0](context, mapping, args[0][1])
206 206 if util.safehasattr(joinset, '__call__'):
207 207 joinset = [x.values()[0] for x in joinset()]
208 208
209 209 joiner = " "
210 210 if len(args) > 1:
211 211 joiner = args[1][0](context, mapping, args[1][1])
212 212
213 213 first = True
214 214 for x in joinset:
215 215 if first:
216 216 first = False
217 217 else:
218 218 yield joiner
219 219 yield x
220 220
221 221 def sub(context, mapping, args):
222 222 if len(args) != 3:
223 223 raise error.ParseError(_("sub expects three arguments"))
224 224
225 225 pat = stringify(args[0][0](context, mapping, args[0][1]))
226 226 rpl = stringify(args[1][0](context, mapping, args[1][1]))
227 227 src = stringify(args[2][0](context, mapping, args[2][1]))
228 228 yield re.sub(pat, rpl, src)
229 229
230 def if_(context, mapping, args):
231 if not (2 <= len(args) <= 3):
232 raise error.ParseError(_("if expects two or three arguments"))
233
234 test = stringify(args[0][0](context, mapping, args[0][1]))
235 if test:
236 t = stringify(args[1][0](context, mapping, args[1][1]))
237 yield runtemplate(context, mapping, compiletemplate(t, context))
238 elif len(args) == 3:
239 t = stringify(args[2][0](context, mapping, args[2][1]))
240 yield runtemplate(context, mapping, compiletemplate(t, context))
241
242 def ifeq(context, mapping, args):
243 if not (3 <= len(args) <= 4):
244 raise error.ParseError(_("ifeq expects three or four arguments"))
245
246 test = stringify(args[0][0](context, mapping, args[0][1]))
247 match = stringify(args[1][0](context, mapping, args[1][1]))
248 if test == match:
249 t = stringify(args[2][0](context, mapping, args[2][1]))
250 yield runtemplate(context, mapping, compiletemplate(t, context))
251 elif len(args) == 4:
252 t = stringify(args[3][0](context, mapping, args[3][1]))
253 yield runtemplate(context, mapping, compiletemplate(t, context))
254
230 255 methods = {
231 256 "string": lambda e, c: (runstring, e[1]),
232 257 "symbol": lambda e, c: (runsymbol, e[1]),
233 258 "group": lambda e, c: compileexp(e[1], c),
234 259 # ".": buildmember,
235 260 "|": buildfilter,
236 261 "%": buildmap,
237 262 "func": buildfunc,
238 263 }
239 264
240 265 funcs = {
266 "if": if_,
267 "ifeq": ifeq,
241 268 "join": join,
242 269 "sub": sub,
243 270 }
244 271
245 272 # template engine
246 273
247 274 path = ['templates', '../templates']
248 275 stringify = templatefilters.stringify
249 276
250 277 def _flatten(thing):
251 278 '''yield a single stream from a possibly nested set of iterators'''
252 279 if isinstance(thing, str):
253 280 yield thing
254 281 elif not util.safehasattr(thing, '__iter__'):
255 282 if thing is not None:
256 283 yield str(thing)
257 284 else:
258 285 for i in thing:
259 286 if isinstance(i, str):
260 287 yield i
261 288 elif not util.safehasattr(i, '__iter__'):
262 289 if i is not None:
263 290 yield str(i)
264 291 elif i is not None:
265 292 for j in _flatten(i):
266 293 yield j
267 294
268 295 def parsestring(s, quoted=True):
269 296 '''parse a string using simple c-like syntax.
270 297 string must be in quotes if quoted is True.'''
271 298 if quoted:
272 299 if len(s) < 2 or s[0] != s[-1]:
273 300 raise SyntaxError(_('unmatched quotes'))
274 301 return s[1:-1].decode('string_escape')
275 302
276 303 return s.decode('string_escape')
277 304
278 305 class engine(object):
279 306 '''template expansion engine.
280 307
281 308 template expansion works like this. a map file contains key=value
282 309 pairs. if value is quoted, it is treated as string. otherwise, it
283 310 is treated as name of template file.
284 311
285 312 templater is asked to expand a key in map. it looks up key, and
286 313 looks for strings like this: {foo}. it expands {foo} by looking up
287 314 foo in map, and substituting it. expansion is recursive: it stops
288 315 when there is no more {foo} to replace.
289 316
290 317 expansion also allows formatting and filtering.
291 318
292 319 format uses key to expand each item in list. syntax is
293 320 {key%format}.
294 321
295 322 filter uses function to transform value. syntax is
296 323 {key|filter1|filter2|...}.'''
297 324
298 325 def __init__(self, loader, filters={}, defaults={}):
299 326 self._loader = loader
300 327 self._filters = filters
301 328 self._defaults = defaults
302 329 self._cache = {}
303 330
304 331 def _load(self, t):
305 332 '''load, parse, and cache a template'''
306 333 if t not in self._cache:
307 334 self._cache[t] = compiletemplate(self._loader(t), self)
308 335 return self._cache[t]
309 336
310 337 def process(self, t, mapping):
311 338 '''Perform expansion. t is name of map element to expand.
312 339 mapping contains added elements for use during expansion. Is a
313 340 generator.'''
314 341 return _flatten(func(self, mapping, data) for func, data in
315 342 self._load(t))
316 343 return _flatten(runtemplate(self, mapping, self._load(t)))
317 344
318 345 engines = {'default': engine}
319 346
320 347 class templater(object):
321 348
322 349 def __init__(self, mapfile, filters={}, defaults={}, cache={},
323 350 minchunk=1024, maxchunk=65536):
324 351 '''set up template engine.
325 352 mapfile is name of file to read map definitions from.
326 353 filters is dict of functions. each transforms a value into another.
327 354 defaults is dict of default map definitions.'''
328 355 self.mapfile = mapfile or 'template'
329 356 self.cache = cache.copy()
330 357 self.map = {}
331 358 self.base = (mapfile and os.path.dirname(mapfile)) or ''
332 359 self.filters = templatefilters.filters.copy()
333 360 self.filters.update(filters)
334 361 self.defaults = defaults
335 362 self.minchunk, self.maxchunk = minchunk, maxchunk
336 363 self.ecache = {}
337 364
338 365 if not mapfile:
339 366 return
340 367 if not os.path.exists(mapfile):
341 368 raise util.Abort(_('style not found: %s') % mapfile)
342 369
343 370 conf = config.config()
344 371 conf.read(mapfile)
345 372
346 373 for key, val in conf[''].items():
347 374 if not val:
348 375 raise SyntaxError(_('%s: missing value') % conf.source('', key))
349 376 if val[0] in "'\"":
350 377 try:
351 378 self.cache[key] = parsestring(val)
352 379 except SyntaxError, inst:
353 380 raise SyntaxError('%s: %s' %
354 381 (conf.source('', key), inst.args[0]))
355 382 else:
356 383 val = 'default', val
357 384 if ':' in val[1]:
358 385 val = val[1].split(':', 1)
359 386 self.map[key] = val[0], os.path.join(self.base, val[1])
360 387
361 388 def __contains__(self, key):
362 389 return key in self.cache or key in self.map
363 390
364 391 def load(self, t):
365 392 '''Get the template for the given template name. Use a local cache.'''
366 393 if t not in self.cache:
367 394 try:
368 395 self.cache[t] = util.readfile(self.map[t][1])
369 396 except KeyError, inst:
370 397 raise util.Abort(_('"%s" not in template map') % inst.args[0])
371 398 except IOError, inst:
372 399 raise IOError(inst.args[0], _('template file %s: %s') %
373 400 (self.map[t][1], inst.args[1]))
374 401 return self.cache[t]
375 402
376 403 def __call__(self, t, **mapping):
377 404 ttype = t in self.map and self.map[t][0] or 'default'
378 405 if ttype not in self.ecache:
379 406 self.ecache[ttype] = engines[ttype](self.load,
380 407 self.filters, self.defaults)
381 408 proc = self.ecache[ttype]
382 409
383 410 stream = proc.process(t, mapping)
384 411 if self.minchunk:
385 412 stream = util.increasingchunks(stream, min=self.minchunk,
386 413 max=self.maxchunk)
387 414 return stream
388 415
389 416 def templatepath(name=None):
390 417 '''return location of template file or directory (if no name).
391 418 returns None if not found.'''
392 419 normpaths = []
393 420
394 421 # executable version (py2exe) doesn't support __file__
395 422 if util.mainfrozen():
396 423 module = sys.executable
397 424 else:
398 425 module = __file__
399 426 for f in path:
400 427 if f.startswith('/'):
401 428 p = f
402 429 else:
403 430 fl = f.split('/')
404 431 p = os.path.join(os.path.dirname(module), *fl)
405 432 if name:
406 433 p = os.path.join(p, name)
407 434 if name and os.path.exists(p):
408 435 return os.path.normpath(p)
409 436 elif os.path.isdir(p):
410 437 normpaths.append(os.path.normpath(p))
411 438
412 439 return normpaths
413 440
414 441 def stylemap(styles, paths=None):
415 442 """Return path to mapfile for a given style.
416 443
417 444 Searches mapfile in the following locations:
418 445 1. templatepath/style/map
419 446 2. templatepath/map-style
420 447 3. templatepath/map
421 448 """
422 449
423 450 if paths is None:
424 451 paths = templatepath()
425 452 elif isinstance(paths, str):
426 453 paths = [paths]
427 454
428 455 if isinstance(styles, str):
429 456 styles = [styles]
430 457
431 458 for style in styles:
432 459 if not style:
433 460 continue
434 461 locations = [os.path.join(style, 'map'), 'map-' + style]
435 462 locations.append('map')
436 463
437 464 for path in paths:
438 465 for location in locations:
439 466 mapfile = os.path.join(path, location)
440 467 if os.path.isfile(mapfile):
441 468 return style, mapfile
442 469
443 470 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now