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