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