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