comparison mercurial/templatefuncs.py @ 36928:521f6c7e1756

templater: split template functions to new module It has grown enough to be a dedicated module.
author Yuya Nishihara <yuya@tcha.org>
date Thu, 08 Mar 2018 22:23:02 +0900
parents mercurial/templater.py@32f9b7e3f056
children a318bb154d42
comparison
equal deleted inserted replaced
36927:32f9b7e3f056 36928:521f6c7e1756
1 # templatefuncs.py - common template functions
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from __future__ import absolute_import
9
10 import re
11
12 from .i18n import _
13 from . import (
14 color,
15 encoding,
16 error,
17 minirst,
18 obsutil,
19 pycompat,
20 registrar,
21 revset as revsetmod,
22 revsetlang,
23 scmutil,
24 templatefilters,
25 templatekw,
26 templateutil,
27 util,
28 )
29 from .utils import dateutil
30
31 evalrawexp = templateutil.evalrawexp
32 evalfuncarg = templateutil.evalfuncarg
33 evalboolean = templateutil.evalboolean
34 evalinteger = templateutil.evalinteger
35 evalstring = templateutil.evalstring
36 evalstringliteral = templateutil.evalstringliteral
37 evalastype = templateutil.evalastype
38
39 # dict of template built-in functions
40 funcs = {}
41 templatefunc = registrar.templatefunc(funcs)
42
43 @templatefunc('date(date[, fmt])')
44 def date(context, mapping, args):
45 """Format a date. See :hg:`help dates` for formatting
46 strings. The default is a Unix date format, including the timezone:
47 "Mon Sep 04 15:13:13 2006 0700"."""
48 if not (1 <= len(args) <= 2):
49 # i18n: "date" is a keyword
50 raise error.ParseError(_("date expects one or two arguments"))
51
52 date = evalfuncarg(context, mapping, args[0])
53 fmt = None
54 if len(args) == 2:
55 fmt = evalstring(context, mapping, args[1])
56 try:
57 if fmt is None:
58 return dateutil.datestr(date)
59 else:
60 return dateutil.datestr(date, fmt)
61 except (TypeError, ValueError):
62 # i18n: "date" is a keyword
63 raise error.ParseError(_("date expects a date information"))
64
65 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
66 def dict_(context, mapping, args):
67 """Construct a dict from key-value pairs. A key may be omitted if
68 a value expression can provide an unambiguous name."""
69 data = util.sortdict()
70
71 for v in args['args']:
72 k = templateutil.findsymbolicname(v)
73 if not k:
74 raise error.ParseError(_('dict key cannot be inferred'))
75 if k in data or k in args['kwargs']:
76 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
77 data[k] = evalfuncarg(context, mapping, v)
78
79 data.update((k, evalfuncarg(context, mapping, v))
80 for k, v in args['kwargs'].iteritems())
81 return templateutil.hybriddict(data)
82
83 @templatefunc('diff([includepattern [, excludepattern]])')
84 def diff(context, mapping, args):
85 """Show a diff, optionally
86 specifying files to include or exclude."""
87 if len(args) > 2:
88 # i18n: "diff" is a keyword
89 raise error.ParseError(_("diff expects zero, one, or two arguments"))
90
91 def getpatterns(i):
92 if i < len(args):
93 s = evalstring(context, mapping, args[i]).strip()
94 if s:
95 return [s]
96 return []
97
98 ctx = context.resource(mapping, 'ctx')
99 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
100
101 return ''.join(chunks)
102
103 @templatefunc('extdata(source)', argspec='source')
104 def extdata(context, mapping, args):
105 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
106 if 'source' not in args:
107 # i18n: "extdata" is a keyword
108 raise error.ParseError(_('extdata expects one argument'))
109
110 source = evalstring(context, mapping, args['source'])
111 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
112 ctx = context.resource(mapping, 'ctx')
113 if source in cache:
114 data = cache[source]
115 else:
116 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
117 return data.get(ctx.rev(), '')
118
119 @templatefunc('files(pattern)')
120 def files(context, mapping, args):
121 """All files of the current changeset matching the pattern. See
122 :hg:`help patterns`."""
123 if not len(args) == 1:
124 # i18n: "files" is a keyword
125 raise error.ParseError(_("files expects one argument"))
126
127 raw = evalstring(context, mapping, args[0])
128 ctx = context.resource(mapping, 'ctx')
129 m = ctx.match([raw])
130 files = list(ctx.matches(m))
131 return templateutil.compatlist(context, mapping, "file", files)
132
133 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
134 def fill(context, mapping, args):
135 """Fill many
136 paragraphs with optional indentation. See the "fill" filter."""
137 if not (1 <= len(args) <= 4):
138 # i18n: "fill" is a keyword
139 raise error.ParseError(_("fill expects one to four arguments"))
140
141 text = evalstring(context, mapping, args[0])
142 width = 76
143 initindent = ''
144 hangindent = ''
145 if 2 <= len(args) <= 4:
146 width = evalinteger(context, mapping, args[1],
147 # i18n: "fill" is a keyword
148 _("fill expects an integer width"))
149 try:
150 initindent = evalstring(context, mapping, args[2])
151 hangindent = evalstring(context, mapping, args[3])
152 except IndexError:
153 pass
154
155 return templatefilters.fill(text, width, initindent, hangindent)
156
157 @templatefunc('formatnode(node)')
158 def formatnode(context, mapping, args):
159 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
160 if len(args) != 1:
161 # i18n: "formatnode" is a keyword
162 raise error.ParseError(_("formatnode expects one argument"))
163
164 ui = context.resource(mapping, 'ui')
165 node = evalstring(context, mapping, args[0])
166 if ui.debugflag:
167 return node
168 return templatefilters.short(node)
169
170 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
171 argspec='text width fillchar left')
172 def pad(context, mapping, args):
173 """Pad text with a
174 fill character."""
175 if 'text' not in args or 'width' not in args:
176 # i18n: "pad" is a keyword
177 raise error.ParseError(_("pad() expects two to four arguments"))
178
179 width = evalinteger(context, mapping, args['width'],
180 # i18n: "pad" is a keyword
181 _("pad() expects an integer width"))
182
183 text = evalstring(context, mapping, args['text'])
184
185 left = False
186 fillchar = ' '
187 if 'fillchar' in args:
188 fillchar = evalstring(context, mapping, args['fillchar'])
189 if len(color.stripeffects(fillchar)) != 1:
190 # i18n: "pad" is a keyword
191 raise error.ParseError(_("pad() expects a single fill character"))
192 if 'left' in args:
193 left = evalboolean(context, mapping, args['left'])
194
195 fillwidth = width - encoding.colwidth(color.stripeffects(text))
196 if fillwidth <= 0:
197 return text
198 if left:
199 return fillchar * fillwidth + text
200 else:
201 return text + fillchar * fillwidth
202
203 @templatefunc('indent(text, indentchars[, firstline])')
204 def indent(context, mapping, args):
205 """Indents all non-empty lines
206 with the characters given in the indentchars string. An optional
207 third parameter will override the indent for the first line only
208 if present."""
209 if not (2 <= len(args) <= 3):
210 # i18n: "indent" is a keyword
211 raise error.ParseError(_("indent() expects two or three arguments"))
212
213 text = evalstring(context, mapping, args[0])
214 indent = evalstring(context, mapping, args[1])
215
216 if len(args) == 3:
217 firstline = evalstring(context, mapping, args[2])
218 else:
219 firstline = indent
220
221 # the indent function doesn't indent the first line, so we do it here
222 return templatefilters.indent(firstline + text, indent)
223
224 @templatefunc('get(dict, key)')
225 def get(context, mapping, args):
226 """Get an attribute/key from an object. Some keywords
227 are complex types. This function allows you to obtain the value of an
228 attribute on these types."""
229 if len(args) != 2:
230 # i18n: "get" is a keyword
231 raise error.ParseError(_("get() expects two arguments"))
232
233 dictarg = evalfuncarg(context, mapping, args[0])
234 if not util.safehasattr(dictarg, 'get'):
235 # i18n: "get" is a keyword
236 raise error.ParseError(_("get() expects a dict as first argument"))
237
238 key = evalfuncarg(context, mapping, args[1])
239 return templateutil.getdictitem(dictarg, key)
240
241 @templatefunc('if(expr, then[, else])')
242 def if_(context, mapping, args):
243 """Conditionally execute based on the result of
244 an expression."""
245 if not (2 <= len(args) <= 3):
246 # i18n: "if" is a keyword
247 raise error.ParseError(_("if expects two or three arguments"))
248
249 test = evalboolean(context, mapping, args[0])
250 if test:
251 yield evalrawexp(context, mapping, args[1])
252 elif len(args) == 3:
253 yield evalrawexp(context, mapping, args[2])
254
255 @templatefunc('ifcontains(needle, haystack, then[, else])')
256 def ifcontains(context, mapping, args):
257 """Conditionally execute based
258 on whether the item "needle" is in "haystack"."""
259 if not (3 <= len(args) <= 4):
260 # i18n: "ifcontains" is a keyword
261 raise error.ParseError(_("ifcontains expects three or four arguments"))
262
263 haystack = evalfuncarg(context, mapping, args[1])
264 try:
265 needle = evalastype(context, mapping, args[0],
266 getattr(haystack, 'keytype', None) or bytes)
267 found = (needle in haystack)
268 except error.ParseError:
269 found = False
270
271 if found:
272 yield evalrawexp(context, mapping, args[2])
273 elif len(args) == 4:
274 yield evalrawexp(context, mapping, args[3])
275
276 @templatefunc('ifeq(expr1, expr2, then[, else])')
277 def ifeq(context, mapping, args):
278 """Conditionally execute based on
279 whether 2 items are equivalent."""
280 if not (3 <= len(args) <= 4):
281 # i18n: "ifeq" is a keyword
282 raise error.ParseError(_("ifeq expects three or four arguments"))
283
284 test = evalstring(context, mapping, args[0])
285 match = evalstring(context, mapping, args[1])
286 if test == match:
287 yield evalrawexp(context, mapping, args[2])
288 elif len(args) == 4:
289 yield evalrawexp(context, mapping, args[3])
290
291 @templatefunc('join(list, sep)')
292 def join(context, mapping, args):
293 """Join items in a list with a delimiter."""
294 if not (1 <= len(args) <= 2):
295 # i18n: "join" is a keyword
296 raise error.ParseError(_("join expects one or two arguments"))
297
298 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
299 # abuses generator as a keyword that returns a list of dicts.
300 joinset = evalrawexp(context, mapping, args[0])
301 joinset = templateutil.unwrapvalue(joinset)
302 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
303 joiner = " "
304 if len(args) > 1:
305 joiner = evalstring(context, mapping, args[1])
306
307 first = True
308 for x in pycompat.maybebytestr(joinset):
309 if first:
310 first = False
311 else:
312 yield joiner
313 yield joinfmt(x)
314
315 @templatefunc('label(label, expr)')
316 def label(context, mapping, args):
317 """Apply a label to generated content. Content with
318 a label applied can result in additional post-processing, such as
319 automatic colorization."""
320 if len(args) != 2:
321 # i18n: "label" is a keyword
322 raise error.ParseError(_("label expects two arguments"))
323
324 ui = context.resource(mapping, 'ui')
325 thing = evalstring(context, mapping, args[1])
326 # preserve unknown symbol as literal so effects like 'red', 'bold',
327 # etc. don't need to be quoted
328 label = evalstringliteral(context, mapping, args[0])
329
330 return ui.label(thing, label)
331
332 @templatefunc('latesttag([pattern])')
333 def latesttag(context, mapping, args):
334 """The global tags matching the given pattern on the
335 most recent globally tagged ancestor of this changeset.
336 If no such tags exist, the "{tag}" template resolves to
337 the string "null"."""
338 if len(args) > 1:
339 # i18n: "latesttag" is a keyword
340 raise error.ParseError(_("latesttag expects at most one argument"))
341
342 pattern = None
343 if len(args) == 1:
344 pattern = evalstring(context, mapping, args[0])
345 return templatekw.showlatesttags(context, mapping, pattern)
346
347 @templatefunc('localdate(date[, tz])')
348 def localdate(context, mapping, args):
349 """Converts a date to the specified timezone.
350 The default is local date."""
351 if not (1 <= len(args) <= 2):
352 # i18n: "localdate" is a keyword
353 raise error.ParseError(_("localdate expects one or two arguments"))
354
355 date = evalfuncarg(context, mapping, args[0])
356 try:
357 date = dateutil.parsedate(date)
358 except AttributeError: # not str nor date tuple
359 # i18n: "localdate" is a keyword
360 raise error.ParseError(_("localdate expects a date information"))
361 if len(args) >= 2:
362 tzoffset = None
363 tz = evalfuncarg(context, mapping, args[1])
364 if isinstance(tz, bytes):
365 tzoffset, remainder = dateutil.parsetimezone(tz)
366 if remainder:
367 tzoffset = None
368 if tzoffset is None:
369 try:
370 tzoffset = int(tz)
371 except (TypeError, ValueError):
372 # i18n: "localdate" is a keyword
373 raise error.ParseError(_("localdate expects a timezone"))
374 else:
375 tzoffset = dateutil.makedate()[1]
376 return (date[0], tzoffset)
377
378 @templatefunc('max(iterable)')
379 def max_(context, mapping, args, **kwargs):
380 """Return the max of an iterable"""
381 if len(args) != 1:
382 # i18n: "max" is a keyword
383 raise error.ParseError(_("max expects one argument"))
384
385 iterable = evalfuncarg(context, mapping, args[0])
386 try:
387 x = max(pycompat.maybebytestr(iterable))
388 except (TypeError, ValueError):
389 # i18n: "max" is a keyword
390 raise error.ParseError(_("max first argument should be an iterable"))
391 return templateutil.wraphybridvalue(iterable, x, x)
392
393 @templatefunc('min(iterable)')
394 def min_(context, mapping, args, **kwargs):
395 """Return the min of an iterable"""
396 if len(args) != 1:
397 # i18n: "min" is a keyword
398 raise error.ParseError(_("min expects one argument"))
399
400 iterable = evalfuncarg(context, mapping, args[0])
401 try:
402 x = min(pycompat.maybebytestr(iterable))
403 except (TypeError, ValueError):
404 # i18n: "min" is a keyword
405 raise error.ParseError(_("min first argument should be an iterable"))
406 return templateutil.wraphybridvalue(iterable, x, x)
407
408 @templatefunc('mod(a, b)')
409 def mod(context, mapping, args):
410 """Calculate a mod b such that a / b + a mod b == a"""
411 if not len(args) == 2:
412 # i18n: "mod" is a keyword
413 raise error.ParseError(_("mod expects two arguments"))
414
415 func = lambda a, b: a % b
416 return templateutil.runarithmetic(context, mapping,
417 (func, args[0], args[1]))
418
419 @templatefunc('obsfateoperations(markers)')
420 def obsfateoperations(context, mapping, args):
421 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
422 if len(args) != 1:
423 # i18n: "obsfateoperations" is a keyword
424 raise error.ParseError(_("obsfateoperations expects one argument"))
425
426 markers = evalfuncarg(context, mapping, args[0])
427
428 try:
429 data = obsutil.markersoperations(markers)
430 return templateutil.hybridlist(data, name='operation')
431 except (TypeError, KeyError):
432 # i18n: "obsfateoperations" is a keyword
433 errmsg = _("obsfateoperations first argument should be an iterable")
434 raise error.ParseError(errmsg)
435
436 @templatefunc('obsfatedate(markers)')
437 def obsfatedate(context, mapping, args):
438 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
439 if len(args) != 1:
440 # i18n: "obsfatedate" is a keyword
441 raise error.ParseError(_("obsfatedate expects one argument"))
442
443 markers = evalfuncarg(context, mapping, args[0])
444
445 try:
446 data = obsutil.markersdates(markers)
447 return templateutil.hybridlist(data, name='date', fmt='%d %d')
448 except (TypeError, KeyError):
449 # i18n: "obsfatedate" is a keyword
450 errmsg = _("obsfatedate first argument should be an iterable")
451 raise error.ParseError(errmsg)
452
453 @templatefunc('obsfateusers(markers)')
454 def obsfateusers(context, mapping, args):
455 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
456 if len(args) != 1:
457 # i18n: "obsfateusers" is a keyword
458 raise error.ParseError(_("obsfateusers expects one argument"))
459
460 markers = evalfuncarg(context, mapping, args[0])
461
462 try:
463 data = obsutil.markersusers(markers)
464 return templateutil.hybridlist(data, name='user')
465 except (TypeError, KeyError, ValueError):
466 # i18n: "obsfateusers" is a keyword
467 msg = _("obsfateusers first argument should be an iterable of "
468 "obsmakers")
469 raise error.ParseError(msg)
470
471 @templatefunc('obsfateverb(successors, markers)')
472 def obsfateverb(context, mapping, args):
473 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
474 if len(args) != 2:
475 # i18n: "obsfateverb" is a keyword
476 raise error.ParseError(_("obsfateverb expects two arguments"))
477
478 successors = evalfuncarg(context, mapping, args[0])
479 markers = evalfuncarg(context, mapping, args[1])
480
481 try:
482 return obsutil.obsfateverb(successors, markers)
483 except TypeError:
484 # i18n: "obsfateverb" is a keyword
485 errmsg = _("obsfateverb first argument should be countable")
486 raise error.ParseError(errmsg)
487
488 @templatefunc('relpath(path)')
489 def relpath(context, mapping, args):
490 """Convert a repository-absolute path into a filesystem path relative to
491 the current working directory."""
492 if len(args) != 1:
493 # i18n: "relpath" is a keyword
494 raise error.ParseError(_("relpath expects one argument"))
495
496 repo = context.resource(mapping, 'ctx').repo()
497 path = evalstring(context, mapping, args[0])
498 return repo.pathto(path)
499
500 @templatefunc('revset(query[, formatargs...])')
501 def revset(context, mapping, args):
502 """Execute a revision set query. See
503 :hg:`help revset`."""
504 if not len(args) > 0:
505 # i18n: "revset" is a keyword
506 raise error.ParseError(_("revset expects one or more arguments"))
507
508 raw = evalstring(context, mapping, args[0])
509 ctx = context.resource(mapping, 'ctx')
510 repo = ctx.repo()
511
512 def query(expr):
513 m = revsetmod.match(repo.ui, expr, repo=repo)
514 return m(repo)
515
516 if len(args) > 1:
517 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
518 revs = query(revsetlang.formatspec(raw, *formatargs))
519 revs = list(revs)
520 else:
521 cache = context.resource(mapping, 'cache')
522 revsetcache = cache.setdefault("revsetcache", {})
523 if raw in revsetcache:
524 revs = revsetcache[raw]
525 else:
526 revs = query(raw)
527 revs = list(revs)
528 revsetcache[raw] = revs
529 return templatekw.showrevslist(context, mapping, "revision", revs)
530
531 @templatefunc('rstdoc(text, style)')
532 def rstdoc(context, mapping, args):
533 """Format reStructuredText."""
534 if len(args) != 2:
535 # i18n: "rstdoc" is a keyword
536 raise error.ParseError(_("rstdoc expects two arguments"))
537
538 text = evalstring(context, mapping, args[0])
539 style = evalstring(context, mapping, args[1])
540
541 return minirst.format(text, style=style, keep=['verbose'])
542
543 @templatefunc('separate(sep, args)', argspec='sep *args')
544 def separate(context, mapping, args):
545 """Add a separator between non-empty arguments."""
546 if 'sep' not in args:
547 # i18n: "separate" is a keyword
548 raise error.ParseError(_("separate expects at least one argument"))
549
550 sep = evalstring(context, mapping, args['sep'])
551 first = True
552 for arg in args['args']:
553 argstr = evalstring(context, mapping, arg)
554 if not argstr:
555 continue
556 if first:
557 first = False
558 else:
559 yield sep
560 yield argstr
561
562 @templatefunc('shortest(node, minlength=4)')
563 def shortest(context, mapping, args):
564 """Obtain the shortest representation of
565 a node."""
566 if not (1 <= len(args) <= 2):
567 # i18n: "shortest" is a keyword
568 raise error.ParseError(_("shortest() expects one or two arguments"))
569
570 node = evalstring(context, mapping, args[0])
571
572 minlength = 4
573 if len(args) > 1:
574 minlength = evalinteger(context, mapping, args[1],
575 # i18n: "shortest" is a keyword
576 _("shortest() expects an integer minlength"))
577
578 # _partialmatch() of filtered changelog could take O(len(repo)) time,
579 # which would be unacceptably slow. so we look for hash collision in
580 # unfiltered space, which means some hashes may be slightly longer.
581 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
582 return cl.shortest(node, minlength)
583
584 @templatefunc('strip(text[, chars])')
585 def strip(context, mapping, args):
586 """Strip characters from a string. By default,
587 strips all leading and trailing whitespace."""
588 if not (1 <= len(args) <= 2):
589 # i18n: "strip" is a keyword
590 raise error.ParseError(_("strip expects one or two arguments"))
591
592 text = evalstring(context, mapping, args[0])
593 if len(args) == 2:
594 chars = evalstring(context, mapping, args[1])
595 return text.strip(chars)
596 return text.strip()
597
598 @templatefunc('sub(pattern, replacement, expression)')
599 def sub(context, mapping, args):
600 """Perform text substitution
601 using regular expressions."""
602 if len(args) != 3:
603 # i18n: "sub" is a keyword
604 raise error.ParseError(_("sub expects three arguments"))
605
606 pat = evalstring(context, mapping, args[0])
607 rpl = evalstring(context, mapping, args[1])
608 src = evalstring(context, mapping, args[2])
609 try:
610 patre = re.compile(pat)
611 except re.error:
612 # i18n: "sub" is a keyword
613 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
614 try:
615 yield patre.sub(rpl, src)
616 except re.error:
617 # i18n: "sub" is a keyword
618 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
619
620 @templatefunc('startswith(pattern, text)')
621 def startswith(context, mapping, args):
622 """Returns the value from the "text" argument
623 if it begins with the content from the "pattern" argument."""
624 if len(args) != 2:
625 # i18n: "startswith" is a keyword
626 raise error.ParseError(_("startswith expects two arguments"))
627
628 patn = evalstring(context, mapping, args[0])
629 text = evalstring(context, mapping, args[1])
630 if text.startswith(patn):
631 return text
632 return ''
633
634 @templatefunc('word(number, text[, separator])')
635 def word(context, mapping, args):
636 """Return the nth word from a string."""
637 if not (2 <= len(args) <= 3):
638 # i18n: "word" is a keyword
639 raise error.ParseError(_("word expects two or three arguments, got %d")
640 % len(args))
641
642 num = evalinteger(context, mapping, args[0],
643 # i18n: "word" is a keyword
644 _("word expects an integer index"))
645 text = evalstring(context, mapping, args[1])
646 if len(args) == 3:
647 splitter = evalstring(context, mapping, args[2])
648 else:
649 splitter = None
650
651 tokens = text.split(splitter)
652 if num >= len(tokens) or num < -len(tokens):
653 return ''
654 else:
655 return tokens[num]
656
657 def loadfunction(ui, extname, registrarobj):
658 """Load template function from specified registrarobj
659 """
660 for name, func in registrarobj._table.iteritems():
661 funcs[name] = func
662
663 # tell hggettext to extract docstrings from these functions:
664 i18nfunctions = funcs.values()