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