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