##// END OF EJS Templates
py3: use bytes[n:n + 1] to get bytes in templater._parsetemplate()
Yuya Nishihara -
r34073:f55769e4 default
parent child Browse files
Show More
@@ -1,1460 +1,1460 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
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('foo{bar}"baz', 0, 12)
151 151 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
152 152 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
153 153 ([('string', 'foo'), ('symbol', 'bar')], 9)
154 154 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
155 155 ([('string', 'foo')], 4)
156 156 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
157 157 ([('string', 'foo"'), ('string', 'bar')], 9)
158 158 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
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 c = tmpl[n]
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 prettyformat(_unnesttemplatelist(tree))
196 196 >>> f(('template', []))
197 197 ('string', '')
198 198 >>> f(('template', [('string', 'foo')]))
199 199 ('string', 'foo')
200 200 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
201 201 (template
202 202 ('string', 'foo')
203 203 ('symbol', 'rev'))
204 204 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
205 205 (template
206 206 ('symbol', 'rev'))
207 207 >>> f(('template', [('template', [('string', '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('"foo"')
235 235 ('string', 'foo')
236 236 >>> _parseexpr('foo(bar)')
237 237 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
238 238 >>> _parseexpr('foo(')
239 239 Traceback (most recent call last):
240 240 ...
241 241 ParseError: ('not a prefix: end', 4)
242 242 >>> _parseexpr('"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 426 if func is runsymbol:
427 427 raise error.ParseError(_("keyword '%s' is not iterable") % data)
428 428 else:
429 429 raise error.ParseError(_("%r is not iterable") % d)
430 430
431 431 for i, v in enumerate(diter):
432 432 lm = mapping.copy()
433 433 lm['index'] = i
434 434 if isinstance(v, dict):
435 435 lm.update(v)
436 436 lm['originalnode'] = mapping.get('node')
437 437 yield tfunc(context, lm, tdata)
438 438 else:
439 439 # v is not an iterable of dicts, this happen when 'key'
440 440 # has been fully expanded already and format is useless.
441 441 # If so, return the expanded value.
442 442 yield v
443 443
444 444 def buildnegate(exp, context):
445 445 arg = compileexp(exp[1], context, exprmethods)
446 446 return (runnegate, arg)
447 447
448 448 def runnegate(context, mapping, data):
449 449 data = evalinteger(context, mapping, data,
450 450 _('negation needs an integer argument'))
451 451 return -data
452 452
453 453 def buildarithmetic(exp, context, func):
454 454 left = compileexp(exp[1], context, exprmethods)
455 455 right = compileexp(exp[2], context, exprmethods)
456 456 return (runarithmetic, (func, left, right))
457 457
458 458 def runarithmetic(context, mapping, data):
459 459 func, left, right = data
460 460 left = evalinteger(context, mapping, left,
461 461 _('arithmetic only defined on integers'))
462 462 right = evalinteger(context, mapping, right,
463 463 _('arithmetic only defined on integers'))
464 464 try:
465 465 return func(left, right)
466 466 except ZeroDivisionError:
467 467 raise error.Abort(_('division by zero is not defined'))
468 468
469 469 def buildfunc(exp, context):
470 470 n = getsymbol(exp[1])
471 471 if n in funcs:
472 472 f = funcs[n]
473 473 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
474 474 return (f, args)
475 475 if n in context._filters:
476 476 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
477 477 if len(args) != 1:
478 478 raise error.ParseError(_("filter %s expects one argument") % n)
479 479 f = context._filters[n]
480 480 return (runfilter, (args[0], f))
481 481 raise error.ParseError(_("unknown function '%s'") % n)
482 482
483 483 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
484 484 """Compile parsed tree of function arguments into list or dict of
485 485 (func, data) pairs
486 486
487 487 >>> context = engine(lambda t: (runsymbol, t))
488 488 >>> def fargs(expr, argspec):
489 489 ... x = _parseexpr(expr)
490 490 ... n = getsymbol(x[1])
491 491 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
492 492 >>> fargs('a(l=1, k=2)', 'k l m').keys()
493 493 ['l', 'k']
494 494 >>> args = fargs('a(opts=1, k=2)', '**opts')
495 495 >>> args.keys(), args['opts'].keys()
496 496 (['opts'], ['opts', 'k'])
497 497 """
498 498 def compiledict(xs):
499 499 return util.sortdict((k, compileexp(x, context, curmethods))
500 500 for k, x in xs.iteritems())
501 501 def compilelist(xs):
502 502 return [compileexp(x, context, curmethods) for x in xs]
503 503
504 504 if not argspec:
505 505 # filter or function with no argspec: return list of positional args
506 506 return compilelist(getlist(exp))
507 507
508 508 # function with argspec: return dict of named args
509 509 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
510 510 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
511 511 keyvaluenode='keyvalue', keynode='symbol')
512 512 compargs = util.sortdict()
513 513 if varkey:
514 514 compargs[varkey] = compilelist(treeargs.pop(varkey))
515 515 if optkey:
516 516 compargs[optkey] = compiledict(treeargs.pop(optkey))
517 517 compargs.update(compiledict(treeargs))
518 518 return compargs
519 519
520 520 def buildkeyvaluepair(exp, content):
521 521 raise error.ParseError(_("can't use a key-value pair in this context"))
522 522
523 523 # dict of template built-in functions
524 524 funcs = {}
525 525
526 526 templatefunc = registrar.templatefunc(funcs)
527 527
528 528 @templatefunc('date(date[, fmt])')
529 529 def date(context, mapping, args):
530 530 """Format a date. See :hg:`help dates` for formatting
531 531 strings. The default is a Unix date format, including the timezone:
532 532 "Mon Sep 04 15:13:13 2006 0700"."""
533 533 if not (1 <= len(args) <= 2):
534 534 # i18n: "date" is a keyword
535 535 raise error.ParseError(_("date expects one or two arguments"))
536 536
537 537 date = evalfuncarg(context, mapping, args[0])
538 538 fmt = None
539 539 if len(args) == 2:
540 540 fmt = evalstring(context, mapping, args[1])
541 541 try:
542 542 if fmt is None:
543 543 return util.datestr(date)
544 544 else:
545 545 return util.datestr(date, fmt)
546 546 except (TypeError, ValueError):
547 547 # i18n: "date" is a keyword
548 548 raise error.ParseError(_("date expects a date information"))
549 549
550 550 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
551 551 def dict_(context, mapping, args):
552 552 """Construct a dict from key-value pairs. A key may be omitted if
553 553 a value expression can provide an unambiguous name."""
554 554 data = util.sortdict()
555 555
556 556 for v in args['args']:
557 557 k = findsymbolicname(v)
558 558 if not k:
559 559 raise error.ParseError(_('dict key cannot be inferred'))
560 560 if k in data or k in args['kwargs']:
561 561 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
562 562 data[k] = evalfuncarg(context, mapping, v)
563 563
564 564 data.update((k, evalfuncarg(context, mapping, v))
565 565 for k, v in args['kwargs'].iteritems())
566 566 return templatekw.hybriddict(data)
567 567
568 568 @templatefunc('diff([includepattern [, excludepattern]])')
569 569 def diff(context, mapping, args):
570 570 """Show a diff, optionally
571 571 specifying files to include or exclude."""
572 572 if len(args) > 2:
573 573 # i18n: "diff" is a keyword
574 574 raise error.ParseError(_("diff expects zero, one, or two arguments"))
575 575
576 576 def getpatterns(i):
577 577 if i < len(args):
578 578 s = evalstring(context, mapping, args[i]).strip()
579 579 if s:
580 580 return [s]
581 581 return []
582 582
583 583 ctx = mapping['ctx']
584 584 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
585 585
586 586 return ''.join(chunks)
587 587
588 588 @templatefunc('files(pattern)')
589 589 def files(context, mapping, args):
590 590 """All files of the current changeset matching the pattern. See
591 591 :hg:`help patterns`."""
592 592 if not len(args) == 1:
593 593 # i18n: "files" is a keyword
594 594 raise error.ParseError(_("files expects one argument"))
595 595
596 596 raw = evalstring(context, mapping, args[0])
597 597 ctx = mapping['ctx']
598 598 m = ctx.match([raw])
599 599 files = list(ctx.matches(m))
600 600 return templatekw.showlist("file", files, mapping)
601 601
602 602 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
603 603 def fill(context, mapping, args):
604 604 """Fill many
605 605 paragraphs with optional indentation. See the "fill" filter."""
606 606 if not (1 <= len(args) <= 4):
607 607 # i18n: "fill" is a keyword
608 608 raise error.ParseError(_("fill expects one to four arguments"))
609 609
610 610 text = evalstring(context, mapping, args[0])
611 611 width = 76
612 612 initindent = ''
613 613 hangindent = ''
614 614 if 2 <= len(args) <= 4:
615 615 width = evalinteger(context, mapping, args[1],
616 616 # i18n: "fill" is a keyword
617 617 _("fill expects an integer width"))
618 618 try:
619 619 initindent = evalstring(context, mapping, args[2])
620 620 hangindent = evalstring(context, mapping, args[3])
621 621 except IndexError:
622 622 pass
623 623
624 624 return templatefilters.fill(text, width, initindent, hangindent)
625 625
626 626 @templatefunc('formatnode(node)')
627 627 def formatnode(context, mapping, args):
628 628 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
629 629 if len(args) != 1:
630 630 # i18n: "formatnode" is a keyword
631 631 raise error.ParseError(_("formatnode expects one argument"))
632 632
633 633 ui = mapping['ui']
634 634 node = evalstring(context, mapping, args[0])
635 635 if ui.debugflag:
636 636 return node
637 637 return templatefilters.short(node)
638 638
639 639 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
640 640 argspec='text width fillchar left')
641 641 def pad(context, mapping, args):
642 642 """Pad text with a
643 643 fill character."""
644 644 if 'text' not in args or 'width' not in args:
645 645 # i18n: "pad" is a keyword
646 646 raise error.ParseError(_("pad() expects two to four arguments"))
647 647
648 648 width = evalinteger(context, mapping, args['width'],
649 649 # i18n: "pad" is a keyword
650 650 _("pad() expects an integer width"))
651 651
652 652 text = evalstring(context, mapping, args['text'])
653 653
654 654 left = False
655 655 fillchar = ' '
656 656 if 'fillchar' in args:
657 657 fillchar = evalstring(context, mapping, args['fillchar'])
658 658 if len(color.stripeffects(fillchar)) != 1:
659 659 # i18n: "pad" is a keyword
660 660 raise error.ParseError(_("pad() expects a single fill character"))
661 661 if 'left' in args:
662 662 left = evalboolean(context, mapping, args['left'])
663 663
664 664 fillwidth = width - encoding.colwidth(color.stripeffects(text))
665 665 if fillwidth <= 0:
666 666 return text
667 667 if left:
668 668 return fillchar * fillwidth + text
669 669 else:
670 670 return text + fillchar * fillwidth
671 671
672 672 @templatefunc('indent(text, indentchars[, firstline])')
673 673 def indent(context, mapping, args):
674 674 """Indents all non-empty lines
675 675 with the characters given in the indentchars string. An optional
676 676 third parameter will override the indent for the first line only
677 677 if present."""
678 678 if not (2 <= len(args) <= 3):
679 679 # i18n: "indent" is a keyword
680 680 raise error.ParseError(_("indent() expects two or three arguments"))
681 681
682 682 text = evalstring(context, mapping, args[0])
683 683 indent = evalstring(context, mapping, args[1])
684 684
685 685 if len(args) == 3:
686 686 firstline = evalstring(context, mapping, args[2])
687 687 else:
688 688 firstline = indent
689 689
690 690 # the indent function doesn't indent the first line, so we do it here
691 691 return templatefilters.indent(firstline + text, indent)
692 692
693 693 @templatefunc('get(dict, key)')
694 694 def get(context, mapping, args):
695 695 """Get an attribute/key from an object. Some keywords
696 696 are complex types. This function allows you to obtain the value of an
697 697 attribute on these types."""
698 698 if len(args) != 2:
699 699 # i18n: "get" is a keyword
700 700 raise error.ParseError(_("get() expects two arguments"))
701 701
702 702 dictarg = evalfuncarg(context, mapping, args[0])
703 703 if not util.safehasattr(dictarg, 'get'):
704 704 # i18n: "get" is a keyword
705 705 raise error.ParseError(_("get() expects a dict as first argument"))
706 706
707 707 key = evalfuncarg(context, mapping, args[1])
708 708 return dictarg.get(key)
709 709
710 710 @templatefunc('if(expr, then[, else])')
711 711 def if_(context, mapping, args):
712 712 """Conditionally execute based on the result of
713 713 an expression."""
714 714 if not (2 <= len(args) <= 3):
715 715 # i18n: "if" is a keyword
716 716 raise error.ParseError(_("if expects two or three arguments"))
717 717
718 718 test = evalboolean(context, mapping, args[0])
719 719 if test:
720 720 yield args[1][0](context, mapping, args[1][1])
721 721 elif len(args) == 3:
722 722 yield args[2][0](context, mapping, args[2][1])
723 723
724 724 @templatefunc('ifcontains(needle, haystack, then[, else])')
725 725 def ifcontains(context, mapping, args):
726 726 """Conditionally execute based
727 727 on whether the item "needle" is in "haystack"."""
728 728 if not (3 <= len(args) <= 4):
729 729 # i18n: "ifcontains" is a keyword
730 730 raise error.ParseError(_("ifcontains expects three or four arguments"))
731 731
732 732 needle = evalstring(context, mapping, args[0])
733 733 haystack = evalfuncarg(context, mapping, args[1])
734 734
735 735 if needle in haystack:
736 736 yield args[2][0](context, mapping, args[2][1])
737 737 elif len(args) == 4:
738 738 yield args[3][0](context, mapping, args[3][1])
739 739
740 740 @templatefunc('ifeq(expr1, expr2, then[, else])')
741 741 def ifeq(context, mapping, args):
742 742 """Conditionally execute based on
743 743 whether 2 items are equivalent."""
744 744 if not (3 <= len(args) <= 4):
745 745 # i18n: "ifeq" is a keyword
746 746 raise error.ParseError(_("ifeq expects three or four arguments"))
747 747
748 748 test = evalstring(context, mapping, args[0])
749 749 match = evalstring(context, mapping, args[1])
750 750 if test == match:
751 751 yield args[2][0](context, mapping, args[2][1])
752 752 elif len(args) == 4:
753 753 yield args[3][0](context, mapping, args[3][1])
754 754
755 755 @templatefunc('join(list, sep)')
756 756 def join(context, mapping, args):
757 757 """Join items in a list with a delimiter."""
758 758 if not (1 <= len(args) <= 2):
759 759 # i18n: "join" is a keyword
760 760 raise error.ParseError(_("join expects one or two arguments"))
761 761
762 762 joinset = args[0][0](context, mapping, args[0][1])
763 763 if util.safehasattr(joinset, 'itermaps'):
764 764 jf = joinset.joinfmt
765 765 joinset = [jf(x) for x in joinset.itermaps()]
766 766
767 767 joiner = " "
768 768 if len(args) > 1:
769 769 joiner = evalstring(context, mapping, args[1])
770 770
771 771 first = True
772 772 for x in joinset:
773 773 if first:
774 774 first = False
775 775 else:
776 776 yield joiner
777 777 yield x
778 778
779 779 @templatefunc('label(label, expr)')
780 780 def label(context, mapping, args):
781 781 """Apply a label to generated content. Content with
782 782 a label applied can result in additional post-processing, such as
783 783 automatic colorization."""
784 784 if len(args) != 2:
785 785 # i18n: "label" is a keyword
786 786 raise error.ParseError(_("label expects two arguments"))
787 787
788 788 ui = mapping['ui']
789 789 thing = evalstring(context, mapping, args[1])
790 790 # preserve unknown symbol as literal so effects like 'red', 'bold',
791 791 # etc. don't need to be quoted
792 792 label = evalstringliteral(context, mapping, args[0])
793 793
794 794 return ui.label(thing, label)
795 795
796 796 @templatefunc('latesttag([pattern])')
797 797 def latesttag(context, mapping, args):
798 798 """The global tags matching the given pattern on the
799 799 most recent globally tagged ancestor of this changeset.
800 800 If no such tags exist, the "{tag}" template resolves to
801 801 the string "null"."""
802 802 if len(args) > 1:
803 803 # i18n: "latesttag" is a keyword
804 804 raise error.ParseError(_("latesttag expects at most one argument"))
805 805
806 806 pattern = None
807 807 if len(args) == 1:
808 808 pattern = evalstring(context, mapping, args[0])
809 809
810 810 return templatekw.showlatesttags(pattern, **mapping)
811 811
812 812 @templatefunc('localdate(date[, tz])')
813 813 def localdate(context, mapping, args):
814 814 """Converts a date to the specified timezone.
815 815 The default is local date."""
816 816 if not (1 <= len(args) <= 2):
817 817 # i18n: "localdate" is a keyword
818 818 raise error.ParseError(_("localdate expects one or two arguments"))
819 819
820 820 date = evalfuncarg(context, mapping, args[0])
821 821 try:
822 822 date = util.parsedate(date)
823 823 except AttributeError: # not str nor date tuple
824 824 # i18n: "localdate" is a keyword
825 825 raise error.ParseError(_("localdate expects a date information"))
826 826 if len(args) >= 2:
827 827 tzoffset = None
828 828 tz = evalfuncarg(context, mapping, args[1])
829 829 if isinstance(tz, str):
830 830 tzoffset, remainder = util.parsetimezone(tz)
831 831 if remainder:
832 832 tzoffset = None
833 833 if tzoffset is None:
834 834 try:
835 835 tzoffset = int(tz)
836 836 except (TypeError, ValueError):
837 837 # i18n: "localdate" is a keyword
838 838 raise error.ParseError(_("localdate expects a timezone"))
839 839 else:
840 840 tzoffset = util.makedate()[1]
841 841 return (date[0], tzoffset)
842 842
843 843 @templatefunc('max(iterable)')
844 844 def max_(context, mapping, args, **kwargs):
845 845 """Return the max of an iterable"""
846 846 if len(args) != 1:
847 847 # i18n: "max" is a keyword
848 848 raise error.ParseError(_("max expects one arguments"))
849 849
850 850 iterable = evalfuncarg(context, mapping, args[0])
851 851 try:
852 852 return max(iterable)
853 853 except (TypeError, ValueError):
854 854 # i18n: "max" is a keyword
855 855 raise error.ParseError(_("max first argument should be an iterable"))
856 856
857 857 @templatefunc('min(iterable)')
858 858 def min_(context, mapping, args, **kwargs):
859 859 """Return the min of an iterable"""
860 860 if len(args) != 1:
861 861 # i18n: "min" is a keyword
862 862 raise error.ParseError(_("min expects one arguments"))
863 863
864 864 iterable = evalfuncarg(context, mapping, args[0])
865 865 try:
866 866 return min(iterable)
867 867 except (TypeError, ValueError):
868 868 # i18n: "min" is a keyword
869 869 raise error.ParseError(_("min first argument should be an iterable"))
870 870
871 871 @templatefunc('mod(a, b)')
872 872 def mod(context, mapping, args):
873 873 """Calculate a mod b such that a / b + a mod b == a"""
874 874 if not len(args) == 2:
875 875 # i18n: "mod" is a keyword
876 876 raise error.ParseError(_("mod expects two arguments"))
877 877
878 878 func = lambda a, b: a % b
879 879 return runarithmetic(context, mapping, (func, args[0], args[1]))
880 880
881 881 @templatefunc('obsfatedate(markers)')
882 882 def obsfatedate(context, mapping, args):
883 883 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
884 884 if len(args) != 1:
885 885 # i18n: "obsfatedate" is a keyword
886 886 raise error.ParseError(_("obsfatedate expects one arguments"))
887 887
888 888 markers = evalfuncarg(context, mapping, args[0])
889 889
890 890 try:
891 891 data = obsutil.markersdates(markers)
892 892 return templatekw.hybridlist(data, name='date', fmt='%d %d')
893 893 except (TypeError, KeyError):
894 894 # i18n: "obsfatedate" is a keyword
895 895 errmsg = _("obsfatedate first argument should be an iterable")
896 896 raise error.ParseError(errmsg)
897 897
898 898 @templatefunc('obsfateusers(markers)')
899 899 def obsfateusers(context, mapping, args):
900 900 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
901 901 if len(args) != 1:
902 902 # i18n: "obsfateusers" is a keyword
903 903 raise error.ParseError(_("obsfateusers expects one arguments"))
904 904
905 905 markers = evalfuncarg(context, mapping, args[0])
906 906
907 907 try:
908 908 data = obsutil.markersusers(markers)
909 909 return templatekw.hybridlist(data, name='user')
910 910 except (TypeError, KeyError, ValueError):
911 911 # i18n: "obsfateusers" is a keyword
912 912 msg = _("obsfateusers first argument should be an iterable of "
913 913 "obsmakers")
914 914 raise error.ParseError(msg)
915 915
916 916 @templatefunc('obsfateverb(successors)')
917 917 def obsfateverb(context, mapping, args):
918 918 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
919 919 if len(args) != 1:
920 920 # i18n: "obsfateverb" is a keyword
921 921 raise error.ParseError(_("obsfateverb expects one arguments"))
922 922
923 923 successors = evalfuncarg(context, mapping, args[0])
924 924
925 925 try:
926 926 return obsutil.successorsetverb(successors)
927 927 except TypeError:
928 928 # i18n: "obsfateverb" is a keyword
929 929 errmsg = _("obsfateverb first argument should be countable")
930 930 raise error.ParseError(errmsg)
931 931
932 932 @templatefunc('relpath(path)')
933 933 def relpath(context, mapping, args):
934 934 """Convert a repository-absolute path into a filesystem path relative to
935 935 the current working directory."""
936 936 if len(args) != 1:
937 937 # i18n: "relpath" is a keyword
938 938 raise error.ParseError(_("relpath expects one argument"))
939 939
940 940 repo = mapping['ctx'].repo()
941 941 path = evalstring(context, mapping, args[0])
942 942 return repo.pathto(path)
943 943
944 944 @templatefunc('revset(query[, formatargs...])')
945 945 def revset(context, mapping, args):
946 946 """Execute a revision set query. See
947 947 :hg:`help revset`."""
948 948 if not len(args) > 0:
949 949 # i18n: "revset" is a keyword
950 950 raise error.ParseError(_("revset expects one or more arguments"))
951 951
952 952 raw = evalstring(context, mapping, args[0])
953 953 ctx = mapping['ctx']
954 954 repo = ctx.repo()
955 955
956 956 def query(expr):
957 957 m = revsetmod.match(repo.ui, expr, repo=repo)
958 958 return m(repo)
959 959
960 960 if len(args) > 1:
961 961 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
962 962 revs = query(revsetlang.formatspec(raw, *formatargs))
963 963 revs = list(revs)
964 964 else:
965 965 revsetcache = mapping['cache'].setdefault("revsetcache", {})
966 966 if raw in revsetcache:
967 967 revs = revsetcache[raw]
968 968 else:
969 969 revs = query(raw)
970 970 revs = list(revs)
971 971 revsetcache[raw] = revs
972 972
973 973 return templatekw.showrevslist("revision", revs, **mapping)
974 974
975 975 @templatefunc('rstdoc(text, style)')
976 976 def rstdoc(context, mapping, args):
977 977 """Format reStructuredText."""
978 978 if len(args) != 2:
979 979 # i18n: "rstdoc" is a keyword
980 980 raise error.ParseError(_("rstdoc expects two arguments"))
981 981
982 982 text = evalstring(context, mapping, args[0])
983 983 style = evalstring(context, mapping, args[1])
984 984
985 985 return minirst.format(text, style=style, keep=['verbose'])
986 986
987 987 @templatefunc('separate(sep, args)', argspec='sep *args')
988 988 def separate(context, mapping, args):
989 989 """Add a separator between non-empty arguments."""
990 990 if 'sep' not in args:
991 991 # i18n: "separate" is a keyword
992 992 raise error.ParseError(_("separate expects at least one argument"))
993 993
994 994 sep = evalstring(context, mapping, args['sep'])
995 995 first = True
996 996 for arg in args['args']:
997 997 argstr = evalstring(context, mapping, arg)
998 998 if not argstr:
999 999 continue
1000 1000 if first:
1001 1001 first = False
1002 1002 else:
1003 1003 yield sep
1004 1004 yield argstr
1005 1005
1006 1006 @templatefunc('shortest(node, minlength=4)')
1007 1007 def shortest(context, mapping, args):
1008 1008 """Obtain the shortest representation of
1009 1009 a node."""
1010 1010 if not (1 <= len(args) <= 2):
1011 1011 # i18n: "shortest" is a keyword
1012 1012 raise error.ParseError(_("shortest() expects one or two arguments"))
1013 1013
1014 1014 node = evalstring(context, mapping, args[0])
1015 1015
1016 1016 minlength = 4
1017 1017 if len(args) > 1:
1018 1018 minlength = evalinteger(context, mapping, args[1],
1019 1019 # i18n: "shortest" is a keyword
1020 1020 _("shortest() expects an integer minlength"))
1021 1021
1022 1022 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1023 1023 # which would be unacceptably slow. so we look for hash collision in
1024 1024 # unfiltered space, which means some hashes may be slightly longer.
1025 1025 cl = mapping['ctx']._repo.unfiltered().changelog
1026 1026 def isvalid(test):
1027 1027 try:
1028 1028 if cl._partialmatch(test) is None:
1029 1029 return False
1030 1030
1031 1031 try:
1032 1032 i = int(test)
1033 1033 # if we are a pure int, then starting with zero will not be
1034 1034 # confused as a rev; or, obviously, if the int is larger than
1035 1035 # the value of the tip rev
1036 1036 if test[0] == '0' or i > len(cl):
1037 1037 return True
1038 1038 return False
1039 1039 except ValueError:
1040 1040 return True
1041 1041 except error.RevlogError:
1042 1042 return False
1043 1043 except error.WdirUnsupported:
1044 1044 # single 'ff...' match
1045 1045 return True
1046 1046
1047 1047 shortest = node
1048 1048 startlength = max(6, minlength)
1049 1049 length = startlength
1050 1050 while True:
1051 1051 test = node[:length]
1052 1052 if isvalid(test):
1053 1053 shortest = test
1054 1054 if length == minlength or length > startlength:
1055 1055 return shortest
1056 1056 length -= 1
1057 1057 else:
1058 1058 length += 1
1059 1059 if len(shortest) <= length:
1060 1060 return shortest
1061 1061
1062 1062 @templatefunc('strip(text[, chars])')
1063 1063 def strip(context, mapping, args):
1064 1064 """Strip characters from a string. By default,
1065 1065 strips all leading and trailing whitespace."""
1066 1066 if not (1 <= len(args) <= 2):
1067 1067 # i18n: "strip" is a keyword
1068 1068 raise error.ParseError(_("strip expects one or two arguments"))
1069 1069
1070 1070 text = evalstring(context, mapping, args[0])
1071 1071 if len(args) == 2:
1072 1072 chars = evalstring(context, mapping, args[1])
1073 1073 return text.strip(chars)
1074 1074 return text.strip()
1075 1075
1076 1076 @templatefunc('sub(pattern, replacement, expression)')
1077 1077 def sub(context, mapping, args):
1078 1078 """Perform text substitution
1079 1079 using regular expressions."""
1080 1080 if len(args) != 3:
1081 1081 # i18n: "sub" is a keyword
1082 1082 raise error.ParseError(_("sub expects three arguments"))
1083 1083
1084 1084 pat = evalstring(context, mapping, args[0])
1085 1085 rpl = evalstring(context, mapping, args[1])
1086 1086 src = evalstring(context, mapping, args[2])
1087 1087 try:
1088 1088 patre = re.compile(pat)
1089 1089 except re.error:
1090 1090 # i18n: "sub" is a keyword
1091 1091 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1092 1092 try:
1093 1093 yield patre.sub(rpl, src)
1094 1094 except re.error:
1095 1095 # i18n: "sub" is a keyword
1096 1096 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1097 1097
1098 1098 @templatefunc('startswith(pattern, text)')
1099 1099 def startswith(context, mapping, args):
1100 1100 """Returns the value from the "text" argument
1101 1101 if it begins with the content from the "pattern" argument."""
1102 1102 if len(args) != 2:
1103 1103 # i18n: "startswith" is a keyword
1104 1104 raise error.ParseError(_("startswith expects two arguments"))
1105 1105
1106 1106 patn = evalstring(context, mapping, args[0])
1107 1107 text = evalstring(context, mapping, args[1])
1108 1108 if text.startswith(patn):
1109 1109 return text
1110 1110 return ''
1111 1111
1112 1112 @templatefunc('word(number, text[, separator])')
1113 1113 def word(context, mapping, args):
1114 1114 """Return the nth word from a string."""
1115 1115 if not (2 <= len(args) <= 3):
1116 1116 # i18n: "word" is a keyword
1117 1117 raise error.ParseError(_("word expects two or three arguments, got %d")
1118 1118 % len(args))
1119 1119
1120 1120 num = evalinteger(context, mapping, args[0],
1121 1121 # i18n: "word" is a keyword
1122 1122 _("word expects an integer index"))
1123 1123 text = evalstring(context, mapping, args[1])
1124 1124 if len(args) == 3:
1125 1125 splitter = evalstring(context, mapping, args[2])
1126 1126 else:
1127 1127 splitter = None
1128 1128
1129 1129 tokens = text.split(splitter)
1130 1130 if num >= len(tokens) or num < -len(tokens):
1131 1131 return ''
1132 1132 else:
1133 1133 return tokens[num]
1134 1134
1135 1135 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1136 1136 exprmethods = {
1137 1137 "integer": lambda e, c: (runinteger, e[1]),
1138 1138 "string": lambda e, c: (runstring, e[1]),
1139 1139 "symbol": lambda e, c: (runsymbol, e[1]),
1140 1140 "template": buildtemplate,
1141 1141 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1142 1142 # ".": buildmember,
1143 1143 "|": buildfilter,
1144 1144 "%": buildmap,
1145 1145 "func": buildfunc,
1146 1146 "keyvalue": buildkeyvaluepair,
1147 1147 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1148 1148 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1149 1149 "negate": buildnegate,
1150 1150 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1151 1151 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1152 1152 }
1153 1153
1154 1154 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1155 1155 methods = exprmethods.copy()
1156 1156 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1157 1157
1158 1158 class _aliasrules(parser.basealiasrules):
1159 1159 """Parsing and expansion rule set of template aliases"""
1160 1160 _section = _('template alias')
1161 1161 _parse = staticmethod(_parseexpr)
1162 1162
1163 1163 @staticmethod
1164 1164 def _trygetfunc(tree):
1165 1165 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1166 1166 None"""
1167 1167 if tree[0] == 'func' and tree[1][0] == 'symbol':
1168 1168 return tree[1][1], getlist(tree[2])
1169 1169 if tree[0] == '|' and tree[2][0] == 'symbol':
1170 1170 return tree[2][1], [tree[1]]
1171 1171
1172 1172 def expandaliases(tree, aliases):
1173 1173 """Return new tree of aliases are expanded"""
1174 1174 aliasmap = _aliasrules.buildmap(aliases)
1175 1175 return _aliasrules.expand(aliasmap, tree)
1176 1176
1177 1177 # template engine
1178 1178
1179 1179 stringify = templatefilters.stringify
1180 1180
1181 1181 def _flatten(thing):
1182 1182 '''yield a single stream from a possibly nested set of iterators'''
1183 1183 thing = templatekw.unwraphybrid(thing)
1184 1184 if isinstance(thing, bytes):
1185 1185 yield thing
1186 1186 elif thing is None:
1187 1187 pass
1188 1188 elif not util.safehasattr(thing, '__iter__'):
1189 1189 yield pycompat.bytestr(thing)
1190 1190 else:
1191 1191 for i in thing:
1192 1192 i = templatekw.unwraphybrid(i)
1193 1193 if isinstance(i, bytes):
1194 1194 yield i
1195 1195 elif i is None:
1196 1196 pass
1197 1197 elif not util.safehasattr(i, '__iter__'):
1198 1198 yield pycompat.bytestr(i)
1199 1199 else:
1200 1200 for j in _flatten(i):
1201 1201 yield j
1202 1202
1203 1203 def unquotestring(s):
1204 1204 '''unwrap quotes if any; otherwise returns unmodified string'''
1205 1205 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1206 1206 return s
1207 1207 return s[1:-1]
1208 1208
1209 1209 class engine(object):
1210 1210 '''template expansion engine.
1211 1211
1212 1212 template expansion works like this. a map file contains key=value
1213 1213 pairs. if value is quoted, it is treated as string. otherwise, it
1214 1214 is treated as name of template file.
1215 1215
1216 1216 templater is asked to expand a key in map. it looks up key, and
1217 1217 looks for strings like this: {foo}. it expands {foo} by looking up
1218 1218 foo in map, and substituting it. expansion is recursive: it stops
1219 1219 when there is no more {foo} to replace.
1220 1220
1221 1221 expansion also allows formatting and filtering.
1222 1222
1223 1223 format uses key to expand each item in list. syntax is
1224 1224 {key%format}.
1225 1225
1226 1226 filter uses function to transform value. syntax is
1227 1227 {key|filter1|filter2|...}.'''
1228 1228
1229 1229 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1230 1230 self._loader = loader
1231 1231 if filters is None:
1232 1232 filters = {}
1233 1233 self._filters = filters
1234 1234 if defaults is None:
1235 1235 defaults = {}
1236 1236 self._defaults = defaults
1237 1237 self._aliasmap = _aliasrules.buildmap(aliases)
1238 1238 self._cache = {} # key: (func, data)
1239 1239
1240 1240 def _load(self, t):
1241 1241 '''load, parse, and cache a template'''
1242 1242 if t not in self._cache:
1243 1243 # put poison to cut recursion while compiling 't'
1244 1244 self._cache[t] = (_runrecursivesymbol, t)
1245 1245 try:
1246 1246 x = parse(self._loader(t))
1247 1247 if self._aliasmap:
1248 1248 x = _aliasrules.expand(self._aliasmap, x)
1249 1249 self._cache[t] = compileexp(x, self, methods)
1250 1250 except: # re-raises
1251 1251 del self._cache[t]
1252 1252 raise
1253 1253 return self._cache[t]
1254 1254
1255 1255 def process(self, t, mapping):
1256 1256 '''Perform expansion. t is name of map element to expand.
1257 1257 mapping contains added elements for use during expansion. Is a
1258 1258 generator.'''
1259 1259 func, data = self._load(t)
1260 1260 return _flatten(func(self, mapping, data))
1261 1261
1262 1262 engines = {'default': engine}
1263 1263
1264 1264 def stylelist():
1265 1265 paths = templatepaths()
1266 1266 if not paths:
1267 1267 return _('no templates found, try `hg debuginstall` for more info')
1268 1268 dirlist = os.listdir(paths[0])
1269 1269 stylelist = []
1270 1270 for file in dirlist:
1271 1271 split = file.split(".")
1272 1272 if split[-1] in ('orig', 'rej'):
1273 1273 continue
1274 1274 if split[0] == "map-cmdline":
1275 1275 stylelist.append(split[1])
1276 1276 return ", ".join(sorted(stylelist))
1277 1277
1278 1278 def _readmapfile(mapfile):
1279 1279 """Load template elements from the given map file"""
1280 1280 if not os.path.exists(mapfile):
1281 1281 raise error.Abort(_("style '%s' not found") % mapfile,
1282 1282 hint=_("available styles: %s") % stylelist())
1283 1283
1284 1284 base = os.path.dirname(mapfile)
1285 1285 conf = config.config(includepaths=templatepaths())
1286 1286 conf.read(mapfile)
1287 1287
1288 1288 cache = {}
1289 1289 tmap = {}
1290 1290 for key, val in conf[''].items():
1291 1291 if not val:
1292 1292 raise error.ParseError(_('missing value'), conf.source('', key))
1293 1293 if val[0] in "'\"":
1294 1294 if val[0] != val[-1]:
1295 1295 raise error.ParseError(_('unmatched quotes'),
1296 1296 conf.source('', key))
1297 1297 cache[key] = unquotestring(val)
1298 1298 elif key == "__base__":
1299 1299 # treat as a pointer to a base class for this style
1300 1300 path = util.normpath(os.path.join(base, val))
1301 1301
1302 1302 # fallback check in template paths
1303 1303 if not os.path.exists(path):
1304 1304 for p in templatepaths():
1305 1305 p2 = util.normpath(os.path.join(p, val))
1306 1306 if os.path.isfile(p2):
1307 1307 path = p2
1308 1308 break
1309 1309 p3 = util.normpath(os.path.join(p2, "map"))
1310 1310 if os.path.isfile(p3):
1311 1311 path = p3
1312 1312 break
1313 1313
1314 1314 bcache, btmap = _readmapfile(path)
1315 1315 for k in bcache:
1316 1316 if k not in cache:
1317 1317 cache[k] = bcache[k]
1318 1318 for k in btmap:
1319 1319 if k not in tmap:
1320 1320 tmap[k] = btmap[k]
1321 1321 else:
1322 1322 val = 'default', val
1323 1323 if ':' in val[1]:
1324 1324 val = val[1].split(':', 1)
1325 1325 tmap[key] = val[0], os.path.join(base, val[1])
1326 1326 return cache, tmap
1327 1327
1328 1328 class TemplateNotFound(error.Abort):
1329 1329 pass
1330 1330
1331 1331 class templater(object):
1332 1332
1333 1333 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1334 1334 minchunk=1024, maxchunk=65536):
1335 1335 '''set up template engine.
1336 1336 filters is dict of functions. each transforms a value into another.
1337 1337 defaults is dict of default map definitions.
1338 1338 aliases is list of alias (name, replacement) pairs.
1339 1339 '''
1340 1340 if filters is None:
1341 1341 filters = {}
1342 1342 if defaults is None:
1343 1343 defaults = {}
1344 1344 if cache is None:
1345 1345 cache = {}
1346 1346 self.cache = cache.copy()
1347 1347 self.map = {}
1348 1348 self.filters = templatefilters.filters.copy()
1349 1349 self.filters.update(filters)
1350 1350 self.defaults = defaults
1351 1351 self._aliases = aliases
1352 1352 self.minchunk, self.maxchunk = minchunk, maxchunk
1353 1353 self.ecache = {}
1354 1354
1355 1355 @classmethod
1356 1356 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1357 1357 minchunk=1024, maxchunk=65536):
1358 1358 """Create templater from the specified map file"""
1359 1359 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1360 1360 cache, tmap = _readmapfile(mapfile)
1361 1361 t.cache.update(cache)
1362 1362 t.map = tmap
1363 1363 return t
1364 1364
1365 1365 def __contains__(self, key):
1366 1366 return key in self.cache or key in self.map
1367 1367
1368 1368 def load(self, t):
1369 1369 '''Get the template for the given template name. Use a local cache.'''
1370 1370 if t not in self.cache:
1371 1371 try:
1372 1372 self.cache[t] = util.readfile(self.map[t][1])
1373 1373 except KeyError as inst:
1374 1374 raise TemplateNotFound(_('"%s" not in template map') %
1375 1375 inst.args[0])
1376 1376 except IOError as inst:
1377 1377 raise IOError(inst.args[0], _('template file %s: %s') %
1378 1378 (self.map[t][1], inst.args[1]))
1379 1379 return self.cache[t]
1380 1380
1381 1381 def render(self, mapping):
1382 1382 """Render the default unnamed template and return result as string"""
1383 1383 return stringify(self('', **mapping))
1384 1384
1385 1385 def __call__(self, t, **mapping):
1386 1386 mapping = pycompat.byteskwargs(mapping)
1387 1387 ttype = t in self.map and self.map[t][0] or 'default'
1388 1388 if ttype not in self.ecache:
1389 1389 try:
1390 1390 ecls = engines[ttype]
1391 1391 except KeyError:
1392 1392 raise error.Abort(_('invalid template engine: %s') % ttype)
1393 1393 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1394 1394 self._aliases)
1395 1395 proc = self.ecache[ttype]
1396 1396
1397 1397 stream = proc.process(t, mapping)
1398 1398 if self.minchunk:
1399 1399 stream = util.increasingchunks(stream, min=self.minchunk,
1400 1400 max=self.maxchunk)
1401 1401 return stream
1402 1402
1403 1403 def templatepaths():
1404 1404 '''return locations used for template files.'''
1405 1405 pathsrel = ['templates']
1406 1406 paths = [os.path.normpath(os.path.join(util.datapath, f))
1407 1407 for f in pathsrel]
1408 1408 return [p for p in paths if os.path.isdir(p)]
1409 1409
1410 1410 def templatepath(name):
1411 1411 '''return location of template file. returns None if not found.'''
1412 1412 for p in templatepaths():
1413 1413 f = os.path.join(p, name)
1414 1414 if os.path.exists(f):
1415 1415 return f
1416 1416 return None
1417 1417
1418 1418 def stylemap(styles, paths=None):
1419 1419 """Return path to mapfile for a given style.
1420 1420
1421 1421 Searches mapfile in the following locations:
1422 1422 1. templatepath/style/map
1423 1423 2. templatepath/map-style
1424 1424 3. templatepath/map
1425 1425 """
1426 1426
1427 1427 if paths is None:
1428 1428 paths = templatepaths()
1429 1429 elif isinstance(paths, str):
1430 1430 paths = [paths]
1431 1431
1432 1432 if isinstance(styles, str):
1433 1433 styles = [styles]
1434 1434
1435 1435 for style in styles:
1436 1436 # only plain name is allowed to honor template paths
1437 1437 if (not style
1438 1438 or style in (os.curdir, os.pardir)
1439 1439 or pycompat.ossep in style
1440 1440 or pycompat.osaltsep and pycompat.osaltsep in style):
1441 1441 continue
1442 1442 locations = [os.path.join(style, 'map'), 'map-' + style]
1443 1443 locations.append('map')
1444 1444
1445 1445 for path in paths:
1446 1446 for location in locations:
1447 1447 mapfile = os.path.join(path, location)
1448 1448 if os.path.isfile(mapfile):
1449 1449 return style, mapfile
1450 1450
1451 1451 raise RuntimeError("No hgweb templates found in %r" % paths)
1452 1452
1453 1453 def loadfunction(ui, extname, registrarobj):
1454 1454 """Load template function from specified registrarobj
1455 1455 """
1456 1456 for name, func in registrarobj._table.iteritems():
1457 1457 funcs[name] = func
1458 1458
1459 1459 # tell hggettext to extract docstrings from these functions:
1460 1460 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now