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