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