##// END OF EJS Templates
templater: use pycompat.sysbytes to bytes-ify some __name__ attrs...
Augie Fackler -
r34839:d3ea6a1c default
parent child Browse files
Show More
@@ -1,1524 +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 % (filt.__name__.encode('ascii'), sym))
430 % (pycompat.sysbytes(filt.__name__), sym))
431 431 else:
432 msg = _("incompatible use of template filter '%s'") % filt.__name__
432 msg = (_("incompatible use of template filter '%s'")
433 % pycompat.sysbytes(filt.__name__))
433 434 raise error.Abort(msg)
434 435
435 436 def buildmap(exp, context):
436 437 darg = compileexp(exp[1], context, methods)
437 438 targ = gettemplate(exp[2], context)
438 439 return (runmap, (darg, targ))
439 440
440 441 def runmap(context, mapping, data):
441 442 darg, targ = data
442 443 d = evalrawexp(context, mapping, darg)
443 444 if util.safehasattr(d, 'itermaps'):
444 445 diter = d.itermaps()
445 446 else:
446 447 try:
447 448 diter = iter(d)
448 449 except TypeError:
449 450 sym = findsymbolicname(darg)
450 451 if sym:
451 452 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
452 453 else:
453 454 raise error.ParseError(_("%r is not iterable") % d)
454 455
455 456 for i, v in enumerate(diter):
456 457 lm = mapping.copy()
457 458 lm['index'] = i
458 459 if isinstance(v, dict):
459 460 lm.update(v)
460 461 lm['originalnode'] = mapping.get('node')
461 462 yield evalrawexp(context, lm, targ)
462 463 else:
463 464 # v is not an iterable of dicts, this happen when 'key'
464 465 # has been fully expanded already and format is useless.
465 466 # If so, return the expanded value.
466 467 yield v
467 468
468 469 def buildmember(exp, context):
469 470 darg = compileexp(exp[1], context, methods)
470 471 memb = getsymbol(exp[2])
471 472 return (runmember, (darg, memb))
472 473
473 474 def runmember(context, mapping, data):
474 475 darg, memb = data
475 476 d = evalrawexp(context, mapping, darg)
476 477 if util.safehasattr(d, 'tomap'):
477 478 lm = mapping.copy()
478 479 lm.update(d.tomap())
479 480 return runsymbol(context, lm, memb)
480 481 if util.safehasattr(d, 'get'):
481 482 return _getdictitem(d, memb)
482 483
483 484 sym = findsymbolicname(darg)
484 485 if sym:
485 486 raise error.ParseError(_("keyword '%s' has no member") % sym)
486 487 else:
487 488 raise error.ParseError(_("%r has no member") % d)
488 489
489 490 def buildnegate(exp, context):
490 491 arg = compileexp(exp[1], context, exprmethods)
491 492 return (runnegate, arg)
492 493
493 494 def runnegate(context, mapping, data):
494 495 data = evalinteger(context, mapping, data,
495 496 _('negation needs an integer argument'))
496 497 return -data
497 498
498 499 def buildarithmetic(exp, context, func):
499 500 left = compileexp(exp[1], context, exprmethods)
500 501 right = compileexp(exp[2], context, exprmethods)
501 502 return (runarithmetic, (func, left, right))
502 503
503 504 def runarithmetic(context, mapping, data):
504 505 func, left, right = data
505 506 left = evalinteger(context, mapping, left,
506 507 _('arithmetic only defined on integers'))
507 508 right = evalinteger(context, mapping, right,
508 509 _('arithmetic only defined on integers'))
509 510 try:
510 511 return func(left, right)
511 512 except ZeroDivisionError:
512 513 raise error.Abort(_('division by zero is not defined'))
513 514
514 515 def buildfunc(exp, context):
515 516 n = getsymbol(exp[1])
516 517 if n in funcs:
517 518 f = funcs[n]
518 519 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
519 520 return (f, args)
520 521 if n in context._filters:
521 522 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
522 523 if len(args) != 1:
523 524 raise error.ParseError(_("filter %s expects one argument") % n)
524 525 f = context._filters[n]
525 526 return (runfilter, (args[0], f))
526 527 raise error.ParseError(_("unknown function '%s'") % n)
527 528
528 529 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
529 530 """Compile parsed tree of function arguments into list or dict of
530 531 (func, data) pairs
531 532
532 533 >>> context = engine(lambda t: (runsymbol, t))
533 534 >>> def fargs(expr, argspec):
534 535 ... x = _parseexpr(expr)
535 536 ... n = getsymbol(x[1])
536 537 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
537 538 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
538 539 ['l', 'k']
539 540 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
540 541 >>> list(args.keys()), list(args[b'opts'].keys())
541 542 (['opts'], ['opts', 'k'])
542 543 """
543 544 def compiledict(xs):
544 545 return util.sortdict((k, compileexp(x, context, curmethods))
545 546 for k, x in xs.iteritems())
546 547 def compilelist(xs):
547 548 return [compileexp(x, context, curmethods) for x in xs]
548 549
549 550 if not argspec:
550 551 # filter or function with no argspec: return list of positional args
551 552 return compilelist(getlist(exp))
552 553
553 554 # function with argspec: return dict of named args
554 555 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
555 556 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
556 557 keyvaluenode='keyvalue', keynode='symbol')
557 558 compargs = util.sortdict()
558 559 if varkey:
559 560 compargs[varkey] = compilelist(treeargs.pop(varkey))
560 561 if optkey:
561 562 compargs[optkey] = compiledict(treeargs.pop(optkey))
562 563 compargs.update(compiledict(treeargs))
563 564 return compargs
564 565
565 566 def buildkeyvaluepair(exp, content):
566 567 raise error.ParseError(_("can't use a key-value pair in this context"))
567 568
568 569 # dict of template built-in functions
569 570 funcs = {}
570 571
571 572 templatefunc = registrar.templatefunc(funcs)
572 573
573 574 @templatefunc('date(date[, fmt])')
574 575 def date(context, mapping, args):
575 576 """Format a date. See :hg:`help dates` for formatting
576 577 strings. The default is a Unix date format, including the timezone:
577 578 "Mon Sep 04 15:13:13 2006 0700"."""
578 579 if not (1 <= len(args) <= 2):
579 580 # i18n: "date" is a keyword
580 581 raise error.ParseError(_("date expects one or two arguments"))
581 582
582 583 date = evalfuncarg(context, mapping, args[0])
583 584 fmt = None
584 585 if len(args) == 2:
585 586 fmt = evalstring(context, mapping, args[1])
586 587 try:
587 588 if fmt is None:
588 589 return util.datestr(date)
589 590 else:
590 591 return util.datestr(date, fmt)
591 592 except (TypeError, ValueError):
592 593 # i18n: "date" is a keyword
593 594 raise error.ParseError(_("date expects a date information"))
594 595
595 596 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
596 597 def dict_(context, mapping, args):
597 598 """Construct a dict from key-value pairs. A key may be omitted if
598 599 a value expression can provide an unambiguous name."""
599 600 data = util.sortdict()
600 601
601 602 for v in args['args']:
602 603 k = findsymbolicname(v)
603 604 if not k:
604 605 raise error.ParseError(_('dict key cannot be inferred'))
605 606 if k in data or k in args['kwargs']:
606 607 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
607 608 data[k] = evalfuncarg(context, mapping, v)
608 609
609 610 data.update((k, evalfuncarg(context, mapping, v))
610 611 for k, v in args['kwargs'].iteritems())
611 612 return templatekw.hybriddict(data)
612 613
613 614 @templatefunc('diff([includepattern [, excludepattern]])')
614 615 def diff(context, mapping, args):
615 616 """Show a diff, optionally
616 617 specifying files to include or exclude."""
617 618 if len(args) > 2:
618 619 # i18n: "diff" is a keyword
619 620 raise error.ParseError(_("diff expects zero, one, or two arguments"))
620 621
621 622 def getpatterns(i):
622 623 if i < len(args):
623 624 s = evalstring(context, mapping, args[i]).strip()
624 625 if s:
625 626 return [s]
626 627 return []
627 628
628 629 ctx = mapping['ctx']
629 630 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
630 631
631 632 return ''.join(chunks)
632 633
633 634 @templatefunc('extdata(source)', argspec='source')
634 635 def extdata(context, mapping, args):
635 636 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
636 637 if 'source' not in args:
637 638 # i18n: "extdata" is a keyword
638 639 raise error.ParseError(_('extdata expects one argument'))
639 640
640 641 source = evalstring(context, mapping, args['source'])
641 642 cache = mapping['cache'].setdefault('extdata', {})
642 643 ctx = mapping['ctx']
643 644 if source in cache:
644 645 data = cache[source]
645 646 else:
646 647 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
647 648 return data.get(ctx.rev(), '')
648 649
649 650 @templatefunc('files(pattern)')
650 651 def files(context, mapping, args):
651 652 """All files of the current changeset matching the pattern. See
652 653 :hg:`help patterns`."""
653 654 if not len(args) == 1:
654 655 # i18n: "files" is a keyword
655 656 raise error.ParseError(_("files expects one argument"))
656 657
657 658 raw = evalstring(context, mapping, args[0])
658 659 ctx = mapping['ctx']
659 660 m = ctx.match([raw])
660 661 files = list(ctx.matches(m))
661 662 return templatekw.showlist("file", files, mapping)
662 663
663 664 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
664 665 def fill(context, mapping, args):
665 666 """Fill many
666 667 paragraphs with optional indentation. See the "fill" filter."""
667 668 if not (1 <= len(args) <= 4):
668 669 # i18n: "fill" is a keyword
669 670 raise error.ParseError(_("fill expects one to four arguments"))
670 671
671 672 text = evalstring(context, mapping, args[0])
672 673 width = 76
673 674 initindent = ''
674 675 hangindent = ''
675 676 if 2 <= len(args) <= 4:
676 677 width = evalinteger(context, mapping, args[1],
677 678 # i18n: "fill" is a keyword
678 679 _("fill expects an integer width"))
679 680 try:
680 681 initindent = evalstring(context, mapping, args[2])
681 682 hangindent = evalstring(context, mapping, args[3])
682 683 except IndexError:
683 684 pass
684 685
685 686 return templatefilters.fill(text, width, initindent, hangindent)
686 687
687 688 @templatefunc('formatnode(node)')
688 689 def formatnode(context, mapping, args):
689 690 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
690 691 if len(args) != 1:
691 692 # i18n: "formatnode" is a keyword
692 693 raise error.ParseError(_("formatnode expects one argument"))
693 694
694 695 ui = mapping['ui']
695 696 node = evalstring(context, mapping, args[0])
696 697 if ui.debugflag:
697 698 return node
698 699 return templatefilters.short(node)
699 700
700 701 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
701 702 argspec='text width fillchar left')
702 703 def pad(context, mapping, args):
703 704 """Pad text with a
704 705 fill character."""
705 706 if 'text' not in args or 'width' not in args:
706 707 # i18n: "pad" is a keyword
707 708 raise error.ParseError(_("pad() expects two to four arguments"))
708 709
709 710 width = evalinteger(context, mapping, args['width'],
710 711 # i18n: "pad" is a keyword
711 712 _("pad() expects an integer width"))
712 713
713 714 text = evalstring(context, mapping, args['text'])
714 715
715 716 left = False
716 717 fillchar = ' '
717 718 if 'fillchar' in args:
718 719 fillchar = evalstring(context, mapping, args['fillchar'])
719 720 if len(color.stripeffects(fillchar)) != 1:
720 721 # i18n: "pad" is a keyword
721 722 raise error.ParseError(_("pad() expects a single fill character"))
722 723 if 'left' in args:
723 724 left = evalboolean(context, mapping, args['left'])
724 725
725 726 fillwidth = width - encoding.colwidth(color.stripeffects(text))
726 727 if fillwidth <= 0:
727 728 return text
728 729 if left:
729 730 return fillchar * fillwidth + text
730 731 else:
731 732 return text + fillchar * fillwidth
732 733
733 734 @templatefunc('indent(text, indentchars[, firstline])')
734 735 def indent(context, mapping, args):
735 736 """Indents all non-empty lines
736 737 with the characters given in the indentchars string. An optional
737 738 third parameter will override the indent for the first line only
738 739 if present."""
739 740 if not (2 <= len(args) <= 3):
740 741 # i18n: "indent" is a keyword
741 742 raise error.ParseError(_("indent() expects two or three arguments"))
742 743
743 744 text = evalstring(context, mapping, args[0])
744 745 indent = evalstring(context, mapping, args[1])
745 746
746 747 if len(args) == 3:
747 748 firstline = evalstring(context, mapping, args[2])
748 749 else:
749 750 firstline = indent
750 751
751 752 # the indent function doesn't indent the first line, so we do it here
752 753 return templatefilters.indent(firstline + text, indent)
753 754
754 755 @templatefunc('get(dict, key)')
755 756 def get(context, mapping, args):
756 757 """Get an attribute/key from an object. Some keywords
757 758 are complex types. This function allows you to obtain the value of an
758 759 attribute on these types."""
759 760 if len(args) != 2:
760 761 # i18n: "get" is a keyword
761 762 raise error.ParseError(_("get() expects two arguments"))
762 763
763 764 dictarg = evalfuncarg(context, mapping, args[0])
764 765 if not util.safehasattr(dictarg, 'get'):
765 766 # i18n: "get" is a keyword
766 767 raise error.ParseError(_("get() expects a dict as first argument"))
767 768
768 769 key = evalfuncarg(context, mapping, args[1])
769 770 return _getdictitem(dictarg, key)
770 771
771 772 def _getdictitem(dictarg, key):
772 773 val = dictarg.get(key)
773 774 if val is None:
774 775 return
775 776 return templatekw.wraphybridvalue(dictarg, key, val)
776 777
777 778 @templatefunc('if(expr, then[, else])')
778 779 def if_(context, mapping, args):
779 780 """Conditionally execute based on the result of
780 781 an expression."""
781 782 if not (2 <= len(args) <= 3):
782 783 # i18n: "if" is a keyword
783 784 raise error.ParseError(_("if expects two or three arguments"))
784 785
785 786 test = evalboolean(context, mapping, args[0])
786 787 if test:
787 788 yield evalrawexp(context, mapping, args[1])
788 789 elif len(args) == 3:
789 790 yield evalrawexp(context, mapping, args[2])
790 791
791 792 @templatefunc('ifcontains(needle, haystack, then[, else])')
792 793 def ifcontains(context, mapping, args):
793 794 """Conditionally execute based
794 795 on whether the item "needle" is in "haystack"."""
795 796 if not (3 <= len(args) <= 4):
796 797 # i18n: "ifcontains" is a keyword
797 798 raise error.ParseError(_("ifcontains expects three or four arguments"))
798 799
799 800 haystack = evalfuncarg(context, mapping, args[1])
800 801 try:
801 802 needle = evalastype(context, mapping, args[0],
802 803 getattr(haystack, 'keytype', None) or bytes)
803 804 found = (needle in haystack)
804 805 except error.ParseError:
805 806 found = False
806 807
807 808 if found:
808 809 yield evalrawexp(context, mapping, args[2])
809 810 elif len(args) == 4:
810 811 yield evalrawexp(context, mapping, args[3])
811 812
812 813 @templatefunc('ifeq(expr1, expr2, then[, else])')
813 814 def ifeq(context, mapping, args):
814 815 """Conditionally execute based on
815 816 whether 2 items are equivalent."""
816 817 if not (3 <= len(args) <= 4):
817 818 # i18n: "ifeq" is a keyword
818 819 raise error.ParseError(_("ifeq expects three or four arguments"))
819 820
820 821 test = evalstring(context, mapping, args[0])
821 822 match = evalstring(context, mapping, args[1])
822 823 if test == match:
823 824 yield evalrawexp(context, mapping, args[2])
824 825 elif len(args) == 4:
825 826 yield evalrawexp(context, mapping, args[3])
826 827
827 828 @templatefunc('join(list, sep)')
828 829 def join(context, mapping, args):
829 830 """Join items in a list with a delimiter."""
830 831 if not (1 <= len(args) <= 2):
831 832 # i18n: "join" is a keyword
832 833 raise error.ParseError(_("join expects one or two arguments"))
833 834
834 835 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
835 836 # abuses generator as a keyword that returns a list of dicts.
836 837 joinset = evalrawexp(context, mapping, args[0])
837 838 joinset = templatekw.unwrapvalue(joinset)
838 839 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
839 840 joiner = " "
840 841 if len(args) > 1:
841 842 joiner = evalstring(context, mapping, args[1])
842 843
843 844 first = True
844 845 for x in joinset:
845 846 if first:
846 847 first = False
847 848 else:
848 849 yield joiner
849 850 yield joinfmt(x)
850 851
851 852 @templatefunc('label(label, expr)')
852 853 def label(context, mapping, args):
853 854 """Apply a label to generated content. Content with
854 855 a label applied can result in additional post-processing, such as
855 856 automatic colorization."""
856 857 if len(args) != 2:
857 858 # i18n: "label" is a keyword
858 859 raise error.ParseError(_("label expects two arguments"))
859 860
860 861 ui = mapping['ui']
861 862 thing = evalstring(context, mapping, args[1])
862 863 # preserve unknown symbol as literal so effects like 'red', 'bold',
863 864 # etc. don't need to be quoted
864 865 label = evalstringliteral(context, mapping, args[0])
865 866
866 867 return ui.label(thing, label)
867 868
868 869 @templatefunc('latesttag([pattern])')
869 870 def latesttag(context, mapping, args):
870 871 """The global tags matching the given pattern on the
871 872 most recent globally tagged ancestor of this changeset.
872 873 If no such tags exist, the "{tag}" template resolves to
873 874 the string "null"."""
874 875 if len(args) > 1:
875 876 # i18n: "latesttag" is a keyword
876 877 raise error.ParseError(_("latesttag expects at most one argument"))
877 878
878 879 pattern = None
879 880 if len(args) == 1:
880 881 pattern = evalstring(context, mapping, args[0])
881 882
882 883 return templatekw.showlatesttags(pattern, **mapping)
883 884
884 885 @templatefunc('localdate(date[, tz])')
885 886 def localdate(context, mapping, args):
886 887 """Converts a date to the specified timezone.
887 888 The default is local date."""
888 889 if not (1 <= len(args) <= 2):
889 890 # i18n: "localdate" is a keyword
890 891 raise error.ParseError(_("localdate expects one or two arguments"))
891 892
892 893 date = evalfuncarg(context, mapping, args[0])
893 894 try:
894 895 date = util.parsedate(date)
895 896 except AttributeError: # not str nor date tuple
896 897 # i18n: "localdate" is a keyword
897 898 raise error.ParseError(_("localdate expects a date information"))
898 899 if len(args) >= 2:
899 900 tzoffset = None
900 901 tz = evalfuncarg(context, mapping, args[1])
901 902 if isinstance(tz, str):
902 903 tzoffset, remainder = util.parsetimezone(tz)
903 904 if remainder:
904 905 tzoffset = None
905 906 if tzoffset is None:
906 907 try:
907 908 tzoffset = int(tz)
908 909 except (TypeError, ValueError):
909 910 # i18n: "localdate" is a keyword
910 911 raise error.ParseError(_("localdate expects a timezone"))
911 912 else:
912 913 tzoffset = util.makedate()[1]
913 914 return (date[0], tzoffset)
914 915
915 916 @templatefunc('max(iterable)')
916 917 def max_(context, mapping, args, **kwargs):
917 918 """Return the max of an iterable"""
918 919 if len(args) != 1:
919 920 # i18n: "max" is a keyword
920 921 raise error.ParseError(_("max expects one arguments"))
921 922
922 923 iterable = evalfuncarg(context, mapping, args[0])
923 924 try:
924 925 x = max(iterable)
925 926 except (TypeError, ValueError):
926 927 # i18n: "max" is a keyword
927 928 raise error.ParseError(_("max first argument should be an iterable"))
928 929 return templatekw.wraphybridvalue(iterable, x, x)
929 930
930 931 @templatefunc('min(iterable)')
931 932 def min_(context, mapping, args, **kwargs):
932 933 """Return the min of an iterable"""
933 934 if len(args) != 1:
934 935 # i18n: "min" is a keyword
935 936 raise error.ParseError(_("min expects one arguments"))
936 937
937 938 iterable = evalfuncarg(context, mapping, args[0])
938 939 try:
939 940 x = min(iterable)
940 941 except (TypeError, ValueError):
941 942 # i18n: "min" is a keyword
942 943 raise error.ParseError(_("min first argument should be an iterable"))
943 944 return templatekw.wraphybridvalue(iterable, x, x)
944 945
945 946 @templatefunc('mod(a, b)')
946 947 def mod(context, mapping, args):
947 948 """Calculate a mod b such that a / b + a mod b == a"""
948 949 if not len(args) == 2:
949 950 # i18n: "mod" is a keyword
950 951 raise error.ParseError(_("mod expects two arguments"))
951 952
952 953 func = lambda a, b: a % b
953 954 return runarithmetic(context, mapping, (func, args[0], args[1]))
954 955
955 956 @templatefunc('obsfateoperations(markers)')
956 957 def obsfateoperations(context, mapping, args):
957 958 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
958 959 if len(args) != 1:
959 960 # i18n: "obsfateoperations" is a keyword
960 961 raise error.ParseError(_("obsfateoperations expects one arguments"))
961 962
962 963 markers = evalfuncarg(context, mapping, args[0])
963 964
964 965 try:
965 966 data = obsutil.markersoperations(markers)
966 967 return templatekw.hybridlist(data, name='operation')
967 968 except (TypeError, KeyError):
968 969 # i18n: "obsfateoperations" is a keyword
969 970 errmsg = _("obsfateoperations first argument should be an iterable")
970 971 raise error.ParseError(errmsg)
971 972
972 973 @templatefunc('obsfatedate(markers)')
973 974 def obsfatedate(context, mapping, args):
974 975 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
975 976 if len(args) != 1:
976 977 # i18n: "obsfatedate" is a keyword
977 978 raise error.ParseError(_("obsfatedate expects one arguments"))
978 979
979 980 markers = evalfuncarg(context, mapping, args[0])
980 981
981 982 try:
982 983 data = obsutil.markersdates(markers)
983 984 return templatekw.hybridlist(data, name='date', fmt='%d %d')
984 985 except (TypeError, KeyError):
985 986 # i18n: "obsfatedate" is a keyword
986 987 errmsg = _("obsfatedate first argument should be an iterable")
987 988 raise error.ParseError(errmsg)
988 989
989 990 @templatefunc('obsfateusers(markers)')
990 991 def obsfateusers(context, mapping, args):
991 992 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
992 993 if len(args) != 1:
993 994 # i18n: "obsfateusers" is a keyword
994 995 raise error.ParseError(_("obsfateusers expects one arguments"))
995 996
996 997 markers = evalfuncarg(context, mapping, args[0])
997 998
998 999 try:
999 1000 data = obsutil.markersusers(markers)
1000 1001 return templatekw.hybridlist(data, name='user')
1001 1002 except (TypeError, KeyError, ValueError):
1002 1003 # i18n: "obsfateusers" is a keyword
1003 1004 msg = _("obsfateusers first argument should be an iterable of "
1004 1005 "obsmakers")
1005 1006 raise error.ParseError(msg)
1006 1007
1007 1008 @templatefunc('obsfateverb(successors)')
1008 1009 def obsfateverb(context, mapping, args):
1009 1010 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1010 1011 if len(args) != 1:
1011 1012 # i18n: "obsfateverb" is a keyword
1012 1013 raise error.ParseError(_("obsfateverb expects one arguments"))
1013 1014
1014 1015 successors = evalfuncarg(context, mapping, args[0])
1015 1016
1016 1017 try:
1017 1018 return obsutil.successorsetverb(successors)
1018 1019 except TypeError:
1019 1020 # i18n: "obsfateverb" is a keyword
1020 1021 errmsg = _("obsfateverb first argument should be countable")
1021 1022 raise error.ParseError(errmsg)
1022 1023
1023 1024 @templatefunc('relpath(path)')
1024 1025 def relpath(context, mapping, args):
1025 1026 """Convert a repository-absolute path into a filesystem path relative to
1026 1027 the current working directory."""
1027 1028 if len(args) != 1:
1028 1029 # i18n: "relpath" is a keyword
1029 1030 raise error.ParseError(_("relpath expects one argument"))
1030 1031
1031 1032 repo = mapping['ctx'].repo()
1032 1033 path = evalstring(context, mapping, args[0])
1033 1034 return repo.pathto(path)
1034 1035
1035 1036 @templatefunc('revset(query[, formatargs...])')
1036 1037 def revset(context, mapping, args):
1037 1038 """Execute a revision set query. See
1038 1039 :hg:`help revset`."""
1039 1040 if not len(args) > 0:
1040 1041 # i18n: "revset" is a keyword
1041 1042 raise error.ParseError(_("revset expects one or more arguments"))
1042 1043
1043 1044 raw = evalstring(context, mapping, args[0])
1044 1045 ctx = mapping['ctx']
1045 1046 repo = ctx.repo()
1046 1047
1047 1048 def query(expr):
1048 1049 m = revsetmod.match(repo.ui, expr, repo=repo)
1049 1050 return m(repo)
1050 1051
1051 1052 if len(args) > 1:
1052 1053 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1053 1054 revs = query(revsetlang.formatspec(raw, *formatargs))
1054 1055 revs = list(revs)
1055 1056 else:
1056 1057 revsetcache = mapping['cache'].setdefault("revsetcache", {})
1057 1058 if raw in revsetcache:
1058 1059 revs = revsetcache[raw]
1059 1060 else:
1060 1061 revs = query(raw)
1061 1062 revs = list(revs)
1062 1063 revsetcache[raw] = revs
1063 1064
1064 1065 return templatekw.showrevslist("revision", revs, **mapping)
1065 1066
1066 1067 @templatefunc('rstdoc(text, style)')
1067 1068 def rstdoc(context, mapping, args):
1068 1069 """Format reStructuredText."""
1069 1070 if len(args) != 2:
1070 1071 # i18n: "rstdoc" is a keyword
1071 1072 raise error.ParseError(_("rstdoc expects two arguments"))
1072 1073
1073 1074 text = evalstring(context, mapping, args[0])
1074 1075 style = evalstring(context, mapping, args[1])
1075 1076
1076 1077 return minirst.format(text, style=style, keep=['verbose'])
1077 1078
1078 1079 @templatefunc('separate(sep, args)', argspec='sep *args')
1079 1080 def separate(context, mapping, args):
1080 1081 """Add a separator between non-empty arguments."""
1081 1082 if 'sep' not in args:
1082 1083 # i18n: "separate" is a keyword
1083 1084 raise error.ParseError(_("separate expects at least one argument"))
1084 1085
1085 1086 sep = evalstring(context, mapping, args['sep'])
1086 1087 first = True
1087 1088 for arg in args['args']:
1088 1089 argstr = evalstring(context, mapping, arg)
1089 1090 if not argstr:
1090 1091 continue
1091 1092 if first:
1092 1093 first = False
1093 1094 else:
1094 1095 yield sep
1095 1096 yield argstr
1096 1097
1097 1098 @templatefunc('shortest(node, minlength=4)')
1098 1099 def shortest(context, mapping, args):
1099 1100 """Obtain the shortest representation of
1100 1101 a node."""
1101 1102 if not (1 <= len(args) <= 2):
1102 1103 # i18n: "shortest" is a keyword
1103 1104 raise error.ParseError(_("shortest() expects one or two arguments"))
1104 1105
1105 1106 node = evalstring(context, mapping, args[0])
1106 1107
1107 1108 minlength = 4
1108 1109 if len(args) > 1:
1109 1110 minlength = evalinteger(context, mapping, args[1],
1110 1111 # i18n: "shortest" is a keyword
1111 1112 _("shortest() expects an integer minlength"))
1112 1113
1113 1114 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1114 1115 # which would be unacceptably slow. so we look for hash collision in
1115 1116 # unfiltered space, which means some hashes may be slightly longer.
1116 1117 cl = mapping['ctx']._repo.unfiltered().changelog
1117 1118 return cl.shortest(node, minlength)
1118 1119
1119 1120 @templatefunc('strip(text[, chars])')
1120 1121 def strip(context, mapping, args):
1121 1122 """Strip characters from a string. By default,
1122 1123 strips all leading and trailing whitespace."""
1123 1124 if not (1 <= len(args) <= 2):
1124 1125 # i18n: "strip" is a keyword
1125 1126 raise error.ParseError(_("strip expects one or two arguments"))
1126 1127
1127 1128 text = evalstring(context, mapping, args[0])
1128 1129 if len(args) == 2:
1129 1130 chars = evalstring(context, mapping, args[1])
1130 1131 return text.strip(chars)
1131 1132 return text.strip()
1132 1133
1133 1134 @templatefunc('sub(pattern, replacement, expression)')
1134 1135 def sub(context, mapping, args):
1135 1136 """Perform text substitution
1136 1137 using regular expressions."""
1137 1138 if len(args) != 3:
1138 1139 # i18n: "sub" is a keyword
1139 1140 raise error.ParseError(_("sub expects three arguments"))
1140 1141
1141 1142 pat = evalstring(context, mapping, args[0])
1142 1143 rpl = evalstring(context, mapping, args[1])
1143 1144 src = evalstring(context, mapping, args[2])
1144 1145 try:
1145 1146 patre = re.compile(pat)
1146 1147 except re.error:
1147 1148 # i18n: "sub" is a keyword
1148 1149 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1149 1150 try:
1150 1151 yield patre.sub(rpl, src)
1151 1152 except re.error:
1152 1153 # i18n: "sub" is a keyword
1153 1154 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1154 1155
1155 1156 @templatefunc('startswith(pattern, text)')
1156 1157 def startswith(context, mapping, args):
1157 1158 """Returns the value from the "text" argument
1158 1159 if it begins with the content from the "pattern" argument."""
1159 1160 if len(args) != 2:
1160 1161 # i18n: "startswith" is a keyword
1161 1162 raise error.ParseError(_("startswith expects two arguments"))
1162 1163
1163 1164 patn = evalstring(context, mapping, args[0])
1164 1165 text = evalstring(context, mapping, args[1])
1165 1166 if text.startswith(patn):
1166 1167 return text
1167 1168 return ''
1168 1169
1169 1170 @templatefunc('word(number, text[, separator])')
1170 1171 def word(context, mapping, args):
1171 1172 """Return the nth word from a string."""
1172 1173 if not (2 <= len(args) <= 3):
1173 1174 # i18n: "word" is a keyword
1174 1175 raise error.ParseError(_("word expects two or three arguments, got %d")
1175 1176 % len(args))
1176 1177
1177 1178 num = evalinteger(context, mapping, args[0],
1178 1179 # i18n: "word" is a keyword
1179 1180 _("word expects an integer index"))
1180 1181 text = evalstring(context, mapping, args[1])
1181 1182 if len(args) == 3:
1182 1183 splitter = evalstring(context, mapping, args[2])
1183 1184 else:
1184 1185 splitter = None
1185 1186
1186 1187 tokens = text.split(splitter)
1187 1188 if num >= len(tokens) or num < -len(tokens):
1188 1189 return ''
1189 1190 else:
1190 1191 return tokens[num]
1191 1192
1192 1193 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1193 1194 exprmethods = {
1194 1195 "integer": lambda e, c: (runinteger, e[1]),
1195 1196 "string": lambda e, c: (runstring, e[1]),
1196 1197 "symbol": lambda e, c: (runsymbol, e[1]),
1197 1198 "template": buildtemplate,
1198 1199 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1199 1200 ".": buildmember,
1200 1201 "|": buildfilter,
1201 1202 "%": buildmap,
1202 1203 "func": buildfunc,
1203 1204 "keyvalue": buildkeyvaluepair,
1204 1205 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1205 1206 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1206 1207 "negate": buildnegate,
1207 1208 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1208 1209 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1209 1210 }
1210 1211
1211 1212 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1212 1213 methods = exprmethods.copy()
1213 1214 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1214 1215
1215 1216 class _aliasrules(parser.basealiasrules):
1216 1217 """Parsing and expansion rule set of template aliases"""
1217 1218 _section = _('template alias')
1218 1219 _parse = staticmethod(_parseexpr)
1219 1220
1220 1221 @staticmethod
1221 1222 def _trygetfunc(tree):
1222 1223 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1223 1224 None"""
1224 1225 if tree[0] == 'func' and tree[1][0] == 'symbol':
1225 1226 return tree[1][1], getlist(tree[2])
1226 1227 if tree[0] == '|' and tree[2][0] == 'symbol':
1227 1228 return tree[2][1], [tree[1]]
1228 1229
1229 1230 def expandaliases(tree, aliases):
1230 1231 """Return new tree of aliases are expanded"""
1231 1232 aliasmap = _aliasrules.buildmap(aliases)
1232 1233 return _aliasrules.expand(aliasmap, tree)
1233 1234
1234 1235 # template engine
1235 1236
1236 1237 stringify = templatefilters.stringify
1237 1238
1238 1239 def _flatten(thing):
1239 1240 '''yield a single stream from a possibly nested set of iterators'''
1240 1241 thing = templatekw.unwraphybrid(thing)
1241 1242 if isinstance(thing, bytes):
1242 1243 yield thing
1243 1244 elif isinstance(thing, str):
1244 1245 # We can only hit this on Python 3, and it's here to guard
1245 1246 # against infinite recursion.
1246 1247 raise error.ProgrammingError('Mercurial IO including templates is done'
1247 1248 ' with bytes, not strings')
1248 1249 elif thing is None:
1249 1250 pass
1250 1251 elif not util.safehasattr(thing, '__iter__'):
1251 1252 yield pycompat.bytestr(thing)
1252 1253 else:
1253 1254 for i in thing:
1254 1255 i = templatekw.unwraphybrid(i)
1255 1256 if isinstance(i, bytes):
1256 1257 yield i
1257 1258 elif i is None:
1258 1259 pass
1259 1260 elif not util.safehasattr(i, '__iter__'):
1260 1261 yield pycompat.bytestr(i)
1261 1262 else:
1262 1263 for j in _flatten(i):
1263 1264 yield j
1264 1265
1265 1266 def unquotestring(s):
1266 1267 '''unwrap quotes if any; otherwise returns unmodified string'''
1267 1268 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1268 1269 return s
1269 1270 return s[1:-1]
1270 1271
1271 1272 class engine(object):
1272 1273 '''template expansion engine.
1273 1274
1274 1275 template expansion works like this. a map file contains key=value
1275 1276 pairs. if value is quoted, it is treated as string. otherwise, it
1276 1277 is treated as name of template file.
1277 1278
1278 1279 templater is asked to expand a key in map. it looks up key, and
1279 1280 looks for strings like this: {foo}. it expands {foo} by looking up
1280 1281 foo in map, and substituting it. expansion is recursive: it stops
1281 1282 when there is no more {foo} to replace.
1282 1283
1283 1284 expansion also allows formatting and filtering.
1284 1285
1285 1286 format uses key to expand each item in list. syntax is
1286 1287 {key%format}.
1287 1288
1288 1289 filter uses function to transform value. syntax is
1289 1290 {key|filter1|filter2|...}.'''
1290 1291
1291 1292 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1292 1293 self._loader = loader
1293 1294 if filters is None:
1294 1295 filters = {}
1295 1296 self._filters = filters
1296 1297 if defaults is None:
1297 1298 defaults = {}
1298 1299 self._defaults = defaults
1299 1300 self._aliasmap = _aliasrules.buildmap(aliases)
1300 1301 self._cache = {} # key: (func, data)
1301 1302
1302 1303 def _load(self, t):
1303 1304 '''load, parse, and cache a template'''
1304 1305 if t not in self._cache:
1305 1306 # put poison to cut recursion while compiling 't'
1306 1307 self._cache[t] = (_runrecursivesymbol, t)
1307 1308 try:
1308 1309 x = parse(self._loader(t))
1309 1310 if self._aliasmap:
1310 1311 x = _aliasrules.expand(self._aliasmap, x)
1311 1312 self._cache[t] = compileexp(x, self, methods)
1312 1313 except: # re-raises
1313 1314 del self._cache[t]
1314 1315 raise
1315 1316 return self._cache[t]
1316 1317
1317 1318 def process(self, t, mapping):
1318 1319 '''Perform expansion. t is name of map element to expand.
1319 1320 mapping contains added elements for use during expansion. Is a
1320 1321 generator.'''
1321 1322 func, data = self._load(t)
1322 1323 return _flatten(func(self, mapping, data))
1323 1324
1324 1325 engines = {'default': engine}
1325 1326
1326 1327 def stylelist():
1327 1328 paths = templatepaths()
1328 1329 if not paths:
1329 1330 return _('no templates found, try `hg debuginstall` for more info')
1330 1331 dirlist = os.listdir(paths[0])
1331 1332 stylelist = []
1332 1333 for file in dirlist:
1333 1334 split = file.split(".")
1334 1335 if split[-1] in ('orig', 'rej'):
1335 1336 continue
1336 1337 if split[0] == "map-cmdline":
1337 1338 stylelist.append(split[1])
1338 1339 return ", ".join(sorted(stylelist))
1339 1340
1340 1341 def _readmapfile(mapfile):
1341 1342 """Load template elements from the given map file"""
1342 1343 if not os.path.exists(mapfile):
1343 1344 raise error.Abort(_("style '%s' not found") % mapfile,
1344 1345 hint=_("available styles: %s") % stylelist())
1345 1346
1346 1347 base = os.path.dirname(mapfile)
1347 1348 conf = config.config(includepaths=templatepaths())
1348 1349 conf.read(mapfile, remap={'': 'templates'})
1349 1350
1350 1351 cache = {}
1351 1352 tmap = {}
1352 1353 aliases = []
1353 1354
1354 1355 val = conf.get('templates', '__base__')
1355 1356 if val and val[0] not in "'\"":
1356 1357 # treat as a pointer to a base class for this style
1357 1358 path = util.normpath(os.path.join(base, val))
1358 1359
1359 1360 # fallback check in template paths
1360 1361 if not os.path.exists(path):
1361 1362 for p in templatepaths():
1362 1363 p2 = util.normpath(os.path.join(p, val))
1363 1364 if os.path.isfile(p2):
1364 1365 path = p2
1365 1366 break
1366 1367 p3 = util.normpath(os.path.join(p2, "map"))
1367 1368 if os.path.isfile(p3):
1368 1369 path = p3
1369 1370 break
1370 1371
1371 1372 cache, tmap, aliases = _readmapfile(path)
1372 1373
1373 1374 for key, val in conf['templates'].items():
1374 1375 if not val:
1375 1376 raise error.ParseError(_('missing value'),
1376 1377 conf.source('templates', key))
1377 1378 if val[0] in "'\"":
1378 1379 if val[0] != val[-1]:
1379 1380 raise error.ParseError(_('unmatched quotes'),
1380 1381 conf.source('templates', key))
1381 1382 cache[key] = unquotestring(val)
1382 1383 elif key != '__base__':
1383 1384 val = 'default', val
1384 1385 if ':' in val[1]:
1385 1386 val = val[1].split(':', 1)
1386 1387 tmap[key] = val[0], os.path.join(base, val[1])
1387 1388 aliases.extend(conf['templatealias'].items())
1388 1389 return cache, tmap, aliases
1389 1390
1390 1391 class TemplateNotFound(error.Abort):
1391 1392 pass
1392 1393
1393 1394 class templater(object):
1394 1395
1395 1396 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1396 1397 minchunk=1024, maxchunk=65536):
1397 1398 '''set up template engine.
1398 1399 filters is dict of functions. each transforms a value into another.
1399 1400 defaults is dict of default map definitions.
1400 1401 aliases is list of alias (name, replacement) pairs.
1401 1402 '''
1402 1403 if filters is None:
1403 1404 filters = {}
1404 1405 if defaults is None:
1405 1406 defaults = {}
1406 1407 if cache is None:
1407 1408 cache = {}
1408 1409 self.cache = cache.copy()
1409 1410 self.map = {}
1410 1411 self.filters = templatefilters.filters.copy()
1411 1412 self.filters.update(filters)
1412 1413 self.defaults = defaults
1413 1414 self._aliases = aliases
1414 1415 self.minchunk, self.maxchunk = minchunk, maxchunk
1415 1416 self.ecache = {}
1416 1417
1417 1418 @classmethod
1418 1419 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1419 1420 minchunk=1024, maxchunk=65536):
1420 1421 """Create templater from the specified map file"""
1421 1422 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1422 1423 cache, tmap, aliases = _readmapfile(mapfile)
1423 1424 t.cache.update(cache)
1424 1425 t.map = tmap
1425 1426 t._aliases = aliases
1426 1427 return t
1427 1428
1428 1429 def __contains__(self, key):
1429 1430 return key in self.cache or key in self.map
1430 1431
1431 1432 def load(self, t):
1432 1433 '''Get the template for the given template name. Use a local cache.'''
1433 1434 if t not in self.cache:
1434 1435 try:
1435 1436 self.cache[t] = util.readfile(self.map[t][1])
1436 1437 except KeyError as inst:
1437 1438 raise TemplateNotFound(_('"%s" not in template map') %
1438 1439 inst.args[0])
1439 1440 except IOError as inst:
1440 1441 raise IOError(inst.args[0], _('template file %s: %s') %
1441 1442 (self.map[t][1], inst.args[1]))
1442 1443 return self.cache[t]
1443 1444
1444 1445 def render(self, mapping):
1445 1446 """Render the default unnamed template and return result as string"""
1446 1447 mapping = pycompat.strkwargs(mapping)
1447 1448 return stringify(self('', **mapping))
1448 1449
1449 1450 def __call__(self, t, **mapping):
1450 1451 mapping = pycompat.byteskwargs(mapping)
1451 1452 ttype = t in self.map and self.map[t][0] or 'default'
1452 1453 if ttype not in self.ecache:
1453 1454 try:
1454 1455 ecls = engines[ttype]
1455 1456 except KeyError:
1456 1457 raise error.Abort(_('invalid template engine: %s') % ttype)
1457 1458 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1458 1459 self._aliases)
1459 1460 proc = self.ecache[ttype]
1460 1461
1461 1462 stream = proc.process(t, mapping)
1462 1463 if self.minchunk:
1463 1464 stream = util.increasingchunks(stream, min=self.minchunk,
1464 1465 max=self.maxchunk)
1465 1466 return stream
1466 1467
1467 1468 def templatepaths():
1468 1469 '''return locations used for template files.'''
1469 1470 pathsrel = ['templates']
1470 1471 paths = [os.path.normpath(os.path.join(util.datapath, f))
1471 1472 for f in pathsrel]
1472 1473 return [p for p in paths if os.path.isdir(p)]
1473 1474
1474 1475 def templatepath(name):
1475 1476 '''return location of template file. returns None if not found.'''
1476 1477 for p in templatepaths():
1477 1478 f = os.path.join(p, name)
1478 1479 if os.path.exists(f):
1479 1480 return f
1480 1481 return None
1481 1482
1482 1483 def stylemap(styles, paths=None):
1483 1484 """Return path to mapfile for a given style.
1484 1485
1485 1486 Searches mapfile in the following locations:
1486 1487 1. templatepath/style/map
1487 1488 2. templatepath/map-style
1488 1489 3. templatepath/map
1489 1490 """
1490 1491
1491 1492 if paths is None:
1492 1493 paths = templatepaths()
1493 1494 elif isinstance(paths, str):
1494 1495 paths = [paths]
1495 1496
1496 1497 if isinstance(styles, str):
1497 1498 styles = [styles]
1498 1499
1499 1500 for style in styles:
1500 1501 # only plain name is allowed to honor template paths
1501 1502 if (not style
1502 1503 or style in (os.curdir, os.pardir)
1503 1504 or pycompat.ossep in style
1504 1505 or pycompat.osaltsep and pycompat.osaltsep in style):
1505 1506 continue
1506 1507 locations = [os.path.join(style, 'map'), 'map-' + style]
1507 1508 locations.append('map')
1508 1509
1509 1510 for path in paths:
1510 1511 for location in locations:
1511 1512 mapfile = os.path.join(path, location)
1512 1513 if os.path.isfile(mapfile):
1513 1514 return style, mapfile
1514 1515
1515 1516 raise RuntimeError("No hgweb templates found in %r" % paths)
1516 1517
1517 1518 def loadfunction(ui, extname, registrarobj):
1518 1519 """Load template function from specified registrarobj
1519 1520 """
1520 1521 for name, func in registrarobj._table.iteritems():
1521 1522 funcs[name] = func
1522 1523
1523 1524 # tell hggettext to extract docstrings from these functions:
1524 1525 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now