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