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