##// END OF EJS Templates
templater: unroll loop over mapfile directories...
Martin von Zweigbergk -
r45872:d9a502a0 default
parent child Browse files
Show More
@@ -1,1099 +1,1104 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 templatedirs = [base, templatedir()]
833 for dir in templatedirs:
834 if dir is None:
835 continue
832 subresource = None
833 if base:
834 abs = os.path.normpath(os.path.join(base, rel))
835 if os.path.isfile(abs):
836 subresource = util.posixfile(abs, b'rb')
837 if not subresource:
838 dir = templatedir()
839 if dir:
836 840 abs = os.path.normpath(os.path.join(dir, rel))
837 841 if os.path.isfile(abs):
838 data = util.posixfile(abs, b'rb').read()
842 subresource = util.posixfile(abs, b'rb')
843 if subresource:
844 data = subresource.read()
839 845 conf.parse(
840 abs, data, sections=sections, remap=remap, include=include
846 abs, data, sections=sections, remap=remap, include=include,
841 847 )
842 break
843 848
844 849 data = fp.read()
845 850 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
846 851
847 852 cache = {}
848 853 tmap = {}
849 854 aliases = []
850 855
851 856 val = conf.get(b'templates', b'__base__')
852 857 if val and val[0] not in b"'\"":
853 858 # treat as a pointer to a base class for this style
854 859 path = os.path.normpath(os.path.join(base, val))
855 860
856 861 # fallback check in template paths
857 862 if not os.path.exists(path):
858 863 dir = templatedir()
859 864 if dir is not None:
860 865 p2 = os.path.normpath(os.path.join(dir, val))
861 866 if os.path.isfile(p2):
862 867 path = p2
863 868 else:
864 869 p3 = os.path.normpath(os.path.join(p2, b"map"))
865 870 if os.path.isfile(p3):
866 871 path = p3
867 872
868 873 fp = _open_mapfile(path)
869 874 cache, tmap, aliases = _readmapfile(fp, path)
870 875
871 876 for key, val in conf[b'templates'].items():
872 877 if not val:
873 878 raise error.ParseError(
874 879 _(b'missing value'), conf.source(b'templates', key)
875 880 )
876 881 if val[0] in b"'\"":
877 882 if val[0] != val[-1]:
878 883 raise error.ParseError(
879 884 _(b'unmatched quotes'), conf.source(b'templates', key)
880 885 )
881 886 cache[key] = unquotestring(val)
882 887 elif key != b'__base__':
883 888 tmap[key] = os.path.join(base, val)
884 889 aliases.extend(conf[b'templatealias'].items())
885 890 return cache, tmap, aliases
886 891
887 892
888 893 class loader(object):
889 894 """Load template fragments optionally from a map file"""
890 895
891 896 def __init__(self, cache, aliases):
892 897 if cache is None:
893 898 cache = {}
894 899 self.cache = cache.copy()
895 900 self._map = {}
896 901 self._aliasmap = _aliasrules.buildmap(aliases)
897 902
898 903 def __contains__(self, key):
899 904 return key in self.cache or key in self._map
900 905
901 906 def load(self, t):
902 907 """Get parsed tree for the given template name. Use a local cache."""
903 908 if t not in self.cache:
904 909 try:
905 910 self.cache[t] = util.readfile(self._map[t])
906 911 except KeyError as inst:
907 912 raise templateutil.TemplateNotFound(
908 913 _(b'"%s" not in template map') % inst.args[0]
909 914 )
910 915 except IOError as inst:
911 916 reason = _(b'template file %s: %s') % (
912 917 self._map[t],
913 918 stringutil.forcebytestr(inst.args[1]),
914 919 )
915 920 raise IOError(inst.args[0], encoding.strfromlocal(reason))
916 921 return self._parse(self.cache[t])
917 922
918 923 def _parse(self, tmpl):
919 924 x = parse(tmpl)
920 925 if self._aliasmap:
921 926 x = _aliasrules.expand(self._aliasmap, x)
922 927 return x
923 928
924 929 def _findsymbolsused(self, tree, syms):
925 930 if not tree:
926 931 return
927 932 op = tree[0]
928 933 if op == b'symbol':
929 934 s = tree[1]
930 935 if s in syms[0]:
931 936 return # avoid recursion: s -> cache[s] -> s
932 937 syms[0].add(s)
933 938 if s in self.cache or s in self._map:
934 939 # s may be a reference for named template
935 940 self._findsymbolsused(self.load(s), syms)
936 941 return
937 942 if op in {b'integer', b'string'}:
938 943 return
939 944 # '{arg|func}' == '{func(arg)}'
940 945 if op == b'|':
941 946 syms[1].add(getsymbol(tree[2]))
942 947 self._findsymbolsused(tree[1], syms)
943 948 return
944 949 if op == b'func':
945 950 syms[1].add(getsymbol(tree[1]))
946 951 self._findsymbolsused(tree[2], syms)
947 952 return
948 953 for x in tree[1:]:
949 954 self._findsymbolsused(x, syms)
950 955
951 956 def symbolsused(self, t):
952 957 """Look up (keywords, filters/functions) referenced from the name
953 958 template 't'
954 959
955 960 This may load additional templates from the map file.
956 961 """
957 962 syms = (set(), set())
958 963 self._findsymbolsused(self.load(t), syms)
959 964 return syms
960 965
961 966
962 967 class templater(object):
963 968 def __init__(
964 969 self,
965 970 filters=None,
966 971 defaults=None,
967 972 resources=None,
968 973 cache=None,
969 974 aliases=(),
970 975 minchunk=1024,
971 976 maxchunk=65536,
972 977 ):
973 978 """Create template engine optionally with preloaded template fragments
974 979
975 980 - ``filters``: a dict of functions to transform a value into another.
976 981 - ``defaults``: a dict of symbol values/functions; may be overridden
977 982 by a ``mapping`` dict.
978 983 - ``resources``: a resourcemapper object to look up internal data
979 984 (e.g. cache), inaccessible from user template.
980 985 - ``cache``: a dict of preloaded template fragments.
981 986 - ``aliases``: a list of alias (name, replacement) pairs.
982 987
983 988 self.cache may be updated later to register additional template
984 989 fragments.
985 990 """
986 991 allfilters = templatefilters.filters.copy()
987 992 if filters:
988 993 allfilters.update(filters)
989 994 self._loader = loader(cache, aliases)
990 995 self._proc = engine(self._loader.load, allfilters, defaults, resources)
991 996 self._minchunk, self._maxchunk = minchunk, maxchunk
992 997
993 998 @classmethod
994 999 def frommapfile(
995 1000 cls,
996 1001 mapfile,
997 1002 fp=None,
998 1003 filters=None,
999 1004 defaults=None,
1000 1005 resources=None,
1001 1006 cache=None,
1002 1007 minchunk=1024,
1003 1008 maxchunk=65536,
1004 1009 ):
1005 1010 """Create templater from the specified map file"""
1006 1011 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1007 1012 if not fp:
1008 1013 fp = _open_mapfile(mapfile)
1009 1014 cache, tmap, aliases = _readmapfile(fp, mapfile)
1010 1015 t._loader.cache.update(cache)
1011 1016 t._loader._map = tmap
1012 1017 t._loader._aliasmap = _aliasrules.buildmap(aliases)
1013 1018 return t
1014 1019
1015 1020 def __contains__(self, key):
1016 1021 return key in self._loader
1017 1022
1018 1023 @property
1019 1024 def cache(self):
1020 1025 return self._loader.cache
1021 1026
1022 1027 # for highlight extension to insert one-time 'colorize' filter
1023 1028 @property
1024 1029 def _filters(self):
1025 1030 return self._proc._filters
1026 1031
1027 1032 @property
1028 1033 def defaults(self):
1029 1034 return self._proc._defaults
1030 1035
1031 1036 def load(self, t):
1032 1037 """Get parsed tree for the given template name. Use a local cache."""
1033 1038 return self._loader.load(t)
1034 1039
1035 1040 def symbolsuseddefault(self):
1036 1041 """Look up (keywords, filters/functions) referenced from the default
1037 1042 unnamed template
1038 1043
1039 1044 This may load additional templates from the map file.
1040 1045 """
1041 1046 return self.symbolsused(b'')
1042 1047
1043 1048 def symbolsused(self, t):
1044 1049 """Look up (keywords, filters/functions) referenced from the name
1045 1050 template 't'
1046 1051
1047 1052 This may load additional templates from the map file.
1048 1053 """
1049 1054 return self._loader.symbolsused(t)
1050 1055
1051 1056 def renderdefault(self, mapping):
1052 1057 """Render the default unnamed template and return result as string"""
1053 1058 return self.render(b'', mapping)
1054 1059
1055 1060 def render(self, t, mapping):
1056 1061 """Render the specified named template and return result as string"""
1057 1062 return b''.join(self.generate(t, mapping))
1058 1063
1059 1064 def generate(self, t, mapping):
1060 1065 """Return a generator that renders the specified named template and
1061 1066 yields chunks"""
1062 1067 stream = self._proc.process(t, mapping)
1063 1068 if self._minchunk:
1064 1069 stream = util.increasingchunks(
1065 1070 stream, min=self._minchunk, max=self._maxchunk
1066 1071 )
1067 1072 return stream
1068 1073
1069 1074
1070 1075 def templatedir():
1071 1076 '''return the directory used for template files, or None.'''
1072 1077 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1073 1078 return path if os.path.isdir(path) else None
1074 1079
1075 1080
1076 1081 def open_template(name):
1077 1082 '''returns a file-like object for the given template, and its full path
1078 1083
1079 1084 If the name is a relative path and we're in a frozen binary, the template
1080 1085 will be read from the mercurial.templates package instead. The returned path
1081 1086 will then be the relative path.
1082 1087 '''
1083 1088 templatepath = templatedir()
1084 1089 if templatepath is not None or os.path.isabs(name):
1085 1090 f = os.path.join(templatepath, name)
1086 1091 try:
1087 1092 return f, open(f, mode='rb')
1088 1093 except EnvironmentError:
1089 1094 return None, None
1090 1095 else:
1091 1096 name_parts = pycompat.sysstr(name).split('/')
1092 1097 package_name = '.'.join(['mercurial', 'templates'] + name_parts[:-1])
1093 1098 try:
1094 1099 return (
1095 1100 name,
1096 1101 resourceutil.open_resource(package_name, name_parts[-1]),
1097 1102 )
1098 1103 except (ModuleNotFoundError, FileNotFoundError):
1099 1104 return None, None
General Comments 0
You need to be logged in to leave comments. Login now