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