##// END OF EJS Templates
templater: make it clearer that _flatten() omits None
Yuya Nishihara -
r29815:0d5cc0c1 default
parent child Browse files
Show More
@@ -1,1173 +1,1175
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 compileexp(exp, context, curmethods):
251 251 """Compile parsed template tree to (func, data) pair"""
252 252 t = exp[0]
253 253 if t in curmethods:
254 254 return curmethods[t](exp, context)
255 255 raise error.ParseError(_("unknown method '%s'") % t)
256 256
257 257 # template evaluation
258 258
259 259 def getsymbol(exp):
260 260 if exp[0] == 'symbol':
261 261 return exp[1]
262 262 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
263 263
264 264 def getlist(x):
265 265 if not x:
266 266 return []
267 267 if x[0] == 'list':
268 268 return getlist(x[1]) + [x[2]]
269 269 return [x]
270 270
271 271 def gettemplate(exp, context):
272 272 """Compile given template tree or load named template from map file;
273 273 returns (func, data) pair"""
274 274 if exp[0] in ('template', 'string'):
275 275 return compileexp(exp, context, methods)
276 276 if exp[0] == 'symbol':
277 277 # unlike runsymbol(), here 'symbol' is always taken as template name
278 278 # even if it exists in mapping. this allows us to override mapping
279 279 # by web templates, e.g. 'changelogtag' is redefined in map file.
280 280 return context._load(exp[1])
281 281 raise error.ParseError(_("expected template specifier"))
282 282
283 283 def evalfuncarg(context, mapping, arg):
284 284 func, data = arg
285 285 # func() may return string, generator of strings or arbitrary object such
286 286 # as date tuple, but filter does not want generator.
287 287 thing = func(context, mapping, data)
288 288 if isinstance(thing, types.GeneratorType):
289 289 thing = stringify(thing)
290 290 return thing
291 291
292 292 def evalinteger(context, mapping, arg, err):
293 293 v = evalfuncarg(context, mapping, arg)
294 294 try:
295 295 return int(v)
296 296 except (TypeError, ValueError):
297 297 raise error.ParseError(err)
298 298
299 299 def evalstring(context, mapping, arg):
300 300 func, data = arg
301 301 return stringify(func(context, mapping, data))
302 302
303 303 def evalstringliteral(context, mapping, arg):
304 304 """Evaluate given argument as string template, but returns symbol name
305 305 if it is unknown"""
306 306 func, data = arg
307 307 if func is runsymbol:
308 308 thing = func(context, mapping, data, default=data)
309 309 else:
310 310 thing = func(context, mapping, data)
311 311 return stringify(thing)
312 312
313 313 def runinteger(context, mapping, data):
314 314 return int(data)
315 315
316 316 def runstring(context, mapping, data):
317 317 return data
318 318
319 319 def _recursivesymbolblocker(key):
320 320 def showrecursion(**args):
321 321 raise error.Abort(_("recursive reference '%s' in template") % key)
322 322 return showrecursion
323 323
324 324 def _runrecursivesymbol(context, mapping, key):
325 325 raise error.Abort(_("recursive reference '%s' in template") % key)
326 326
327 327 def runsymbol(context, mapping, key, default=''):
328 328 v = mapping.get(key)
329 329 if v is None:
330 330 v = context._defaults.get(key)
331 331 if v is None:
332 332 # put poison to cut recursion. we can't move this to parsing phase
333 333 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
334 334 safemapping = mapping.copy()
335 335 safemapping[key] = _recursivesymbolblocker(key)
336 336 try:
337 337 v = context.process(key, safemapping)
338 338 except TemplateNotFound:
339 339 v = default
340 340 if callable(v):
341 341 return v(**mapping)
342 342 return v
343 343
344 344 def buildtemplate(exp, context):
345 345 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
346 346 return (runtemplate, ctmpl)
347 347
348 348 def runtemplate(context, mapping, template):
349 349 for func, data in template:
350 350 yield func(context, mapping, data)
351 351
352 352 def buildfilter(exp, context):
353 353 arg = compileexp(exp[1], context, methods)
354 354 n = getsymbol(exp[2])
355 355 if n in context._filters:
356 356 filt = context._filters[n]
357 357 return (runfilter, (arg, filt))
358 358 if n in funcs:
359 359 f = funcs[n]
360 360 return (f, [arg])
361 361 raise error.ParseError(_("unknown function '%s'") % n)
362 362
363 363 def runfilter(context, mapping, data):
364 364 arg, filt = data
365 365 thing = evalfuncarg(context, mapping, arg)
366 366 try:
367 367 return filt(thing)
368 368 except (ValueError, AttributeError, TypeError):
369 369 if isinstance(arg[1], tuple):
370 370 dt = arg[1][1]
371 371 else:
372 372 dt = arg[1]
373 373 raise error.Abort(_("template filter '%s' is not compatible with "
374 374 "keyword '%s'") % (filt.func_name, dt))
375 375
376 376 def buildmap(exp, context):
377 377 func, data = compileexp(exp[1], context, methods)
378 378 tfunc, tdata = gettemplate(exp[2], context)
379 379 return (runmap, (func, data, tfunc, tdata))
380 380
381 381 def runmap(context, mapping, data):
382 382 func, data, tfunc, tdata = data
383 383 d = func(context, mapping, data)
384 384 if util.safehasattr(d, 'itermaps'):
385 385 diter = d.itermaps()
386 386 else:
387 387 try:
388 388 diter = iter(d)
389 389 except TypeError:
390 390 if func is runsymbol:
391 391 raise error.ParseError(_("keyword '%s' is not iterable") % data)
392 392 else:
393 393 raise error.ParseError(_("%r is not iterable") % d)
394 394
395 395 for i in diter:
396 396 lm = mapping.copy()
397 397 if isinstance(i, dict):
398 398 lm.update(i)
399 399 lm['originalnode'] = mapping.get('node')
400 400 yield tfunc(context, lm, tdata)
401 401 else:
402 402 # v is not an iterable of dicts, this happen when 'key'
403 403 # has been fully expanded already and format is useless.
404 404 # If so, return the expanded value.
405 405 yield i
406 406
407 407 def buildfunc(exp, context):
408 408 n = getsymbol(exp[1])
409 409 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
410 410 if n in funcs:
411 411 f = funcs[n]
412 412 return (f, args)
413 413 if n in context._filters:
414 414 if len(args) != 1:
415 415 raise error.ParseError(_("filter %s expects one argument") % n)
416 416 f = context._filters[n]
417 417 return (runfilter, (args[0], f))
418 418 raise error.ParseError(_("unknown function '%s'") % n)
419 419
420 420 # dict of template built-in functions
421 421 funcs = {}
422 422
423 423 templatefunc = registrar.templatefunc(funcs)
424 424
425 425 @templatefunc('date(date[, fmt])')
426 426 def date(context, mapping, args):
427 427 """Format a date. See :hg:`help dates` for formatting
428 428 strings. The default is a Unix date format, including the timezone:
429 429 "Mon Sep 04 15:13:13 2006 0700"."""
430 430 if not (1 <= len(args) <= 2):
431 431 # i18n: "date" is a keyword
432 432 raise error.ParseError(_("date expects one or two arguments"))
433 433
434 434 date = evalfuncarg(context, mapping, args[0])
435 435 fmt = None
436 436 if len(args) == 2:
437 437 fmt = evalstring(context, mapping, args[1])
438 438 try:
439 439 if fmt is None:
440 440 return util.datestr(date)
441 441 else:
442 442 return util.datestr(date, fmt)
443 443 except (TypeError, ValueError):
444 444 # i18n: "date" is a keyword
445 445 raise error.ParseError(_("date expects a date information"))
446 446
447 447 @templatefunc('diff([includepattern [, excludepattern]])')
448 448 def diff(context, mapping, args):
449 449 """Show a diff, optionally
450 450 specifying files to include or exclude."""
451 451 if len(args) > 2:
452 452 # i18n: "diff" is a keyword
453 453 raise error.ParseError(_("diff expects zero, one, or two arguments"))
454 454
455 455 def getpatterns(i):
456 456 if i < len(args):
457 457 s = evalstring(context, mapping, args[i]).strip()
458 458 if s:
459 459 return [s]
460 460 return []
461 461
462 462 ctx = mapping['ctx']
463 463 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
464 464
465 465 return ''.join(chunks)
466 466
467 467 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
468 468 def fill(context, mapping, args):
469 469 """Fill many
470 470 paragraphs with optional indentation. See the "fill" filter."""
471 471 if not (1 <= len(args) <= 4):
472 472 # i18n: "fill" is a keyword
473 473 raise error.ParseError(_("fill expects one to four arguments"))
474 474
475 475 text = evalstring(context, mapping, args[0])
476 476 width = 76
477 477 initindent = ''
478 478 hangindent = ''
479 479 if 2 <= len(args) <= 4:
480 480 width = evalinteger(context, mapping, args[1],
481 481 # i18n: "fill" is a keyword
482 482 _("fill expects an integer width"))
483 483 try:
484 484 initindent = evalstring(context, mapping, args[2])
485 485 hangindent = evalstring(context, mapping, args[3])
486 486 except IndexError:
487 487 pass
488 488
489 489 return templatefilters.fill(text, width, initindent, hangindent)
490 490
491 491 @templatefunc('pad(text, width[, fillchar=\' \'[, right=False]])')
492 492 def pad(context, mapping, args):
493 493 """Pad text with a
494 494 fill character."""
495 495 if not (2 <= len(args) <= 4):
496 496 # i18n: "pad" is a keyword
497 497 raise error.ParseError(_("pad() expects two to four arguments"))
498 498
499 499 width = evalinteger(context, mapping, args[1],
500 500 # i18n: "pad" is a keyword
501 501 _("pad() expects an integer width"))
502 502
503 503 text = evalstring(context, mapping, args[0])
504 504
505 505 right = False
506 506 fillchar = ' '
507 507 if len(args) > 2:
508 508 fillchar = evalstring(context, mapping, args[2])
509 509 if len(args) > 3:
510 510 right = util.parsebool(args[3][1])
511 511
512 512 if right:
513 513 return text.rjust(width, fillchar)
514 514 else:
515 515 return text.ljust(width, fillchar)
516 516
517 517 @templatefunc('indent(text, indentchars[, firstline])')
518 518 def indent(context, mapping, args):
519 519 """Indents all non-empty lines
520 520 with the characters given in the indentchars string. An optional
521 521 third parameter will override the indent for the first line only
522 522 if present."""
523 523 if not (2 <= len(args) <= 3):
524 524 # i18n: "indent" is a keyword
525 525 raise error.ParseError(_("indent() expects two or three arguments"))
526 526
527 527 text = evalstring(context, mapping, args[0])
528 528 indent = evalstring(context, mapping, args[1])
529 529
530 530 if len(args) == 3:
531 531 firstline = evalstring(context, mapping, args[2])
532 532 else:
533 533 firstline = indent
534 534
535 535 # the indent function doesn't indent the first line, so we do it here
536 536 return templatefilters.indent(firstline + text, indent)
537 537
538 538 @templatefunc('get(dict, key)')
539 539 def get(context, mapping, args):
540 540 """Get an attribute/key from an object. Some keywords
541 541 are complex types. This function allows you to obtain the value of an
542 542 attribute on these types."""
543 543 if len(args) != 2:
544 544 # i18n: "get" is a keyword
545 545 raise error.ParseError(_("get() expects two arguments"))
546 546
547 547 dictarg = evalfuncarg(context, mapping, args[0])
548 548 if not util.safehasattr(dictarg, 'get'):
549 549 # i18n: "get" is a keyword
550 550 raise error.ParseError(_("get() expects a dict as first argument"))
551 551
552 552 key = evalfuncarg(context, mapping, args[1])
553 553 return dictarg.get(key)
554 554
555 555 @templatefunc('if(expr, then[, else])')
556 556 def if_(context, mapping, args):
557 557 """Conditionally execute based on the result of
558 558 an expression."""
559 559 if not (2 <= len(args) <= 3):
560 560 # i18n: "if" is a keyword
561 561 raise error.ParseError(_("if expects two or three arguments"))
562 562
563 563 test = evalstring(context, mapping, args[0])
564 564 if test:
565 565 yield args[1][0](context, mapping, args[1][1])
566 566 elif len(args) == 3:
567 567 yield args[2][0](context, mapping, args[2][1])
568 568
569 569 @templatefunc('ifcontains(search, thing, then[, else])')
570 570 def ifcontains(context, mapping, args):
571 571 """Conditionally execute based
572 572 on whether the item "search" is in "thing"."""
573 573 if not (3 <= len(args) <= 4):
574 574 # i18n: "ifcontains" is a keyword
575 575 raise error.ParseError(_("ifcontains expects three or four arguments"))
576 576
577 577 item = evalstring(context, mapping, args[0])
578 578 items = evalfuncarg(context, mapping, args[1])
579 579
580 580 if item in items:
581 581 yield args[2][0](context, mapping, args[2][1])
582 582 elif len(args) == 4:
583 583 yield args[3][0](context, mapping, args[3][1])
584 584
585 585 @templatefunc('ifeq(expr1, expr2, then[, else])')
586 586 def ifeq(context, mapping, args):
587 587 """Conditionally execute based on
588 588 whether 2 items are equivalent."""
589 589 if not (3 <= len(args) <= 4):
590 590 # i18n: "ifeq" is a keyword
591 591 raise error.ParseError(_("ifeq expects three or four arguments"))
592 592
593 593 test = evalstring(context, mapping, args[0])
594 594 match = evalstring(context, mapping, args[1])
595 595 if test == match:
596 596 yield args[2][0](context, mapping, args[2][1])
597 597 elif len(args) == 4:
598 598 yield args[3][0](context, mapping, args[3][1])
599 599
600 600 @templatefunc('join(list, sep)')
601 601 def join(context, mapping, args):
602 602 """Join items in a list with a delimiter."""
603 603 if not (1 <= len(args) <= 2):
604 604 # i18n: "join" is a keyword
605 605 raise error.ParseError(_("join expects one or two arguments"))
606 606
607 607 joinset = args[0][0](context, mapping, args[0][1])
608 608 if util.safehasattr(joinset, 'itermaps'):
609 609 jf = joinset.joinfmt
610 610 joinset = [jf(x) for x in joinset.itermaps()]
611 611
612 612 joiner = " "
613 613 if len(args) > 1:
614 614 joiner = evalstring(context, mapping, args[1])
615 615
616 616 first = True
617 617 for x in joinset:
618 618 if first:
619 619 first = False
620 620 else:
621 621 yield joiner
622 622 yield x
623 623
624 624 @templatefunc('label(label, expr)')
625 625 def label(context, mapping, args):
626 626 """Apply a label to generated content. Content with
627 627 a label applied can result in additional post-processing, such as
628 628 automatic colorization."""
629 629 if len(args) != 2:
630 630 # i18n: "label" is a keyword
631 631 raise error.ParseError(_("label expects two arguments"))
632 632
633 633 ui = mapping['ui']
634 634 thing = evalstring(context, mapping, args[1])
635 635 # preserve unknown symbol as literal so effects like 'red', 'bold',
636 636 # etc. don't need to be quoted
637 637 label = evalstringliteral(context, mapping, args[0])
638 638
639 639 return ui.label(thing, label)
640 640
641 641 @templatefunc('latesttag([pattern])')
642 642 def latesttag(context, mapping, args):
643 643 """The global tags matching the given pattern on the
644 644 most recent globally tagged ancestor of this changeset."""
645 645 if len(args) > 1:
646 646 # i18n: "latesttag" is a keyword
647 647 raise error.ParseError(_("latesttag expects at most one argument"))
648 648
649 649 pattern = None
650 650 if len(args) == 1:
651 651 pattern = evalstring(context, mapping, args[0])
652 652
653 653 return templatekw.showlatesttags(pattern, **mapping)
654 654
655 655 @templatefunc('localdate(date[, tz])')
656 656 def localdate(context, mapping, args):
657 657 """Converts a date to the specified timezone.
658 658 The default is local date."""
659 659 if not (1 <= len(args) <= 2):
660 660 # i18n: "localdate" is a keyword
661 661 raise error.ParseError(_("localdate expects one or two arguments"))
662 662
663 663 date = evalfuncarg(context, mapping, args[0])
664 664 try:
665 665 date = util.parsedate(date)
666 666 except AttributeError: # not str nor date tuple
667 667 # i18n: "localdate" is a keyword
668 668 raise error.ParseError(_("localdate expects a date information"))
669 669 if len(args) >= 2:
670 670 tzoffset = None
671 671 tz = evalfuncarg(context, mapping, args[1])
672 672 if isinstance(tz, str):
673 673 tzoffset, remainder = util.parsetimezone(tz)
674 674 if remainder:
675 675 tzoffset = None
676 676 if tzoffset is None:
677 677 try:
678 678 tzoffset = int(tz)
679 679 except (TypeError, ValueError):
680 680 # i18n: "localdate" is a keyword
681 681 raise error.ParseError(_("localdate expects a timezone"))
682 682 else:
683 683 tzoffset = util.makedate()[1]
684 684 return (date[0], tzoffset)
685 685
686 686 @templatefunc('revset(query[, formatargs...])')
687 687 def revset(context, mapping, args):
688 688 """Execute a revision set query. See
689 689 :hg:`help revset`."""
690 690 if not len(args) > 0:
691 691 # i18n: "revset" is a keyword
692 692 raise error.ParseError(_("revset expects one or more arguments"))
693 693
694 694 raw = evalstring(context, mapping, args[0])
695 695 ctx = mapping['ctx']
696 696 repo = ctx.repo()
697 697
698 698 def query(expr):
699 699 m = revsetmod.match(repo.ui, expr)
700 700 return m(repo)
701 701
702 702 if len(args) > 1:
703 703 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
704 704 revs = query(revsetmod.formatspec(raw, *formatargs))
705 705 revs = list(revs)
706 706 else:
707 707 revsetcache = mapping['cache'].setdefault("revsetcache", {})
708 708 if raw in revsetcache:
709 709 revs = revsetcache[raw]
710 710 else:
711 711 revs = query(raw)
712 712 revs = list(revs)
713 713 revsetcache[raw] = revs
714 714
715 715 return templatekw.showrevslist("revision", revs, **mapping)
716 716
717 717 @templatefunc('rstdoc(text, style)')
718 718 def rstdoc(context, mapping, args):
719 719 """Format ReStructuredText."""
720 720 if len(args) != 2:
721 721 # i18n: "rstdoc" is a keyword
722 722 raise error.ParseError(_("rstdoc expects two arguments"))
723 723
724 724 text = evalstring(context, mapping, args[0])
725 725 style = evalstring(context, mapping, args[1])
726 726
727 727 return minirst.format(text, style=style, keep=['verbose'])
728 728
729 729 @templatefunc('separate(sep, args)')
730 730 def separate(context, mapping, args):
731 731 """Add a separator between non-empty arguments."""
732 732 if not args:
733 733 # i18n: "separate" is a keyword
734 734 raise error.ParseError(_("separate expects at least one argument"))
735 735
736 736 sep = evalstring(context, mapping, args[0])
737 737 first = True
738 738 for arg in args[1:]:
739 739 argstr = evalstring(context, mapping, arg)
740 740 if not argstr:
741 741 continue
742 742 if first:
743 743 first = False
744 744 else:
745 745 yield sep
746 746 yield argstr
747 747
748 748 @templatefunc('shortest(node, minlength=4)')
749 749 def shortest(context, mapping, args):
750 750 """Obtain the shortest representation of
751 751 a node."""
752 752 if not (1 <= len(args) <= 2):
753 753 # i18n: "shortest" is a keyword
754 754 raise error.ParseError(_("shortest() expects one or two arguments"))
755 755
756 756 node = evalstring(context, mapping, args[0])
757 757
758 758 minlength = 4
759 759 if len(args) > 1:
760 760 minlength = evalinteger(context, mapping, args[1],
761 761 # i18n: "shortest" is a keyword
762 762 _("shortest() expects an integer minlength"))
763 763
764 764 cl = mapping['ctx']._repo.changelog
765 765 def isvalid(test):
766 766 try:
767 767 try:
768 768 cl.index.partialmatch(test)
769 769 except AttributeError:
770 770 # Pure mercurial doesn't support partialmatch on the index.
771 771 # Fallback to the slow way.
772 772 if cl._partialmatch(test) is None:
773 773 return False
774 774
775 775 try:
776 776 i = int(test)
777 777 # if we are a pure int, then starting with zero will not be
778 778 # confused as a rev; or, obviously, if the int is larger than
779 779 # the value of the tip rev
780 780 if test[0] == '0' or i > len(cl):
781 781 return True
782 782 return False
783 783 except ValueError:
784 784 return True
785 785 except error.RevlogError:
786 786 return False
787 787
788 788 shortest = node
789 789 startlength = max(6, minlength)
790 790 length = startlength
791 791 while True:
792 792 test = node[:length]
793 793 if isvalid(test):
794 794 shortest = test
795 795 if length == minlength or length > startlength:
796 796 return shortest
797 797 length -= 1
798 798 else:
799 799 length += 1
800 800 if len(shortest) <= length:
801 801 return shortest
802 802
803 803 @templatefunc('strip(text[, chars])')
804 804 def strip(context, mapping, args):
805 805 """Strip characters from a string. By default,
806 806 strips all leading and trailing whitespace."""
807 807 if not (1 <= len(args) <= 2):
808 808 # i18n: "strip" is a keyword
809 809 raise error.ParseError(_("strip expects one or two arguments"))
810 810
811 811 text = evalstring(context, mapping, args[0])
812 812 if len(args) == 2:
813 813 chars = evalstring(context, mapping, args[1])
814 814 return text.strip(chars)
815 815 return text.strip()
816 816
817 817 @templatefunc('sub(pattern, replacement, expression)')
818 818 def sub(context, mapping, args):
819 819 """Perform text substitution
820 820 using regular expressions."""
821 821 if len(args) != 3:
822 822 # i18n: "sub" is a keyword
823 823 raise error.ParseError(_("sub expects three arguments"))
824 824
825 825 pat = evalstring(context, mapping, args[0])
826 826 rpl = evalstring(context, mapping, args[1])
827 827 src = evalstring(context, mapping, args[2])
828 828 try:
829 829 patre = re.compile(pat)
830 830 except re.error:
831 831 # i18n: "sub" is a keyword
832 832 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
833 833 try:
834 834 yield patre.sub(rpl, src)
835 835 except re.error:
836 836 # i18n: "sub" is a keyword
837 837 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
838 838
839 839 @templatefunc('startswith(pattern, text)')
840 840 def startswith(context, mapping, args):
841 841 """Returns the value from the "text" argument
842 842 if it begins with the content from the "pattern" argument."""
843 843 if len(args) != 2:
844 844 # i18n: "startswith" is a keyword
845 845 raise error.ParseError(_("startswith expects two arguments"))
846 846
847 847 patn = evalstring(context, mapping, args[0])
848 848 text = evalstring(context, mapping, args[1])
849 849 if text.startswith(patn):
850 850 return text
851 851 return ''
852 852
853 853 @templatefunc('word(number, text[, separator])')
854 854 def word(context, mapping, args):
855 855 """Return the nth word from a string."""
856 856 if not (2 <= len(args) <= 3):
857 857 # i18n: "word" is a keyword
858 858 raise error.ParseError(_("word expects two or three arguments, got %d")
859 859 % len(args))
860 860
861 861 num = evalinteger(context, mapping, args[0],
862 862 # i18n: "word" is a keyword
863 863 _("word expects an integer index"))
864 864 text = evalstring(context, mapping, args[1])
865 865 if len(args) == 3:
866 866 splitter = evalstring(context, mapping, args[2])
867 867 else:
868 868 splitter = None
869 869
870 870 tokens = text.split(splitter)
871 871 if num >= len(tokens) or num < -len(tokens):
872 872 return ''
873 873 else:
874 874 return tokens[num]
875 875
876 876 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
877 877 exprmethods = {
878 878 "integer": lambda e, c: (runinteger, e[1]),
879 879 "string": lambda e, c: (runstring, e[1]),
880 880 "symbol": lambda e, c: (runsymbol, e[1]),
881 881 "template": buildtemplate,
882 882 "group": lambda e, c: compileexp(e[1], c, exprmethods),
883 883 # ".": buildmember,
884 884 "|": buildfilter,
885 885 "%": buildmap,
886 886 "func": buildfunc,
887 887 }
888 888
889 889 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
890 890 methods = exprmethods.copy()
891 891 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
892 892
893 893 class _aliasrules(parser.basealiasrules):
894 894 """Parsing and expansion rule set of template aliases"""
895 895 _section = _('template alias')
896 896 _parse = staticmethod(_parseexpr)
897 897
898 898 @staticmethod
899 899 def _trygetfunc(tree):
900 900 """Return (name, args) if tree is func(...) or ...|filter; otherwise
901 901 None"""
902 902 if tree[0] == 'func' and tree[1][0] == 'symbol':
903 903 return tree[1][1], getlist(tree[2])
904 904 if tree[0] == '|' and tree[2][0] == 'symbol':
905 905 return tree[2][1], [tree[1]]
906 906
907 907 def expandaliases(tree, aliases):
908 908 """Return new tree of aliases are expanded"""
909 909 aliasmap = _aliasrules.buildmap(aliases)
910 910 return _aliasrules.expand(aliasmap, tree)
911 911
912 912 # template engine
913 913
914 914 stringify = templatefilters.stringify
915 915
916 916 def _flatten(thing):
917 917 '''yield a single stream from a possibly nested set of iterators'''
918 918 if isinstance(thing, str):
919 919 yield thing
920 elif thing is None:
921 pass
920 922 elif not util.safehasattr(thing, '__iter__'):
921 if thing is not None:
922 yield str(thing)
923 yield str(thing)
923 924 else:
924 925 for i in thing:
925 926 if isinstance(i, str):
926 927 yield i
928 elif i is None:
929 pass
927 930 elif not util.safehasattr(i, '__iter__'):
928 if i is not None:
929 yield str(i)
930 elif i is not None:
931 yield str(i)
932 else:
931 933 for j in _flatten(i):
932 934 yield j
933 935
934 936 def unquotestring(s):
935 937 '''unwrap quotes if any; otherwise returns unmodified string'''
936 938 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
937 939 return s
938 940 return s[1:-1]
939 941
940 942 class engine(object):
941 943 '''template expansion engine.
942 944
943 945 template expansion works like this. a map file contains key=value
944 946 pairs. if value is quoted, it is treated as string. otherwise, it
945 947 is treated as name of template file.
946 948
947 949 templater is asked to expand a key in map. it looks up key, and
948 950 looks for strings like this: {foo}. it expands {foo} by looking up
949 951 foo in map, and substituting it. expansion is recursive: it stops
950 952 when there is no more {foo} to replace.
951 953
952 954 expansion also allows formatting and filtering.
953 955
954 956 format uses key to expand each item in list. syntax is
955 957 {key%format}.
956 958
957 959 filter uses function to transform value. syntax is
958 960 {key|filter1|filter2|...}.'''
959 961
960 962 def __init__(self, loader, filters=None, defaults=None, aliases=()):
961 963 self._loader = loader
962 964 if filters is None:
963 965 filters = {}
964 966 self._filters = filters
965 967 if defaults is None:
966 968 defaults = {}
967 969 self._defaults = defaults
968 970 self._aliasmap = _aliasrules.buildmap(aliases)
969 971 self._cache = {} # key: (func, data)
970 972
971 973 def _load(self, t):
972 974 '''load, parse, and cache a template'''
973 975 if t not in self._cache:
974 976 # put poison to cut recursion while compiling 't'
975 977 self._cache[t] = (_runrecursivesymbol, t)
976 978 try:
977 979 x = parse(self._loader(t))
978 980 if self._aliasmap:
979 981 x = _aliasrules.expand(self._aliasmap, x)
980 982 self._cache[t] = compileexp(x, self, methods)
981 983 except: # re-raises
982 984 del self._cache[t]
983 985 raise
984 986 return self._cache[t]
985 987
986 988 def process(self, t, mapping):
987 989 '''Perform expansion. t is name of map element to expand.
988 990 mapping contains added elements for use during expansion. Is a
989 991 generator.'''
990 992 func, data = self._load(t)
991 993 return _flatten(func(self, mapping, data))
992 994
993 995 engines = {'default': engine}
994 996
995 997 def stylelist():
996 998 paths = templatepaths()
997 999 if not paths:
998 1000 return _('no templates found, try `hg debuginstall` for more info')
999 1001 dirlist = os.listdir(paths[0])
1000 1002 stylelist = []
1001 1003 for file in dirlist:
1002 1004 split = file.split(".")
1003 1005 if split[-1] in ('orig', 'rej'):
1004 1006 continue
1005 1007 if split[0] == "map-cmdline":
1006 1008 stylelist.append(split[1])
1007 1009 return ", ".join(sorted(stylelist))
1008 1010
1009 1011 def _readmapfile(mapfile):
1010 1012 """Load template elements from the given map file"""
1011 1013 if not os.path.exists(mapfile):
1012 1014 raise error.Abort(_("style '%s' not found") % mapfile,
1013 1015 hint=_("available styles: %s") % stylelist())
1014 1016
1015 1017 base = os.path.dirname(mapfile)
1016 1018 conf = config.config(includepaths=templatepaths())
1017 1019 conf.read(mapfile)
1018 1020
1019 1021 cache = {}
1020 1022 tmap = {}
1021 1023 for key, val in conf[''].items():
1022 1024 if not val:
1023 1025 raise error.ParseError(_('missing value'), conf.source('', key))
1024 1026 if val[0] in "'\"":
1025 1027 if val[0] != val[-1]:
1026 1028 raise error.ParseError(_('unmatched quotes'),
1027 1029 conf.source('', key))
1028 1030 cache[key] = unquotestring(val)
1029 1031 elif key == "__base__":
1030 1032 # treat as a pointer to a base class for this style
1031 1033 path = util.normpath(os.path.join(base, val))
1032 1034 bcache, btmap = _readmapfile(path)
1033 1035 for k in bcache:
1034 1036 if k not in cache:
1035 1037 cache[k] = bcache[k]
1036 1038 for k in btmap:
1037 1039 if k not in tmap:
1038 1040 tmap[k] = btmap[k]
1039 1041 else:
1040 1042 val = 'default', val
1041 1043 if ':' in val[1]:
1042 1044 val = val[1].split(':', 1)
1043 1045 tmap[key] = val[0], os.path.join(base, val[1])
1044 1046 return cache, tmap
1045 1047
1046 1048 class TemplateNotFound(error.Abort):
1047 1049 pass
1048 1050
1049 1051 class templater(object):
1050 1052
1051 1053 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1052 1054 minchunk=1024, maxchunk=65536):
1053 1055 '''set up template engine.
1054 1056 filters is dict of functions. each transforms a value into another.
1055 1057 defaults is dict of default map definitions.
1056 1058 aliases is list of alias (name, replacement) pairs.
1057 1059 '''
1058 1060 if filters is None:
1059 1061 filters = {}
1060 1062 if defaults is None:
1061 1063 defaults = {}
1062 1064 if cache is None:
1063 1065 cache = {}
1064 1066 self.cache = cache.copy()
1065 1067 self.map = {}
1066 1068 self.filters = templatefilters.filters.copy()
1067 1069 self.filters.update(filters)
1068 1070 self.defaults = defaults
1069 1071 self._aliases = aliases
1070 1072 self.minchunk, self.maxchunk = minchunk, maxchunk
1071 1073 self.ecache = {}
1072 1074
1073 1075 @classmethod
1074 1076 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1075 1077 minchunk=1024, maxchunk=65536):
1076 1078 """Create templater from the specified map file"""
1077 1079 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1078 1080 cache, tmap = _readmapfile(mapfile)
1079 1081 t.cache.update(cache)
1080 1082 t.map = tmap
1081 1083 return t
1082 1084
1083 1085 def __contains__(self, key):
1084 1086 return key in self.cache or key in self.map
1085 1087
1086 1088 def load(self, t):
1087 1089 '''Get the template for the given template name. Use a local cache.'''
1088 1090 if t not in self.cache:
1089 1091 try:
1090 1092 self.cache[t] = util.readfile(self.map[t][1])
1091 1093 except KeyError as inst:
1092 1094 raise TemplateNotFound(_('"%s" not in template map') %
1093 1095 inst.args[0])
1094 1096 except IOError as inst:
1095 1097 raise IOError(inst.args[0], _('template file %s: %s') %
1096 1098 (self.map[t][1], inst.args[1]))
1097 1099 return self.cache[t]
1098 1100
1099 1101 def __call__(self, t, **mapping):
1100 1102 ttype = t in self.map and self.map[t][0] or 'default'
1101 1103 if ttype not in self.ecache:
1102 1104 try:
1103 1105 ecls = engines[ttype]
1104 1106 except KeyError:
1105 1107 raise error.Abort(_('invalid template engine: %s') % ttype)
1106 1108 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1107 1109 self._aliases)
1108 1110 proc = self.ecache[ttype]
1109 1111
1110 1112 stream = proc.process(t, mapping)
1111 1113 if self.minchunk:
1112 1114 stream = util.increasingchunks(stream, min=self.minchunk,
1113 1115 max=self.maxchunk)
1114 1116 return stream
1115 1117
1116 1118 def templatepaths():
1117 1119 '''return locations used for template files.'''
1118 1120 pathsrel = ['templates']
1119 1121 paths = [os.path.normpath(os.path.join(util.datapath, f))
1120 1122 for f in pathsrel]
1121 1123 return [p for p in paths if os.path.isdir(p)]
1122 1124
1123 1125 def templatepath(name):
1124 1126 '''return location of template file. returns None if not found.'''
1125 1127 for p in templatepaths():
1126 1128 f = os.path.join(p, name)
1127 1129 if os.path.exists(f):
1128 1130 return f
1129 1131 return None
1130 1132
1131 1133 def stylemap(styles, paths=None):
1132 1134 """Return path to mapfile for a given style.
1133 1135
1134 1136 Searches mapfile in the following locations:
1135 1137 1. templatepath/style/map
1136 1138 2. templatepath/map-style
1137 1139 3. templatepath/map
1138 1140 """
1139 1141
1140 1142 if paths is None:
1141 1143 paths = templatepaths()
1142 1144 elif isinstance(paths, str):
1143 1145 paths = [paths]
1144 1146
1145 1147 if isinstance(styles, str):
1146 1148 styles = [styles]
1147 1149
1148 1150 for style in styles:
1149 1151 # only plain name is allowed to honor template paths
1150 1152 if (not style
1151 1153 or style in (os.curdir, os.pardir)
1152 1154 or os.sep in style
1153 1155 or os.altsep and os.altsep in style):
1154 1156 continue
1155 1157 locations = [os.path.join(style, 'map'), 'map-' + style]
1156 1158 locations.append('map')
1157 1159
1158 1160 for path in paths:
1159 1161 for location in locations:
1160 1162 mapfile = os.path.join(path, location)
1161 1163 if os.path.isfile(mapfile):
1162 1164 return style, mapfile
1163 1165
1164 1166 raise RuntimeError("No hgweb templates found in %r" % paths)
1165 1167
1166 1168 def loadfunction(ui, extname, registrarobj):
1167 1169 """Load template function from specified registrarobj
1168 1170 """
1169 1171 for name, func in registrarobj._table.iteritems():
1170 1172 funcs[name] = func
1171 1173
1172 1174 # tell hggettext to extract docstrings from these functions:
1173 1175 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now