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