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