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