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