mercurial/templatefuncs.py
changeset 36922 521f6c7e1756
parent 36921 32f9b7e3f056
child 37015 a318bb154d42
equal deleted inserted replaced
36921:32f9b7e3f056 36922: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()