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