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