##// END OF EJS Templates
templater: make open_template() read from resources if in frozen binary...
Martin von Zweigbergk -
r45871:3b27ed8e default
parent child Browse files
Show More
@@ -1,1087 +1,1099 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 len(inst.args) <= 1:
316 316 return # no location
317 317 loc = inst.args[1]
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 >>> parseexpr(b'foo(')
380 380 Traceback (most recent call last):
381 381 ...
382 382 ParseError: ('not a prefix: end', 4)
383 383 >>> parseexpr(b'"foo" "bar"')
384 384 Traceback (most recent call last):
385 385 ...
386 386 ParseError: ('invalid token', 7)
387 387 """
388 388 try:
389 389 return _parseexpr(expr)
390 390 except error.ParseError as inst:
391 391 _addparseerrorhint(inst, expr)
392 392 raise
393 393
394 394
395 395 def _parseexpr(expr):
396 396 p = parser.parser(elements)
397 397 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
398 398 if pos != len(expr):
399 399 raise error.ParseError(_(b'invalid token'), pos)
400 400 return _unnesttemplatelist(tree)
401 401
402 402
403 403 def prettyformat(tree):
404 404 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
405 405
406 406
407 407 def compileexp(exp, context, curmethods):
408 408 """Compile parsed template tree to (func, data) pair"""
409 409 if not exp:
410 410 raise error.ParseError(_(b"missing argument"))
411 411 t = exp[0]
412 412 return curmethods[t](exp, context)
413 413
414 414
415 415 # template evaluation
416 416
417 417
418 418 def getsymbol(exp):
419 419 if exp[0] == b'symbol':
420 420 return exp[1]
421 421 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
422 422
423 423
424 424 def getlist(x):
425 425 if not x:
426 426 return []
427 427 if x[0] == b'list':
428 428 return getlist(x[1]) + [x[2]]
429 429 return [x]
430 430
431 431
432 432 def gettemplate(exp, context):
433 433 """Compile given template tree or load named template from map file;
434 434 returns (func, data) pair"""
435 435 if exp[0] in (b'template', b'string'):
436 436 return compileexp(exp, context, methods)
437 437 if exp[0] == b'symbol':
438 438 # unlike runsymbol(), here 'symbol' is always taken as template name
439 439 # even if it exists in mapping. this allows us to override mapping
440 440 # by web templates, e.g. 'changelogtag' is redefined in map file.
441 441 return context._load(exp[1])
442 442 raise error.ParseError(_(b"expected template specifier"))
443 443
444 444
445 445 def _runrecursivesymbol(context, mapping, key):
446 446 raise error.Abort(_(b"recursive reference '%s' in template") % key)
447 447
448 448
449 449 def buildtemplate(exp, context):
450 450 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
451 451 return (templateutil.runtemplate, ctmpl)
452 452
453 453
454 454 def buildfilter(exp, context):
455 455 n = getsymbol(exp[2])
456 456 if n in context._filters:
457 457 filt = context._filters[n]
458 458 arg = compileexp(exp[1], context, methods)
459 459 return (templateutil.runfilter, (arg, filt))
460 460 if n in context._funcs:
461 461 f = context._funcs[n]
462 462 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
463 463 return (f, args)
464 464 raise error.ParseError(_(b"unknown function '%s'") % n)
465 465
466 466
467 467 def buildmap(exp, context):
468 468 darg = compileexp(exp[1], context, methods)
469 469 targ = gettemplate(exp[2], context)
470 470 return (templateutil.runmap, (darg, targ))
471 471
472 472
473 473 def buildmember(exp, context):
474 474 darg = compileexp(exp[1], context, methods)
475 475 memb = getsymbol(exp[2])
476 476 return (templateutil.runmember, (darg, memb))
477 477
478 478
479 479 def buildnegate(exp, context):
480 480 arg = compileexp(exp[1], context, exprmethods)
481 481 return (templateutil.runnegate, arg)
482 482
483 483
484 484 def buildarithmetic(exp, context, func):
485 485 left = compileexp(exp[1], context, exprmethods)
486 486 right = compileexp(exp[2], context, exprmethods)
487 487 return (templateutil.runarithmetic, (func, left, right))
488 488
489 489
490 490 def buildfunc(exp, context):
491 491 n = getsymbol(exp[1])
492 492 if n in context._funcs:
493 493 f = context._funcs[n]
494 494 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
495 495 return (f, args)
496 496 if n in context._filters:
497 497 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
498 498 if len(args) != 1:
499 499 raise error.ParseError(_(b"filter %s expects one argument") % n)
500 500 f = context._filters[n]
501 501 return (templateutil.runfilter, (args[0], f))
502 502 raise error.ParseError(_(b"unknown function '%s'") % n)
503 503
504 504
505 505 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
506 506 """Compile parsed tree of function arguments into list or dict of
507 507 (func, data) pairs
508 508
509 509 >>> context = engine(lambda t: (templateutil.runsymbol, t))
510 510 >>> def fargs(expr, argspec):
511 511 ... x = _parseexpr(expr)
512 512 ... n = getsymbol(x[1])
513 513 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
514 514 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
515 515 ['l', 'k']
516 516 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
517 517 >>> list(args.keys()), list(args[b'opts'].keys())
518 518 (['opts'], ['opts', 'k'])
519 519 """
520 520
521 521 def compiledict(xs):
522 522 return util.sortdict(
523 523 (k, compileexp(x, context, curmethods))
524 524 for k, x in pycompat.iteritems(xs)
525 525 )
526 526
527 527 def compilelist(xs):
528 528 return [compileexp(x, context, curmethods) for x in xs]
529 529
530 530 if not argspec:
531 531 # filter or function with no argspec: return list of positional args
532 532 return compilelist(getlist(exp))
533 533
534 534 # function with argspec: return dict of named args
535 535 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
536 536 treeargs = parser.buildargsdict(
537 537 getlist(exp),
538 538 funcname,
539 539 argspec,
540 540 keyvaluenode=b'keyvalue',
541 541 keynode=b'symbol',
542 542 )
543 543 compargs = util.sortdict()
544 544 if varkey:
545 545 compargs[varkey] = compilelist(treeargs.pop(varkey))
546 546 if optkey:
547 547 compargs[optkey] = compiledict(treeargs.pop(optkey))
548 548 compargs.update(compiledict(treeargs))
549 549 return compargs
550 550
551 551
552 552 def buildkeyvaluepair(exp, content):
553 553 raise error.ParseError(_(b"can't use a key-value pair in this context"))
554 554
555 555
556 556 def buildlist(exp, context):
557 557 raise error.ParseError(
558 558 _(b"can't use a list in this context"),
559 559 hint=_(b'check place of comma and parens'),
560 560 )
561 561
562 562
563 563 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
564 564 exprmethods = {
565 565 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
566 566 b"string": lambda e, c: (templateutil.runstring, e[1]),
567 567 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
568 568 b"template": buildtemplate,
569 569 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
570 570 b".": buildmember,
571 571 b"|": buildfilter,
572 572 b"%": buildmap,
573 573 b"func": buildfunc,
574 574 b"keyvalue": buildkeyvaluepair,
575 575 b"list": buildlist,
576 576 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
577 577 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
578 578 b"negate": buildnegate,
579 579 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
580 580 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
581 581 }
582 582
583 583 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
584 584 methods = exprmethods.copy()
585 585 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
586 586
587 587
588 588 class _aliasrules(parser.basealiasrules):
589 589 """Parsing and expansion rule set of template aliases"""
590 590
591 591 _section = _(b'template alias')
592 592 _parse = staticmethod(_parseexpr)
593 593
594 594 @staticmethod
595 595 def _trygetfunc(tree):
596 596 """Return (name, args) if tree is func(...) or ...|filter; otherwise
597 597 None"""
598 598 if tree[0] == b'func' and tree[1][0] == b'symbol':
599 599 return tree[1][1], getlist(tree[2])
600 600 if tree[0] == b'|' and tree[2][0] == b'symbol':
601 601 return tree[2][1], [tree[1]]
602 602
603 603
604 604 def expandaliases(tree, aliases):
605 605 """Return new tree of aliases are expanded"""
606 606 aliasmap = _aliasrules.buildmap(aliases)
607 607 return _aliasrules.expand(aliasmap, tree)
608 608
609 609
610 610 # template engine
611 611
612 612
613 613 def unquotestring(s):
614 614 '''unwrap quotes if any; otherwise returns unmodified string'''
615 615 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
616 616 return s
617 617 return s[1:-1]
618 618
619 619
620 620 class resourcemapper(object): # pytype: disable=ignored-metaclass
621 621 """Mapper of internal template resources"""
622 622
623 623 __metaclass__ = abc.ABCMeta
624 624
625 625 @abc.abstractmethod
626 626 def availablekeys(self, mapping):
627 627 """Return a set of available resource keys based on the given mapping"""
628 628
629 629 @abc.abstractmethod
630 630 def knownkeys(self):
631 631 """Return a set of supported resource keys"""
632 632
633 633 @abc.abstractmethod
634 634 def lookup(self, mapping, key):
635 635 """Return a resource for the key if available; otherwise None"""
636 636
637 637 @abc.abstractmethod
638 638 def populatemap(self, context, origmapping, newmapping):
639 639 """Return a dict of additional mapping items which should be paired
640 640 with the given new mapping"""
641 641
642 642
643 643 class nullresourcemapper(resourcemapper):
644 644 def availablekeys(self, mapping):
645 645 return set()
646 646
647 647 def knownkeys(self):
648 648 return set()
649 649
650 650 def lookup(self, mapping, key):
651 651 return None
652 652
653 653 def populatemap(self, context, origmapping, newmapping):
654 654 return {}
655 655
656 656
657 657 class engine(object):
658 658 '''template expansion engine.
659 659
660 660 template expansion works like this. a map file contains key=value
661 661 pairs. if value is quoted, it is treated as string. otherwise, it
662 662 is treated as name of template file.
663 663
664 664 templater is asked to expand a key in map. it looks up key, and
665 665 looks for strings like this: {foo}. it expands {foo} by looking up
666 666 foo in map, and substituting it. expansion is recursive: it stops
667 667 when there is no more {foo} to replace.
668 668
669 669 expansion also allows formatting and filtering.
670 670
671 671 format uses key to expand each item in list. syntax is
672 672 {key%format}.
673 673
674 674 filter uses function to transform value. syntax is
675 675 {key|filter1|filter2|...}.'''
676 676
677 677 def __init__(self, loader, filters=None, defaults=None, resources=None):
678 678 self._loader = loader
679 679 if filters is None:
680 680 filters = {}
681 681 self._filters = filters
682 682 self._funcs = templatefuncs.funcs # make this a parameter if needed
683 683 if defaults is None:
684 684 defaults = {}
685 685 if resources is None:
686 686 resources = nullresourcemapper()
687 687 self._defaults = defaults
688 688 self._resources = resources
689 689 self._cache = {} # key: (func, data)
690 690 self._tmplcache = {} # literal template: (func, data)
691 691
692 692 def overlaymap(self, origmapping, newmapping):
693 693 """Create combined mapping from the original mapping and partial
694 694 mapping to override the original"""
695 695 # do not copy symbols which overrides the defaults depending on
696 696 # new resources, so the defaults will be re-evaluated (issue5612)
697 697 knownres = self._resources.knownkeys()
698 698 newres = self._resources.availablekeys(newmapping)
699 699 mapping = {
700 700 k: v
701 701 for k, v in pycompat.iteritems(origmapping)
702 702 if (
703 703 k in knownres # not a symbol per self.symbol()
704 704 or newres.isdisjoint(self._defaultrequires(k))
705 705 )
706 706 }
707 707 mapping.update(newmapping)
708 708 mapping.update(
709 709 self._resources.populatemap(self, origmapping, newmapping)
710 710 )
711 711 return mapping
712 712
713 713 def _defaultrequires(self, key):
714 714 """Resource keys required by the specified default symbol function"""
715 715 v = self._defaults.get(key)
716 716 if v is None or not callable(v):
717 717 return ()
718 718 return getattr(v, '_requires', ())
719 719
720 720 def symbol(self, mapping, key):
721 721 """Resolve symbol to value or function; None if nothing found"""
722 722 v = None
723 723 if key not in self._resources.knownkeys():
724 724 v = mapping.get(key)
725 725 if v is None:
726 726 v = self._defaults.get(key)
727 727 return v
728 728
729 729 def availableresourcekeys(self, mapping):
730 730 """Return a set of available resource keys based on the given mapping"""
731 731 return self._resources.availablekeys(mapping)
732 732
733 733 def knownresourcekeys(self):
734 734 """Return a set of supported resource keys"""
735 735 return self._resources.knownkeys()
736 736
737 737 def resource(self, mapping, key):
738 738 """Return internal data (e.g. cache) used for keyword/function
739 739 evaluation"""
740 740 v = self._resources.lookup(mapping, key)
741 741 if v is None:
742 742 raise templateutil.ResourceUnavailable(
743 743 _(b'template resource not available: %s') % key
744 744 )
745 745 return v
746 746
747 747 def _load(self, t):
748 748 '''load, parse, and cache a template'''
749 749 if t not in self._cache:
750 750 x = self._loader(t)
751 751 # put poison to cut recursion while compiling 't'
752 752 self._cache[t] = (_runrecursivesymbol, t)
753 753 try:
754 754 self._cache[t] = compileexp(x, self, methods)
755 755 except: # re-raises
756 756 del self._cache[t]
757 757 raise
758 758 return self._cache[t]
759 759
760 760 def _parse(self, tmpl):
761 761 """Parse and cache a literal template"""
762 762 if tmpl not in self._tmplcache:
763 763 x = parse(tmpl)
764 764 self._tmplcache[tmpl] = compileexp(x, self, methods)
765 765 return self._tmplcache[tmpl]
766 766
767 767 def preload(self, t):
768 768 """Load, parse, and cache the specified template if available"""
769 769 try:
770 770 self._load(t)
771 771 return True
772 772 except templateutil.TemplateNotFound:
773 773 return False
774 774
775 775 def process(self, t, mapping):
776 776 '''Perform expansion. t is name of map element to expand.
777 777 mapping contains added elements for use during expansion. Is a
778 778 generator.'''
779 779 func, data = self._load(t)
780 780 return self._expand(func, data, mapping)
781 781
782 782 def expand(self, tmpl, mapping):
783 783 """Perform expansion over a literal template
784 784
785 785 No user aliases will be expanded since this is supposed to be called
786 786 with an internal template string.
787 787 """
788 788 func, data = self._parse(tmpl)
789 789 return self._expand(func, data, mapping)
790 790
791 791 def _expand(self, func, data, mapping):
792 792 # populate additional items only if they don't exist in the given
793 793 # mapping. this is slightly different from overlaymap() because the
794 794 # initial 'revcache' may contain pre-computed items.
795 795 extramapping = self._resources.populatemap(self, {}, mapping)
796 796 if extramapping:
797 797 extramapping.update(mapping)
798 798 mapping = extramapping
799 799 return templateutil.flatten(self, mapping, func(self, mapping, data))
800 800
801 801
802 802 def stylelist():
803 803 path = templatedir()
804 804 if not path:
805 805 return _(b'no templates found, try `hg debuginstall` for more info')
806 806 dirlist = os.listdir(path)
807 807 stylelist = []
808 808 for file in dirlist:
809 809 split = file.split(b".")
810 810 if split[-1] in (b'orig', b'rej'):
811 811 continue
812 812 if split[0] == b"map-cmdline":
813 813 stylelist.append(split[1])
814 814 return b", ".join(sorted(stylelist))
815 815
816 816
817 817 def _open_mapfile(mapfile):
818 818 if os.path.exists(mapfile):
819 819 return util.posixfile(mapfile, b'rb')
820 820 raise error.Abort(
821 821 _(b"style '%s' not found") % mapfile,
822 822 hint=_(b"available styles: %s") % stylelist(),
823 823 )
824 824
825 825
826 826 def _readmapfile(fp, mapfile):
827 827 """Load template elements from the given map file"""
828 828 base = os.path.dirname(mapfile)
829 829 conf = config.config()
830 830
831 831 def include(rel, remap, sections):
832 832 templatedirs = [base, templatedir()]
833 833 for dir in templatedirs:
834 834 if dir is None:
835 835 continue
836 836 abs = os.path.normpath(os.path.join(dir, rel))
837 837 if os.path.isfile(abs):
838 838 data = util.posixfile(abs, b'rb').read()
839 839 conf.parse(
840 840 abs, data, sections=sections, remap=remap, include=include
841 841 )
842 842 break
843 843
844 844 data = fp.read()
845 845 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
846 846
847 847 cache = {}
848 848 tmap = {}
849 849 aliases = []
850 850
851 851 val = conf.get(b'templates', b'__base__')
852 852 if val and val[0] not in b"'\"":
853 853 # treat as a pointer to a base class for this style
854 854 path = os.path.normpath(os.path.join(base, val))
855 855
856 856 # fallback check in template paths
857 857 if not os.path.exists(path):
858 858 dir = templatedir()
859 859 if dir is not None:
860 860 p2 = os.path.normpath(os.path.join(dir, val))
861 861 if os.path.isfile(p2):
862 862 path = p2
863 863 else:
864 864 p3 = os.path.normpath(os.path.join(p2, b"map"))
865 865 if os.path.isfile(p3):
866 866 path = p3
867 867
868 868 fp = _open_mapfile(path)
869 869 cache, tmap, aliases = _readmapfile(fp, path)
870 870
871 871 for key, val in conf[b'templates'].items():
872 872 if not val:
873 873 raise error.ParseError(
874 874 _(b'missing value'), conf.source(b'templates', key)
875 875 )
876 876 if val[0] in b"'\"":
877 877 if val[0] != val[-1]:
878 878 raise error.ParseError(
879 879 _(b'unmatched quotes'), conf.source(b'templates', key)
880 880 )
881 881 cache[key] = unquotestring(val)
882 882 elif key != b'__base__':
883 883 tmap[key] = os.path.join(base, val)
884 884 aliases.extend(conf[b'templatealias'].items())
885 885 return cache, tmap, aliases
886 886
887 887
888 888 class loader(object):
889 889 """Load template fragments optionally from a map file"""
890 890
891 891 def __init__(self, cache, aliases):
892 892 if cache is None:
893 893 cache = {}
894 894 self.cache = cache.copy()
895 895 self._map = {}
896 896 self._aliasmap = _aliasrules.buildmap(aliases)
897 897
898 898 def __contains__(self, key):
899 899 return key in self.cache or key in self._map
900 900
901 901 def load(self, t):
902 902 """Get parsed tree for the given template name. Use a local cache."""
903 903 if t not in self.cache:
904 904 try:
905 905 self.cache[t] = util.readfile(self._map[t])
906 906 except KeyError as inst:
907 907 raise templateutil.TemplateNotFound(
908 908 _(b'"%s" not in template map') % inst.args[0]
909 909 )
910 910 except IOError as inst:
911 911 reason = _(b'template file %s: %s') % (
912 912 self._map[t],
913 913 stringutil.forcebytestr(inst.args[1]),
914 914 )
915 915 raise IOError(inst.args[0], encoding.strfromlocal(reason))
916 916 return self._parse(self.cache[t])
917 917
918 918 def _parse(self, tmpl):
919 919 x = parse(tmpl)
920 920 if self._aliasmap:
921 921 x = _aliasrules.expand(self._aliasmap, x)
922 922 return x
923 923
924 924 def _findsymbolsused(self, tree, syms):
925 925 if not tree:
926 926 return
927 927 op = tree[0]
928 928 if op == b'symbol':
929 929 s = tree[1]
930 930 if s in syms[0]:
931 931 return # avoid recursion: s -> cache[s] -> s
932 932 syms[0].add(s)
933 933 if s in self.cache or s in self._map:
934 934 # s may be a reference for named template
935 935 self._findsymbolsused(self.load(s), syms)
936 936 return
937 937 if op in {b'integer', b'string'}:
938 938 return
939 939 # '{arg|func}' == '{func(arg)}'
940 940 if op == b'|':
941 941 syms[1].add(getsymbol(tree[2]))
942 942 self._findsymbolsused(tree[1], syms)
943 943 return
944 944 if op == b'func':
945 945 syms[1].add(getsymbol(tree[1]))
946 946 self._findsymbolsused(tree[2], syms)
947 947 return
948 948 for x in tree[1:]:
949 949 self._findsymbolsused(x, syms)
950 950
951 951 def symbolsused(self, t):
952 952 """Look up (keywords, filters/functions) referenced from the name
953 953 template 't'
954 954
955 955 This may load additional templates from the map file.
956 956 """
957 957 syms = (set(), set())
958 958 self._findsymbolsused(self.load(t), syms)
959 959 return syms
960 960
961 961
962 962 class templater(object):
963 963 def __init__(
964 964 self,
965 965 filters=None,
966 966 defaults=None,
967 967 resources=None,
968 968 cache=None,
969 969 aliases=(),
970 970 minchunk=1024,
971 971 maxchunk=65536,
972 972 ):
973 973 """Create template engine optionally with preloaded template fragments
974 974
975 975 - ``filters``: a dict of functions to transform a value into another.
976 976 - ``defaults``: a dict of symbol values/functions; may be overridden
977 977 by a ``mapping`` dict.
978 978 - ``resources``: a resourcemapper object to look up internal data
979 979 (e.g. cache), inaccessible from user template.
980 980 - ``cache``: a dict of preloaded template fragments.
981 981 - ``aliases``: a list of alias (name, replacement) pairs.
982 982
983 983 self.cache may be updated later to register additional template
984 984 fragments.
985 985 """
986 986 allfilters = templatefilters.filters.copy()
987 987 if filters:
988 988 allfilters.update(filters)
989 989 self._loader = loader(cache, aliases)
990 990 self._proc = engine(self._loader.load, allfilters, defaults, resources)
991 991 self._minchunk, self._maxchunk = minchunk, maxchunk
992 992
993 993 @classmethod
994 994 def frommapfile(
995 995 cls,
996 996 mapfile,
997 997 fp=None,
998 998 filters=None,
999 999 defaults=None,
1000 1000 resources=None,
1001 1001 cache=None,
1002 1002 minchunk=1024,
1003 1003 maxchunk=65536,
1004 1004 ):
1005 1005 """Create templater from the specified map file"""
1006 1006 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1007 1007 if not fp:
1008 1008 fp = _open_mapfile(mapfile)
1009 1009 cache, tmap, aliases = _readmapfile(fp, mapfile)
1010 1010 t._loader.cache.update(cache)
1011 1011 t._loader._map = tmap
1012 1012 t._loader._aliasmap = _aliasrules.buildmap(aliases)
1013 1013 return t
1014 1014
1015 1015 def __contains__(self, key):
1016 1016 return key in self._loader
1017 1017
1018 1018 @property
1019 1019 def cache(self):
1020 1020 return self._loader.cache
1021 1021
1022 1022 # for highlight extension to insert one-time 'colorize' filter
1023 1023 @property
1024 1024 def _filters(self):
1025 1025 return self._proc._filters
1026 1026
1027 1027 @property
1028 1028 def defaults(self):
1029 1029 return self._proc._defaults
1030 1030
1031 1031 def load(self, t):
1032 1032 """Get parsed tree for the given template name. Use a local cache."""
1033 1033 return self._loader.load(t)
1034 1034
1035 1035 def symbolsuseddefault(self):
1036 1036 """Look up (keywords, filters/functions) referenced from the default
1037 1037 unnamed template
1038 1038
1039 1039 This may load additional templates from the map file.
1040 1040 """
1041 1041 return self.symbolsused(b'')
1042 1042
1043 1043 def symbolsused(self, t):
1044 1044 """Look up (keywords, filters/functions) referenced from the name
1045 1045 template 't'
1046 1046
1047 1047 This may load additional templates from the map file.
1048 1048 """
1049 1049 return self._loader.symbolsused(t)
1050 1050
1051 1051 def renderdefault(self, mapping):
1052 1052 """Render the default unnamed template and return result as string"""
1053 1053 return self.render(b'', mapping)
1054 1054
1055 1055 def render(self, t, mapping):
1056 1056 """Render the specified named template and return result as string"""
1057 1057 return b''.join(self.generate(t, mapping))
1058 1058
1059 1059 def generate(self, t, mapping):
1060 1060 """Return a generator that renders the specified named template and
1061 1061 yields chunks"""
1062 1062 stream = self._proc.process(t, mapping)
1063 1063 if self._minchunk:
1064 1064 stream = util.increasingchunks(
1065 1065 stream, min=self._minchunk, max=self._maxchunk
1066 1066 )
1067 1067 return stream
1068 1068
1069 1069
1070 1070 def templatedir():
1071 1071 '''return the directory used for template files, or None.'''
1072 1072 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1073 1073 return path if os.path.isdir(path) else None
1074 1074
1075 1075
1076 1076 def open_template(name):
1077 '''returns a file-like object for the given template, and its full path'''
1077 '''returns a file-like object for the given template, and its full path
1078
1079 If the name is a relative path and we're in a frozen binary, the template
1080 will be read from the mercurial.templates package instead. The returned path
1081 will then be the relative path.
1082 '''
1078 1083 templatepath = templatedir()
1079 1084 if templatepath is not None or os.path.isabs(name):
1080 1085 f = os.path.join(templatepath, name)
1081 1086 try:
1082 1087 return f, open(f, mode='rb')
1083 1088 except EnvironmentError:
1084 1089 return None, None
1085 1090 else:
1086 # TODO: read from resources here
1091 name_parts = pycompat.sysstr(name).split('/')
1092 package_name = '.'.join(['mercurial', 'templates'] + name_parts[:-1])
1093 try:
1094 return (
1095 name,
1096 resourceutil.open_resource(package_name, name_parts[-1]),
1097 )
1098 except (ModuleNotFoundError, FileNotFoundError):
1087 1099 return None, None
General Comments 0
You need to be logged in to leave comments. Login now