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