##// END OF EJS Templates
templater: resurrect cache of engine instance...
Yuya Nishihara -
r38458:25658148 default
parent child Browse files
Show More
@@ -1,981 +1,982 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 """Slightly complicated template engine for commands and hgweb
9 9
10 10 This module provides low-level interface to the template engine. See the
11 11 formatter and cmdutil modules if you are looking for high-level functions
12 12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13 13
14 14 Internal Data Types
15 15 -------------------
16 16
17 17 Template keywords and functions take a dictionary of current symbols and
18 18 resources (a "mapping") and return result. Inputs and outputs must be one
19 19 of the following data types:
20 20
21 21 bytes
22 22 a byte string, which is generally a human-readable text in local encoding.
23 23
24 24 generator
25 25 a lazily-evaluated byte string, which is a possibly nested generator of
26 26 values of any printable types, and will be folded by ``stringify()``
27 27 or ``flatten()``.
28 28
29 29 None
30 30 sometimes represents an empty value, which can be stringified to ''.
31 31
32 32 True, False, int, float
33 33 can be stringified as such.
34 34
35 35 wrappedbytes, wrappedvalue
36 36 a wrapper for the above printable types.
37 37
38 38 date
39 39 represents a (unixtime, offset) tuple.
40 40
41 41 hybrid
42 42 represents a list/dict of printable values, which can also be converted
43 43 to mappings by % operator.
44 44
45 45 hybriditem
46 46 represents a scalar printable value, also supports % operator.
47 47
48 48 mappinggenerator, mappinglist
49 49 represents mappings (i.e. a list of dicts), which may have default
50 50 output format.
51 51
52 52 mappedgenerator
53 53 a lazily-evaluated list of byte strings, which is e.g. a result of %
54 54 operation.
55 55 """
56 56
57 57 from __future__ import absolute_import, print_function
58 58
59 59 import abc
60 60 import os
61 61
62 62 from .i18n import _
63 63 from . import (
64 64 config,
65 65 encoding,
66 66 error,
67 67 parser,
68 68 pycompat,
69 69 templatefilters,
70 70 templatefuncs,
71 71 templateutil,
72 72 util,
73 73 )
74 74 from .utils import (
75 75 stringutil,
76 76 )
77 77
78 78 # template parsing
79 79
80 80 elements = {
81 81 # token-type: binding-strength, primary, prefix, infix, suffix
82 82 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
83 83 ".": (18, None, None, (".", 18), None),
84 84 "%": (15, None, None, ("%", 15), None),
85 85 "|": (15, None, None, ("|", 15), None),
86 86 "*": (5, None, None, ("*", 5), None),
87 87 "/": (5, None, None, ("/", 5), None),
88 88 "+": (4, None, None, ("+", 4), None),
89 89 "-": (4, None, ("negate", 19), ("-", 4), None),
90 90 "=": (3, None, None, ("keyvalue", 3), None),
91 91 ",": (2, None, None, ("list", 2), None),
92 92 ")": (0, None, None, None, None),
93 93 "integer": (0, "integer", None, None, None),
94 94 "symbol": (0, "symbol", None, None, None),
95 95 "string": (0, "string", None, None, None),
96 96 "template": (0, "template", None, None, None),
97 97 "end": (0, None, None, None, None),
98 98 }
99 99
100 100 def tokenize(program, start, end, term=None):
101 101 """Parse a template expression into a stream of tokens, which must end
102 102 with term if specified"""
103 103 pos = start
104 104 program = pycompat.bytestr(program)
105 105 while pos < end:
106 106 c = program[pos]
107 107 if c.isspace(): # skip inter-token whitespace
108 108 pass
109 109 elif c in "(=,).%|+-*/": # handle simple operators
110 110 yield (c, None, pos)
111 111 elif c in '"\'': # handle quoted templates
112 112 s = pos + 1
113 113 data, pos = _parsetemplate(program, s, end, c)
114 114 yield ('template', data, s)
115 115 pos -= 1
116 116 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
117 117 # handle quoted strings
118 118 c = program[pos + 1]
119 119 s = pos = pos + 2
120 120 while pos < end: # find closing quote
121 121 d = program[pos]
122 122 if d == '\\': # skip over escaped characters
123 123 pos += 2
124 124 continue
125 125 if d == c:
126 126 yield ('string', program[s:pos], s)
127 127 break
128 128 pos += 1
129 129 else:
130 130 raise error.ParseError(_("unterminated string"), s)
131 131 elif c.isdigit():
132 132 s = pos
133 133 while pos < end:
134 134 d = program[pos]
135 135 if not d.isdigit():
136 136 break
137 137 pos += 1
138 138 yield ('integer', program[s:pos], s)
139 139 pos -= 1
140 140 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
141 141 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
142 142 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
143 143 # where some of nested templates were preprocessed as strings and
144 144 # then compiled. therefore, \"...\" was allowed. (issue4733)
145 145 #
146 146 # processing flow of _evalifliteral() at 5ab28a2e9962:
147 147 # outer template string -> stringify() -> compiletemplate()
148 148 # ------------------------ ------------ ------------------
149 149 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
150 150 # ~~~~~~~~
151 151 # escaped quoted string
152 152 if c == 'r':
153 153 pos += 1
154 154 token = 'string'
155 155 else:
156 156 token = 'template'
157 157 quote = program[pos:pos + 2]
158 158 s = pos = pos + 2
159 159 while pos < end: # find closing escaped quote
160 160 if program.startswith('\\\\\\', pos, end):
161 161 pos += 4 # skip over double escaped characters
162 162 continue
163 163 if program.startswith(quote, pos, end):
164 164 # interpret as if it were a part of an outer string
165 165 data = parser.unescapestr(program[s:pos])
166 166 if token == 'template':
167 167 data = _parsetemplate(data, 0, len(data))[0]
168 168 yield (token, data, s)
169 169 pos += 1
170 170 break
171 171 pos += 1
172 172 else:
173 173 raise error.ParseError(_("unterminated string"), s)
174 174 elif c.isalnum() or c in '_':
175 175 s = pos
176 176 pos += 1
177 177 while pos < end: # find end of symbol
178 178 d = program[pos]
179 179 if not (d.isalnum() or d == "_"):
180 180 break
181 181 pos += 1
182 182 sym = program[s:pos]
183 183 yield ('symbol', sym, s)
184 184 pos -= 1
185 185 elif c == term:
186 186 yield ('end', None, pos)
187 187 return
188 188 else:
189 189 raise error.ParseError(_("syntax error"), pos)
190 190 pos += 1
191 191 if term:
192 192 raise error.ParseError(_("unterminated template expansion"), start)
193 193 yield ('end', None, pos)
194 194
195 195 def _parsetemplate(tmpl, start, stop, quote=''):
196 196 r"""
197 197 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
198 198 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
199 199 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
200 200 ([('string', 'foo'), ('symbol', 'bar')], 9)
201 201 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
202 202 ([('string', 'foo')], 4)
203 203 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
204 204 ([('string', 'foo"'), ('string', 'bar')], 9)
205 205 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
206 206 ([('string', 'foo\\')], 6)
207 207 """
208 208 parsed = []
209 209 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
210 210 if typ == 'string':
211 211 parsed.append((typ, val))
212 212 elif typ == 'template':
213 213 parsed.append(val)
214 214 elif typ == 'end':
215 215 return parsed, pos
216 216 else:
217 217 raise error.ProgrammingError('unexpected type: %s' % typ)
218 218 raise error.ProgrammingError('unterminated scanning of template')
219 219
220 220 def scantemplate(tmpl, raw=False):
221 221 r"""Scan (type, start, end) positions of outermost elements in template
222 222
223 223 If raw=True, a backslash is not taken as an escape character just like
224 224 r'' string in Python. Note that this is different from r'' literal in
225 225 template in that no template fragment can appear in r'', e.g. r'{foo}'
226 226 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
227 227 'foo'.
228 228
229 229 >>> list(scantemplate(b'foo{bar}"baz'))
230 230 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
231 231 >>> list(scantemplate(b'outer{"inner"}outer'))
232 232 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
233 233 >>> list(scantemplate(b'foo\\{escaped}'))
234 234 [('string', 0, 5), ('string', 5, 13)]
235 235 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
236 236 [('string', 0, 4), ('template', 4, 13)]
237 237 """
238 238 last = None
239 239 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
240 240 if last:
241 241 yield last + (pos,)
242 242 if typ == 'end':
243 243 return
244 244 else:
245 245 last = (typ, pos)
246 246 raise error.ProgrammingError('unterminated scanning of template')
247 247
248 248 def _scantemplate(tmpl, start, stop, quote='', raw=False):
249 249 """Parse template string into chunks of strings and template expressions"""
250 250 sepchars = '{' + quote
251 251 unescape = [parser.unescapestr, pycompat.identity][raw]
252 252 pos = start
253 253 p = parser.parser(elements)
254 254 try:
255 255 while pos < stop:
256 256 n = min((tmpl.find(c, pos, stop)
257 257 for c in pycompat.bytestr(sepchars)),
258 258 key=lambda n: (n < 0, n))
259 259 if n < 0:
260 260 yield ('string', unescape(tmpl[pos:stop]), pos)
261 261 pos = stop
262 262 break
263 263 c = tmpl[n:n + 1]
264 264 bs = 0 # count leading backslashes
265 265 if not raw:
266 266 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
267 267 if bs % 2 == 1:
268 268 # escaped (e.g. '\{', '\\\{', but not '\\{')
269 269 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
270 270 pos = n + 1
271 271 continue
272 272 if n > pos:
273 273 yield ('string', unescape(tmpl[pos:n]), pos)
274 274 if c == quote:
275 275 yield ('end', None, n + 1)
276 276 return
277 277
278 278 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
279 279 if not tmpl.startswith('}', pos):
280 280 raise error.ParseError(_("invalid token"), pos)
281 281 yield ('template', parseres, n)
282 282 pos += 1
283 283
284 284 if quote:
285 285 raise error.ParseError(_("unterminated string"), start)
286 286 except error.ParseError as inst:
287 287 if len(inst.args) > 1: # has location
288 288 loc = inst.args[1]
289 289 # Offset the caret location by the number of newlines before the
290 290 # location of the error, since we will replace one-char newlines
291 291 # with the two-char literal r'\n'.
292 292 offset = tmpl[:loc].count('\n')
293 293 tmpl = tmpl.replace('\n', br'\n')
294 294 # We want the caret to point to the place in the template that
295 295 # failed to parse, but in a hint we get a open paren at the
296 296 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
297 297 # to line up the caret with the location of the error.
298 298 inst.hint = (tmpl + '\n'
299 299 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
300 300 raise
301 301 yield ('end', None, pos)
302 302
303 303 def _unnesttemplatelist(tree):
304 304 """Expand list of templates to node tuple
305 305
306 306 >>> def f(tree):
307 307 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
308 308 >>> f((b'template', []))
309 309 (string '')
310 310 >>> f((b'template', [(b'string', b'foo')]))
311 311 (string 'foo')
312 312 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
313 313 (template
314 314 (string 'foo')
315 315 (symbol 'rev'))
316 316 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
317 317 (template
318 318 (symbol 'rev'))
319 319 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
320 320 (string 'foo')
321 321 """
322 322 if not isinstance(tree, tuple):
323 323 return tree
324 324 op = tree[0]
325 325 if op != 'template':
326 326 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
327 327
328 328 assert len(tree) == 2
329 329 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
330 330 if not xs:
331 331 return ('string', '') # empty template ""
332 332 elif len(xs) == 1 and xs[0][0] == 'string':
333 333 return xs[0] # fast path for string with no template fragment "x"
334 334 else:
335 335 return (op,) + xs
336 336
337 337 def parse(tmpl):
338 338 """Parse template string into tree"""
339 339 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
340 340 assert pos == len(tmpl), 'unquoted template should be consumed'
341 341 return _unnesttemplatelist(('template', parsed))
342 342
343 343 def _parseexpr(expr):
344 344 """Parse a template expression into tree
345 345
346 346 >>> _parseexpr(b'"foo"')
347 347 ('string', 'foo')
348 348 >>> _parseexpr(b'foo(bar)')
349 349 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
350 350 >>> _parseexpr(b'foo(')
351 351 Traceback (most recent call last):
352 352 ...
353 353 ParseError: ('not a prefix: end', 4)
354 354 >>> _parseexpr(b'"foo" "bar"')
355 355 Traceback (most recent call last):
356 356 ...
357 357 ParseError: ('invalid token', 7)
358 358 """
359 359 p = parser.parser(elements)
360 360 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
361 361 if pos != len(expr):
362 362 raise error.ParseError(_('invalid token'), pos)
363 363 return _unnesttemplatelist(tree)
364 364
365 365 def prettyformat(tree):
366 366 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
367 367
368 368 def compileexp(exp, context, curmethods):
369 369 """Compile parsed template tree to (func, data) pair"""
370 370 if not exp:
371 371 raise error.ParseError(_("missing argument"))
372 372 t = exp[0]
373 373 if t in curmethods:
374 374 return curmethods[t](exp, context)
375 375 raise error.ParseError(_("unknown method '%s'") % t)
376 376
377 377 # template evaluation
378 378
379 379 def getsymbol(exp):
380 380 if exp[0] == 'symbol':
381 381 return exp[1]
382 382 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
383 383
384 384 def getlist(x):
385 385 if not x:
386 386 return []
387 387 if x[0] == 'list':
388 388 return getlist(x[1]) + [x[2]]
389 389 return [x]
390 390
391 391 def gettemplate(exp, context):
392 392 """Compile given template tree or load named template from map file;
393 393 returns (func, data) pair"""
394 394 if exp[0] in ('template', 'string'):
395 395 return compileexp(exp, context, methods)
396 396 if exp[0] == 'symbol':
397 397 # unlike runsymbol(), here 'symbol' is always taken as template name
398 398 # even if it exists in mapping. this allows us to override mapping
399 399 # by web templates, e.g. 'changelogtag' is redefined in map file.
400 400 return context._load(exp[1])
401 401 raise error.ParseError(_("expected template specifier"))
402 402
403 403 def _runrecursivesymbol(context, mapping, key):
404 404 raise error.Abort(_("recursive reference '%s' in template") % key)
405 405
406 406 def buildtemplate(exp, context):
407 407 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
408 408 return (templateutil.runtemplate, ctmpl)
409 409
410 410 def buildfilter(exp, context):
411 411 n = getsymbol(exp[2])
412 412 if n in context._filters:
413 413 filt = context._filters[n]
414 414 arg = compileexp(exp[1], context, methods)
415 415 return (templateutil.runfilter, (arg, filt))
416 416 if n in context._funcs:
417 417 f = context._funcs[n]
418 418 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
419 419 return (f, args)
420 420 raise error.ParseError(_("unknown function '%s'") % n)
421 421
422 422 def buildmap(exp, context):
423 423 darg = compileexp(exp[1], context, methods)
424 424 targ = gettemplate(exp[2], context)
425 425 return (templateutil.runmap, (darg, targ))
426 426
427 427 def buildmember(exp, context):
428 428 darg = compileexp(exp[1], context, methods)
429 429 memb = getsymbol(exp[2])
430 430 return (templateutil.runmember, (darg, memb))
431 431
432 432 def buildnegate(exp, context):
433 433 arg = compileexp(exp[1], context, exprmethods)
434 434 return (templateutil.runnegate, arg)
435 435
436 436 def buildarithmetic(exp, context, func):
437 437 left = compileexp(exp[1], context, exprmethods)
438 438 right = compileexp(exp[2], context, exprmethods)
439 439 return (templateutil.runarithmetic, (func, left, right))
440 440
441 441 def buildfunc(exp, context):
442 442 n = getsymbol(exp[1])
443 443 if n in context._funcs:
444 444 f = context._funcs[n]
445 445 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
446 446 return (f, args)
447 447 if n in context._filters:
448 448 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
449 449 if len(args) != 1:
450 450 raise error.ParseError(_("filter %s expects one argument") % n)
451 451 f = context._filters[n]
452 452 return (templateutil.runfilter, (args[0], f))
453 453 raise error.ParseError(_("unknown function '%s'") % n)
454 454
455 455 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
456 456 """Compile parsed tree of function arguments into list or dict of
457 457 (func, data) pairs
458 458
459 459 >>> context = engine(lambda t: (templateutil.runsymbol, t))
460 460 >>> def fargs(expr, argspec):
461 461 ... x = _parseexpr(expr)
462 462 ... n = getsymbol(x[1])
463 463 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
464 464 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
465 465 ['l', 'k']
466 466 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
467 467 >>> list(args.keys()), list(args[b'opts'].keys())
468 468 (['opts'], ['opts', 'k'])
469 469 """
470 470 def compiledict(xs):
471 471 return util.sortdict((k, compileexp(x, context, curmethods))
472 472 for k, x in xs.iteritems())
473 473 def compilelist(xs):
474 474 return [compileexp(x, context, curmethods) for x in xs]
475 475
476 476 if not argspec:
477 477 # filter or function with no argspec: return list of positional args
478 478 return compilelist(getlist(exp))
479 479
480 480 # function with argspec: return dict of named args
481 481 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
482 482 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
483 483 keyvaluenode='keyvalue', keynode='symbol')
484 484 compargs = util.sortdict()
485 485 if varkey:
486 486 compargs[varkey] = compilelist(treeargs.pop(varkey))
487 487 if optkey:
488 488 compargs[optkey] = compiledict(treeargs.pop(optkey))
489 489 compargs.update(compiledict(treeargs))
490 490 return compargs
491 491
492 492 def buildkeyvaluepair(exp, content):
493 493 raise error.ParseError(_("can't use a key-value pair in this context"))
494 494
495 495 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
496 496 exprmethods = {
497 497 "integer": lambda e, c: (templateutil.runinteger, e[1]),
498 498 "string": lambda e, c: (templateutil.runstring, e[1]),
499 499 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
500 500 "template": buildtemplate,
501 501 "group": lambda e, c: compileexp(e[1], c, exprmethods),
502 502 ".": buildmember,
503 503 "|": buildfilter,
504 504 "%": buildmap,
505 505 "func": buildfunc,
506 506 "keyvalue": buildkeyvaluepair,
507 507 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
508 508 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
509 509 "negate": buildnegate,
510 510 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
511 511 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
512 512 }
513 513
514 514 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
515 515 methods = exprmethods.copy()
516 516 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
517 517
518 518 class _aliasrules(parser.basealiasrules):
519 519 """Parsing and expansion rule set of template aliases"""
520 520 _section = _('template alias')
521 521 _parse = staticmethod(_parseexpr)
522 522
523 523 @staticmethod
524 524 def _trygetfunc(tree):
525 525 """Return (name, args) if tree is func(...) or ...|filter; otherwise
526 526 None"""
527 527 if tree[0] == 'func' and tree[1][0] == 'symbol':
528 528 return tree[1][1], getlist(tree[2])
529 529 if tree[0] == '|' and tree[2][0] == 'symbol':
530 530 return tree[2][1], [tree[1]]
531 531
532 532 def expandaliases(tree, aliases):
533 533 """Return new tree of aliases are expanded"""
534 534 aliasmap = _aliasrules.buildmap(aliases)
535 535 return _aliasrules.expand(aliasmap, tree)
536 536
537 537 # template engine
538 538
539 539 def unquotestring(s):
540 540 '''unwrap quotes if any; otherwise returns unmodified string'''
541 541 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
542 542 return s
543 543 return s[1:-1]
544 544
545 545 class resourcemapper(object):
546 546 """Mapper of internal template resources"""
547 547
548 548 __metaclass__ = abc.ABCMeta
549 549
550 550 @abc.abstractmethod
551 551 def availablekeys(self, context, mapping):
552 552 """Return a set of available resource keys based on the given mapping"""
553 553
554 554 @abc.abstractmethod
555 555 def knownkeys(self):
556 556 """Return a set of supported resource keys"""
557 557
558 558 @abc.abstractmethod
559 559 def lookup(self, context, mapping, key):
560 560 """Return a resource for the key if available; otherwise None"""
561 561
562 562 @abc.abstractmethod
563 563 def populatemap(self, context, origmapping, newmapping):
564 564 """Return a dict of additional mapping items which should be paired
565 565 with the given new mapping"""
566 566
567 567 class nullresourcemapper(resourcemapper):
568 568 def availablekeys(self, context, mapping):
569 569 return set()
570 570
571 571 def knownkeys(self):
572 572 return set()
573 573
574 574 def lookup(self, context, mapping, key):
575 575 return None
576 576
577 577 def populatemap(self, context, origmapping, newmapping):
578 578 return {}
579 579
580 580 class engine(object):
581 581 '''template expansion engine.
582 582
583 583 template expansion works like this. a map file contains key=value
584 584 pairs. if value is quoted, it is treated as string. otherwise, it
585 585 is treated as name of template file.
586 586
587 587 templater is asked to expand a key in map. it looks up key, and
588 588 looks for strings like this: {foo}. it expands {foo} by looking up
589 589 foo in map, and substituting it. expansion is recursive: it stops
590 590 when there is no more {foo} to replace.
591 591
592 592 expansion also allows formatting and filtering.
593 593
594 594 format uses key to expand each item in list. syntax is
595 595 {key%format}.
596 596
597 597 filter uses function to transform value. syntax is
598 598 {key|filter1|filter2|...}.'''
599 599
600 600 def __init__(self, loader, filters=None, defaults=None, resources=None):
601 601 self._loader = loader
602 602 if filters is None:
603 603 filters = {}
604 604 self._filters = filters
605 605 self._funcs = templatefuncs.funcs # make this a parameter if needed
606 606 if defaults is None:
607 607 defaults = {}
608 608 if resources is None:
609 609 resources = nullresourcemapper()
610 610 self._defaults = defaults
611 611 self._resources = resources
612 612 self._cache = {} # key: (func, data)
613 613 self._tmplcache = {} # literal template: (func, data)
614 614
615 615 def overlaymap(self, origmapping, newmapping):
616 616 """Create combined mapping from the original mapping and partial
617 617 mapping to override the original"""
618 618 # do not copy symbols which overrides the defaults depending on
619 619 # new resources, so the defaults will be re-evaluated (issue5612)
620 620 knownres = self._resources.knownkeys()
621 621 newres = self._resources.availablekeys(self, newmapping)
622 622 mapping = {k: v for k, v in origmapping.iteritems()
623 623 if (k in knownres # not a symbol per self.symbol()
624 624 or newres.isdisjoint(self._defaultrequires(k)))}
625 625 mapping.update(newmapping)
626 626 mapping.update(
627 627 self._resources.populatemap(self, origmapping, newmapping))
628 628 return mapping
629 629
630 630 def _defaultrequires(self, key):
631 631 """Resource keys required by the specified default symbol function"""
632 632 v = self._defaults.get(key)
633 633 if v is None or not callable(v):
634 634 return ()
635 635 return getattr(v, '_requires', ())
636 636
637 637 def symbol(self, mapping, key):
638 638 """Resolve symbol to value or function; None if nothing found"""
639 639 v = None
640 640 if key not in self._resources.knownkeys():
641 641 v = mapping.get(key)
642 642 if v is None:
643 643 v = self._defaults.get(key)
644 644 return v
645 645
646 646 def availableresourcekeys(self, mapping):
647 647 """Return a set of available resource keys based on the given mapping"""
648 648 return self._resources.availablekeys(self, mapping)
649 649
650 650 def knownresourcekeys(self):
651 651 """Return a set of supported resource keys"""
652 652 return self._resources.knownkeys()
653 653
654 654 def resource(self, mapping, key):
655 655 """Return internal data (e.g. cache) used for keyword/function
656 656 evaluation"""
657 657 v = self._resources.lookup(self, mapping, key)
658 658 if v is None:
659 659 raise templateutil.ResourceUnavailable(
660 660 _('template resource not available: %s') % key)
661 661 return v
662 662
663 663 def _load(self, t):
664 664 '''load, parse, and cache a template'''
665 665 if t not in self._cache:
666 666 x = self._loader(t)
667 667 # put poison to cut recursion while compiling 't'
668 668 self._cache[t] = (_runrecursivesymbol, t)
669 669 try:
670 670 self._cache[t] = compileexp(x, self, methods)
671 671 except: # re-raises
672 672 del self._cache[t]
673 673 raise
674 674 return self._cache[t]
675 675
676 676 def _parse(self, tmpl):
677 677 """Parse and cache a literal template"""
678 678 if tmpl not in self._tmplcache:
679 679 x = parse(tmpl)
680 680 self._tmplcache[tmpl] = compileexp(x, self, methods)
681 681 return self._tmplcache[tmpl]
682 682
683 683 def preload(self, t):
684 684 """Load, parse, and cache the specified template if available"""
685 685 try:
686 686 self._load(t)
687 687 return True
688 688 except templateutil.TemplateNotFound:
689 689 return False
690 690
691 691 def process(self, t, mapping):
692 692 '''Perform expansion. t is name of map element to expand.
693 693 mapping contains added elements for use during expansion. Is a
694 694 generator.'''
695 695 func, data = self._load(t)
696 696 return self._expand(func, data, mapping)
697 697
698 698 def expand(self, tmpl, mapping):
699 699 """Perform expansion over a literal template
700 700
701 701 No user aliases will be expanded since this is supposed to be called
702 702 with an internal template string.
703 703 """
704 704 func, data = self._parse(tmpl)
705 705 return self._expand(func, data, mapping)
706 706
707 707 def _expand(self, func, data, mapping):
708 708 # populate additional items only if they don't exist in the given
709 709 # mapping. this is slightly different from overlaymap() because the
710 710 # initial 'revcache' may contain pre-computed items.
711 711 extramapping = self._resources.populatemap(self, {}, mapping)
712 712 if extramapping:
713 713 extramapping.update(mapping)
714 714 mapping = extramapping
715 715 return templateutil.flatten(self, mapping, func(self, mapping, data))
716 716
717 717 def stylelist():
718 718 paths = templatepaths()
719 719 if not paths:
720 720 return _('no templates found, try `hg debuginstall` for more info')
721 721 dirlist = os.listdir(paths[0])
722 722 stylelist = []
723 723 for file in dirlist:
724 724 split = file.split(".")
725 725 if split[-1] in ('orig', 'rej'):
726 726 continue
727 727 if split[0] == "map-cmdline":
728 728 stylelist.append(split[1])
729 729 return ", ".join(sorted(stylelist))
730 730
731 731 def _readmapfile(mapfile):
732 732 """Load template elements from the given map file"""
733 733 if not os.path.exists(mapfile):
734 734 raise error.Abort(_("style '%s' not found") % mapfile,
735 735 hint=_("available styles: %s") % stylelist())
736 736
737 737 base = os.path.dirname(mapfile)
738 738 conf = config.config(includepaths=templatepaths())
739 739 conf.read(mapfile, remap={'': 'templates'})
740 740
741 741 cache = {}
742 742 tmap = {}
743 743 aliases = []
744 744
745 745 val = conf.get('templates', '__base__')
746 746 if val and val[0] not in "'\"":
747 747 # treat as a pointer to a base class for this style
748 748 path = util.normpath(os.path.join(base, val))
749 749
750 750 # fallback check in template paths
751 751 if not os.path.exists(path):
752 752 for p in templatepaths():
753 753 p2 = util.normpath(os.path.join(p, val))
754 754 if os.path.isfile(p2):
755 755 path = p2
756 756 break
757 757 p3 = util.normpath(os.path.join(p2, "map"))
758 758 if os.path.isfile(p3):
759 759 path = p3
760 760 break
761 761
762 762 cache, tmap, aliases = _readmapfile(path)
763 763
764 764 for key, val in conf['templates'].items():
765 765 if not val:
766 766 raise error.ParseError(_('missing value'),
767 767 conf.source('templates', key))
768 768 if val[0] in "'\"":
769 769 if val[0] != val[-1]:
770 770 raise error.ParseError(_('unmatched quotes'),
771 771 conf.source('templates', key))
772 772 cache[key] = unquotestring(val)
773 773 elif key != '__base__':
774 774 tmap[key] = os.path.join(base, val)
775 775 aliases.extend(conf['templatealias'].items())
776 776 return cache, tmap, aliases
777 777
778 778 class loader(object):
779 779 """Load template fragments optionally from a map file"""
780 780
781 781 def __init__(self, cache, aliases):
782 782 if cache is None:
783 783 cache = {}
784 784 self.cache = cache.copy()
785 785 self._map = {}
786 786 self._aliasmap = _aliasrules.buildmap(aliases)
787 787
788 788 def __contains__(self, key):
789 789 return key in self.cache or key in self._map
790 790
791 791 def load(self, t):
792 792 """Get parsed tree for the given template name. Use a local cache."""
793 793 if t not in self.cache:
794 794 try:
795 795 self.cache[t] = util.readfile(self._map[t])
796 796 except KeyError as inst:
797 797 raise templateutil.TemplateNotFound(
798 798 _('"%s" not in template map') % inst.args[0])
799 799 except IOError as inst:
800 800 reason = (_('template file %s: %s')
801 801 % (self._map[t],
802 802 stringutil.forcebytestr(inst.args[1])))
803 803 raise IOError(inst.args[0], encoding.strfromlocal(reason))
804 804 return self._parse(self.cache[t])
805 805
806 806 def _parse(self, tmpl):
807 807 x = parse(tmpl)
808 808 if self._aliasmap:
809 809 x = _aliasrules.expand(self._aliasmap, x)
810 810 return x
811 811
812 812 def _findsymbolsused(self, tree, syms):
813 813 if not tree:
814 814 return
815 815 op = tree[0]
816 816 if op == 'symbol':
817 817 s = tree[1]
818 818 if s in syms[0]:
819 819 return # avoid recursion: s -> cache[s] -> s
820 820 syms[0].add(s)
821 821 if s in self.cache or s in self._map:
822 822 # s may be a reference for named template
823 823 self._findsymbolsused(self.load(s), syms)
824 824 return
825 825 if op in {'integer', 'string'}:
826 826 return
827 827 # '{arg|func}' == '{func(arg)}'
828 828 if op == '|':
829 829 syms[1].add(getsymbol(tree[2]))
830 830 self._findsymbolsused(tree[1], syms)
831 831 return
832 832 if op == 'func':
833 833 syms[1].add(getsymbol(tree[1]))
834 834 self._findsymbolsused(tree[2], syms)
835 835 return
836 836 for x in tree[1:]:
837 837 self._findsymbolsused(x, syms)
838 838
839 839 def symbolsused(self, t):
840 840 """Look up (keywords, filters/functions) referenced from the name
841 841 template 't'
842 842
843 843 This may load additional templates from the map file.
844 844 """
845 845 syms = (set(), set())
846 846 self._findsymbolsused(self.load(t), syms)
847 847 return syms
848 848
849 849 class templater(object):
850 850
851 851 def __init__(self, filters=None, defaults=None, resources=None,
852 852 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
853 853 """Create template engine optionally with preloaded template fragments
854 854
855 855 - ``filters``: a dict of functions to transform a value into another.
856 856 - ``defaults``: a dict of symbol values/functions; may be overridden
857 857 by a ``mapping`` dict.
858 858 - ``resources``: a resourcemapper object to look up internal data
859 859 (e.g. cache), inaccessible from user template.
860 860 - ``cache``: a dict of preloaded template fragments.
861 861 - ``aliases``: a list of alias (name, replacement) pairs.
862 862
863 863 self.cache may be updated later to register additional template
864 864 fragments.
865 865 """
866 866 if filters is None:
867 867 filters = {}
868 868 if defaults is None:
869 869 defaults = {}
870 870 self._filters = templatefilters.filters.copy()
871 871 self._filters.update(filters)
872 872 self.defaults = defaults
873 873 self._resources = resources
874 874 self._loader = loader(cache, aliases)
875 self._proc = engine(self._loader.load, self._filters, self.defaults,
876 self._resources)
875 877 self._minchunk, self._maxchunk = minchunk, maxchunk
876 878
877 879 @classmethod
878 880 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
879 881 cache=None, minchunk=1024, maxchunk=65536):
880 882 """Create templater from the specified map file"""
881 883 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
882 884 cache, tmap, aliases = _readmapfile(mapfile)
883 885 t._loader.cache.update(cache)
884 886 t._loader._map = tmap
885 887 t._loader._aliasmap = _aliasrules.buildmap(aliases)
886 888 return t
887 889
888 890 def __contains__(self, key):
889 891 return key in self._loader
890 892
891 893 @property
892 894 def cache(self):
893 895 return self._loader.cache
894 896
895 897 def load(self, t):
896 898 """Get parsed tree for the given template name. Use a local cache."""
897 899 return self._loader.load(t)
898 900
899 901 def symbolsuseddefault(self):
900 902 """Look up (keywords, filters/functions) referenced from the default
901 903 unnamed template
902 904
903 905 This may load additional templates from the map file.
904 906 """
905 907 return self.symbolsused('')
906 908
907 909 def symbolsused(self, t):
908 910 """Look up (keywords, filters/functions) referenced from the name
909 911 template 't'
910 912
911 913 This may load additional templates from the map file.
912 914 """
913 915 return self._loader.symbolsused(t)
914 916
915 917 def renderdefault(self, mapping):
916 918 """Render the default unnamed template and return result as string"""
917 919 return self.render('', mapping)
918 920
919 921 def render(self, t, mapping):
920 922 """Render the specified named template and return result as string"""
921 923 return b''.join(self.generate(t, mapping))
922 924
923 925 def generate(self, t, mapping):
924 926 """Return a generator that renders the specified named template and
925 927 yields chunks"""
926 proc = engine(self.load, self._filters, self.defaults, self._resources)
927 stream = proc.process(t, mapping)
928 stream = self._proc.process(t, mapping)
928 929 if self._minchunk:
929 930 stream = util.increasingchunks(stream, min=self._minchunk,
930 931 max=self._maxchunk)
931 932 return stream
932 933
933 934 def templatepaths():
934 935 '''return locations used for template files.'''
935 936 pathsrel = ['templates']
936 937 paths = [os.path.normpath(os.path.join(util.datapath, f))
937 938 for f in pathsrel]
938 939 return [p for p in paths if os.path.isdir(p)]
939 940
940 941 def templatepath(name):
941 942 '''return location of template file. returns None if not found.'''
942 943 for p in templatepaths():
943 944 f = os.path.join(p, name)
944 945 if os.path.exists(f):
945 946 return f
946 947 return None
947 948
948 949 def stylemap(styles, paths=None):
949 950 """Return path to mapfile for a given style.
950 951
951 952 Searches mapfile in the following locations:
952 953 1. templatepath/style/map
953 954 2. templatepath/map-style
954 955 3. templatepath/map
955 956 """
956 957
957 958 if paths is None:
958 959 paths = templatepaths()
959 960 elif isinstance(paths, bytes):
960 961 paths = [paths]
961 962
962 963 if isinstance(styles, bytes):
963 964 styles = [styles]
964 965
965 966 for style in styles:
966 967 # only plain name is allowed to honor template paths
967 968 if (not style
968 969 or style in (pycompat.oscurdir, pycompat.ospardir)
969 970 or pycompat.ossep in style
970 971 or pycompat.osaltsep and pycompat.osaltsep in style):
971 972 continue
972 973 locations = [os.path.join(style, 'map'), 'map-' + style]
973 974 locations.append('map')
974 975
975 976 for path in paths:
976 977 for location in locations:
977 978 mapfile = os.path.join(path, location)
978 979 if os.path.isfile(mapfile):
979 980 return style, mapfile
980 981
981 982 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now