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