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