##// END OF EJS Templates
templatefuncs: use evaldate() where seems appropriate...
Yuya Nishihara -
r37242:e70a90a7 default
parent child Browse files
Show More
@@ -1,683 +1,681 b''
1 1 # templatefuncs.py - common template functions
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 re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 color,
15 15 encoding,
16 16 error,
17 17 minirst,
18 18 obsutil,
19 19 pycompat,
20 20 registrar,
21 21 revset as revsetmod,
22 22 revsetlang,
23 23 scmutil,
24 24 templatefilters,
25 25 templatekw,
26 26 templateutil,
27 27 util,
28 28 )
29 29 from .utils import (
30 30 dateutil,
31 31 stringutil,
32 32 )
33 33
34 34 evalrawexp = templateutil.evalrawexp
35 35 evalfuncarg = templateutil.evalfuncarg
36 36 evalboolean = templateutil.evalboolean
37 37 evaldate = templateutil.evaldate
38 38 evalinteger = templateutil.evalinteger
39 39 evalstring = templateutil.evalstring
40 40 evalstringliteral = templateutil.evalstringliteral
41 41
42 42 # dict of template built-in functions
43 43 funcs = {}
44 44 templatefunc = registrar.templatefunc(funcs)
45 45
46 46 @templatefunc('date(date[, fmt])')
47 47 def date(context, mapping, args):
48 48 """Format a date. See :hg:`help dates` for formatting
49 49 strings. The default is a Unix date format, including the timezone:
50 50 "Mon Sep 04 15:13:13 2006 0700"."""
51 51 if not (1 <= len(args) <= 2):
52 52 # i18n: "date" is a keyword
53 53 raise error.ParseError(_("date expects one or two arguments"))
54 54
55 date = evalfuncarg(context, mapping, args[0])
55 date = evaldate(context, mapping, args[0],
56 # i18n: "date" is a keyword
57 _("date expects a date information"))
56 58 fmt = None
57 59 if len(args) == 2:
58 60 fmt = evalstring(context, mapping, args[1])
59 try:
60 if fmt is None:
61 return dateutil.datestr(date)
62 else:
63 return dateutil.datestr(date, fmt)
64 except (TypeError, ValueError):
65 # i18n: "date" is a keyword
66 raise error.ParseError(_("date expects a date information"))
61 if fmt is None:
62 return dateutil.datestr(date)
63 else:
64 return dateutil.datestr(date, fmt)
67 65
68 66 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
69 67 def dict_(context, mapping, args):
70 68 """Construct a dict from key-value pairs. A key may be omitted if
71 69 a value expression can provide an unambiguous name."""
72 70 data = util.sortdict()
73 71
74 72 for v in args['args']:
75 73 k = templateutil.findsymbolicname(v)
76 74 if not k:
77 75 raise error.ParseError(_('dict key cannot be inferred'))
78 76 if k in data or k in args['kwargs']:
79 77 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
80 78 data[k] = evalfuncarg(context, mapping, v)
81 79
82 80 data.update((k, evalfuncarg(context, mapping, v))
83 81 for k, v in args['kwargs'].iteritems())
84 82 return templateutil.hybriddict(data)
85 83
86 84 @templatefunc('diff([includepattern [, excludepattern]])')
87 85 def diff(context, mapping, args):
88 86 """Show a diff, optionally
89 87 specifying files to include or exclude."""
90 88 if len(args) > 2:
91 89 # i18n: "diff" is a keyword
92 90 raise error.ParseError(_("diff expects zero, one, or two arguments"))
93 91
94 92 def getpatterns(i):
95 93 if i < len(args):
96 94 s = evalstring(context, mapping, args[i]).strip()
97 95 if s:
98 96 return [s]
99 97 return []
100 98
101 99 ctx = context.resource(mapping, 'ctx')
102 100 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
103 101
104 102 return ''.join(chunks)
105 103
106 104 @templatefunc('extdata(source)', argspec='source')
107 105 def extdata(context, mapping, args):
108 106 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
109 107 if 'source' not in args:
110 108 # i18n: "extdata" is a keyword
111 109 raise error.ParseError(_('extdata expects one argument'))
112 110
113 111 source = evalstring(context, mapping, args['source'])
114 112 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
115 113 ctx = context.resource(mapping, 'ctx')
116 114 if source in cache:
117 115 data = cache[source]
118 116 else:
119 117 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
120 118 return data.get(ctx.rev(), '')
121 119
122 120 @templatefunc('files(pattern)')
123 121 def files(context, mapping, args):
124 122 """All files of the current changeset matching the pattern. See
125 123 :hg:`help patterns`."""
126 124 if not len(args) == 1:
127 125 # i18n: "files" is a keyword
128 126 raise error.ParseError(_("files expects one argument"))
129 127
130 128 raw = evalstring(context, mapping, args[0])
131 129 ctx = context.resource(mapping, 'ctx')
132 130 m = ctx.match([raw])
133 131 files = list(ctx.matches(m))
134 132 return templateutil.compatlist(context, mapping, "file", files)
135 133
136 134 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
137 135 def fill(context, mapping, args):
138 136 """Fill many
139 137 paragraphs with optional indentation. See the "fill" filter."""
140 138 if not (1 <= len(args) <= 4):
141 139 # i18n: "fill" is a keyword
142 140 raise error.ParseError(_("fill expects one to four arguments"))
143 141
144 142 text = evalstring(context, mapping, args[0])
145 143 width = 76
146 144 initindent = ''
147 145 hangindent = ''
148 146 if 2 <= len(args) <= 4:
149 147 width = evalinteger(context, mapping, args[1],
150 148 # i18n: "fill" is a keyword
151 149 _("fill expects an integer width"))
152 150 try:
153 151 initindent = evalstring(context, mapping, args[2])
154 152 hangindent = evalstring(context, mapping, args[3])
155 153 except IndexError:
156 154 pass
157 155
158 156 return templatefilters.fill(text, width, initindent, hangindent)
159 157
160 158 @templatefunc('formatnode(node)')
161 159 def formatnode(context, mapping, args):
162 160 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
163 161 if len(args) != 1:
164 162 # i18n: "formatnode" is a keyword
165 163 raise error.ParseError(_("formatnode expects one argument"))
166 164
167 165 ui = context.resource(mapping, 'ui')
168 166 node = evalstring(context, mapping, args[0])
169 167 if ui.debugflag:
170 168 return node
171 169 return templatefilters.short(node)
172 170
173 171 @templatefunc('mailmap(author)')
174 172 def mailmap(context, mapping, args):
175 173 """Return the author, updated according to the value
176 174 set in the .mailmap file"""
177 175 if len(args) != 1:
178 176 raise error.ParseError(_("mailmap expects one argument"))
179 177
180 178 author = evalfuncarg(context, mapping, args[0])
181 179
182 180 cache = context.resource(mapping, 'cache')
183 181 repo = context.resource(mapping, 'repo')
184 182
185 183 if 'mailmap' not in cache:
186 184 data = repo.wvfs.tryread('.mailmap')
187 185 cache['mailmap'] = stringutil.parsemailmap(data)
188 186
189 187 return stringutil.mapname(cache['mailmap'], author) or author
190 188
191 189 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
192 190 argspec='text width fillchar left')
193 191 def pad(context, mapping, args):
194 192 """Pad text with a
195 193 fill character."""
196 194 if 'text' not in args or 'width' not in args:
197 195 # i18n: "pad" is a keyword
198 196 raise error.ParseError(_("pad() expects two to four arguments"))
199 197
200 198 width = evalinteger(context, mapping, args['width'],
201 199 # i18n: "pad" is a keyword
202 200 _("pad() expects an integer width"))
203 201
204 202 text = evalstring(context, mapping, args['text'])
205 203
206 204 left = False
207 205 fillchar = ' '
208 206 if 'fillchar' in args:
209 207 fillchar = evalstring(context, mapping, args['fillchar'])
210 208 if len(color.stripeffects(fillchar)) != 1:
211 209 # i18n: "pad" is a keyword
212 210 raise error.ParseError(_("pad() expects a single fill character"))
213 211 if 'left' in args:
214 212 left = evalboolean(context, mapping, args['left'])
215 213
216 214 fillwidth = width - encoding.colwidth(color.stripeffects(text))
217 215 if fillwidth <= 0:
218 216 return text
219 217 if left:
220 218 return fillchar * fillwidth + text
221 219 else:
222 220 return text + fillchar * fillwidth
223 221
224 222 @templatefunc('indent(text, indentchars[, firstline])')
225 223 def indent(context, mapping, args):
226 224 """Indents all non-empty lines
227 225 with the characters given in the indentchars string. An optional
228 226 third parameter will override the indent for the first line only
229 227 if present."""
230 228 if not (2 <= len(args) <= 3):
231 229 # i18n: "indent" is a keyword
232 230 raise error.ParseError(_("indent() expects two or three arguments"))
233 231
234 232 text = evalstring(context, mapping, args[0])
235 233 indent = evalstring(context, mapping, args[1])
236 234
237 235 if len(args) == 3:
238 236 firstline = evalstring(context, mapping, args[2])
239 237 else:
240 238 firstline = indent
241 239
242 240 # the indent function doesn't indent the first line, so we do it here
243 241 return templatefilters.indent(firstline + text, indent)
244 242
245 243 @templatefunc('get(dict, key)')
246 244 def get(context, mapping, args):
247 245 """Get an attribute/key from an object. Some keywords
248 246 are complex types. This function allows you to obtain the value of an
249 247 attribute on these types."""
250 248 if len(args) != 2:
251 249 # i18n: "get" is a keyword
252 250 raise error.ParseError(_("get() expects two arguments"))
253 251
254 252 dictarg = evalfuncarg(context, mapping, args[0])
255 253 if not util.safehasattr(dictarg, 'get'):
256 254 # i18n: "get" is a keyword
257 255 raise error.ParseError(_("get() expects a dict as first argument"))
258 256
259 257 key = evalfuncarg(context, mapping, args[1])
260 258 return templateutil.getdictitem(dictarg, key)
261 259
262 260 @templatefunc('if(expr, then[, else])')
263 261 def if_(context, mapping, args):
264 262 """Conditionally execute based on the result of
265 263 an expression."""
266 264 if not (2 <= len(args) <= 3):
267 265 # i18n: "if" is a keyword
268 266 raise error.ParseError(_("if expects two or three arguments"))
269 267
270 268 test = evalboolean(context, mapping, args[0])
271 269 if test:
272 270 return evalrawexp(context, mapping, args[1])
273 271 elif len(args) == 3:
274 272 return evalrawexp(context, mapping, args[2])
275 273
276 274 @templatefunc('ifcontains(needle, haystack, then[, else])')
277 275 def ifcontains(context, mapping, args):
278 276 """Conditionally execute based
279 277 on whether the item "needle" is in "haystack"."""
280 278 if not (3 <= len(args) <= 4):
281 279 # i18n: "ifcontains" is a keyword
282 280 raise error.ParseError(_("ifcontains expects three or four arguments"))
283 281
284 282 haystack = evalfuncarg(context, mapping, args[1])
285 283 keytype = getattr(haystack, 'keytype', None)
286 284 try:
287 285 needle = evalrawexp(context, mapping, args[0])
288 286 needle = templateutil.unwrapastype(needle, keytype or bytes)
289 287 found = (needle in haystack)
290 288 except error.ParseError:
291 289 found = False
292 290
293 291 if found:
294 292 return evalrawexp(context, mapping, args[2])
295 293 elif len(args) == 4:
296 294 return evalrawexp(context, mapping, args[3])
297 295
298 296 @templatefunc('ifeq(expr1, expr2, then[, else])')
299 297 def ifeq(context, mapping, args):
300 298 """Conditionally execute based on
301 299 whether 2 items are equivalent."""
302 300 if not (3 <= len(args) <= 4):
303 301 # i18n: "ifeq" is a keyword
304 302 raise error.ParseError(_("ifeq expects three or four arguments"))
305 303
306 304 test = evalstring(context, mapping, args[0])
307 305 match = evalstring(context, mapping, args[1])
308 306 if test == match:
309 307 return evalrawexp(context, mapping, args[2])
310 308 elif len(args) == 4:
311 309 return evalrawexp(context, mapping, args[3])
312 310
313 311 @templatefunc('join(list, sep)')
314 312 def join(context, mapping, args):
315 313 """Join items in a list with a delimiter."""
316 314 if not (1 <= len(args) <= 2):
317 315 # i18n: "join" is a keyword
318 316 raise error.ParseError(_("join expects one or two arguments"))
319 317
320 318 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
321 319 # abuses generator as a keyword that returns a list of dicts.
322 320 joinset = evalrawexp(context, mapping, args[0])
323 321 joinset = templateutil.unwrapvalue(joinset)
324 322 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
325 323 joiner = " "
326 324 if len(args) > 1:
327 325 joiner = evalstring(context, mapping, args[1])
328 326
329 327 first = True
330 328 for x in pycompat.maybebytestr(joinset):
331 329 if first:
332 330 first = False
333 331 else:
334 332 yield joiner
335 333 yield joinfmt(x)
336 334
337 335 @templatefunc('label(label, expr)')
338 336 def label(context, mapping, args):
339 337 """Apply a label to generated content. Content with
340 338 a label applied can result in additional post-processing, such as
341 339 automatic colorization."""
342 340 if len(args) != 2:
343 341 # i18n: "label" is a keyword
344 342 raise error.ParseError(_("label expects two arguments"))
345 343
346 344 ui = context.resource(mapping, 'ui')
347 345 thing = evalstring(context, mapping, args[1])
348 346 # preserve unknown symbol as literal so effects like 'red', 'bold',
349 347 # etc. don't need to be quoted
350 348 label = evalstringliteral(context, mapping, args[0])
351 349
352 350 return ui.label(thing, label)
353 351
354 352 @templatefunc('latesttag([pattern])')
355 353 def latesttag(context, mapping, args):
356 354 """The global tags matching the given pattern on the
357 355 most recent globally tagged ancestor of this changeset.
358 356 If no such tags exist, the "{tag}" template resolves to
359 357 the string "null"."""
360 358 if len(args) > 1:
361 359 # i18n: "latesttag" is a keyword
362 360 raise error.ParseError(_("latesttag expects at most one argument"))
363 361
364 362 pattern = None
365 363 if len(args) == 1:
366 364 pattern = evalstring(context, mapping, args[0])
367 365 return templatekw.showlatesttags(context, mapping, pattern)
368 366
369 367 @templatefunc('localdate(date[, tz])')
370 368 def localdate(context, mapping, args):
371 369 """Converts a date to the specified timezone.
372 370 The default is local date."""
373 371 if not (1 <= len(args) <= 2):
374 372 # i18n: "localdate" is a keyword
375 373 raise error.ParseError(_("localdate expects one or two arguments"))
376 374
377 375 date = evaldate(context, mapping, args[0],
378 376 # i18n: "localdate" is a keyword
379 377 _("localdate expects a date information"))
380 378 if len(args) >= 2:
381 379 tzoffset = None
382 380 tz = evalfuncarg(context, mapping, args[1])
383 381 if isinstance(tz, bytes):
384 382 tzoffset, remainder = dateutil.parsetimezone(tz)
385 383 if remainder:
386 384 tzoffset = None
387 385 if tzoffset is None:
388 386 try:
389 387 tzoffset = int(tz)
390 388 except (TypeError, ValueError):
391 389 # i18n: "localdate" is a keyword
392 390 raise error.ParseError(_("localdate expects a timezone"))
393 391 else:
394 392 tzoffset = dateutil.makedate()[1]
395 393 return (date[0], tzoffset)
396 394
397 395 @templatefunc('max(iterable)')
398 396 def max_(context, mapping, args, **kwargs):
399 397 """Return the max of an iterable"""
400 398 if len(args) != 1:
401 399 # i18n: "max" is a keyword
402 400 raise error.ParseError(_("max expects one argument"))
403 401
404 402 iterable = evalfuncarg(context, mapping, args[0])
405 403 try:
406 404 x = max(pycompat.maybebytestr(iterable))
407 405 except (TypeError, ValueError):
408 406 # i18n: "max" is a keyword
409 407 raise error.ParseError(_("max first argument should be an iterable"))
410 408 return templateutil.wraphybridvalue(iterable, x, x)
411 409
412 410 @templatefunc('min(iterable)')
413 411 def min_(context, mapping, args, **kwargs):
414 412 """Return the min of an iterable"""
415 413 if len(args) != 1:
416 414 # i18n: "min" is a keyword
417 415 raise error.ParseError(_("min expects one argument"))
418 416
419 417 iterable = evalfuncarg(context, mapping, args[0])
420 418 try:
421 419 x = min(pycompat.maybebytestr(iterable))
422 420 except (TypeError, ValueError):
423 421 # i18n: "min" is a keyword
424 422 raise error.ParseError(_("min first argument should be an iterable"))
425 423 return templateutil.wraphybridvalue(iterable, x, x)
426 424
427 425 @templatefunc('mod(a, b)')
428 426 def mod(context, mapping, args):
429 427 """Calculate a mod b such that a / b + a mod b == a"""
430 428 if not len(args) == 2:
431 429 # i18n: "mod" is a keyword
432 430 raise error.ParseError(_("mod expects two arguments"))
433 431
434 432 func = lambda a, b: a % b
435 433 return templateutil.runarithmetic(context, mapping,
436 434 (func, args[0], args[1]))
437 435
438 436 @templatefunc('obsfateoperations(markers)')
439 437 def obsfateoperations(context, mapping, args):
440 438 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
441 439 if len(args) != 1:
442 440 # i18n: "obsfateoperations" is a keyword
443 441 raise error.ParseError(_("obsfateoperations expects one argument"))
444 442
445 443 markers = evalfuncarg(context, mapping, args[0])
446 444
447 445 try:
448 446 data = obsutil.markersoperations(markers)
449 447 return templateutil.hybridlist(data, name='operation')
450 448 except (TypeError, KeyError):
451 449 # i18n: "obsfateoperations" is a keyword
452 450 errmsg = _("obsfateoperations first argument should be an iterable")
453 451 raise error.ParseError(errmsg)
454 452
455 453 @templatefunc('obsfatedate(markers)')
456 454 def obsfatedate(context, mapping, args):
457 455 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
458 456 if len(args) != 1:
459 457 # i18n: "obsfatedate" is a keyword
460 458 raise error.ParseError(_("obsfatedate expects one argument"))
461 459
462 460 markers = evalfuncarg(context, mapping, args[0])
463 461
464 462 try:
465 463 data = obsutil.markersdates(markers)
466 464 return templateutil.hybridlist(data, name='date', fmt='%d %d')
467 465 except (TypeError, KeyError):
468 466 # i18n: "obsfatedate" is a keyword
469 467 errmsg = _("obsfatedate first argument should be an iterable")
470 468 raise error.ParseError(errmsg)
471 469
472 470 @templatefunc('obsfateusers(markers)')
473 471 def obsfateusers(context, mapping, args):
474 472 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
475 473 if len(args) != 1:
476 474 # i18n: "obsfateusers" is a keyword
477 475 raise error.ParseError(_("obsfateusers expects one argument"))
478 476
479 477 markers = evalfuncarg(context, mapping, args[0])
480 478
481 479 try:
482 480 data = obsutil.markersusers(markers)
483 481 return templateutil.hybridlist(data, name='user')
484 482 except (TypeError, KeyError, ValueError):
485 483 # i18n: "obsfateusers" is a keyword
486 484 msg = _("obsfateusers first argument should be an iterable of "
487 485 "obsmakers")
488 486 raise error.ParseError(msg)
489 487
490 488 @templatefunc('obsfateverb(successors, markers)')
491 489 def obsfateverb(context, mapping, args):
492 490 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
493 491 if len(args) != 2:
494 492 # i18n: "obsfateverb" is a keyword
495 493 raise error.ParseError(_("obsfateverb expects two arguments"))
496 494
497 495 successors = evalfuncarg(context, mapping, args[0])
498 496 markers = evalfuncarg(context, mapping, args[1])
499 497
500 498 try:
501 499 return obsutil.obsfateverb(successors, markers)
502 500 except TypeError:
503 501 # i18n: "obsfateverb" is a keyword
504 502 errmsg = _("obsfateverb first argument should be countable")
505 503 raise error.ParseError(errmsg)
506 504
507 505 @templatefunc('relpath(path)')
508 506 def relpath(context, mapping, args):
509 507 """Convert a repository-absolute path into a filesystem path relative to
510 508 the current working directory."""
511 509 if len(args) != 1:
512 510 # i18n: "relpath" is a keyword
513 511 raise error.ParseError(_("relpath expects one argument"))
514 512
515 513 repo = context.resource(mapping, 'ctx').repo()
516 514 path = evalstring(context, mapping, args[0])
517 515 return repo.pathto(path)
518 516
519 517 @templatefunc('revset(query[, formatargs...])')
520 518 def revset(context, mapping, args):
521 519 """Execute a revision set query. See
522 520 :hg:`help revset`."""
523 521 if not len(args) > 0:
524 522 # i18n: "revset" is a keyword
525 523 raise error.ParseError(_("revset expects one or more arguments"))
526 524
527 525 raw = evalstring(context, mapping, args[0])
528 526 ctx = context.resource(mapping, 'ctx')
529 527 repo = ctx.repo()
530 528
531 529 def query(expr):
532 530 m = revsetmod.match(repo.ui, expr, repo=repo)
533 531 return m(repo)
534 532
535 533 if len(args) > 1:
536 534 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
537 535 revs = query(revsetlang.formatspec(raw, *formatargs))
538 536 revs = list(revs)
539 537 else:
540 538 cache = context.resource(mapping, 'cache')
541 539 revsetcache = cache.setdefault("revsetcache", {})
542 540 if raw in revsetcache:
543 541 revs = revsetcache[raw]
544 542 else:
545 543 revs = query(raw)
546 544 revs = list(revs)
547 545 revsetcache[raw] = revs
548 546 return templatekw.showrevslist(context, mapping, "revision", revs)
549 547
550 548 @templatefunc('rstdoc(text, style)')
551 549 def rstdoc(context, mapping, args):
552 550 """Format reStructuredText."""
553 551 if len(args) != 2:
554 552 # i18n: "rstdoc" is a keyword
555 553 raise error.ParseError(_("rstdoc expects two arguments"))
556 554
557 555 text = evalstring(context, mapping, args[0])
558 556 style = evalstring(context, mapping, args[1])
559 557
560 558 return minirst.format(text, style=style, keep=['verbose'])
561 559
562 560 @templatefunc('separate(sep, args)', argspec='sep *args')
563 561 def separate(context, mapping, args):
564 562 """Add a separator between non-empty arguments."""
565 563 if 'sep' not in args:
566 564 # i18n: "separate" is a keyword
567 565 raise error.ParseError(_("separate expects at least one argument"))
568 566
569 567 sep = evalstring(context, mapping, args['sep'])
570 568 first = True
571 569 for arg in args['args']:
572 570 argstr = evalstring(context, mapping, arg)
573 571 if not argstr:
574 572 continue
575 573 if first:
576 574 first = False
577 575 else:
578 576 yield sep
579 577 yield argstr
580 578
581 579 @templatefunc('shortest(node, minlength=4)')
582 580 def shortest(context, mapping, args):
583 581 """Obtain the shortest representation of
584 582 a node."""
585 583 if not (1 <= len(args) <= 2):
586 584 # i18n: "shortest" is a keyword
587 585 raise error.ParseError(_("shortest() expects one or two arguments"))
588 586
589 587 node = evalstring(context, mapping, args[0])
590 588
591 589 minlength = 4
592 590 if len(args) > 1:
593 591 minlength = evalinteger(context, mapping, args[1],
594 592 # i18n: "shortest" is a keyword
595 593 _("shortest() expects an integer minlength"))
596 594
597 595 # _partialmatch() of filtered changelog could take O(len(repo)) time,
598 596 # which would be unacceptably slow. so we look for hash collision in
599 597 # unfiltered space, which means some hashes may be slightly longer.
600 598 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
601 599 return cl.shortest(node, minlength)
602 600
603 601 @templatefunc('strip(text[, chars])')
604 602 def strip(context, mapping, args):
605 603 """Strip characters from a string. By default,
606 604 strips all leading and trailing whitespace."""
607 605 if not (1 <= len(args) <= 2):
608 606 # i18n: "strip" is a keyword
609 607 raise error.ParseError(_("strip expects one or two arguments"))
610 608
611 609 text = evalstring(context, mapping, args[0])
612 610 if len(args) == 2:
613 611 chars = evalstring(context, mapping, args[1])
614 612 return text.strip(chars)
615 613 return text.strip()
616 614
617 615 @templatefunc('sub(pattern, replacement, expression)')
618 616 def sub(context, mapping, args):
619 617 """Perform text substitution
620 618 using regular expressions."""
621 619 if len(args) != 3:
622 620 # i18n: "sub" is a keyword
623 621 raise error.ParseError(_("sub expects three arguments"))
624 622
625 623 pat = evalstring(context, mapping, args[0])
626 624 rpl = evalstring(context, mapping, args[1])
627 625 src = evalstring(context, mapping, args[2])
628 626 try:
629 627 patre = re.compile(pat)
630 628 except re.error:
631 629 # i18n: "sub" is a keyword
632 630 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
633 631 try:
634 632 yield patre.sub(rpl, src)
635 633 except re.error:
636 634 # i18n: "sub" is a keyword
637 635 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
638 636
639 637 @templatefunc('startswith(pattern, text)')
640 638 def startswith(context, mapping, args):
641 639 """Returns the value from the "text" argument
642 640 if it begins with the content from the "pattern" argument."""
643 641 if len(args) != 2:
644 642 # i18n: "startswith" is a keyword
645 643 raise error.ParseError(_("startswith expects two arguments"))
646 644
647 645 patn = evalstring(context, mapping, args[0])
648 646 text = evalstring(context, mapping, args[1])
649 647 if text.startswith(patn):
650 648 return text
651 649 return ''
652 650
653 651 @templatefunc('word(number, text[, separator])')
654 652 def word(context, mapping, args):
655 653 """Return the nth word from a string."""
656 654 if not (2 <= len(args) <= 3):
657 655 # i18n: "word" is a keyword
658 656 raise error.ParseError(_("word expects two or three arguments, got %d")
659 657 % len(args))
660 658
661 659 num = evalinteger(context, mapping, args[0],
662 660 # i18n: "word" is a keyword
663 661 _("word expects an integer index"))
664 662 text = evalstring(context, mapping, args[1])
665 663 if len(args) == 3:
666 664 splitter = evalstring(context, mapping, args[2])
667 665 else:
668 666 splitter = None
669 667
670 668 tokens = text.split(splitter)
671 669 if num >= len(tokens) or num < -len(tokens):
672 670 return ''
673 671 else:
674 672 return tokens[num]
675 673
676 674 def loadfunction(ui, extname, registrarobj):
677 675 """Load template function from specified registrarobj
678 676 """
679 677 for name, func in registrarobj._table.iteritems():
680 678 funcs[name] = func
681 679
682 680 # tell hggettext to extract docstrings from these functions:
683 681 i18nfunctions = funcs.values()
@@ -1,492 +1,496 b''
1 1 # templateutil.py - utility for template evaluation
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 types
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 pycompat,
16 16 util,
17 17 )
18 18 from .utils import (
19 19 dateutil,
20 20 stringutil,
21 21 )
22 22
23 23 class ResourceUnavailable(error.Abort):
24 24 pass
25 25
26 26 class TemplateNotFound(error.Abort):
27 27 pass
28 28
29 29 class hybrid(object):
30 30 """Wrapper for list or dict to support legacy template
31 31
32 32 This class allows us to handle both:
33 33 - "{files}" (legacy command-line-specific list hack) and
34 34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 35 and to access raw values:
36 36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 37 - "{get(extras, key)}"
38 38 - "{files|json}"
39 39 """
40 40
41 41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 42 if gen is not None:
43 43 self.gen = gen # generator or function returning generator
44 44 self._values = values
45 45 self._makemap = makemap
46 46 self.joinfmt = joinfmt
47 47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 48 def gen(self):
49 49 """Default generator to stringify this as {join(self, ' ')}"""
50 50 for i, x in enumerate(self._values):
51 51 if i > 0:
52 52 yield ' '
53 53 yield self.joinfmt(x)
54 54 def itermaps(self):
55 55 makemap = self._makemap
56 56 for x in self._values:
57 57 yield makemap(x)
58 58 def __contains__(self, x):
59 59 return x in self._values
60 60 def __getitem__(self, key):
61 61 return self._values[key]
62 62 def __len__(self):
63 63 return len(self._values)
64 64 def __iter__(self):
65 65 return iter(self._values)
66 66 def __getattr__(self, name):
67 67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 68 r'itervalues', r'keys', r'values'):
69 69 raise AttributeError(name)
70 70 return getattr(self._values, name)
71 71
72 72 class mappable(object):
73 73 """Wrapper for non-list/dict object to support map operation
74 74
75 75 This class allows us to handle both:
76 76 - "{manifest}"
77 77 - "{manifest % '{rev}:{node}'}"
78 78 - "{manifest.rev}"
79 79
80 80 Unlike a hybrid, this does not simulate the behavior of the underling
81 81 value. Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain
82 82 the inner object.
83 83 """
84 84
85 85 def __init__(self, gen, key, value, makemap):
86 86 if gen is not None:
87 87 self.gen = gen # generator or function returning generator
88 88 self._key = key
89 89 self._value = value # may be generator of strings
90 90 self._makemap = makemap
91 91
92 92 def gen(self):
93 93 yield pycompat.bytestr(self._value)
94 94
95 95 def tomap(self):
96 96 return self._makemap(self._key)
97 97
98 98 def itermaps(self):
99 99 yield self.tomap()
100 100
101 101 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
102 102 """Wrap data to support both dict-like and string-like operations"""
103 103 prefmt = pycompat.identity
104 104 if fmt is None:
105 105 fmt = '%s=%s'
106 106 prefmt = pycompat.bytestr
107 107 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
108 108 lambda k: fmt % (prefmt(k), prefmt(data[k])))
109 109
110 110 def hybridlist(data, name, fmt=None, gen=None):
111 111 """Wrap data to support both list-like and string-like operations"""
112 112 prefmt = pycompat.identity
113 113 if fmt is None:
114 114 fmt = '%s'
115 115 prefmt = pycompat.bytestr
116 116 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
117 117
118 118 def unwraphybrid(thing):
119 119 """Return an object which can be stringified possibly by using a legacy
120 120 template"""
121 121 gen = getattr(thing, 'gen', None)
122 122 if gen is None:
123 123 return thing
124 124 if callable(gen):
125 125 return gen()
126 126 return gen
127 127
128 128 def unwrapvalue(thing):
129 129 """Move the inner value object out of the wrapper"""
130 130 if not util.safehasattr(thing, '_value'):
131 131 return thing
132 132 return thing._value
133 133
134 134 def wraphybridvalue(container, key, value):
135 135 """Wrap an element of hybrid container to be mappable
136 136
137 137 The key is passed to the makemap function of the given container, which
138 138 should be an item generated by iter(container).
139 139 """
140 140 makemap = getattr(container, '_makemap', None)
141 141 if makemap is None:
142 142 return value
143 143 if util.safehasattr(value, '_makemap'):
144 144 # a nested hybrid list/dict, which has its own way of map operation
145 145 return value
146 146 return mappable(None, key, value, makemap)
147 147
148 148 def compatdict(context, mapping, name, data, key='key', value='value',
149 149 fmt=None, plural=None, separator=' '):
150 150 """Wrap data like hybriddict(), but also supports old-style list template
151 151
152 152 This exists for backward compatibility with the old-style template. Use
153 153 hybriddict() for new template keywords.
154 154 """
155 155 c = [{key: k, value: v} for k, v in data.iteritems()]
156 156 f = _showcompatlist(context, mapping, name, c, plural, separator)
157 157 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
158 158
159 159 def compatlist(context, mapping, name, data, element=None, fmt=None,
160 160 plural=None, separator=' '):
161 161 """Wrap data like hybridlist(), but also supports old-style list template
162 162
163 163 This exists for backward compatibility with the old-style template. Use
164 164 hybridlist() for new template keywords.
165 165 """
166 166 f = _showcompatlist(context, mapping, name, data, plural, separator)
167 167 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
168 168
169 169 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
170 170 """Return a generator that renders old-style list template
171 171
172 172 name is name of key in template map.
173 173 values is list of strings or dicts.
174 174 plural is plural of name, if not simply name + 's'.
175 175 separator is used to join values as a string
176 176
177 177 expansion works like this, given name 'foo'.
178 178
179 179 if values is empty, expand 'no_foos'.
180 180
181 181 if 'foo' not in template map, return values as a string,
182 182 joined by 'separator'.
183 183
184 184 expand 'start_foos'.
185 185
186 186 for each value, expand 'foo'. if 'last_foo' in template
187 187 map, expand it instead of 'foo' for last key.
188 188
189 189 expand 'end_foos'.
190 190 """
191 191 if not plural:
192 192 plural = name + 's'
193 193 if not values:
194 194 noname = 'no_' + plural
195 195 if context.preload(noname):
196 196 yield context.process(noname, mapping)
197 197 return
198 198 if not context.preload(name):
199 199 if isinstance(values[0], bytes):
200 200 yield separator.join(values)
201 201 else:
202 202 for v in values:
203 203 r = dict(v)
204 204 r.update(mapping)
205 205 yield r
206 206 return
207 207 startname = 'start_' + plural
208 208 if context.preload(startname):
209 209 yield context.process(startname, mapping)
210 210 def one(v, tag=name):
211 211 vmapping = {}
212 212 try:
213 213 vmapping.update(v)
214 214 # Python 2 raises ValueError if the type of v is wrong. Python
215 215 # 3 raises TypeError.
216 216 except (AttributeError, TypeError, ValueError):
217 217 try:
218 218 # Python 2 raises ValueError trying to destructure an e.g.
219 219 # bytes. Python 3 raises TypeError.
220 220 for a, b in v:
221 221 vmapping[a] = b
222 222 except (TypeError, ValueError):
223 223 vmapping[name] = v
224 224 vmapping = context.overlaymap(mapping, vmapping)
225 225 return context.process(tag, vmapping)
226 226 lastname = 'last_' + name
227 227 if context.preload(lastname):
228 228 last = values.pop()
229 229 else:
230 230 last = None
231 231 for v in values:
232 232 yield one(v)
233 233 if last is not None:
234 234 yield one(last, tag=lastname)
235 235 endname = 'end_' + plural
236 236 if context.preload(endname):
237 237 yield context.process(endname, mapping)
238 238
239 239 def flatten(thing):
240 240 """Yield a single stream from a possibly nested set of iterators"""
241 241 thing = unwraphybrid(thing)
242 242 if isinstance(thing, bytes):
243 243 yield thing
244 244 elif isinstance(thing, str):
245 245 # We can only hit this on Python 3, and it's here to guard
246 246 # against infinite recursion.
247 247 raise error.ProgrammingError('Mercurial IO including templates is done'
248 248 ' with bytes, not strings, got %r' % thing)
249 249 elif thing is None:
250 250 pass
251 251 elif not util.safehasattr(thing, '__iter__'):
252 252 yield pycompat.bytestr(thing)
253 253 else:
254 254 for i in thing:
255 255 i = unwraphybrid(i)
256 256 if isinstance(i, bytes):
257 257 yield i
258 258 elif i is None:
259 259 pass
260 260 elif not util.safehasattr(i, '__iter__'):
261 261 yield pycompat.bytestr(i)
262 262 else:
263 263 for j in flatten(i):
264 264 yield j
265 265
266 266 def stringify(thing):
267 267 """Turn values into bytes by converting into text and concatenating them"""
268 268 if isinstance(thing, bytes):
269 269 return thing # retain localstr to be round-tripped
270 270 return b''.join(flatten(thing))
271 271
272 272 def findsymbolicname(arg):
273 273 """Find symbolic name for the given compiled expression; returns None
274 274 if nothing found reliably"""
275 275 while True:
276 276 func, data = arg
277 277 if func is runsymbol:
278 278 return data
279 279 elif func is runfilter:
280 280 arg = data[0]
281 281 else:
282 282 return None
283 283
284 284 def evalrawexp(context, mapping, arg):
285 285 """Evaluate given argument as a bare template object which may require
286 286 further processing (such as folding generator of strings)"""
287 287 func, data = arg
288 288 return func(context, mapping, data)
289 289
290 290 def evalfuncarg(context, mapping, arg):
291 291 """Evaluate given argument as value type"""
292 292 return _unwrapvalue(evalrawexp(context, mapping, arg))
293 293
294 294 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
295 295 # is fixed. we can't do that right now because join() has to take a generator
296 296 # of byte strings as it is, not a lazy byte string.
297 297 def _unwrapvalue(thing):
298 298 thing = unwrapvalue(thing)
299 299 # evalrawexp() may return string, generator of strings or arbitrary object
300 300 # such as date tuple, but filter does not want generator.
301 301 if isinstance(thing, types.GeneratorType):
302 302 thing = stringify(thing)
303 303 return thing
304 304
305 305 def evalboolean(context, mapping, arg):
306 306 """Evaluate given argument as boolean, but also takes boolean literals"""
307 307 func, data = arg
308 308 if func is runsymbol:
309 309 thing = func(context, mapping, data, default=None)
310 310 if thing is None:
311 311 # not a template keyword, takes as a boolean literal
312 312 thing = stringutil.parsebool(data)
313 313 else:
314 314 thing = func(context, mapping, data)
315 315 thing = unwrapvalue(thing)
316 316 if isinstance(thing, bool):
317 317 return thing
318 318 # other objects are evaluated as strings, which means 0 is True, but
319 319 # empty dict/list should be False as they are expected to be ''
320 320 return bool(stringify(thing))
321 321
322 322 def evaldate(context, mapping, arg, err=None):
323 323 """Evaluate given argument as a date tuple or a date string; returns
324 324 a (unixtime, offset) tuple"""
325 325 return unwrapdate(evalrawexp(context, mapping, arg), err)
326 326
327 327 def unwrapdate(thing, err=None):
328 328 thing = _unwrapvalue(thing)
329 329 try:
330 330 return dateutil.parsedate(thing)
331 331 except AttributeError:
332 332 raise error.ParseError(err or _('not a date tuple nor a string'))
333 except error.ParseError:
334 if not err:
335 raise
336 raise error.ParseError(err)
333 337
334 338 def evalinteger(context, mapping, arg, err=None):
335 339 return unwrapinteger(evalrawexp(context, mapping, arg), err)
336 340
337 341 def unwrapinteger(thing, err=None):
338 342 thing = _unwrapvalue(thing)
339 343 try:
340 344 return int(thing)
341 345 except (TypeError, ValueError):
342 346 raise error.ParseError(err or _('not an integer'))
343 347
344 348 def evalstring(context, mapping, arg):
345 349 return stringify(evalrawexp(context, mapping, arg))
346 350
347 351 def evalstringliteral(context, mapping, arg):
348 352 """Evaluate given argument as string template, but returns symbol name
349 353 if it is unknown"""
350 354 func, data = arg
351 355 if func is runsymbol:
352 356 thing = func(context, mapping, data, default=data)
353 357 else:
354 358 thing = func(context, mapping, data)
355 359 return stringify(thing)
356 360
357 361 _unwrapfuncbytype = {
358 362 None: _unwrapvalue,
359 363 bytes: stringify,
360 364 int: unwrapinteger,
361 365 }
362 366
363 367 def unwrapastype(thing, typ):
364 368 """Move the inner value object out of the wrapper and coerce its type"""
365 369 try:
366 370 f = _unwrapfuncbytype[typ]
367 371 except KeyError:
368 372 raise error.ProgrammingError('invalid type specified: %r' % typ)
369 373 return f(thing)
370 374
371 375 def runinteger(context, mapping, data):
372 376 return int(data)
373 377
374 378 def runstring(context, mapping, data):
375 379 return data
376 380
377 381 def _recursivesymbolblocker(key):
378 382 def showrecursion(**args):
379 383 raise error.Abort(_("recursive reference '%s' in template") % key)
380 384 return showrecursion
381 385
382 386 def runsymbol(context, mapping, key, default=''):
383 387 v = context.symbol(mapping, key)
384 388 if v is None:
385 389 # put poison to cut recursion. we can't move this to parsing phase
386 390 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
387 391 safemapping = mapping.copy()
388 392 safemapping[key] = _recursivesymbolblocker(key)
389 393 try:
390 394 v = context.process(key, safemapping)
391 395 except TemplateNotFound:
392 396 v = default
393 397 if callable(v) and getattr(v, '_requires', None) is None:
394 398 # old templatekw: expand all keywords and resources
395 399 # (TODO: deprecate this after porting web template keywords to new API)
396 400 props = {k: context._resources.lookup(context, mapping, k)
397 401 for k in context._resources.knownkeys()}
398 402 # pass context to _showcompatlist() through templatekw._showlist()
399 403 props['templ'] = context
400 404 props.update(mapping)
401 405 return v(**pycompat.strkwargs(props))
402 406 if callable(v):
403 407 # new templatekw
404 408 try:
405 409 return v(context, mapping)
406 410 except ResourceUnavailable:
407 411 # unsupported keyword is mapped to empty just like unknown keyword
408 412 return None
409 413 return v
410 414
411 415 def runtemplate(context, mapping, template):
412 416 for arg in template:
413 417 yield evalrawexp(context, mapping, arg)
414 418
415 419 def runfilter(context, mapping, data):
416 420 arg, filt = data
417 421 thing = evalrawexp(context, mapping, arg)
418 422 try:
419 423 thing = unwrapastype(thing, getattr(filt, '_intype', None))
420 424 return filt(thing)
421 425 except (ValueError, AttributeError, TypeError):
422 426 sym = findsymbolicname(arg)
423 427 if sym:
424 428 msg = (_("template filter '%s' is not compatible with keyword '%s'")
425 429 % (pycompat.sysbytes(filt.__name__), sym))
426 430 else:
427 431 msg = (_("incompatible use of template filter '%s'")
428 432 % pycompat.sysbytes(filt.__name__))
429 433 raise error.Abort(msg)
430 434
431 435 def runmap(context, mapping, data):
432 436 darg, targ = data
433 437 d = evalrawexp(context, mapping, darg)
434 438 if util.safehasattr(d, 'itermaps'):
435 439 diter = d.itermaps()
436 440 else:
437 441 try:
438 442 diter = iter(d)
439 443 except TypeError:
440 444 sym = findsymbolicname(darg)
441 445 if sym:
442 446 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
443 447 else:
444 448 raise error.ParseError(_("%r is not iterable") % d)
445 449
446 450 for i, v in enumerate(diter):
447 451 if isinstance(v, dict):
448 452 lm = context.overlaymap(mapping, v)
449 453 lm['index'] = i
450 454 yield evalrawexp(context, lm, targ)
451 455 else:
452 456 # v is not an iterable of dicts, this happen when 'key'
453 457 # has been fully expanded already and format is useless.
454 458 # If so, return the expanded value.
455 459 yield v
456 460
457 461 def runmember(context, mapping, data):
458 462 darg, memb = data
459 463 d = evalrawexp(context, mapping, darg)
460 464 if util.safehasattr(d, 'tomap'):
461 465 lm = context.overlaymap(mapping, d.tomap())
462 466 return runsymbol(context, lm, memb)
463 467 if util.safehasattr(d, 'get'):
464 468 return getdictitem(d, memb)
465 469
466 470 sym = findsymbolicname(darg)
467 471 if sym:
468 472 raise error.ParseError(_("keyword '%s' has no member") % sym)
469 473 else:
470 474 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
471 475
472 476 def runnegate(context, mapping, data):
473 477 data = evalinteger(context, mapping, data,
474 478 _('negation needs an integer argument'))
475 479 return -data
476 480
477 481 def runarithmetic(context, mapping, data):
478 482 func, left, right = data
479 483 left = evalinteger(context, mapping, left,
480 484 _('arithmetic only defined on integers'))
481 485 right = evalinteger(context, mapping, right,
482 486 _('arithmetic only defined on integers'))
483 487 try:
484 488 return func(left, right)
485 489 except ZeroDivisionError:
486 490 raise error.Abort(_('division by zero is not defined'))
487 491
488 492 def getdictitem(dictarg, key):
489 493 val = dictarg.get(key)
490 494 if val is None:
491 495 return
492 496 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now