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