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