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