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