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