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