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