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