##// END OF EJS Templates
templater: fix "one arguments"
av6 -
r35387:fdd09d87 stable
parent child Browse files
Show More
@@ -1,1525 +1,1525
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 883 return templatekw.showlatesttags(pattern, **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 raise error.ParseError(_("max expects one arguments"))
921 raise error.ParseError(_("max expects one argument"))
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 raise error.ParseError(_("min expects one arguments"))
936 raise error.ParseError(_("min expects one argument"))
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 raise error.ParseError(_("obsfateoperations expects one arguments"))
961 raise error.ParseError(_("obsfateoperations expects one argument"))
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 raise error.ParseError(_("obsfatedate expects one arguments"))
978 raise error.ParseError(_("obsfatedate expects one argument"))
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 raise error.ParseError(_("obsfateusers expects one arguments"))
995 raise error.ParseError(_("obsfateusers expects one argument"))
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)')
1009 1009 def obsfateverb(context, mapping, args):
1010 1010 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1011 1011 if len(args) != 1:
1012 1012 # i18n: "obsfateverb" is a keyword
1013 1013 raise error.ParseError(_("obsfateverb expects one arguments"))
1014 1014
1015 1015 successors = evalfuncarg(context, mapping, args[0])
1016 1016
1017 1017 try:
1018 1018 return obsutil.successorsetverb(successors)
1019 1019 except TypeError:
1020 1020 # i18n: "obsfateverb" is a keyword
1021 1021 errmsg = _("obsfateverb first argument should be countable")
1022 1022 raise error.ParseError(errmsg)
1023 1023
1024 1024 @templatefunc('relpath(path)')
1025 1025 def relpath(context, mapping, args):
1026 1026 """Convert a repository-absolute path into a filesystem path relative to
1027 1027 the current working directory."""
1028 1028 if len(args) != 1:
1029 1029 # i18n: "relpath" is a keyword
1030 1030 raise error.ParseError(_("relpath expects one argument"))
1031 1031
1032 1032 repo = mapping['ctx'].repo()
1033 1033 path = evalstring(context, mapping, args[0])
1034 1034 return repo.pathto(path)
1035 1035
1036 1036 @templatefunc('revset(query[, formatargs...])')
1037 1037 def revset(context, mapping, args):
1038 1038 """Execute a revision set query. See
1039 1039 :hg:`help revset`."""
1040 1040 if not len(args) > 0:
1041 1041 # i18n: "revset" is a keyword
1042 1042 raise error.ParseError(_("revset expects one or more arguments"))
1043 1043
1044 1044 raw = evalstring(context, mapping, args[0])
1045 1045 ctx = mapping['ctx']
1046 1046 repo = ctx.repo()
1047 1047
1048 1048 def query(expr):
1049 1049 m = revsetmod.match(repo.ui, expr, repo=repo)
1050 1050 return m(repo)
1051 1051
1052 1052 if len(args) > 1:
1053 1053 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1054 1054 revs = query(revsetlang.formatspec(raw, *formatargs))
1055 1055 revs = list(revs)
1056 1056 else:
1057 1057 revsetcache = mapping['cache'].setdefault("revsetcache", {})
1058 1058 if raw in revsetcache:
1059 1059 revs = revsetcache[raw]
1060 1060 else:
1061 1061 revs = query(raw)
1062 1062 revs = list(revs)
1063 1063 revsetcache[raw] = revs
1064 1064
1065 1065 return templatekw.showrevslist("revision", revs, **mapping)
1066 1066
1067 1067 @templatefunc('rstdoc(text, style)')
1068 1068 def rstdoc(context, mapping, args):
1069 1069 """Format reStructuredText."""
1070 1070 if len(args) != 2:
1071 1071 # i18n: "rstdoc" is a keyword
1072 1072 raise error.ParseError(_("rstdoc expects two arguments"))
1073 1073
1074 1074 text = evalstring(context, mapping, args[0])
1075 1075 style = evalstring(context, mapping, args[1])
1076 1076
1077 1077 return minirst.format(text, style=style, keep=['verbose'])
1078 1078
1079 1079 @templatefunc('separate(sep, args)', argspec='sep *args')
1080 1080 def separate(context, mapping, args):
1081 1081 """Add a separator between non-empty arguments."""
1082 1082 if 'sep' not in args:
1083 1083 # i18n: "separate" is a keyword
1084 1084 raise error.ParseError(_("separate expects at least one argument"))
1085 1085
1086 1086 sep = evalstring(context, mapping, args['sep'])
1087 1087 first = True
1088 1088 for arg in args['args']:
1089 1089 argstr = evalstring(context, mapping, arg)
1090 1090 if not argstr:
1091 1091 continue
1092 1092 if first:
1093 1093 first = False
1094 1094 else:
1095 1095 yield sep
1096 1096 yield argstr
1097 1097
1098 1098 @templatefunc('shortest(node, minlength=4)')
1099 1099 def shortest(context, mapping, args):
1100 1100 """Obtain the shortest representation of
1101 1101 a node."""
1102 1102 if not (1 <= len(args) <= 2):
1103 1103 # i18n: "shortest" is a keyword
1104 1104 raise error.ParseError(_("shortest() expects one or two arguments"))
1105 1105
1106 1106 node = evalstring(context, mapping, args[0])
1107 1107
1108 1108 minlength = 4
1109 1109 if len(args) > 1:
1110 1110 minlength = evalinteger(context, mapping, args[1],
1111 1111 # i18n: "shortest" is a keyword
1112 1112 _("shortest() expects an integer minlength"))
1113 1113
1114 1114 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1115 1115 # which would be unacceptably slow. so we look for hash collision in
1116 1116 # unfiltered space, which means some hashes may be slightly longer.
1117 1117 cl = mapping['ctx']._repo.unfiltered().changelog
1118 1118 return cl.shortest(node, minlength)
1119 1119
1120 1120 @templatefunc('strip(text[, chars])')
1121 1121 def strip(context, mapping, args):
1122 1122 """Strip characters from a string. By default,
1123 1123 strips all leading and trailing whitespace."""
1124 1124 if not (1 <= len(args) <= 2):
1125 1125 # i18n: "strip" is a keyword
1126 1126 raise error.ParseError(_("strip expects one or two arguments"))
1127 1127
1128 1128 text = evalstring(context, mapping, args[0])
1129 1129 if len(args) == 2:
1130 1130 chars = evalstring(context, mapping, args[1])
1131 1131 return text.strip(chars)
1132 1132 return text.strip()
1133 1133
1134 1134 @templatefunc('sub(pattern, replacement, expression)')
1135 1135 def sub(context, mapping, args):
1136 1136 """Perform text substitution
1137 1137 using regular expressions."""
1138 1138 if len(args) != 3:
1139 1139 # i18n: "sub" is a keyword
1140 1140 raise error.ParseError(_("sub expects three arguments"))
1141 1141
1142 1142 pat = evalstring(context, mapping, args[0])
1143 1143 rpl = evalstring(context, mapping, args[1])
1144 1144 src = evalstring(context, mapping, args[2])
1145 1145 try:
1146 1146 patre = re.compile(pat)
1147 1147 except re.error:
1148 1148 # i18n: "sub" is a keyword
1149 1149 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1150 1150 try:
1151 1151 yield patre.sub(rpl, src)
1152 1152 except re.error:
1153 1153 # i18n: "sub" is a keyword
1154 1154 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1155 1155
1156 1156 @templatefunc('startswith(pattern, text)')
1157 1157 def startswith(context, mapping, args):
1158 1158 """Returns the value from the "text" argument
1159 1159 if it begins with the content from the "pattern" argument."""
1160 1160 if len(args) != 2:
1161 1161 # i18n: "startswith" is a keyword
1162 1162 raise error.ParseError(_("startswith expects two arguments"))
1163 1163
1164 1164 patn = evalstring(context, mapping, args[0])
1165 1165 text = evalstring(context, mapping, args[1])
1166 1166 if text.startswith(patn):
1167 1167 return text
1168 1168 return ''
1169 1169
1170 1170 @templatefunc('word(number, text[, separator])')
1171 1171 def word(context, mapping, args):
1172 1172 """Return the nth word from a string."""
1173 1173 if not (2 <= len(args) <= 3):
1174 1174 # i18n: "word" is a keyword
1175 1175 raise error.ParseError(_("word expects two or three arguments, got %d")
1176 1176 % len(args))
1177 1177
1178 1178 num = evalinteger(context, mapping, args[0],
1179 1179 # i18n: "word" is a keyword
1180 1180 _("word expects an integer index"))
1181 1181 text = evalstring(context, mapping, args[1])
1182 1182 if len(args) == 3:
1183 1183 splitter = evalstring(context, mapping, args[2])
1184 1184 else:
1185 1185 splitter = None
1186 1186
1187 1187 tokens = text.split(splitter)
1188 1188 if num >= len(tokens) or num < -len(tokens):
1189 1189 return ''
1190 1190 else:
1191 1191 return tokens[num]
1192 1192
1193 1193 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1194 1194 exprmethods = {
1195 1195 "integer": lambda e, c: (runinteger, e[1]),
1196 1196 "string": lambda e, c: (runstring, e[1]),
1197 1197 "symbol": lambda e, c: (runsymbol, e[1]),
1198 1198 "template": buildtemplate,
1199 1199 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1200 1200 ".": buildmember,
1201 1201 "|": buildfilter,
1202 1202 "%": buildmap,
1203 1203 "func": buildfunc,
1204 1204 "keyvalue": buildkeyvaluepair,
1205 1205 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1206 1206 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1207 1207 "negate": buildnegate,
1208 1208 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1209 1209 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1210 1210 }
1211 1211
1212 1212 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1213 1213 methods = exprmethods.copy()
1214 1214 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1215 1215
1216 1216 class _aliasrules(parser.basealiasrules):
1217 1217 """Parsing and expansion rule set of template aliases"""
1218 1218 _section = _('template alias')
1219 1219 _parse = staticmethod(_parseexpr)
1220 1220
1221 1221 @staticmethod
1222 1222 def _trygetfunc(tree):
1223 1223 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1224 1224 None"""
1225 1225 if tree[0] == 'func' and tree[1][0] == 'symbol':
1226 1226 return tree[1][1], getlist(tree[2])
1227 1227 if tree[0] == '|' and tree[2][0] == 'symbol':
1228 1228 return tree[2][1], [tree[1]]
1229 1229
1230 1230 def expandaliases(tree, aliases):
1231 1231 """Return new tree of aliases are expanded"""
1232 1232 aliasmap = _aliasrules.buildmap(aliases)
1233 1233 return _aliasrules.expand(aliasmap, tree)
1234 1234
1235 1235 # template engine
1236 1236
1237 1237 stringify = templatefilters.stringify
1238 1238
1239 1239 def _flatten(thing):
1240 1240 '''yield a single stream from a possibly nested set of iterators'''
1241 1241 thing = templatekw.unwraphybrid(thing)
1242 1242 if isinstance(thing, bytes):
1243 1243 yield thing
1244 1244 elif isinstance(thing, str):
1245 1245 # We can only hit this on Python 3, and it's here to guard
1246 1246 # against infinite recursion.
1247 1247 raise error.ProgrammingError('Mercurial IO including templates is done'
1248 1248 ' with bytes, not strings')
1249 1249 elif thing is None:
1250 1250 pass
1251 1251 elif not util.safehasattr(thing, '__iter__'):
1252 1252 yield pycompat.bytestr(thing)
1253 1253 else:
1254 1254 for i in thing:
1255 1255 i = templatekw.unwraphybrid(i)
1256 1256 if isinstance(i, bytes):
1257 1257 yield i
1258 1258 elif i is None:
1259 1259 pass
1260 1260 elif not util.safehasattr(i, '__iter__'):
1261 1261 yield pycompat.bytestr(i)
1262 1262 else:
1263 1263 for j in _flatten(i):
1264 1264 yield j
1265 1265
1266 1266 def unquotestring(s):
1267 1267 '''unwrap quotes if any; otherwise returns unmodified string'''
1268 1268 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1269 1269 return s
1270 1270 return s[1:-1]
1271 1271
1272 1272 class engine(object):
1273 1273 '''template expansion engine.
1274 1274
1275 1275 template expansion works like this. a map file contains key=value
1276 1276 pairs. if value is quoted, it is treated as string. otherwise, it
1277 1277 is treated as name of template file.
1278 1278
1279 1279 templater is asked to expand a key in map. it looks up key, and
1280 1280 looks for strings like this: {foo}. it expands {foo} by looking up
1281 1281 foo in map, and substituting it. expansion is recursive: it stops
1282 1282 when there is no more {foo} to replace.
1283 1283
1284 1284 expansion also allows formatting and filtering.
1285 1285
1286 1286 format uses key to expand each item in list. syntax is
1287 1287 {key%format}.
1288 1288
1289 1289 filter uses function to transform value. syntax is
1290 1290 {key|filter1|filter2|...}.'''
1291 1291
1292 1292 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1293 1293 self._loader = loader
1294 1294 if filters is None:
1295 1295 filters = {}
1296 1296 self._filters = filters
1297 1297 if defaults is None:
1298 1298 defaults = {}
1299 1299 self._defaults = defaults
1300 1300 self._aliasmap = _aliasrules.buildmap(aliases)
1301 1301 self._cache = {} # key: (func, data)
1302 1302
1303 1303 def _load(self, t):
1304 1304 '''load, parse, and cache a template'''
1305 1305 if t not in self._cache:
1306 1306 # put poison to cut recursion while compiling 't'
1307 1307 self._cache[t] = (_runrecursivesymbol, t)
1308 1308 try:
1309 1309 x = parse(self._loader(t))
1310 1310 if self._aliasmap:
1311 1311 x = _aliasrules.expand(self._aliasmap, x)
1312 1312 self._cache[t] = compileexp(x, self, methods)
1313 1313 except: # re-raises
1314 1314 del self._cache[t]
1315 1315 raise
1316 1316 return self._cache[t]
1317 1317
1318 1318 def process(self, t, mapping):
1319 1319 '''Perform expansion. t is name of map element to expand.
1320 1320 mapping contains added elements for use during expansion. Is a
1321 1321 generator.'''
1322 1322 func, data = self._load(t)
1323 1323 return _flatten(func(self, mapping, data))
1324 1324
1325 1325 engines = {'default': engine}
1326 1326
1327 1327 def stylelist():
1328 1328 paths = templatepaths()
1329 1329 if not paths:
1330 1330 return _('no templates found, try `hg debuginstall` for more info')
1331 1331 dirlist = os.listdir(paths[0])
1332 1332 stylelist = []
1333 1333 for file in dirlist:
1334 1334 split = file.split(".")
1335 1335 if split[-1] in ('orig', 'rej'):
1336 1336 continue
1337 1337 if split[0] == "map-cmdline":
1338 1338 stylelist.append(split[1])
1339 1339 return ", ".join(sorted(stylelist))
1340 1340
1341 1341 def _readmapfile(mapfile):
1342 1342 """Load template elements from the given map file"""
1343 1343 if not os.path.exists(mapfile):
1344 1344 raise error.Abort(_("style '%s' not found") % mapfile,
1345 1345 hint=_("available styles: %s") % stylelist())
1346 1346
1347 1347 base = os.path.dirname(mapfile)
1348 1348 conf = config.config(includepaths=templatepaths())
1349 1349 conf.read(mapfile, remap={'': 'templates'})
1350 1350
1351 1351 cache = {}
1352 1352 tmap = {}
1353 1353 aliases = []
1354 1354
1355 1355 val = conf.get('templates', '__base__')
1356 1356 if val and val[0] not in "'\"":
1357 1357 # treat as a pointer to a base class for this style
1358 1358 path = util.normpath(os.path.join(base, val))
1359 1359
1360 1360 # fallback check in template paths
1361 1361 if not os.path.exists(path):
1362 1362 for p in templatepaths():
1363 1363 p2 = util.normpath(os.path.join(p, val))
1364 1364 if os.path.isfile(p2):
1365 1365 path = p2
1366 1366 break
1367 1367 p3 = util.normpath(os.path.join(p2, "map"))
1368 1368 if os.path.isfile(p3):
1369 1369 path = p3
1370 1370 break
1371 1371
1372 1372 cache, tmap, aliases = _readmapfile(path)
1373 1373
1374 1374 for key, val in conf['templates'].items():
1375 1375 if not val:
1376 1376 raise error.ParseError(_('missing value'),
1377 1377 conf.source('templates', key))
1378 1378 if val[0] in "'\"":
1379 1379 if val[0] != val[-1]:
1380 1380 raise error.ParseError(_('unmatched quotes'),
1381 1381 conf.source('templates', key))
1382 1382 cache[key] = unquotestring(val)
1383 1383 elif key != '__base__':
1384 1384 val = 'default', val
1385 1385 if ':' in val[1]:
1386 1386 val = val[1].split(':', 1)
1387 1387 tmap[key] = val[0], os.path.join(base, val[1])
1388 1388 aliases.extend(conf['templatealias'].items())
1389 1389 return cache, tmap, aliases
1390 1390
1391 1391 class TemplateNotFound(error.Abort):
1392 1392 pass
1393 1393
1394 1394 class templater(object):
1395 1395
1396 1396 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1397 1397 minchunk=1024, maxchunk=65536):
1398 1398 '''set up template engine.
1399 1399 filters is dict of functions. each transforms a value into another.
1400 1400 defaults is dict of default map definitions.
1401 1401 aliases is list of alias (name, replacement) pairs.
1402 1402 '''
1403 1403 if filters is None:
1404 1404 filters = {}
1405 1405 if defaults is None:
1406 1406 defaults = {}
1407 1407 if cache is None:
1408 1408 cache = {}
1409 1409 self.cache = cache.copy()
1410 1410 self.map = {}
1411 1411 self.filters = templatefilters.filters.copy()
1412 1412 self.filters.update(filters)
1413 1413 self.defaults = defaults
1414 1414 self._aliases = aliases
1415 1415 self.minchunk, self.maxchunk = minchunk, maxchunk
1416 1416 self.ecache = {}
1417 1417
1418 1418 @classmethod
1419 1419 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1420 1420 minchunk=1024, maxchunk=65536):
1421 1421 """Create templater from the specified map file"""
1422 1422 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1423 1423 cache, tmap, aliases = _readmapfile(mapfile)
1424 1424 t.cache.update(cache)
1425 1425 t.map = tmap
1426 1426 t._aliases = aliases
1427 1427 return t
1428 1428
1429 1429 def __contains__(self, key):
1430 1430 return key in self.cache or key in self.map
1431 1431
1432 1432 def load(self, t):
1433 1433 '''Get the template for the given template name. Use a local cache.'''
1434 1434 if t not in self.cache:
1435 1435 try:
1436 1436 self.cache[t] = util.readfile(self.map[t][1])
1437 1437 except KeyError as inst:
1438 1438 raise TemplateNotFound(_('"%s" not in template map') %
1439 1439 inst.args[0])
1440 1440 except IOError as inst:
1441 1441 raise IOError(inst.args[0], _('template file %s: %s') %
1442 1442 (self.map[t][1], inst.args[1]))
1443 1443 return self.cache[t]
1444 1444
1445 1445 def render(self, mapping):
1446 1446 """Render the default unnamed template and return result as string"""
1447 1447 mapping = pycompat.strkwargs(mapping)
1448 1448 return stringify(self('', **mapping))
1449 1449
1450 1450 def __call__(self, t, **mapping):
1451 1451 mapping = pycompat.byteskwargs(mapping)
1452 1452 ttype = t in self.map and self.map[t][0] or 'default'
1453 1453 if ttype not in self.ecache:
1454 1454 try:
1455 1455 ecls = engines[ttype]
1456 1456 except KeyError:
1457 1457 raise error.Abort(_('invalid template engine: %s') % ttype)
1458 1458 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1459 1459 self._aliases)
1460 1460 proc = self.ecache[ttype]
1461 1461
1462 1462 stream = proc.process(t, mapping)
1463 1463 if self.minchunk:
1464 1464 stream = util.increasingchunks(stream, min=self.minchunk,
1465 1465 max=self.maxchunk)
1466 1466 return stream
1467 1467
1468 1468 def templatepaths():
1469 1469 '''return locations used for template files.'''
1470 1470 pathsrel = ['templates']
1471 1471 paths = [os.path.normpath(os.path.join(util.datapath, f))
1472 1472 for f in pathsrel]
1473 1473 return [p for p in paths if os.path.isdir(p)]
1474 1474
1475 1475 def templatepath(name):
1476 1476 '''return location of template file. returns None if not found.'''
1477 1477 for p in templatepaths():
1478 1478 f = os.path.join(p, name)
1479 1479 if os.path.exists(f):
1480 1480 return f
1481 1481 return None
1482 1482
1483 1483 def stylemap(styles, paths=None):
1484 1484 """Return path to mapfile for a given style.
1485 1485
1486 1486 Searches mapfile in the following locations:
1487 1487 1. templatepath/style/map
1488 1488 2. templatepath/map-style
1489 1489 3. templatepath/map
1490 1490 """
1491 1491
1492 1492 if paths is None:
1493 1493 paths = templatepaths()
1494 1494 elif isinstance(paths, str):
1495 1495 paths = [paths]
1496 1496
1497 1497 if isinstance(styles, str):
1498 1498 styles = [styles]
1499 1499
1500 1500 for style in styles:
1501 1501 # only plain name is allowed to honor template paths
1502 1502 if (not style
1503 1503 or style in (os.curdir, os.pardir)
1504 1504 or pycompat.ossep in style
1505 1505 or pycompat.osaltsep and pycompat.osaltsep in style):
1506 1506 continue
1507 1507 locations = [os.path.join(style, 'map'), 'map-' + style]
1508 1508 locations.append('map')
1509 1509
1510 1510 for path in paths:
1511 1511 for location in locations:
1512 1512 mapfile = os.path.join(path, location)
1513 1513 if os.path.isfile(mapfile):
1514 1514 return style, mapfile
1515 1515
1516 1516 raise RuntimeError("No hgweb templates found in %r" % paths)
1517 1517
1518 1518 def loadfunction(ui, extname, registrarobj):
1519 1519 """Load template function from specified registrarobj
1520 1520 """
1521 1521 for name, func in registrarobj._table.iteritems():
1522 1522 funcs[name] = func
1523 1523
1524 1524 # tell hggettext to extract docstrings from these functions:
1525 1525 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now