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