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