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