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