##// END OF EJS Templates
templater: move specialized exception types to top...
Yuya Nishihara -
r36461:8dbd97ae default
parent child Browse files
Show More
@@ -1,1614 +1,1614 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 from __future__ import absolute_import, print_function
9 9
10 10 import os
11 11 import re
12 12 import types
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 color,
17 17 config,
18 18 encoding,
19 19 error,
20 20 minirst,
21 21 obsutil,
22 22 parser,
23 23 pycompat,
24 24 registrar,
25 25 revset as revsetmod,
26 26 revsetlang,
27 27 scmutil,
28 28 templatefilters,
29 29 templatekw,
30 30 util,
31 31 )
32 32
33 class TemplateNotFound(error.Abort):
34 pass
35
33 36 # template parsing
34 37
35 38 elements = {
36 39 # token-type: binding-strength, primary, prefix, infix, suffix
37 40 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
38 41 ".": (18, None, None, (".", 18), None),
39 42 "%": (15, None, None, ("%", 15), None),
40 43 "|": (15, None, None, ("|", 15), None),
41 44 "*": (5, None, None, ("*", 5), None),
42 45 "/": (5, None, None, ("/", 5), None),
43 46 "+": (4, None, None, ("+", 4), None),
44 47 "-": (4, None, ("negate", 19), ("-", 4), None),
45 48 "=": (3, None, None, ("keyvalue", 3), None),
46 49 ",": (2, None, None, ("list", 2), None),
47 50 ")": (0, None, None, None, None),
48 51 "integer": (0, "integer", None, None, None),
49 52 "symbol": (0, "symbol", None, None, None),
50 53 "string": (0, "string", None, None, None),
51 54 "template": (0, "template", None, None, None),
52 55 "end": (0, None, None, None, None),
53 56 }
54 57
55 58 def tokenize(program, start, end, term=None):
56 59 """Parse a template expression into a stream of tokens, which must end
57 60 with term if specified"""
58 61 pos = start
59 62 program = pycompat.bytestr(program)
60 63 while pos < end:
61 64 c = program[pos]
62 65 if c.isspace(): # skip inter-token whitespace
63 66 pass
64 67 elif c in "(=,).%|+-*/": # handle simple operators
65 68 yield (c, None, pos)
66 69 elif c in '"\'': # handle quoted templates
67 70 s = pos + 1
68 71 data, pos = _parsetemplate(program, s, end, c)
69 72 yield ('template', data, s)
70 73 pos -= 1
71 74 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
72 75 # handle quoted strings
73 76 c = program[pos + 1]
74 77 s = pos = pos + 2
75 78 while pos < end: # find closing quote
76 79 d = program[pos]
77 80 if d == '\\': # skip over escaped characters
78 81 pos += 2
79 82 continue
80 83 if d == c:
81 84 yield ('string', program[s:pos], s)
82 85 break
83 86 pos += 1
84 87 else:
85 88 raise error.ParseError(_("unterminated string"), s)
86 89 elif c.isdigit():
87 90 s = pos
88 91 while pos < end:
89 92 d = program[pos]
90 93 if not d.isdigit():
91 94 break
92 95 pos += 1
93 96 yield ('integer', program[s:pos], s)
94 97 pos -= 1
95 98 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
96 99 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
97 100 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
98 101 # where some of nested templates were preprocessed as strings and
99 102 # then compiled. therefore, \"...\" was allowed. (issue4733)
100 103 #
101 104 # processing flow of _evalifliteral() at 5ab28a2e9962:
102 105 # outer template string -> stringify() -> compiletemplate()
103 106 # ------------------------ ------------ ------------------
104 107 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
105 108 # ~~~~~~~~
106 109 # escaped quoted string
107 110 if c == 'r':
108 111 pos += 1
109 112 token = 'string'
110 113 else:
111 114 token = 'template'
112 115 quote = program[pos:pos + 2]
113 116 s = pos = pos + 2
114 117 while pos < end: # find closing escaped quote
115 118 if program.startswith('\\\\\\', pos, end):
116 119 pos += 4 # skip over double escaped characters
117 120 continue
118 121 if program.startswith(quote, pos, end):
119 122 # interpret as if it were a part of an outer string
120 123 data = parser.unescapestr(program[s:pos])
121 124 if token == 'template':
122 125 data = _parsetemplate(data, 0, len(data))[0]
123 126 yield (token, data, s)
124 127 pos += 1
125 128 break
126 129 pos += 1
127 130 else:
128 131 raise error.ParseError(_("unterminated string"), s)
129 132 elif c.isalnum() or c in '_':
130 133 s = pos
131 134 pos += 1
132 135 while pos < end: # find end of symbol
133 136 d = program[pos]
134 137 if not (d.isalnum() or d == "_"):
135 138 break
136 139 pos += 1
137 140 sym = program[s:pos]
138 141 yield ('symbol', sym, s)
139 142 pos -= 1
140 143 elif c == term:
141 144 yield ('end', None, pos + 1)
142 145 return
143 146 else:
144 147 raise error.ParseError(_("syntax error"), pos)
145 148 pos += 1
146 149 if term:
147 150 raise error.ParseError(_("unterminated template expansion"), start)
148 151 yield ('end', None, pos)
149 152
150 153 def _parsetemplate(tmpl, start, stop, quote=''):
151 154 r"""
152 155 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
153 156 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
154 157 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
155 158 ([('string', 'foo'), ('symbol', 'bar')], 9)
156 159 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
157 160 ([('string', 'foo')], 4)
158 161 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
159 162 ([('string', 'foo"'), ('string', 'bar')], 9)
160 163 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
161 164 ([('string', 'foo\\')], 6)
162 165 """
163 166 parsed = []
164 167 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
165 168 if typ == 'string':
166 169 parsed.append((typ, val))
167 170 elif typ == 'template':
168 171 parsed.append(val)
169 172 elif typ == 'end':
170 173 return parsed, pos
171 174 else:
172 175 raise error.ProgrammingError('unexpected type: %s' % typ)
173 176 raise error.ProgrammingError('unterminated scanning of template')
174 177
175 178 def scantemplate(tmpl):
176 179 """Scan (type, start, end) positions of outermost elements in template
177 180
178 181 >>> list(scantemplate(b'foo{bar}"baz'))
179 182 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
180 183 >>> list(scantemplate(b'outer{"inner"}outer'))
181 184 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
182 185 >>> list(scantemplate(b'foo\\{escaped}'))
183 186 [('string', 0, 5), ('string', 5, 13)]
184 187 """
185 188 last = None
186 189 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl)):
187 190 if last:
188 191 yield last + (pos,)
189 192 if typ == 'end':
190 193 return
191 194 else:
192 195 last = (typ, pos)
193 196 raise error.ProgrammingError('unterminated scanning of template')
194 197
195 198 def _scantemplate(tmpl, start, stop, quote=''):
196 199 """Parse template string into chunks of strings and template expressions"""
197 200 sepchars = '{' + quote
198 201 pos = start
199 202 p = parser.parser(elements)
200 203 while pos < stop:
201 204 n = min((tmpl.find(c, pos, stop) for c in sepchars),
202 205 key=lambda n: (n < 0, n))
203 206 if n < 0:
204 207 yield ('string', parser.unescapestr(tmpl[pos:stop]), pos)
205 208 pos = stop
206 209 break
207 210 c = tmpl[n:n + 1]
208 211 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
209 212 if bs % 2 == 1:
210 213 # escaped (e.g. '\{', '\\\{', but not '\\{')
211 214 yield ('string', parser.unescapestr(tmpl[pos:n - 1]) + c, pos)
212 215 pos = n + 1
213 216 continue
214 217 if n > pos:
215 218 yield ('string', parser.unescapestr(tmpl[pos:n]), pos)
216 219 if c == quote:
217 220 yield ('end', None, n + 1)
218 221 return
219 222
220 223 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
221 224 if not tmpl.endswith('}', n + 1, pos):
222 225 raise error.ParseError(_("invalid token"), pos)
223 226 yield ('template', parseres, n)
224 227
225 228 if quote:
226 229 raise error.ParseError(_("unterminated string"), start)
227 230 yield ('end', None, pos)
228 231
229 232 def _unnesttemplatelist(tree):
230 233 """Expand list of templates to node tuple
231 234
232 235 >>> def f(tree):
233 236 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
234 237 >>> f((b'template', []))
235 238 (string '')
236 239 >>> f((b'template', [(b'string', b'foo')]))
237 240 (string 'foo')
238 241 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
239 242 (template
240 243 (string 'foo')
241 244 (symbol 'rev'))
242 245 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
243 246 (template
244 247 (symbol 'rev'))
245 248 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
246 249 (string 'foo')
247 250 """
248 251 if not isinstance(tree, tuple):
249 252 return tree
250 253 op = tree[0]
251 254 if op != 'template':
252 255 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
253 256
254 257 assert len(tree) == 2
255 258 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
256 259 if not xs:
257 260 return ('string', '') # empty template ""
258 261 elif len(xs) == 1 and xs[0][0] == 'string':
259 262 return xs[0] # fast path for string with no template fragment "x"
260 263 else:
261 264 return (op,) + xs
262 265
263 266 def parse(tmpl):
264 267 """Parse template string into tree"""
265 268 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
266 269 assert pos == len(tmpl), 'unquoted template should be consumed'
267 270 return _unnesttemplatelist(('template', parsed))
268 271
269 272 def _parseexpr(expr):
270 273 """Parse a template expression into tree
271 274
272 275 >>> _parseexpr(b'"foo"')
273 276 ('string', 'foo')
274 277 >>> _parseexpr(b'foo(bar)')
275 278 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
276 279 >>> _parseexpr(b'foo(')
277 280 Traceback (most recent call last):
278 281 ...
279 282 ParseError: ('not a prefix: end', 4)
280 283 >>> _parseexpr(b'"foo" "bar"')
281 284 Traceback (most recent call last):
282 285 ...
283 286 ParseError: ('invalid token', 7)
284 287 """
285 288 p = parser.parser(elements)
286 289 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
287 290 if pos != len(expr):
288 291 raise error.ParseError(_('invalid token'), pos)
289 292 return _unnesttemplatelist(tree)
290 293
291 294 def prettyformat(tree):
292 295 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
293 296
294 297 def compileexp(exp, context, curmethods):
295 298 """Compile parsed template tree to (func, data) pair"""
296 299 if not exp:
297 300 raise error.ParseError(_("missing argument"))
298 301 t = exp[0]
299 302 if t in curmethods:
300 303 return curmethods[t](exp, context)
301 304 raise error.ParseError(_("unknown method '%s'") % t)
302 305
303 306 # template evaluation
304 307
305 308 def getsymbol(exp):
306 309 if exp[0] == 'symbol':
307 310 return exp[1]
308 311 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
309 312
310 313 def getlist(x):
311 314 if not x:
312 315 return []
313 316 if x[0] == 'list':
314 317 return getlist(x[1]) + [x[2]]
315 318 return [x]
316 319
317 320 def gettemplate(exp, context):
318 321 """Compile given template tree or load named template from map file;
319 322 returns (func, data) pair"""
320 323 if exp[0] in ('template', 'string'):
321 324 return compileexp(exp, context, methods)
322 325 if exp[0] == 'symbol':
323 326 # unlike runsymbol(), here 'symbol' is always taken as template name
324 327 # even if it exists in mapping. this allows us to override mapping
325 328 # by web templates, e.g. 'changelogtag' is redefined in map file.
326 329 return context._load(exp[1])
327 330 raise error.ParseError(_("expected template specifier"))
328 331
329 332 def findsymbolicname(arg):
330 333 """Find symbolic name for the given compiled expression; returns None
331 334 if nothing found reliably"""
332 335 while True:
333 336 func, data = arg
334 337 if func is runsymbol:
335 338 return data
336 339 elif func is runfilter:
337 340 arg = data[0]
338 341 else:
339 342 return None
340 343
341 344 def evalrawexp(context, mapping, arg):
342 345 """Evaluate given argument as a bare template object which may require
343 346 further processing (such as folding generator of strings)"""
344 347 func, data = arg
345 348 return func(context, mapping, data)
346 349
347 350 def evalfuncarg(context, mapping, arg):
348 351 """Evaluate given argument as value type"""
349 352 thing = evalrawexp(context, mapping, arg)
350 353 thing = templatekw.unwrapvalue(thing)
351 354 # evalrawexp() may return string, generator of strings or arbitrary object
352 355 # such as date tuple, but filter does not want generator.
353 356 if isinstance(thing, types.GeneratorType):
354 357 thing = stringify(thing)
355 358 return thing
356 359
357 360 def evalboolean(context, mapping, arg):
358 361 """Evaluate given argument as boolean, but also takes boolean literals"""
359 362 func, data = arg
360 363 if func is runsymbol:
361 364 thing = func(context, mapping, data, default=None)
362 365 if thing is None:
363 366 # not a template keyword, takes as a boolean literal
364 367 thing = util.parsebool(data)
365 368 else:
366 369 thing = func(context, mapping, data)
367 370 thing = templatekw.unwrapvalue(thing)
368 371 if isinstance(thing, bool):
369 372 return thing
370 373 # other objects are evaluated as strings, which means 0 is True, but
371 374 # empty dict/list should be False as they are expected to be ''
372 375 return bool(stringify(thing))
373 376
374 377 def evalinteger(context, mapping, arg, err=None):
375 378 v = evalfuncarg(context, mapping, arg)
376 379 try:
377 380 return int(v)
378 381 except (TypeError, ValueError):
379 382 raise error.ParseError(err or _('not an integer'))
380 383
381 384 def evalstring(context, mapping, arg):
382 385 return stringify(evalrawexp(context, mapping, arg))
383 386
384 387 def evalstringliteral(context, mapping, arg):
385 388 """Evaluate given argument as string template, but returns symbol name
386 389 if it is unknown"""
387 390 func, data = arg
388 391 if func is runsymbol:
389 392 thing = func(context, mapping, data, default=data)
390 393 else:
391 394 thing = func(context, mapping, data)
392 395 return stringify(thing)
393 396
394 397 _evalfuncbytype = {
395 398 bool: evalboolean,
396 399 bytes: evalstring,
397 400 int: evalinteger,
398 401 }
399 402
400 403 def evalastype(context, mapping, arg, typ):
401 404 """Evaluate given argument and coerce its type"""
402 405 try:
403 406 f = _evalfuncbytype[typ]
404 407 except KeyError:
405 408 raise error.ProgrammingError('invalid type specified: %r' % typ)
406 409 return f(context, mapping, arg)
407 410
408 411 def runinteger(context, mapping, data):
409 412 return int(data)
410 413
411 414 def runstring(context, mapping, data):
412 415 return data
413 416
414 417 def _recursivesymbolblocker(key):
415 418 def showrecursion(**args):
416 419 raise error.Abort(_("recursive reference '%s' in template") % key)
417 420 return showrecursion
418 421
419 422 def _runrecursivesymbol(context, mapping, key):
420 423 raise error.Abort(_("recursive reference '%s' in template") % key)
421 424
422 425 def runsymbol(context, mapping, key, default=''):
423 426 v = context.symbol(mapping, key)
424 427 if v is None:
425 428 # put poison to cut recursion. we can't move this to parsing phase
426 429 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
427 430 safemapping = mapping.copy()
428 431 safemapping[key] = _recursivesymbolblocker(key)
429 432 try:
430 433 v = context.process(key, safemapping)
431 434 except TemplateNotFound:
432 435 v = default
433 436 if callable(v):
434 437 # TODO: templatekw functions will be updated to take (context, mapping)
435 438 # pair instead of **props
436 439 props = context._resources.copy()
437 440 props.update(mapping)
438 441 return v(**pycompat.strkwargs(props))
439 442 return v
440 443
441 444 def buildtemplate(exp, context):
442 445 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
443 446 return (runtemplate, ctmpl)
444 447
445 448 def runtemplate(context, mapping, template):
446 449 for arg in template:
447 450 yield evalrawexp(context, mapping, arg)
448 451
449 452 def buildfilter(exp, context):
450 453 n = getsymbol(exp[2])
451 454 if n in context._filters:
452 455 filt = context._filters[n]
453 456 arg = compileexp(exp[1], context, methods)
454 457 return (runfilter, (arg, filt))
455 458 if n in funcs:
456 459 f = funcs[n]
457 460 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
458 461 return (f, args)
459 462 raise error.ParseError(_("unknown function '%s'") % n)
460 463
461 464 def runfilter(context, mapping, data):
462 465 arg, filt = data
463 466 thing = evalfuncarg(context, mapping, arg)
464 467 try:
465 468 return filt(thing)
466 469 except (ValueError, AttributeError, TypeError):
467 470 sym = findsymbolicname(arg)
468 471 if sym:
469 472 msg = (_("template filter '%s' is not compatible with keyword '%s'")
470 473 % (pycompat.sysbytes(filt.__name__), sym))
471 474 else:
472 475 msg = (_("incompatible use of template filter '%s'")
473 476 % pycompat.sysbytes(filt.__name__))
474 477 raise error.Abort(msg)
475 478
476 479 def buildmap(exp, context):
477 480 darg = compileexp(exp[1], context, methods)
478 481 targ = gettemplate(exp[2], context)
479 482 return (runmap, (darg, targ))
480 483
481 484 def runmap(context, mapping, data):
482 485 darg, targ = data
483 486 d = evalrawexp(context, mapping, darg)
484 487 if util.safehasattr(d, 'itermaps'):
485 488 diter = d.itermaps()
486 489 else:
487 490 try:
488 491 diter = iter(d)
489 492 except TypeError:
490 493 sym = findsymbolicname(darg)
491 494 if sym:
492 495 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
493 496 else:
494 497 raise error.ParseError(_("%r is not iterable") % d)
495 498
496 499 for i, v in enumerate(diter):
497 500 lm = mapping.copy()
498 501 lm['index'] = i
499 502 if isinstance(v, dict):
500 503 lm.update(v)
501 504 lm['originalnode'] = mapping.get('node')
502 505 yield evalrawexp(context, lm, targ)
503 506 else:
504 507 # v is not an iterable of dicts, this happen when 'key'
505 508 # has been fully expanded already and format is useless.
506 509 # If so, return the expanded value.
507 510 yield v
508 511
509 512 def buildmember(exp, context):
510 513 darg = compileexp(exp[1], context, methods)
511 514 memb = getsymbol(exp[2])
512 515 return (runmember, (darg, memb))
513 516
514 517 def runmember(context, mapping, data):
515 518 darg, memb = data
516 519 d = evalrawexp(context, mapping, darg)
517 520 if util.safehasattr(d, 'tomap'):
518 521 lm = mapping.copy()
519 522 lm.update(d.tomap())
520 523 return runsymbol(context, lm, memb)
521 524 if util.safehasattr(d, 'get'):
522 525 return _getdictitem(d, memb)
523 526
524 527 sym = findsymbolicname(darg)
525 528 if sym:
526 529 raise error.ParseError(_("keyword '%s' has no member") % sym)
527 530 else:
528 531 raise error.ParseError(_("%r has no member") % d)
529 532
530 533 def buildnegate(exp, context):
531 534 arg = compileexp(exp[1], context, exprmethods)
532 535 return (runnegate, arg)
533 536
534 537 def runnegate(context, mapping, data):
535 538 data = evalinteger(context, mapping, data,
536 539 _('negation needs an integer argument'))
537 540 return -data
538 541
539 542 def buildarithmetic(exp, context, func):
540 543 left = compileexp(exp[1], context, exprmethods)
541 544 right = compileexp(exp[2], context, exprmethods)
542 545 return (runarithmetic, (func, left, right))
543 546
544 547 def runarithmetic(context, mapping, data):
545 548 func, left, right = data
546 549 left = evalinteger(context, mapping, left,
547 550 _('arithmetic only defined on integers'))
548 551 right = evalinteger(context, mapping, right,
549 552 _('arithmetic only defined on integers'))
550 553 try:
551 554 return func(left, right)
552 555 except ZeroDivisionError:
553 556 raise error.Abort(_('division by zero is not defined'))
554 557
555 558 def buildfunc(exp, context):
556 559 n = getsymbol(exp[1])
557 560 if n in funcs:
558 561 f = funcs[n]
559 562 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
560 563 return (f, args)
561 564 if n in context._filters:
562 565 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
563 566 if len(args) != 1:
564 567 raise error.ParseError(_("filter %s expects one argument") % n)
565 568 f = context._filters[n]
566 569 return (runfilter, (args[0], f))
567 570 raise error.ParseError(_("unknown function '%s'") % n)
568 571
569 572 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
570 573 """Compile parsed tree of function arguments into list or dict of
571 574 (func, data) pairs
572 575
573 576 >>> context = engine(lambda t: (runsymbol, t))
574 577 >>> def fargs(expr, argspec):
575 578 ... x = _parseexpr(expr)
576 579 ... n = getsymbol(x[1])
577 580 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
578 581 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
579 582 ['l', 'k']
580 583 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
581 584 >>> list(args.keys()), list(args[b'opts'].keys())
582 585 (['opts'], ['opts', 'k'])
583 586 """
584 587 def compiledict(xs):
585 588 return util.sortdict((k, compileexp(x, context, curmethods))
586 589 for k, x in xs.iteritems())
587 590 def compilelist(xs):
588 591 return [compileexp(x, context, curmethods) for x in xs]
589 592
590 593 if not argspec:
591 594 # filter or function with no argspec: return list of positional args
592 595 return compilelist(getlist(exp))
593 596
594 597 # function with argspec: return dict of named args
595 598 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
596 599 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
597 600 keyvaluenode='keyvalue', keynode='symbol')
598 601 compargs = util.sortdict()
599 602 if varkey:
600 603 compargs[varkey] = compilelist(treeargs.pop(varkey))
601 604 if optkey:
602 605 compargs[optkey] = compiledict(treeargs.pop(optkey))
603 606 compargs.update(compiledict(treeargs))
604 607 return compargs
605 608
606 609 def buildkeyvaluepair(exp, content):
607 610 raise error.ParseError(_("can't use a key-value pair in this context"))
608 611
609 612 # dict of template built-in functions
610 613 funcs = {}
611 614
612 615 templatefunc = registrar.templatefunc(funcs)
613 616
614 617 @templatefunc('date(date[, fmt])')
615 618 def date(context, mapping, args):
616 619 """Format a date. See :hg:`help dates` for formatting
617 620 strings. The default is a Unix date format, including the timezone:
618 621 "Mon Sep 04 15:13:13 2006 0700"."""
619 622 if not (1 <= len(args) <= 2):
620 623 # i18n: "date" is a keyword
621 624 raise error.ParseError(_("date expects one or two arguments"))
622 625
623 626 date = evalfuncarg(context, mapping, args[0])
624 627 fmt = None
625 628 if len(args) == 2:
626 629 fmt = evalstring(context, mapping, args[1])
627 630 try:
628 631 if fmt is None:
629 632 return util.datestr(date)
630 633 else:
631 634 return util.datestr(date, fmt)
632 635 except (TypeError, ValueError):
633 636 # i18n: "date" is a keyword
634 637 raise error.ParseError(_("date expects a date information"))
635 638
636 639 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
637 640 def dict_(context, mapping, args):
638 641 """Construct a dict from key-value pairs. A key may be omitted if
639 642 a value expression can provide an unambiguous name."""
640 643 data = util.sortdict()
641 644
642 645 for v in args['args']:
643 646 k = findsymbolicname(v)
644 647 if not k:
645 648 raise error.ParseError(_('dict key cannot be inferred'))
646 649 if k in data or k in args['kwargs']:
647 650 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
648 651 data[k] = evalfuncarg(context, mapping, v)
649 652
650 653 data.update((k, evalfuncarg(context, mapping, v))
651 654 for k, v in args['kwargs'].iteritems())
652 655 return templatekw.hybriddict(data)
653 656
654 657 @templatefunc('diff([includepattern [, excludepattern]])')
655 658 def diff(context, mapping, args):
656 659 """Show a diff, optionally
657 660 specifying files to include or exclude."""
658 661 if len(args) > 2:
659 662 # i18n: "diff" is a keyword
660 663 raise error.ParseError(_("diff expects zero, one, or two arguments"))
661 664
662 665 def getpatterns(i):
663 666 if i < len(args):
664 667 s = evalstring(context, mapping, args[i]).strip()
665 668 if s:
666 669 return [s]
667 670 return []
668 671
669 672 ctx = context.resource(mapping, 'ctx')
670 673 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
671 674
672 675 return ''.join(chunks)
673 676
674 677 @templatefunc('extdata(source)', argspec='source')
675 678 def extdata(context, mapping, args):
676 679 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
677 680 if 'source' not in args:
678 681 # i18n: "extdata" is a keyword
679 682 raise error.ParseError(_('extdata expects one argument'))
680 683
681 684 source = evalstring(context, mapping, args['source'])
682 685 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
683 686 ctx = context.resource(mapping, 'ctx')
684 687 if source in cache:
685 688 data = cache[source]
686 689 else:
687 690 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
688 691 return data.get(ctx.rev(), '')
689 692
690 693 @templatefunc('files(pattern)')
691 694 def files(context, mapping, args):
692 695 """All files of the current changeset matching the pattern. See
693 696 :hg:`help patterns`."""
694 697 if not len(args) == 1:
695 698 # i18n: "files" is a keyword
696 699 raise error.ParseError(_("files expects one argument"))
697 700
698 701 raw = evalstring(context, mapping, args[0])
699 702 ctx = context.resource(mapping, 'ctx')
700 703 m = ctx.match([raw])
701 704 files = list(ctx.matches(m))
702 705 # TODO: pass (context, mapping) pair to keyword function
703 706 props = context._resources.copy()
704 707 props.update(mapping)
705 708 return templatekw.showlist("file", files, props)
706 709
707 710 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
708 711 def fill(context, mapping, args):
709 712 """Fill many
710 713 paragraphs with optional indentation. See the "fill" filter."""
711 714 if not (1 <= len(args) <= 4):
712 715 # i18n: "fill" is a keyword
713 716 raise error.ParseError(_("fill expects one to four arguments"))
714 717
715 718 text = evalstring(context, mapping, args[0])
716 719 width = 76
717 720 initindent = ''
718 721 hangindent = ''
719 722 if 2 <= len(args) <= 4:
720 723 width = evalinteger(context, mapping, args[1],
721 724 # i18n: "fill" is a keyword
722 725 _("fill expects an integer width"))
723 726 try:
724 727 initindent = evalstring(context, mapping, args[2])
725 728 hangindent = evalstring(context, mapping, args[3])
726 729 except IndexError:
727 730 pass
728 731
729 732 return templatefilters.fill(text, width, initindent, hangindent)
730 733
731 734 @templatefunc('formatnode(node)')
732 735 def formatnode(context, mapping, args):
733 736 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
734 737 if len(args) != 1:
735 738 # i18n: "formatnode" is a keyword
736 739 raise error.ParseError(_("formatnode expects one argument"))
737 740
738 741 ui = context.resource(mapping, 'ui')
739 742 node = evalstring(context, mapping, args[0])
740 743 if ui.debugflag:
741 744 return node
742 745 return templatefilters.short(node)
743 746
744 747 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
745 748 argspec='text width fillchar left')
746 749 def pad(context, mapping, args):
747 750 """Pad text with a
748 751 fill character."""
749 752 if 'text' not in args or 'width' not in args:
750 753 # i18n: "pad" is a keyword
751 754 raise error.ParseError(_("pad() expects two to four arguments"))
752 755
753 756 width = evalinteger(context, mapping, args['width'],
754 757 # i18n: "pad" is a keyword
755 758 _("pad() expects an integer width"))
756 759
757 760 text = evalstring(context, mapping, args['text'])
758 761
759 762 left = False
760 763 fillchar = ' '
761 764 if 'fillchar' in args:
762 765 fillchar = evalstring(context, mapping, args['fillchar'])
763 766 if len(color.stripeffects(fillchar)) != 1:
764 767 # i18n: "pad" is a keyword
765 768 raise error.ParseError(_("pad() expects a single fill character"))
766 769 if 'left' in args:
767 770 left = evalboolean(context, mapping, args['left'])
768 771
769 772 fillwidth = width - encoding.colwidth(color.stripeffects(text))
770 773 if fillwidth <= 0:
771 774 return text
772 775 if left:
773 776 return fillchar * fillwidth + text
774 777 else:
775 778 return text + fillchar * fillwidth
776 779
777 780 @templatefunc('indent(text, indentchars[, firstline])')
778 781 def indent(context, mapping, args):
779 782 """Indents all non-empty lines
780 783 with the characters given in the indentchars string. An optional
781 784 third parameter will override the indent for the first line only
782 785 if present."""
783 786 if not (2 <= len(args) <= 3):
784 787 # i18n: "indent" is a keyword
785 788 raise error.ParseError(_("indent() expects two or three arguments"))
786 789
787 790 text = evalstring(context, mapping, args[0])
788 791 indent = evalstring(context, mapping, args[1])
789 792
790 793 if len(args) == 3:
791 794 firstline = evalstring(context, mapping, args[2])
792 795 else:
793 796 firstline = indent
794 797
795 798 # the indent function doesn't indent the first line, so we do it here
796 799 return templatefilters.indent(firstline + text, indent)
797 800
798 801 @templatefunc('get(dict, key)')
799 802 def get(context, mapping, args):
800 803 """Get an attribute/key from an object. Some keywords
801 804 are complex types. This function allows you to obtain the value of an
802 805 attribute on these types."""
803 806 if len(args) != 2:
804 807 # i18n: "get" is a keyword
805 808 raise error.ParseError(_("get() expects two arguments"))
806 809
807 810 dictarg = evalfuncarg(context, mapping, args[0])
808 811 if not util.safehasattr(dictarg, 'get'):
809 812 # i18n: "get" is a keyword
810 813 raise error.ParseError(_("get() expects a dict as first argument"))
811 814
812 815 key = evalfuncarg(context, mapping, args[1])
813 816 return _getdictitem(dictarg, key)
814 817
815 818 def _getdictitem(dictarg, key):
816 819 val = dictarg.get(key)
817 820 if val is None:
818 821 return
819 822 return templatekw.wraphybridvalue(dictarg, key, val)
820 823
821 824 @templatefunc('if(expr, then[, else])')
822 825 def if_(context, mapping, args):
823 826 """Conditionally execute based on the result of
824 827 an expression."""
825 828 if not (2 <= len(args) <= 3):
826 829 # i18n: "if" is a keyword
827 830 raise error.ParseError(_("if expects two or three arguments"))
828 831
829 832 test = evalboolean(context, mapping, args[0])
830 833 if test:
831 834 yield evalrawexp(context, mapping, args[1])
832 835 elif len(args) == 3:
833 836 yield evalrawexp(context, mapping, args[2])
834 837
835 838 @templatefunc('ifcontains(needle, haystack, then[, else])')
836 839 def ifcontains(context, mapping, args):
837 840 """Conditionally execute based
838 841 on whether the item "needle" is in "haystack"."""
839 842 if not (3 <= len(args) <= 4):
840 843 # i18n: "ifcontains" is a keyword
841 844 raise error.ParseError(_("ifcontains expects three or four arguments"))
842 845
843 846 haystack = evalfuncarg(context, mapping, args[1])
844 847 try:
845 848 needle = evalastype(context, mapping, args[0],
846 849 getattr(haystack, 'keytype', None) or bytes)
847 850 found = (needle in haystack)
848 851 except error.ParseError:
849 852 found = False
850 853
851 854 if found:
852 855 yield evalrawexp(context, mapping, args[2])
853 856 elif len(args) == 4:
854 857 yield evalrawexp(context, mapping, args[3])
855 858
856 859 @templatefunc('ifeq(expr1, expr2, then[, else])')
857 860 def ifeq(context, mapping, args):
858 861 """Conditionally execute based on
859 862 whether 2 items are equivalent."""
860 863 if not (3 <= len(args) <= 4):
861 864 # i18n: "ifeq" is a keyword
862 865 raise error.ParseError(_("ifeq expects three or four arguments"))
863 866
864 867 test = evalstring(context, mapping, args[0])
865 868 match = evalstring(context, mapping, args[1])
866 869 if test == match:
867 870 yield evalrawexp(context, mapping, args[2])
868 871 elif len(args) == 4:
869 872 yield evalrawexp(context, mapping, args[3])
870 873
871 874 @templatefunc('join(list, sep)')
872 875 def join(context, mapping, args):
873 876 """Join items in a list with a delimiter."""
874 877 if not (1 <= len(args) <= 2):
875 878 # i18n: "join" is a keyword
876 879 raise error.ParseError(_("join expects one or two arguments"))
877 880
878 881 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
879 882 # abuses generator as a keyword that returns a list of dicts.
880 883 joinset = evalrawexp(context, mapping, args[0])
881 884 joinset = templatekw.unwrapvalue(joinset)
882 885 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
883 886 joiner = " "
884 887 if len(args) > 1:
885 888 joiner = evalstring(context, mapping, args[1])
886 889
887 890 first = True
888 891 for x in joinset:
889 892 if first:
890 893 first = False
891 894 else:
892 895 yield joiner
893 896 yield joinfmt(x)
894 897
895 898 @templatefunc('label(label, expr)')
896 899 def label(context, mapping, args):
897 900 """Apply a label to generated content. Content with
898 901 a label applied can result in additional post-processing, such as
899 902 automatic colorization."""
900 903 if len(args) != 2:
901 904 # i18n: "label" is a keyword
902 905 raise error.ParseError(_("label expects two arguments"))
903 906
904 907 ui = context.resource(mapping, 'ui')
905 908 thing = evalstring(context, mapping, args[1])
906 909 # preserve unknown symbol as literal so effects like 'red', 'bold',
907 910 # etc. don't need to be quoted
908 911 label = evalstringliteral(context, mapping, args[0])
909 912
910 913 return ui.label(thing, label)
911 914
912 915 @templatefunc('latesttag([pattern])')
913 916 def latesttag(context, mapping, args):
914 917 """The global tags matching the given pattern on the
915 918 most recent globally tagged ancestor of this changeset.
916 919 If no such tags exist, the "{tag}" template resolves to
917 920 the string "null"."""
918 921 if len(args) > 1:
919 922 # i18n: "latesttag" is a keyword
920 923 raise error.ParseError(_("latesttag expects at most one argument"))
921 924
922 925 pattern = None
923 926 if len(args) == 1:
924 927 pattern = evalstring(context, mapping, args[0])
925 928
926 929 # TODO: pass (context, mapping) pair to keyword function
927 930 props = context._resources.copy()
928 931 props.update(mapping)
929 932 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(props))
930 933
931 934 @templatefunc('localdate(date[, tz])')
932 935 def localdate(context, mapping, args):
933 936 """Converts a date to the specified timezone.
934 937 The default is local date."""
935 938 if not (1 <= len(args) <= 2):
936 939 # i18n: "localdate" is a keyword
937 940 raise error.ParseError(_("localdate expects one or two arguments"))
938 941
939 942 date = evalfuncarg(context, mapping, args[0])
940 943 try:
941 944 date = util.parsedate(date)
942 945 except AttributeError: # not str nor date tuple
943 946 # i18n: "localdate" is a keyword
944 947 raise error.ParseError(_("localdate expects a date information"))
945 948 if len(args) >= 2:
946 949 tzoffset = None
947 950 tz = evalfuncarg(context, mapping, args[1])
948 951 if isinstance(tz, str):
949 952 tzoffset, remainder = util.parsetimezone(tz)
950 953 if remainder:
951 954 tzoffset = None
952 955 if tzoffset is None:
953 956 try:
954 957 tzoffset = int(tz)
955 958 except (TypeError, ValueError):
956 959 # i18n: "localdate" is a keyword
957 960 raise error.ParseError(_("localdate expects a timezone"))
958 961 else:
959 962 tzoffset = util.makedate()[1]
960 963 return (date[0], tzoffset)
961 964
962 965 @templatefunc('max(iterable)')
963 966 def max_(context, mapping, args, **kwargs):
964 967 """Return the max of an iterable"""
965 968 if len(args) != 1:
966 969 # i18n: "max" is a keyword
967 970 raise error.ParseError(_("max expects one argument"))
968 971
969 972 iterable = evalfuncarg(context, mapping, args[0])
970 973 try:
971 974 x = max(iterable)
972 975 except (TypeError, ValueError):
973 976 # i18n: "max" is a keyword
974 977 raise error.ParseError(_("max first argument should be an iterable"))
975 978 return templatekw.wraphybridvalue(iterable, x, x)
976 979
977 980 @templatefunc('min(iterable)')
978 981 def min_(context, mapping, args, **kwargs):
979 982 """Return the min of an iterable"""
980 983 if len(args) != 1:
981 984 # i18n: "min" is a keyword
982 985 raise error.ParseError(_("min expects one argument"))
983 986
984 987 iterable = evalfuncarg(context, mapping, args[0])
985 988 try:
986 989 x = min(iterable)
987 990 except (TypeError, ValueError):
988 991 # i18n: "min" is a keyword
989 992 raise error.ParseError(_("min first argument should be an iterable"))
990 993 return templatekw.wraphybridvalue(iterable, x, x)
991 994
992 995 @templatefunc('mod(a, b)')
993 996 def mod(context, mapping, args):
994 997 """Calculate a mod b such that a / b + a mod b == a"""
995 998 if not len(args) == 2:
996 999 # i18n: "mod" is a keyword
997 1000 raise error.ParseError(_("mod expects two arguments"))
998 1001
999 1002 func = lambda a, b: a % b
1000 1003 return runarithmetic(context, mapping, (func, args[0], args[1]))
1001 1004
1002 1005 @templatefunc('obsfateoperations(markers)')
1003 1006 def obsfateoperations(context, mapping, args):
1004 1007 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1005 1008 if len(args) != 1:
1006 1009 # i18n: "obsfateoperations" is a keyword
1007 1010 raise error.ParseError(_("obsfateoperations expects one argument"))
1008 1011
1009 1012 markers = evalfuncarg(context, mapping, args[0])
1010 1013
1011 1014 try:
1012 1015 data = obsutil.markersoperations(markers)
1013 1016 return templatekw.hybridlist(data, name='operation')
1014 1017 except (TypeError, KeyError):
1015 1018 # i18n: "obsfateoperations" is a keyword
1016 1019 errmsg = _("obsfateoperations first argument should be an iterable")
1017 1020 raise error.ParseError(errmsg)
1018 1021
1019 1022 @templatefunc('obsfatedate(markers)')
1020 1023 def obsfatedate(context, mapping, args):
1021 1024 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1022 1025 if len(args) != 1:
1023 1026 # i18n: "obsfatedate" is a keyword
1024 1027 raise error.ParseError(_("obsfatedate expects one argument"))
1025 1028
1026 1029 markers = evalfuncarg(context, mapping, args[0])
1027 1030
1028 1031 try:
1029 1032 data = obsutil.markersdates(markers)
1030 1033 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1031 1034 except (TypeError, KeyError):
1032 1035 # i18n: "obsfatedate" is a keyword
1033 1036 errmsg = _("obsfatedate first argument should be an iterable")
1034 1037 raise error.ParseError(errmsg)
1035 1038
1036 1039 @templatefunc('obsfateusers(markers)')
1037 1040 def obsfateusers(context, mapping, args):
1038 1041 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1039 1042 if len(args) != 1:
1040 1043 # i18n: "obsfateusers" is a keyword
1041 1044 raise error.ParseError(_("obsfateusers expects one argument"))
1042 1045
1043 1046 markers = evalfuncarg(context, mapping, args[0])
1044 1047
1045 1048 try:
1046 1049 data = obsutil.markersusers(markers)
1047 1050 return templatekw.hybridlist(data, name='user')
1048 1051 except (TypeError, KeyError, ValueError):
1049 1052 # i18n: "obsfateusers" is a keyword
1050 1053 msg = _("obsfateusers first argument should be an iterable of "
1051 1054 "obsmakers")
1052 1055 raise error.ParseError(msg)
1053 1056
1054 1057 @templatefunc('obsfateverb(successors, markers)')
1055 1058 def obsfateverb(context, mapping, args):
1056 1059 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1057 1060 if len(args) != 2:
1058 1061 # i18n: "obsfateverb" is a keyword
1059 1062 raise error.ParseError(_("obsfateverb expects two arguments"))
1060 1063
1061 1064 successors = evalfuncarg(context, mapping, args[0])
1062 1065 markers = evalfuncarg(context, mapping, args[1])
1063 1066
1064 1067 try:
1065 1068 return obsutil.obsfateverb(successors, markers)
1066 1069 except TypeError:
1067 1070 # i18n: "obsfateverb" is a keyword
1068 1071 errmsg = _("obsfateverb first argument should be countable")
1069 1072 raise error.ParseError(errmsg)
1070 1073
1071 1074 @templatefunc('relpath(path)')
1072 1075 def relpath(context, mapping, args):
1073 1076 """Convert a repository-absolute path into a filesystem path relative to
1074 1077 the current working directory."""
1075 1078 if len(args) != 1:
1076 1079 # i18n: "relpath" is a keyword
1077 1080 raise error.ParseError(_("relpath expects one argument"))
1078 1081
1079 1082 repo = context.resource(mapping, 'ctx').repo()
1080 1083 path = evalstring(context, mapping, args[0])
1081 1084 return repo.pathto(path)
1082 1085
1083 1086 @templatefunc('revset(query[, formatargs...])')
1084 1087 def revset(context, mapping, args):
1085 1088 """Execute a revision set query. See
1086 1089 :hg:`help revset`."""
1087 1090 if not len(args) > 0:
1088 1091 # i18n: "revset" is a keyword
1089 1092 raise error.ParseError(_("revset expects one or more arguments"))
1090 1093
1091 1094 raw = evalstring(context, mapping, args[0])
1092 1095 ctx = context.resource(mapping, 'ctx')
1093 1096 repo = ctx.repo()
1094 1097
1095 1098 def query(expr):
1096 1099 m = revsetmod.match(repo.ui, expr, repo=repo)
1097 1100 return m(repo)
1098 1101
1099 1102 if len(args) > 1:
1100 1103 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1101 1104 revs = query(revsetlang.formatspec(raw, *formatargs))
1102 1105 revs = list(revs)
1103 1106 else:
1104 1107 cache = context.resource(mapping, 'cache')
1105 1108 revsetcache = cache.setdefault("revsetcache", {})
1106 1109 if raw in revsetcache:
1107 1110 revs = revsetcache[raw]
1108 1111 else:
1109 1112 revs = query(raw)
1110 1113 revs = list(revs)
1111 1114 revsetcache[raw] = revs
1112 1115
1113 1116 # TODO: pass (context, mapping) pair to keyword function
1114 1117 props = context._resources.copy()
1115 1118 props.update(mapping)
1116 1119 return templatekw.showrevslist("revision", revs,
1117 1120 **pycompat.strkwargs(props))
1118 1121
1119 1122 @templatefunc('rstdoc(text, style)')
1120 1123 def rstdoc(context, mapping, args):
1121 1124 """Format reStructuredText."""
1122 1125 if len(args) != 2:
1123 1126 # i18n: "rstdoc" is a keyword
1124 1127 raise error.ParseError(_("rstdoc expects two arguments"))
1125 1128
1126 1129 text = evalstring(context, mapping, args[0])
1127 1130 style = evalstring(context, mapping, args[1])
1128 1131
1129 1132 return minirst.format(text, style=style, keep=['verbose'])
1130 1133
1131 1134 @templatefunc('separate(sep, args)', argspec='sep *args')
1132 1135 def separate(context, mapping, args):
1133 1136 """Add a separator between non-empty arguments."""
1134 1137 if 'sep' not in args:
1135 1138 # i18n: "separate" is a keyword
1136 1139 raise error.ParseError(_("separate expects at least one argument"))
1137 1140
1138 1141 sep = evalstring(context, mapping, args['sep'])
1139 1142 first = True
1140 1143 for arg in args['args']:
1141 1144 argstr = evalstring(context, mapping, arg)
1142 1145 if not argstr:
1143 1146 continue
1144 1147 if first:
1145 1148 first = False
1146 1149 else:
1147 1150 yield sep
1148 1151 yield argstr
1149 1152
1150 1153 @templatefunc('shortest(node, minlength=4)')
1151 1154 def shortest(context, mapping, args):
1152 1155 """Obtain the shortest representation of
1153 1156 a node."""
1154 1157 if not (1 <= len(args) <= 2):
1155 1158 # i18n: "shortest" is a keyword
1156 1159 raise error.ParseError(_("shortest() expects one or two arguments"))
1157 1160
1158 1161 node = evalstring(context, mapping, args[0])
1159 1162
1160 1163 minlength = 4
1161 1164 if len(args) > 1:
1162 1165 minlength = evalinteger(context, mapping, args[1],
1163 1166 # i18n: "shortest" is a keyword
1164 1167 _("shortest() expects an integer minlength"))
1165 1168
1166 1169 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1167 1170 # which would be unacceptably slow. so we look for hash collision in
1168 1171 # unfiltered space, which means some hashes may be slightly longer.
1169 1172 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1170 1173 return cl.shortest(node, minlength)
1171 1174
1172 1175 @templatefunc('strip(text[, chars])')
1173 1176 def strip(context, mapping, args):
1174 1177 """Strip characters from a string. By default,
1175 1178 strips all leading and trailing whitespace."""
1176 1179 if not (1 <= len(args) <= 2):
1177 1180 # i18n: "strip" is a keyword
1178 1181 raise error.ParseError(_("strip expects one or two arguments"))
1179 1182
1180 1183 text = evalstring(context, mapping, args[0])
1181 1184 if len(args) == 2:
1182 1185 chars = evalstring(context, mapping, args[1])
1183 1186 return text.strip(chars)
1184 1187 return text.strip()
1185 1188
1186 1189 @templatefunc('sub(pattern, replacement, expression)')
1187 1190 def sub(context, mapping, args):
1188 1191 """Perform text substitution
1189 1192 using regular expressions."""
1190 1193 if len(args) != 3:
1191 1194 # i18n: "sub" is a keyword
1192 1195 raise error.ParseError(_("sub expects three arguments"))
1193 1196
1194 1197 pat = evalstring(context, mapping, args[0])
1195 1198 rpl = evalstring(context, mapping, args[1])
1196 1199 src = evalstring(context, mapping, args[2])
1197 1200 try:
1198 1201 patre = re.compile(pat)
1199 1202 except re.error:
1200 1203 # i18n: "sub" is a keyword
1201 1204 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1202 1205 try:
1203 1206 yield patre.sub(rpl, src)
1204 1207 except re.error:
1205 1208 # i18n: "sub" is a keyword
1206 1209 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1207 1210
1208 1211 @templatefunc('startswith(pattern, text)')
1209 1212 def startswith(context, mapping, args):
1210 1213 """Returns the value from the "text" argument
1211 1214 if it begins with the content from the "pattern" argument."""
1212 1215 if len(args) != 2:
1213 1216 # i18n: "startswith" is a keyword
1214 1217 raise error.ParseError(_("startswith expects two arguments"))
1215 1218
1216 1219 patn = evalstring(context, mapping, args[0])
1217 1220 text = evalstring(context, mapping, args[1])
1218 1221 if text.startswith(patn):
1219 1222 return text
1220 1223 return ''
1221 1224
1222 1225 @templatefunc('word(number, text[, separator])')
1223 1226 def word(context, mapping, args):
1224 1227 """Return the nth word from a string."""
1225 1228 if not (2 <= len(args) <= 3):
1226 1229 # i18n: "word" is a keyword
1227 1230 raise error.ParseError(_("word expects two or three arguments, got %d")
1228 1231 % len(args))
1229 1232
1230 1233 num = evalinteger(context, mapping, args[0],
1231 1234 # i18n: "word" is a keyword
1232 1235 _("word expects an integer index"))
1233 1236 text = evalstring(context, mapping, args[1])
1234 1237 if len(args) == 3:
1235 1238 splitter = evalstring(context, mapping, args[2])
1236 1239 else:
1237 1240 splitter = None
1238 1241
1239 1242 tokens = text.split(splitter)
1240 1243 if num >= len(tokens) or num < -len(tokens):
1241 1244 return ''
1242 1245 else:
1243 1246 return tokens[num]
1244 1247
1245 1248 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1246 1249 exprmethods = {
1247 1250 "integer": lambda e, c: (runinteger, e[1]),
1248 1251 "string": lambda e, c: (runstring, e[1]),
1249 1252 "symbol": lambda e, c: (runsymbol, e[1]),
1250 1253 "template": buildtemplate,
1251 1254 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1252 1255 ".": buildmember,
1253 1256 "|": buildfilter,
1254 1257 "%": buildmap,
1255 1258 "func": buildfunc,
1256 1259 "keyvalue": buildkeyvaluepair,
1257 1260 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1258 1261 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1259 1262 "negate": buildnegate,
1260 1263 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1261 1264 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1262 1265 }
1263 1266
1264 1267 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1265 1268 methods = exprmethods.copy()
1266 1269 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1267 1270
1268 1271 class _aliasrules(parser.basealiasrules):
1269 1272 """Parsing and expansion rule set of template aliases"""
1270 1273 _section = _('template alias')
1271 1274 _parse = staticmethod(_parseexpr)
1272 1275
1273 1276 @staticmethod
1274 1277 def _trygetfunc(tree):
1275 1278 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1276 1279 None"""
1277 1280 if tree[0] == 'func' and tree[1][0] == 'symbol':
1278 1281 return tree[1][1], getlist(tree[2])
1279 1282 if tree[0] == '|' and tree[2][0] == 'symbol':
1280 1283 return tree[2][1], [tree[1]]
1281 1284
1282 1285 def expandaliases(tree, aliases):
1283 1286 """Return new tree of aliases are expanded"""
1284 1287 aliasmap = _aliasrules.buildmap(aliases)
1285 1288 return _aliasrules.expand(aliasmap, tree)
1286 1289
1287 1290 # template engine
1288 1291
1289 1292 stringify = templatefilters.stringify
1290 1293
1291 1294 def _flatten(thing):
1292 1295 '''yield a single stream from a possibly nested set of iterators'''
1293 1296 thing = templatekw.unwraphybrid(thing)
1294 1297 if isinstance(thing, bytes):
1295 1298 yield thing
1296 1299 elif isinstance(thing, str):
1297 1300 # We can only hit this on Python 3, and it's here to guard
1298 1301 # against infinite recursion.
1299 1302 raise error.ProgrammingError('Mercurial IO including templates is done'
1300 1303 ' with bytes, not strings')
1301 1304 elif thing is None:
1302 1305 pass
1303 1306 elif not util.safehasattr(thing, '__iter__'):
1304 1307 yield pycompat.bytestr(thing)
1305 1308 else:
1306 1309 for i in thing:
1307 1310 i = templatekw.unwraphybrid(i)
1308 1311 if isinstance(i, bytes):
1309 1312 yield i
1310 1313 elif i is None:
1311 1314 pass
1312 1315 elif not util.safehasattr(i, '__iter__'):
1313 1316 yield pycompat.bytestr(i)
1314 1317 else:
1315 1318 for j in _flatten(i):
1316 1319 yield j
1317 1320
1318 1321 def unquotestring(s):
1319 1322 '''unwrap quotes if any; otherwise returns unmodified string'''
1320 1323 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1321 1324 return s
1322 1325 return s[1:-1]
1323 1326
1324 1327 class engine(object):
1325 1328 '''template expansion engine.
1326 1329
1327 1330 template expansion works like this. a map file contains key=value
1328 1331 pairs. if value is quoted, it is treated as string. otherwise, it
1329 1332 is treated as name of template file.
1330 1333
1331 1334 templater is asked to expand a key in map. it looks up key, and
1332 1335 looks for strings like this: {foo}. it expands {foo} by looking up
1333 1336 foo in map, and substituting it. expansion is recursive: it stops
1334 1337 when there is no more {foo} to replace.
1335 1338
1336 1339 expansion also allows formatting and filtering.
1337 1340
1338 1341 format uses key to expand each item in list. syntax is
1339 1342 {key%format}.
1340 1343
1341 1344 filter uses function to transform value. syntax is
1342 1345 {key|filter1|filter2|...}.'''
1343 1346
1344 1347 def __init__(self, loader, filters=None, defaults=None, resources=None,
1345 1348 aliases=()):
1346 1349 self._loader = loader
1347 1350 if filters is None:
1348 1351 filters = {}
1349 1352 self._filters = filters
1350 1353 if defaults is None:
1351 1354 defaults = {}
1352 1355 if resources is None:
1353 1356 resources = {}
1354 1357 self._defaults = defaults
1355 1358 self._resources = resources
1356 1359 self._aliasmap = _aliasrules.buildmap(aliases)
1357 1360 self._cache = {} # key: (func, data)
1358 1361
1359 1362 def symbol(self, mapping, key):
1360 1363 """Resolve symbol to value or function; None if nothing found"""
1361 1364 v = None
1362 1365 if key not in self._resources:
1363 1366 v = mapping.get(key)
1364 1367 if v is None:
1365 1368 v = self._defaults.get(key)
1366 1369 return v
1367 1370
1368 1371 def resource(self, mapping, key):
1369 1372 """Return internal data (e.g. cache) used for keyword/function
1370 1373 evaluation"""
1371 1374 v = None
1372 1375 if key in self._resources:
1373 1376 v = mapping.get(key)
1374 1377 if v is None:
1375 1378 v = self._resources.get(key)
1376 1379 if v is None:
1377 1380 raise error.Abort(_('template resource not available: %s') % key)
1378 1381 return v
1379 1382
1380 1383 def _load(self, t):
1381 1384 '''load, parse, and cache a template'''
1382 1385 if t not in self._cache:
1383 1386 # put poison to cut recursion while compiling 't'
1384 1387 self._cache[t] = (_runrecursivesymbol, t)
1385 1388 try:
1386 1389 x = parse(self._loader(t))
1387 1390 if self._aliasmap:
1388 1391 x = _aliasrules.expand(self._aliasmap, x)
1389 1392 self._cache[t] = compileexp(x, self, methods)
1390 1393 except: # re-raises
1391 1394 del self._cache[t]
1392 1395 raise
1393 1396 return self._cache[t]
1394 1397
1395 1398 def process(self, t, mapping):
1396 1399 '''Perform expansion. t is name of map element to expand.
1397 1400 mapping contains added elements for use during expansion. Is a
1398 1401 generator.'''
1399 1402 func, data = self._load(t)
1400 1403 return _flatten(func(self, mapping, data))
1401 1404
1402 1405 engines = {'default': engine}
1403 1406
1404 1407 def stylelist():
1405 1408 paths = templatepaths()
1406 1409 if not paths:
1407 1410 return _('no templates found, try `hg debuginstall` for more info')
1408 1411 dirlist = os.listdir(paths[0])
1409 1412 stylelist = []
1410 1413 for file in dirlist:
1411 1414 split = file.split(".")
1412 1415 if split[-1] in ('orig', 'rej'):
1413 1416 continue
1414 1417 if split[0] == "map-cmdline":
1415 1418 stylelist.append(split[1])
1416 1419 return ", ".join(sorted(stylelist))
1417 1420
1418 1421 def _readmapfile(mapfile):
1419 1422 """Load template elements from the given map file"""
1420 1423 if not os.path.exists(mapfile):
1421 1424 raise error.Abort(_("style '%s' not found") % mapfile,
1422 1425 hint=_("available styles: %s") % stylelist())
1423 1426
1424 1427 base = os.path.dirname(mapfile)
1425 1428 conf = config.config(includepaths=templatepaths())
1426 1429 conf.read(mapfile, remap={'': 'templates'})
1427 1430
1428 1431 cache = {}
1429 1432 tmap = {}
1430 1433 aliases = []
1431 1434
1432 1435 val = conf.get('templates', '__base__')
1433 1436 if val and val[0] not in "'\"":
1434 1437 # treat as a pointer to a base class for this style
1435 1438 path = util.normpath(os.path.join(base, val))
1436 1439
1437 1440 # fallback check in template paths
1438 1441 if not os.path.exists(path):
1439 1442 for p in templatepaths():
1440 1443 p2 = util.normpath(os.path.join(p, val))
1441 1444 if os.path.isfile(p2):
1442 1445 path = p2
1443 1446 break
1444 1447 p3 = util.normpath(os.path.join(p2, "map"))
1445 1448 if os.path.isfile(p3):
1446 1449 path = p3
1447 1450 break
1448 1451
1449 1452 cache, tmap, aliases = _readmapfile(path)
1450 1453
1451 1454 for key, val in conf['templates'].items():
1452 1455 if not val:
1453 1456 raise error.ParseError(_('missing value'),
1454 1457 conf.source('templates', key))
1455 1458 if val[0] in "'\"":
1456 1459 if val[0] != val[-1]:
1457 1460 raise error.ParseError(_('unmatched quotes'),
1458 1461 conf.source('templates', key))
1459 1462 cache[key] = unquotestring(val)
1460 1463 elif key != '__base__':
1461 1464 val = 'default', val
1462 1465 if ':' in val[1]:
1463 1466 val = val[1].split(':', 1)
1464 1467 tmap[key] = val[0], os.path.join(base, val[1])
1465 1468 aliases.extend(conf['templatealias'].items())
1466 1469 return cache, tmap, aliases
1467 1470
1468 class TemplateNotFound(error.Abort):
1469 pass
1470
1471 1471 class templater(object):
1472 1472
1473 1473 def __init__(self, filters=None, defaults=None, resources=None,
1474 1474 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1475 1475 """Create template engine optionally with preloaded template fragments
1476 1476
1477 1477 - ``filters``: a dict of functions to transform a value into another.
1478 1478 - ``defaults``: a dict of symbol values/functions; may be overridden
1479 1479 by a ``mapping`` dict.
1480 1480 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1481 1481 from user template; may be overridden by a ``mapping`` dict.
1482 1482 - ``cache``: a dict of preloaded template fragments.
1483 1483 - ``aliases``: a list of alias (name, replacement) pairs.
1484 1484
1485 1485 self.cache may be updated later to register additional template
1486 1486 fragments.
1487 1487 """
1488 1488 if filters is None:
1489 1489 filters = {}
1490 1490 if defaults is None:
1491 1491 defaults = {}
1492 1492 if resources is None:
1493 1493 resources = {}
1494 1494 if cache is None:
1495 1495 cache = {}
1496 1496 self.cache = cache.copy()
1497 1497 self.map = {}
1498 1498 self.filters = templatefilters.filters.copy()
1499 1499 self.filters.update(filters)
1500 1500 self.defaults = defaults
1501 1501 self._resources = {'templ': self}
1502 1502 self._resources.update(resources)
1503 1503 self._aliases = aliases
1504 1504 self.minchunk, self.maxchunk = minchunk, maxchunk
1505 1505 self.ecache = {}
1506 1506
1507 1507 @classmethod
1508 1508 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1509 1509 cache=None, minchunk=1024, maxchunk=65536):
1510 1510 """Create templater from the specified map file"""
1511 1511 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1512 1512 cache, tmap, aliases = _readmapfile(mapfile)
1513 1513 t.cache.update(cache)
1514 1514 t.map = tmap
1515 1515 t._aliases = aliases
1516 1516 return t
1517 1517
1518 1518 def __contains__(self, key):
1519 1519 return key in self.cache or key in self.map
1520 1520
1521 1521 def load(self, t):
1522 1522 '''Get the template for the given template name. Use a local cache.'''
1523 1523 if t not in self.cache:
1524 1524 try:
1525 1525 self.cache[t] = util.readfile(self.map[t][1])
1526 1526 except KeyError as inst:
1527 1527 raise TemplateNotFound(_('"%s" not in template map') %
1528 1528 inst.args[0])
1529 1529 except IOError as inst:
1530 1530 raise IOError(inst.args[0], _('template file %s: %s') %
1531 1531 (self.map[t][1], inst.args[1]))
1532 1532 return self.cache[t]
1533 1533
1534 1534 def render(self, mapping):
1535 1535 """Render the default unnamed template and return result as string"""
1536 1536 mapping = pycompat.strkwargs(mapping)
1537 1537 return stringify(self('', **mapping))
1538 1538
1539 1539 def __call__(self, t, **mapping):
1540 1540 mapping = pycompat.byteskwargs(mapping)
1541 1541 ttype = t in self.map and self.map[t][0] or 'default'
1542 1542 if ttype not in self.ecache:
1543 1543 try:
1544 1544 ecls = engines[ttype]
1545 1545 except KeyError:
1546 1546 raise error.Abort(_('invalid template engine: %s') % ttype)
1547 1547 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1548 1548 self._resources, self._aliases)
1549 1549 proc = self.ecache[ttype]
1550 1550
1551 1551 stream = proc.process(t, mapping)
1552 1552 if self.minchunk:
1553 1553 stream = util.increasingchunks(stream, min=self.minchunk,
1554 1554 max=self.maxchunk)
1555 1555 return stream
1556 1556
1557 1557 def templatepaths():
1558 1558 '''return locations used for template files.'''
1559 1559 pathsrel = ['templates']
1560 1560 paths = [os.path.normpath(os.path.join(util.datapath, f))
1561 1561 for f in pathsrel]
1562 1562 return [p for p in paths if os.path.isdir(p)]
1563 1563
1564 1564 def templatepath(name):
1565 1565 '''return location of template file. returns None if not found.'''
1566 1566 for p in templatepaths():
1567 1567 f = os.path.join(p, name)
1568 1568 if os.path.exists(f):
1569 1569 return f
1570 1570 return None
1571 1571
1572 1572 def stylemap(styles, paths=None):
1573 1573 """Return path to mapfile for a given style.
1574 1574
1575 1575 Searches mapfile in the following locations:
1576 1576 1. templatepath/style/map
1577 1577 2. templatepath/map-style
1578 1578 3. templatepath/map
1579 1579 """
1580 1580
1581 1581 if paths is None:
1582 1582 paths = templatepaths()
1583 1583 elif isinstance(paths, str):
1584 1584 paths = [paths]
1585 1585
1586 1586 if isinstance(styles, str):
1587 1587 styles = [styles]
1588 1588
1589 1589 for style in styles:
1590 1590 # only plain name is allowed to honor template paths
1591 1591 if (not style
1592 1592 or style in (os.curdir, os.pardir)
1593 1593 or pycompat.ossep in style
1594 1594 or pycompat.osaltsep and pycompat.osaltsep in style):
1595 1595 continue
1596 1596 locations = [os.path.join(style, 'map'), 'map-' + style]
1597 1597 locations.append('map')
1598 1598
1599 1599 for path in paths:
1600 1600 for location in locations:
1601 1601 mapfile = os.path.join(path, location)
1602 1602 if os.path.isfile(mapfile):
1603 1603 return style, mapfile
1604 1604
1605 1605 raise RuntimeError("No hgweb templates found in %r" % paths)
1606 1606
1607 1607 def loadfunction(ui, extname, registrarobj):
1608 1608 """Load template function from specified registrarobj
1609 1609 """
1610 1610 for name, func in registrarobj._table.iteritems():
1611 1611 funcs[name] = func
1612 1612
1613 1613 # tell hggettext to extract docstrings from these functions:
1614 1614 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now