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