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