mercurial/templater.py
changeset 36928 521f6c7e1756
parent 36927 32f9b7e3f056
child 36985 255f635c3204
equal deleted inserted replaced
36927:32f9b7e3f056 36928:521f6c7e1756
     6 # GNU General Public License version 2 or any later version.
     6 # GNU General Public License version 2 or any later version.
     7 
     7 
     8 from __future__ import absolute_import, print_function
     8 from __future__ import absolute_import, print_function
     9 
     9 
    10 import os
    10 import os
    11 import re
       
    12 
    11 
    13 from .i18n import _
    12 from .i18n import _
    14 from . import (
    13 from . import (
    15     color,
       
    16     config,
    14     config,
    17     encoding,
    15     encoding,
    18     error,
    16     error,
    19     minirst,
       
    20     obsutil,
       
    21     parser,
    17     parser,
    22     pycompat,
    18     pycompat,
    23     registrar,
       
    24     revset as revsetmod,
       
    25     revsetlang,
       
    26     scmutil,
       
    27     templatefilters,
    19     templatefilters,
    28     templatekw,
    20     templatefuncs,
    29     templateutil,
    21     templateutil,
    30     util,
    22     util,
    31 )
    23 )
    32 from .utils import dateutil
       
    33 
       
    34 evalrawexp = templateutil.evalrawexp
       
    35 evalfuncarg = templateutil.evalfuncarg
       
    36 evalboolean = templateutil.evalboolean
       
    37 evalinteger = templateutil.evalinteger
       
    38 evalstring = templateutil.evalstring
       
    39 evalstringliteral = templateutil.evalstringliteral
       
    40 evalastype = templateutil.evalastype
       
    41 
    24 
    42 # template parsing
    25 # template parsing
    43 
    26 
    44 elements = {
    27 elements = {
    45     # token-type: binding-strength, primary, prefix, infix, suffix
    28     # token-type: binding-strength, primary, prefix, infix, suffix
   453     return compargs
   436     return compargs
   454 
   437 
   455 def buildkeyvaluepair(exp, content):
   438 def buildkeyvaluepair(exp, content):
   456     raise error.ParseError(_("can't use a key-value pair in this context"))
   439     raise error.ParseError(_("can't use a key-value pair in this context"))
   457 
   440 
   458 # dict of template built-in functions
       
   459 funcs = {}
       
   460 
       
   461 templatefunc = registrar.templatefunc(funcs)
       
   462 
       
   463 @templatefunc('date(date[, fmt])')
       
   464 def date(context, mapping, args):
       
   465     """Format a date. See :hg:`help dates` for formatting
       
   466     strings. The default is a Unix date format, including the timezone:
       
   467     "Mon Sep 04 15:13:13 2006 0700"."""
       
   468     if not (1 <= len(args) <= 2):
       
   469         # i18n: "date" is a keyword
       
   470         raise error.ParseError(_("date expects one or two arguments"))
       
   471 
       
   472     date = evalfuncarg(context, mapping, args[0])
       
   473     fmt = None
       
   474     if len(args) == 2:
       
   475         fmt = evalstring(context, mapping, args[1])
       
   476     try:
       
   477         if fmt is None:
       
   478             return dateutil.datestr(date)
       
   479         else:
       
   480             return dateutil.datestr(date, fmt)
       
   481     except (TypeError, ValueError):
       
   482         # i18n: "date" is a keyword
       
   483         raise error.ParseError(_("date expects a date information"))
       
   484 
       
   485 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
       
   486 def dict_(context, mapping, args):
       
   487     """Construct a dict from key-value pairs. A key may be omitted if
       
   488     a value expression can provide an unambiguous name."""
       
   489     data = util.sortdict()
       
   490 
       
   491     for v in args['args']:
       
   492         k = templateutil.findsymbolicname(v)
       
   493         if not k:
       
   494             raise error.ParseError(_('dict key cannot be inferred'))
       
   495         if k in data or k in args['kwargs']:
       
   496             raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
       
   497         data[k] = evalfuncarg(context, mapping, v)
       
   498 
       
   499     data.update((k, evalfuncarg(context, mapping, v))
       
   500                 for k, v in args['kwargs'].iteritems())
       
   501     return templateutil.hybriddict(data)
       
   502 
       
   503 @templatefunc('diff([includepattern [, excludepattern]])')
       
   504 def diff(context, mapping, args):
       
   505     """Show a diff, optionally
       
   506     specifying files to include or exclude."""
       
   507     if len(args) > 2:
       
   508         # i18n: "diff" is a keyword
       
   509         raise error.ParseError(_("diff expects zero, one, or two arguments"))
       
   510 
       
   511     def getpatterns(i):
       
   512         if i < len(args):
       
   513             s = evalstring(context, mapping, args[i]).strip()
       
   514             if s:
       
   515                 return [s]
       
   516         return []
       
   517 
       
   518     ctx = context.resource(mapping, 'ctx')
       
   519     chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
       
   520 
       
   521     return ''.join(chunks)
       
   522 
       
   523 @templatefunc('extdata(source)', argspec='source')
       
   524 def extdata(context, mapping, args):
       
   525     """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
       
   526     if 'source' not in args:
       
   527         # i18n: "extdata" is a keyword
       
   528         raise error.ParseError(_('extdata expects one argument'))
       
   529 
       
   530     source = evalstring(context, mapping, args['source'])
       
   531     cache = context.resource(mapping, 'cache').setdefault('extdata', {})
       
   532     ctx = context.resource(mapping, 'ctx')
       
   533     if source in cache:
       
   534         data = cache[source]
       
   535     else:
       
   536         data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
       
   537     return data.get(ctx.rev(), '')
       
   538 
       
   539 @templatefunc('files(pattern)')
       
   540 def files(context, mapping, args):
       
   541     """All files of the current changeset matching the pattern. See
       
   542     :hg:`help patterns`."""
       
   543     if not len(args) == 1:
       
   544         # i18n: "files" is a keyword
       
   545         raise error.ParseError(_("files expects one argument"))
       
   546 
       
   547     raw = evalstring(context, mapping, args[0])
       
   548     ctx = context.resource(mapping, 'ctx')
       
   549     m = ctx.match([raw])
       
   550     files = list(ctx.matches(m))
       
   551     return templateutil.compatlist(context, mapping, "file", files)
       
   552 
       
   553 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
       
   554 def fill(context, mapping, args):
       
   555     """Fill many
       
   556     paragraphs with optional indentation. See the "fill" filter."""
       
   557     if not (1 <= len(args) <= 4):
       
   558         # i18n: "fill" is a keyword
       
   559         raise error.ParseError(_("fill expects one to four arguments"))
       
   560 
       
   561     text = evalstring(context, mapping, args[0])
       
   562     width = 76
       
   563     initindent = ''
       
   564     hangindent = ''
       
   565     if 2 <= len(args) <= 4:
       
   566         width = evalinteger(context, mapping, args[1],
       
   567                             # i18n: "fill" is a keyword
       
   568                             _("fill expects an integer width"))
       
   569         try:
       
   570             initindent = evalstring(context, mapping, args[2])
       
   571             hangindent = evalstring(context, mapping, args[3])
       
   572         except IndexError:
       
   573             pass
       
   574 
       
   575     return templatefilters.fill(text, width, initindent, hangindent)
       
   576 
       
   577 @templatefunc('formatnode(node)')
       
   578 def formatnode(context, mapping, args):
       
   579     """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
       
   580     if len(args) != 1:
       
   581         # i18n: "formatnode" is a keyword
       
   582         raise error.ParseError(_("formatnode expects one argument"))
       
   583 
       
   584     ui = context.resource(mapping, 'ui')
       
   585     node = evalstring(context, mapping, args[0])
       
   586     if ui.debugflag:
       
   587         return node
       
   588     return templatefilters.short(node)
       
   589 
       
   590 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
       
   591               argspec='text width fillchar left')
       
   592 def pad(context, mapping, args):
       
   593     """Pad text with a
       
   594     fill character."""
       
   595     if 'text' not in args or 'width' not in args:
       
   596         # i18n: "pad" is a keyword
       
   597         raise error.ParseError(_("pad() expects two to four arguments"))
       
   598 
       
   599     width = evalinteger(context, mapping, args['width'],
       
   600                         # i18n: "pad" is a keyword
       
   601                         _("pad() expects an integer width"))
       
   602 
       
   603     text = evalstring(context, mapping, args['text'])
       
   604 
       
   605     left = False
       
   606     fillchar = ' '
       
   607     if 'fillchar' in args:
       
   608         fillchar = evalstring(context, mapping, args['fillchar'])
       
   609         if len(color.stripeffects(fillchar)) != 1:
       
   610             # i18n: "pad" is a keyword
       
   611             raise error.ParseError(_("pad() expects a single fill character"))
       
   612     if 'left' in args:
       
   613         left = evalboolean(context, mapping, args['left'])
       
   614 
       
   615     fillwidth = width - encoding.colwidth(color.stripeffects(text))
       
   616     if fillwidth <= 0:
       
   617         return text
       
   618     if left:
       
   619         return fillchar * fillwidth + text
       
   620     else:
       
   621         return text + fillchar * fillwidth
       
   622 
       
   623 @templatefunc('indent(text, indentchars[, firstline])')
       
   624 def indent(context, mapping, args):
       
   625     """Indents all non-empty lines
       
   626     with the characters given in the indentchars string. An optional
       
   627     third parameter will override the indent for the first line only
       
   628     if present."""
       
   629     if not (2 <= len(args) <= 3):
       
   630         # i18n: "indent" is a keyword
       
   631         raise error.ParseError(_("indent() expects two or three arguments"))
       
   632 
       
   633     text = evalstring(context, mapping, args[0])
       
   634     indent = evalstring(context, mapping, args[1])
       
   635 
       
   636     if len(args) == 3:
       
   637         firstline = evalstring(context, mapping, args[2])
       
   638     else:
       
   639         firstline = indent
       
   640 
       
   641     # the indent function doesn't indent the first line, so we do it here
       
   642     return templatefilters.indent(firstline + text, indent)
       
   643 
       
   644 @templatefunc('get(dict, key)')
       
   645 def get(context, mapping, args):
       
   646     """Get an attribute/key from an object. Some keywords
       
   647     are complex types. This function allows you to obtain the value of an
       
   648     attribute on these types."""
       
   649     if len(args) != 2:
       
   650         # i18n: "get" is a keyword
       
   651         raise error.ParseError(_("get() expects two arguments"))
       
   652 
       
   653     dictarg = evalfuncarg(context, mapping, args[0])
       
   654     if not util.safehasattr(dictarg, 'get'):
       
   655         # i18n: "get" is a keyword
       
   656         raise error.ParseError(_("get() expects a dict as first argument"))
       
   657 
       
   658     key = evalfuncarg(context, mapping, args[1])
       
   659     return templateutil.getdictitem(dictarg, key)
       
   660 
       
   661 @templatefunc('if(expr, then[, else])')
       
   662 def if_(context, mapping, args):
       
   663     """Conditionally execute based on the result of
       
   664     an expression."""
       
   665     if not (2 <= len(args) <= 3):
       
   666         # i18n: "if" is a keyword
       
   667         raise error.ParseError(_("if expects two or three arguments"))
       
   668 
       
   669     test = evalboolean(context, mapping, args[0])
       
   670     if test:
       
   671         yield evalrawexp(context, mapping, args[1])
       
   672     elif len(args) == 3:
       
   673         yield evalrawexp(context, mapping, args[2])
       
   674 
       
   675 @templatefunc('ifcontains(needle, haystack, then[, else])')
       
   676 def ifcontains(context, mapping, args):
       
   677     """Conditionally execute based
       
   678     on whether the item "needle" is in "haystack"."""
       
   679     if not (3 <= len(args) <= 4):
       
   680         # i18n: "ifcontains" is a keyword
       
   681         raise error.ParseError(_("ifcontains expects three or four arguments"))
       
   682 
       
   683     haystack = evalfuncarg(context, mapping, args[1])
       
   684     try:
       
   685         needle = evalastype(context, mapping, args[0],
       
   686                             getattr(haystack, 'keytype', None) or bytes)
       
   687         found = (needle in haystack)
       
   688     except error.ParseError:
       
   689         found = False
       
   690 
       
   691     if found:
       
   692         yield evalrawexp(context, mapping, args[2])
       
   693     elif len(args) == 4:
       
   694         yield evalrawexp(context, mapping, args[3])
       
   695 
       
   696 @templatefunc('ifeq(expr1, expr2, then[, else])')
       
   697 def ifeq(context, mapping, args):
       
   698     """Conditionally execute based on
       
   699     whether 2 items are equivalent."""
       
   700     if not (3 <= len(args) <= 4):
       
   701         # i18n: "ifeq" is a keyword
       
   702         raise error.ParseError(_("ifeq expects three or four arguments"))
       
   703 
       
   704     test = evalstring(context, mapping, args[0])
       
   705     match = evalstring(context, mapping, args[1])
       
   706     if test == match:
       
   707         yield evalrawexp(context, mapping, args[2])
       
   708     elif len(args) == 4:
       
   709         yield evalrawexp(context, mapping, args[3])
       
   710 
       
   711 @templatefunc('join(list, sep)')
       
   712 def join(context, mapping, args):
       
   713     """Join items in a list with a delimiter."""
       
   714     if not (1 <= len(args) <= 2):
       
   715         # i18n: "join" is a keyword
       
   716         raise error.ParseError(_("join expects one or two arguments"))
       
   717 
       
   718     # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
       
   719     # abuses generator as a keyword that returns a list of dicts.
       
   720     joinset = evalrawexp(context, mapping, args[0])
       
   721     joinset = templateutil.unwrapvalue(joinset)
       
   722     joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
       
   723     joiner = " "
       
   724     if len(args) > 1:
       
   725         joiner = evalstring(context, mapping, args[1])
       
   726 
       
   727     first = True
       
   728     for x in pycompat.maybebytestr(joinset):
       
   729         if first:
       
   730             first = False
       
   731         else:
       
   732             yield joiner
       
   733         yield joinfmt(x)
       
   734 
       
   735 @templatefunc('label(label, expr)')
       
   736 def label(context, mapping, args):
       
   737     """Apply a label to generated content. Content with
       
   738     a label applied can result in additional post-processing, such as
       
   739     automatic colorization."""
       
   740     if len(args) != 2:
       
   741         # i18n: "label" is a keyword
       
   742         raise error.ParseError(_("label expects two arguments"))
       
   743 
       
   744     ui = context.resource(mapping, 'ui')
       
   745     thing = evalstring(context, mapping, args[1])
       
   746     # preserve unknown symbol as literal so effects like 'red', 'bold',
       
   747     # etc. don't need to be quoted
       
   748     label = evalstringliteral(context, mapping, args[0])
       
   749 
       
   750     return ui.label(thing, label)
       
   751 
       
   752 @templatefunc('latesttag([pattern])')
       
   753 def latesttag(context, mapping, args):
       
   754     """The global tags matching the given pattern on the
       
   755     most recent globally tagged ancestor of this changeset.
       
   756     If no such tags exist, the "{tag}" template resolves to
       
   757     the string "null"."""
       
   758     if len(args) > 1:
       
   759         # i18n: "latesttag" is a keyword
       
   760         raise error.ParseError(_("latesttag expects at most one argument"))
       
   761 
       
   762     pattern = None
       
   763     if len(args) == 1:
       
   764         pattern = evalstring(context, mapping, args[0])
       
   765     return templatekw.showlatesttags(context, mapping, pattern)
       
   766 
       
   767 @templatefunc('localdate(date[, tz])')
       
   768 def localdate(context, mapping, args):
       
   769     """Converts a date to the specified timezone.
       
   770     The default is local date."""
       
   771     if not (1 <= len(args) <= 2):
       
   772         # i18n: "localdate" is a keyword
       
   773         raise error.ParseError(_("localdate expects one or two arguments"))
       
   774 
       
   775     date = evalfuncarg(context, mapping, args[0])
       
   776     try:
       
   777         date = dateutil.parsedate(date)
       
   778     except AttributeError:  # not str nor date tuple
       
   779         # i18n: "localdate" is a keyword
       
   780         raise error.ParseError(_("localdate expects a date information"))
       
   781     if len(args) >= 2:
       
   782         tzoffset = None
       
   783         tz = evalfuncarg(context, mapping, args[1])
       
   784         if isinstance(tz, bytes):
       
   785             tzoffset, remainder = dateutil.parsetimezone(tz)
       
   786             if remainder:
       
   787                 tzoffset = None
       
   788         if tzoffset is None:
       
   789             try:
       
   790                 tzoffset = int(tz)
       
   791             except (TypeError, ValueError):
       
   792                 # i18n: "localdate" is a keyword
       
   793                 raise error.ParseError(_("localdate expects a timezone"))
       
   794     else:
       
   795         tzoffset = dateutil.makedate()[1]
       
   796     return (date[0], tzoffset)
       
   797 
       
   798 @templatefunc('max(iterable)')
       
   799 def max_(context, mapping, args, **kwargs):
       
   800     """Return the max of an iterable"""
       
   801     if len(args) != 1:
       
   802         # i18n: "max" is a keyword
       
   803         raise error.ParseError(_("max expects one argument"))
       
   804 
       
   805     iterable = evalfuncarg(context, mapping, args[0])
       
   806     try:
       
   807         x = max(pycompat.maybebytestr(iterable))
       
   808     except (TypeError, ValueError):
       
   809         # i18n: "max" is a keyword
       
   810         raise error.ParseError(_("max first argument should be an iterable"))
       
   811     return templateutil.wraphybridvalue(iterable, x, x)
       
   812 
       
   813 @templatefunc('min(iterable)')
       
   814 def min_(context, mapping, args, **kwargs):
       
   815     """Return the min of an iterable"""
       
   816     if len(args) != 1:
       
   817         # i18n: "min" is a keyword
       
   818         raise error.ParseError(_("min expects one argument"))
       
   819 
       
   820     iterable = evalfuncarg(context, mapping, args[0])
       
   821     try:
       
   822         x = min(pycompat.maybebytestr(iterable))
       
   823     except (TypeError, ValueError):
       
   824         # i18n: "min" is a keyword
       
   825         raise error.ParseError(_("min first argument should be an iterable"))
       
   826     return templateutil.wraphybridvalue(iterable, x, x)
       
   827 
       
   828 @templatefunc('mod(a, b)')
       
   829 def mod(context, mapping, args):
       
   830     """Calculate a mod b such that a / b + a mod b == a"""
       
   831     if not len(args) == 2:
       
   832         # i18n: "mod" is a keyword
       
   833         raise error.ParseError(_("mod expects two arguments"))
       
   834 
       
   835     func = lambda a, b: a % b
       
   836     return templateutil.runarithmetic(context, mapping,
       
   837                                       (func, args[0], args[1]))
       
   838 
       
   839 @templatefunc('obsfateoperations(markers)')
       
   840 def obsfateoperations(context, mapping, args):
       
   841     """Compute obsfate related information based on markers (EXPERIMENTAL)"""
       
   842     if len(args) != 1:
       
   843         # i18n: "obsfateoperations" is a keyword
       
   844         raise error.ParseError(_("obsfateoperations expects one argument"))
       
   845 
       
   846     markers = evalfuncarg(context, mapping, args[0])
       
   847 
       
   848     try:
       
   849         data = obsutil.markersoperations(markers)
       
   850         return templateutil.hybridlist(data, name='operation')
       
   851     except (TypeError, KeyError):
       
   852         # i18n: "obsfateoperations" is a keyword
       
   853         errmsg = _("obsfateoperations first argument should be an iterable")
       
   854         raise error.ParseError(errmsg)
       
   855 
       
   856 @templatefunc('obsfatedate(markers)')
       
   857 def obsfatedate(context, mapping, args):
       
   858     """Compute obsfate related information based on markers (EXPERIMENTAL)"""
       
   859     if len(args) != 1:
       
   860         # i18n: "obsfatedate" is a keyword
       
   861         raise error.ParseError(_("obsfatedate expects one argument"))
       
   862 
       
   863     markers = evalfuncarg(context, mapping, args[0])
       
   864 
       
   865     try:
       
   866         data = obsutil.markersdates(markers)
       
   867         return templateutil.hybridlist(data, name='date', fmt='%d %d')
       
   868     except (TypeError, KeyError):
       
   869         # i18n: "obsfatedate" is a keyword
       
   870         errmsg = _("obsfatedate first argument should be an iterable")
       
   871         raise error.ParseError(errmsg)
       
   872 
       
   873 @templatefunc('obsfateusers(markers)')
       
   874 def obsfateusers(context, mapping, args):
       
   875     """Compute obsfate related information based on markers (EXPERIMENTAL)"""
       
   876     if len(args) != 1:
       
   877         # i18n: "obsfateusers" is a keyword
       
   878         raise error.ParseError(_("obsfateusers expects one argument"))
       
   879 
       
   880     markers = evalfuncarg(context, mapping, args[0])
       
   881 
       
   882     try:
       
   883         data = obsutil.markersusers(markers)
       
   884         return templateutil.hybridlist(data, name='user')
       
   885     except (TypeError, KeyError, ValueError):
       
   886         # i18n: "obsfateusers" is a keyword
       
   887         msg = _("obsfateusers first argument should be an iterable of "
       
   888                 "obsmakers")
       
   889         raise error.ParseError(msg)
       
   890 
       
   891 @templatefunc('obsfateverb(successors, markers)')
       
   892 def obsfateverb(context, mapping, args):
       
   893     """Compute obsfate related information based on successors (EXPERIMENTAL)"""
       
   894     if len(args) != 2:
       
   895         # i18n: "obsfateverb" is a keyword
       
   896         raise error.ParseError(_("obsfateverb expects two arguments"))
       
   897 
       
   898     successors = evalfuncarg(context, mapping, args[0])
       
   899     markers = evalfuncarg(context, mapping, args[1])
       
   900 
       
   901     try:
       
   902         return obsutil.obsfateverb(successors, markers)
       
   903     except TypeError:
       
   904         # i18n: "obsfateverb" is a keyword
       
   905         errmsg = _("obsfateverb first argument should be countable")
       
   906         raise error.ParseError(errmsg)
       
   907 
       
   908 @templatefunc('relpath(path)')
       
   909 def relpath(context, mapping, args):
       
   910     """Convert a repository-absolute path into a filesystem path relative to
       
   911     the current working directory."""
       
   912     if len(args) != 1:
       
   913         # i18n: "relpath" is a keyword
       
   914         raise error.ParseError(_("relpath expects one argument"))
       
   915 
       
   916     repo = context.resource(mapping, 'ctx').repo()
       
   917     path = evalstring(context, mapping, args[0])
       
   918     return repo.pathto(path)
       
   919 
       
   920 @templatefunc('revset(query[, formatargs...])')
       
   921 def revset(context, mapping, args):
       
   922     """Execute a revision set query. See
       
   923     :hg:`help revset`."""
       
   924     if not len(args) > 0:
       
   925         # i18n: "revset" is a keyword
       
   926         raise error.ParseError(_("revset expects one or more arguments"))
       
   927 
       
   928     raw = evalstring(context, mapping, args[0])
       
   929     ctx = context.resource(mapping, 'ctx')
       
   930     repo = ctx.repo()
       
   931 
       
   932     def query(expr):
       
   933         m = revsetmod.match(repo.ui, expr, repo=repo)
       
   934         return m(repo)
       
   935 
       
   936     if len(args) > 1:
       
   937         formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
       
   938         revs = query(revsetlang.formatspec(raw, *formatargs))
       
   939         revs = list(revs)
       
   940     else:
       
   941         cache = context.resource(mapping, 'cache')
       
   942         revsetcache = cache.setdefault("revsetcache", {})
       
   943         if raw in revsetcache:
       
   944             revs = revsetcache[raw]
       
   945         else:
       
   946             revs = query(raw)
       
   947             revs = list(revs)
       
   948             revsetcache[raw] = revs
       
   949     return templatekw.showrevslist(context, mapping, "revision", revs)
       
   950 
       
   951 @templatefunc('rstdoc(text, style)')
       
   952 def rstdoc(context, mapping, args):
       
   953     """Format reStructuredText."""
       
   954     if len(args) != 2:
       
   955         # i18n: "rstdoc" is a keyword
       
   956         raise error.ParseError(_("rstdoc expects two arguments"))
       
   957 
       
   958     text = evalstring(context, mapping, args[0])
       
   959     style = evalstring(context, mapping, args[1])
       
   960 
       
   961     return minirst.format(text, style=style, keep=['verbose'])
       
   962 
       
   963 @templatefunc('separate(sep, args)', argspec='sep *args')
       
   964 def separate(context, mapping, args):
       
   965     """Add a separator between non-empty arguments."""
       
   966     if 'sep' not in args:
       
   967         # i18n: "separate" is a keyword
       
   968         raise error.ParseError(_("separate expects at least one argument"))
       
   969 
       
   970     sep = evalstring(context, mapping, args['sep'])
       
   971     first = True
       
   972     for arg in args['args']:
       
   973         argstr = evalstring(context, mapping, arg)
       
   974         if not argstr:
       
   975             continue
       
   976         if first:
       
   977             first = False
       
   978         else:
       
   979             yield sep
       
   980         yield argstr
       
   981 
       
   982 @templatefunc('shortest(node, minlength=4)')
       
   983 def shortest(context, mapping, args):
       
   984     """Obtain the shortest representation of
       
   985     a node."""
       
   986     if not (1 <= len(args) <= 2):
       
   987         # i18n: "shortest" is a keyword
       
   988         raise error.ParseError(_("shortest() expects one or two arguments"))
       
   989 
       
   990     node = evalstring(context, mapping, args[0])
       
   991 
       
   992     minlength = 4
       
   993     if len(args) > 1:
       
   994         minlength = evalinteger(context, mapping, args[1],
       
   995                                 # i18n: "shortest" is a keyword
       
   996                                 _("shortest() expects an integer minlength"))
       
   997 
       
   998     # _partialmatch() of filtered changelog could take O(len(repo)) time,
       
   999     # which would be unacceptably slow. so we look for hash collision in
       
  1000     # unfiltered space, which means some hashes may be slightly longer.
       
  1001     cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
       
  1002     return cl.shortest(node, minlength)
       
  1003 
       
  1004 @templatefunc('strip(text[, chars])')
       
  1005 def strip(context, mapping, args):
       
  1006     """Strip characters from a string. By default,
       
  1007     strips all leading and trailing whitespace."""
       
  1008     if not (1 <= len(args) <= 2):
       
  1009         # i18n: "strip" is a keyword
       
  1010         raise error.ParseError(_("strip expects one or two arguments"))
       
  1011 
       
  1012     text = evalstring(context, mapping, args[0])
       
  1013     if len(args) == 2:
       
  1014         chars = evalstring(context, mapping, args[1])
       
  1015         return text.strip(chars)
       
  1016     return text.strip()
       
  1017 
       
  1018 @templatefunc('sub(pattern, replacement, expression)')
       
  1019 def sub(context, mapping, args):
       
  1020     """Perform text substitution
       
  1021     using regular expressions."""
       
  1022     if len(args) != 3:
       
  1023         # i18n: "sub" is a keyword
       
  1024         raise error.ParseError(_("sub expects three arguments"))
       
  1025 
       
  1026     pat = evalstring(context, mapping, args[0])
       
  1027     rpl = evalstring(context, mapping, args[1])
       
  1028     src = evalstring(context, mapping, args[2])
       
  1029     try:
       
  1030         patre = re.compile(pat)
       
  1031     except re.error:
       
  1032         # i18n: "sub" is a keyword
       
  1033         raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
       
  1034     try:
       
  1035         yield patre.sub(rpl, src)
       
  1036     except re.error:
       
  1037         # i18n: "sub" is a keyword
       
  1038         raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
       
  1039 
       
  1040 @templatefunc('startswith(pattern, text)')
       
  1041 def startswith(context, mapping, args):
       
  1042     """Returns the value from the "text" argument
       
  1043     if it begins with the content from the "pattern" argument."""
       
  1044     if len(args) != 2:
       
  1045         # i18n: "startswith" is a keyword
       
  1046         raise error.ParseError(_("startswith expects two arguments"))
       
  1047 
       
  1048     patn = evalstring(context, mapping, args[0])
       
  1049     text = evalstring(context, mapping, args[1])
       
  1050     if text.startswith(patn):
       
  1051         return text
       
  1052     return ''
       
  1053 
       
  1054 @templatefunc('word(number, text[, separator])')
       
  1055 def word(context, mapping, args):
       
  1056     """Return the nth word from a string."""
       
  1057     if not (2 <= len(args) <= 3):
       
  1058         # i18n: "word" is a keyword
       
  1059         raise error.ParseError(_("word expects two or three arguments, got %d")
       
  1060                                % len(args))
       
  1061 
       
  1062     num = evalinteger(context, mapping, args[0],
       
  1063                       # i18n: "word" is a keyword
       
  1064                       _("word expects an integer index"))
       
  1065     text = evalstring(context, mapping, args[1])
       
  1066     if len(args) == 3:
       
  1067         splitter = evalstring(context, mapping, args[2])
       
  1068     else:
       
  1069         splitter = None
       
  1070 
       
  1071     tokens = text.split(splitter)
       
  1072     if num >= len(tokens) or num < -len(tokens):
       
  1073         return ''
       
  1074     else:
       
  1075         return tokens[num]
       
  1076 
       
  1077 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
   441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
  1078 exprmethods = {
   442 exprmethods = {
  1079     "integer": lambda e, c: (templateutil.runinteger, e[1]),
   443     "integer": lambda e, c: (templateutil.runinteger, e[1]),
  1080     "string": lambda e, c: (templateutil.runstring, e[1]),
   444     "string": lambda e, c: (templateutil.runstring, e[1]),
  1081     "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
   445     "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
  1175                  aliases=()):
   539                  aliases=()):
  1176         self._loader = loader
   540         self._loader = loader
  1177         if filters is None:
   541         if filters is None:
  1178             filters = {}
   542             filters = {}
  1179         self._filters = filters
   543         self._filters = filters
  1180         self._funcs = funcs  # make this a parameter if needed
   544         self._funcs = templatefuncs.funcs  # make this a parameter if needed
  1181         if defaults is None:
   545         if defaults is None:
  1182             defaults = {}
   546             defaults = {}
  1183         if resources is None:
   547         if resources is None:
  1184             resources = {}
   548             resources = {}
  1185         self._defaults = defaults
   549         self._defaults = defaults
  1431                 mapfile = os.path.join(path, location)
   795                 mapfile = os.path.join(path, location)
  1432                 if os.path.isfile(mapfile):
   796                 if os.path.isfile(mapfile):
  1433                     return style, mapfile
   797                     return style, mapfile
  1434 
   798 
  1435     raise RuntimeError("No hgweb templates found in %r" % paths)
   799     raise RuntimeError("No hgweb templates found in %r" % paths)
  1436 
       
  1437 def loadfunction(ui, extname, registrarobj):
       
  1438     """Load template function from specified registrarobj
       
  1439     """
       
  1440     for name, func in registrarobj._table.iteritems():
       
  1441         funcs[name] = func
       
  1442 
       
  1443 # tell hggettext to extract docstrings from these functions:
       
  1444 i18nfunctions = funcs.values()