##// END OF EJS Templates
templater: drop redundant return in _flatten
Matt Mackall -
r17729:9a3cb3ce default
parent child Browse files
Show More
@@ -1,473 +1,471
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 templatefilters.funcs:
196 196 f = templatefilters.funcs[n]
197 197 return (f, args)
198 198 if n in context._filters:
199 199 if len(args) != 1:
200 200 raise error.ParseError(_("filter %s expects one argument") % n)
201 201 f = context._filters[n]
202 202 return (runfilter, (args[0][0], args[0][1], f))
203 203
204 204 def join(context, mapping, args):
205 205 if not (1 <= len(args) <= 2):
206 206 raise error.ParseError(_("join expects one or two arguments"))
207 207
208 208 joinset = args[0][0](context, mapping, args[0][1])
209 209 if util.safehasattr(joinset, '__call__'):
210 210 joinset = [x.values()[0] for x in joinset()]
211 211
212 212 joiner = " "
213 213 if len(args) > 1:
214 214 joiner = args[1][0](context, mapping, args[1][1])
215 215
216 216 first = True
217 217 for x in joinset:
218 218 if first:
219 219 first = False
220 220 else:
221 221 yield joiner
222 222 yield x
223 223
224 224 def sub(context, mapping, args):
225 225 if len(args) != 3:
226 226 raise error.ParseError(_("sub expects three arguments"))
227 227
228 228 pat = stringify(args[0][0](context, mapping, args[0][1]))
229 229 rpl = stringify(args[1][0](context, mapping, args[1][1]))
230 230 src = stringify(args[2][0](context, mapping, args[2][1]))
231 231 yield re.sub(pat, rpl, src)
232 232
233 233 def if_(context, mapping, args):
234 234 if not (2 <= len(args) <= 3):
235 235 raise error.ParseError(_("if expects two or three arguments"))
236 236
237 237 test = stringify(args[0][0](context, mapping, args[0][1]))
238 238 if test:
239 239 t = stringify(args[1][0](context, mapping, args[1][1]))
240 240 yield runtemplate(context, mapping, compiletemplate(t, context))
241 241 elif len(args) == 3:
242 242 t = stringify(args[2][0](context, mapping, args[2][1]))
243 243 yield runtemplate(context, mapping, compiletemplate(t, context))
244 244
245 245 def ifeq(context, mapping, args):
246 246 if not (3 <= len(args) <= 4):
247 247 raise error.ParseError(_("ifeq expects three or four arguments"))
248 248
249 249 test = stringify(args[0][0](context, mapping, args[0][1]))
250 250 match = stringify(args[1][0](context, mapping, args[1][1]))
251 251 if test == match:
252 252 t = stringify(args[2][0](context, mapping, args[2][1]))
253 253 yield runtemplate(context, mapping, compiletemplate(t, context))
254 254 elif len(args) == 4:
255 255 t = stringify(args[3][0](context, mapping, args[3][1]))
256 256 yield runtemplate(context, mapping, compiletemplate(t, context))
257 257
258 258 methods = {
259 259 "string": lambda e, c: (runstring, e[1]),
260 260 "symbol": lambda e, c: (runsymbol, e[1]),
261 261 "group": lambda e, c: compileexp(e[1], c),
262 262 # ".": buildmember,
263 263 "|": buildfilter,
264 264 "%": buildmap,
265 265 "func": buildfunc,
266 266 }
267 267
268 268 funcs = {
269 269 "if": if_,
270 270 "ifeq": ifeq,
271 271 "join": join,
272 272 "sub": sub,
273 273 }
274 274
275 275 # template engine
276 276
277 277 path = ['templates', '../templates']
278 278 stringify = templatefilters.stringify
279 279
280 280 def _flatten(thing):
281 281 '''yield a single stream from a possibly nested set of iterators'''
282 282 if isinstance(thing, str):
283 283 yield thing
284 284 elif not util.safehasattr(thing, '__iter__'):
285 285 if thing is not None:
286 286 yield str(thing)
287 287 else:
288 288 for i in thing:
289 289 if isinstance(i, str):
290 290 yield i
291 291 elif not util.safehasattr(i, '__iter__'):
292 292 if i is not None:
293 293 yield str(i)
294 294 elif i is not None:
295 295 for j in _flatten(i):
296 296 yield j
297 297
298 298 def parsestring(s, quoted=True):
299 299 '''parse a string using simple c-like syntax.
300 300 string must be in quotes if quoted is True.'''
301 301 if quoted:
302 302 if len(s) < 2 or s[0] != s[-1]:
303 303 raise SyntaxError(_('unmatched quotes'))
304 304 return s[1:-1].decode('string_escape')
305 305
306 306 return s.decode('string_escape')
307 307
308 308 class engine(object):
309 309 '''template expansion engine.
310 310
311 311 template expansion works like this. a map file contains key=value
312 312 pairs. if value is quoted, it is treated as string. otherwise, it
313 313 is treated as name of template file.
314 314
315 315 templater is asked to expand a key in map. it looks up key, and
316 316 looks for strings like this: {foo}. it expands {foo} by looking up
317 317 foo in map, and substituting it. expansion is recursive: it stops
318 318 when there is no more {foo} to replace.
319 319
320 320 expansion also allows formatting and filtering.
321 321
322 322 format uses key to expand each item in list. syntax is
323 323 {key%format}.
324 324
325 325 filter uses function to transform value. syntax is
326 326 {key|filter1|filter2|...}.'''
327 327
328 328 def __init__(self, loader, filters={}, defaults={}):
329 329 self._loader = loader
330 330 self._filters = filters
331 331 self._defaults = defaults
332 332 self._cache = {}
333 333
334 334 def _load(self, t):
335 335 '''load, parse, and cache a template'''
336 336 if t not in self._cache:
337 337 self._cache[t] = compiletemplate(self._loader(t), self)
338 338 return self._cache[t]
339 339
340 340 def process(self, t, mapping):
341 341 '''Perform expansion. t is name of map element to expand.
342 342 mapping contains added elements for use during expansion. Is a
343 343 generator.'''
344 return _flatten(func(self, mapping, data) for func, data in
345 self._load(t))
346 344 return _flatten(runtemplate(self, mapping, self._load(t)))
347 345
348 346 engines = {'default': engine}
349 347
350 348 class templater(object):
351 349
352 350 def __init__(self, mapfile, filters={}, defaults={}, cache={},
353 351 minchunk=1024, maxchunk=65536):
354 352 '''set up template engine.
355 353 mapfile is name of file to read map definitions from.
356 354 filters is dict of functions. each transforms a value into another.
357 355 defaults is dict of default map definitions.'''
358 356 self.mapfile = mapfile or 'template'
359 357 self.cache = cache.copy()
360 358 self.map = {}
361 359 self.base = (mapfile and os.path.dirname(mapfile)) or ''
362 360 self.filters = templatefilters.filters.copy()
363 361 self.filters.update(filters)
364 362 self.defaults = defaults
365 363 self.minchunk, self.maxchunk = minchunk, maxchunk
366 364 self.ecache = {}
367 365
368 366 if not mapfile:
369 367 return
370 368 if not os.path.exists(mapfile):
371 369 raise util.Abort(_('style not found: %s') % mapfile)
372 370
373 371 conf = config.config()
374 372 conf.read(mapfile)
375 373
376 374 for key, val in conf[''].items():
377 375 if not val:
378 376 raise SyntaxError(_('%s: missing value') % conf.source('', key))
379 377 if val[0] in "'\"":
380 378 try:
381 379 self.cache[key] = parsestring(val)
382 380 except SyntaxError, inst:
383 381 raise SyntaxError('%s: %s' %
384 382 (conf.source('', key), inst.args[0]))
385 383 else:
386 384 val = 'default', val
387 385 if ':' in val[1]:
388 386 val = val[1].split(':', 1)
389 387 self.map[key] = val[0], os.path.join(self.base, val[1])
390 388
391 389 def __contains__(self, key):
392 390 return key in self.cache or key in self.map
393 391
394 392 def load(self, t):
395 393 '''Get the template for the given template name. Use a local cache.'''
396 394 if t not in self.cache:
397 395 try:
398 396 self.cache[t] = util.readfile(self.map[t][1])
399 397 except KeyError, inst:
400 398 raise util.Abort(_('"%s" not in template map') % inst.args[0])
401 399 except IOError, inst:
402 400 raise IOError(inst.args[0], _('template file %s: %s') %
403 401 (self.map[t][1], inst.args[1]))
404 402 return self.cache[t]
405 403
406 404 def __call__(self, t, **mapping):
407 405 ttype = t in self.map and self.map[t][0] or 'default'
408 406 if ttype not in self.ecache:
409 407 self.ecache[ttype] = engines[ttype](self.load,
410 408 self.filters, self.defaults)
411 409 proc = self.ecache[ttype]
412 410
413 411 stream = proc.process(t, mapping)
414 412 if self.minchunk:
415 413 stream = util.increasingchunks(stream, min=self.minchunk,
416 414 max=self.maxchunk)
417 415 return stream
418 416
419 417 def templatepath(name=None):
420 418 '''return location of template file or directory (if no name).
421 419 returns None if not found.'''
422 420 normpaths = []
423 421
424 422 # executable version (py2exe) doesn't support __file__
425 423 if util.mainfrozen():
426 424 module = sys.executable
427 425 else:
428 426 module = __file__
429 427 for f in path:
430 428 if f.startswith('/'):
431 429 p = f
432 430 else:
433 431 fl = f.split('/')
434 432 p = os.path.join(os.path.dirname(module), *fl)
435 433 if name:
436 434 p = os.path.join(p, name)
437 435 if name and os.path.exists(p):
438 436 return os.path.normpath(p)
439 437 elif os.path.isdir(p):
440 438 normpaths.append(os.path.normpath(p))
441 439
442 440 return normpaths
443 441
444 442 def stylemap(styles, paths=None):
445 443 """Return path to mapfile for a given style.
446 444
447 445 Searches mapfile in the following locations:
448 446 1. templatepath/style/map
449 447 2. templatepath/map-style
450 448 3. templatepath/map
451 449 """
452 450
453 451 if paths is None:
454 452 paths = templatepath()
455 453 elif isinstance(paths, str):
456 454 paths = [paths]
457 455
458 456 if isinstance(styles, str):
459 457 styles = [styles]
460 458
461 459 for style in styles:
462 460 if not style:
463 461 continue
464 462 locations = [os.path.join(style, 'map'), 'map-' + style]
465 463 locations.append('map')
466 464
467 465 for path in paths:
468 466 for location in locations:
469 467 mapfile = os.path.join(path, location)
470 468 if os.path.isfile(mapfile):
471 469 return style, mapfile
472 470
473 471 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now