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