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