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