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