templater: split template functions to new module
authorYuya Nishihara <yuya@tcha.org>
Thu, 08 Mar 2018 22:23:02 +0900
changeset 36928 521f6c7e1756
parent 36927 32f9b7e3f056
child 36929 d40b9e29c114
templater: split template functions to new module It has grown enough to be a dedicated module.
Makefile
mercurial/extensions.py
mercurial/help.py
mercurial/registrar.py
mercurial/templatefuncs.py
mercurial/templater.py
--- a/Makefile	Thu Mar 08 23:15:09 2018 +0900
+++ b/Makefile	Thu Mar 08 22:23:02 2018 +0900
@@ -132,8 +132,9 @@
 	$(PYTHON) i18n/hggettext mercurial/commands.py \
 	  hgext/*.py hgext/*/__init__.py \
 	  mercurial/fileset.py mercurial/revset.py \
-	  mercurial/templatefilters.py mercurial/templatekw.py \
-	  mercurial/templater.py \
+	  mercurial/templatefilters.py \
+	  mercurial/templatefuncs.py \
+	  mercurial/templatekw.py \
 	  mercurial/filemerge.py \
 	  mercurial/hgweb/webcommands.py \
 	  mercurial/util.py \
--- a/mercurial/extensions.py	Thu Mar 08 23:15:09 2018 +0900
+++ b/mercurial/extensions.py	Thu Mar 08 22:23:02 2018 +0900
@@ -290,8 +290,8 @@
         fileset,
         revset,
         templatefilters,
+        templatefuncs,
         templatekw,
-        templater,
     )
 
     # list of (objname, loadermod, loadername) tuple:
@@ -307,7 +307,7 @@
         ('internalmerge', filemerge, 'loadinternalmerge'),
         ('revsetpredicate', revset, 'loadpredicate'),
         ('templatefilter', templatefilters, 'loadfilter'),
-        ('templatefunc', templater, 'loadfunction'),
+        ('templatefunc', templatefuncs, 'loadfunction'),
         ('templatekeyword', templatekw, 'loadkeyword'),
     ]
     _loadextra(ui, newindex, extraloaders)
--- a/mercurial/help.py	Thu Mar 08 23:15:09 2018 +0900
+++ b/mercurial/help.py	Thu Mar 08 22:23:02 2018 +0900
@@ -26,8 +26,8 @@
     pycompat,
     revset,
     templatefilters,
+    templatefuncs,
     templatekw,
-    templater,
     util,
 )
 from .hgweb import (
@@ -309,7 +309,7 @@
 addtopicsymbols('revisions', '.. predicatesmarker', revset.symbols)
 addtopicsymbols('templates', '.. keywordsmarker', templatekw.keywords)
 addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters)
-addtopicsymbols('templates', '.. functionsmarker', templater.funcs)
+addtopicsymbols('templates', '.. functionsmarker', templatefuncs.funcs)
 addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands,
                 dedent=True)
 
--- a/mercurial/registrar.py	Thu Mar 08 23:15:09 2018 +0900
+++ b/mercurial/registrar.py	Thu Mar 08 22:23:02 2018 +0900
@@ -368,7 +368,7 @@
     extension, if an instance named as 'templatefunc' is used for
     decorating in extension.
 
-    Otherwise, explicit 'templater.loadfunction()' is needed.
+    Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
     """
     _getname = _funcregistrarbase._parsefuncdecl
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templatefuncs.py	Thu Mar 08 22:23:02 2018 +0900
@@ -0,0 +1,664 @@
+# templatefuncs.py - common template functions
+#
+# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import
+
+import re
+
+from .i18n import _
+from . import (
+    color,
+    encoding,
+    error,
+    minirst,
+    obsutil,
+    pycompat,
+    registrar,
+    revset as revsetmod,
+    revsetlang,
+    scmutil,
+    templatefilters,
+    templatekw,
+    templateutil,
+    util,
+)
+from .utils import dateutil
+
+evalrawexp = templateutil.evalrawexp
+evalfuncarg = templateutil.evalfuncarg
+evalboolean = templateutil.evalboolean
+evalinteger = templateutil.evalinteger
+evalstring = templateutil.evalstring
+evalstringliteral = templateutil.evalstringliteral
+evalastype = templateutil.evalastype
+
+# dict of template built-in functions
+funcs = {}
+templatefunc = registrar.templatefunc(funcs)
+
+@templatefunc('date(date[, fmt])')
+def date(context, mapping, args):
+    """Format a date. See :hg:`help dates` for formatting
+    strings. The default is a Unix date format, including the timezone:
+    "Mon Sep 04 15:13:13 2006 0700"."""
+    if not (1 <= len(args) <= 2):
+        # i18n: "date" is a keyword
+        raise error.ParseError(_("date expects one or two arguments"))
+
+    date = evalfuncarg(context, mapping, args[0])
+    fmt = None
+    if len(args) == 2:
+        fmt = evalstring(context, mapping, args[1])
+    try:
+        if fmt is None:
+            return dateutil.datestr(date)
+        else:
+            return dateutil.datestr(date, fmt)
+    except (TypeError, ValueError):
+        # i18n: "date" is a keyword
+        raise error.ParseError(_("date expects a date information"))
+
+@templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
+def dict_(context, mapping, args):
+    """Construct a dict from key-value pairs. A key may be omitted if
+    a value expression can provide an unambiguous name."""
+    data = util.sortdict()
+
+    for v in args['args']:
+        k = templateutil.findsymbolicname(v)
+        if not k:
+            raise error.ParseError(_('dict key cannot be inferred'))
+        if k in data or k in args['kwargs']:
+            raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
+        data[k] = evalfuncarg(context, mapping, v)
+
+    data.update((k, evalfuncarg(context, mapping, v))
+                for k, v in args['kwargs'].iteritems())
+    return templateutil.hybriddict(data)
+
+@templatefunc('diff([includepattern [, excludepattern]])')
+def diff(context, mapping, args):
+    """Show a diff, optionally
+    specifying files to include or exclude."""
+    if len(args) > 2:
+        # i18n: "diff" is a keyword
+        raise error.ParseError(_("diff expects zero, one, or two arguments"))
+
+    def getpatterns(i):
+        if i < len(args):
+            s = evalstring(context, mapping, args[i]).strip()
+            if s:
+                return [s]
+        return []
+
+    ctx = context.resource(mapping, 'ctx')
+    chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
+
+    return ''.join(chunks)
+
+@templatefunc('extdata(source)', argspec='source')
+def extdata(context, mapping, args):
+    """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
+    if 'source' not in args:
+        # i18n: "extdata" is a keyword
+        raise error.ParseError(_('extdata expects one argument'))
+
+    source = evalstring(context, mapping, args['source'])
+    cache = context.resource(mapping, 'cache').setdefault('extdata', {})
+    ctx = context.resource(mapping, 'ctx')
+    if source in cache:
+        data = cache[source]
+    else:
+        data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
+    return data.get(ctx.rev(), '')
+
+@templatefunc('files(pattern)')
+def files(context, mapping, args):
+    """All files of the current changeset matching the pattern. See
+    :hg:`help patterns`."""
+    if not len(args) == 1:
+        # i18n: "files" is a keyword
+        raise error.ParseError(_("files expects one argument"))
+
+    raw = evalstring(context, mapping, args[0])
+    ctx = context.resource(mapping, 'ctx')
+    m = ctx.match([raw])
+    files = list(ctx.matches(m))
+    return templateutil.compatlist(context, mapping, "file", files)
+
+@templatefunc('fill(text[, width[, initialident[, hangindent]]])')
+def fill(context, mapping, args):
+    """Fill many
+    paragraphs with optional indentation. See the "fill" filter."""
+    if not (1 <= len(args) <= 4):
+        # i18n: "fill" is a keyword
+        raise error.ParseError(_("fill expects one to four arguments"))
+
+    text = evalstring(context, mapping, args[0])
+    width = 76
+    initindent = ''
+    hangindent = ''
+    if 2 <= len(args) <= 4:
+        width = evalinteger(context, mapping, args[1],
+                            # i18n: "fill" is a keyword
+                            _("fill expects an integer width"))
+        try:
+            initindent = evalstring(context, mapping, args[2])
+            hangindent = evalstring(context, mapping, args[3])
+        except IndexError:
+            pass
+
+    return templatefilters.fill(text, width, initindent, hangindent)
+
+@templatefunc('formatnode(node)')
+def formatnode(context, mapping, args):
+    """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
+    if len(args) != 1:
+        # i18n: "formatnode" is a keyword
+        raise error.ParseError(_("formatnode expects one argument"))
+
+    ui = context.resource(mapping, 'ui')
+    node = evalstring(context, mapping, args[0])
+    if ui.debugflag:
+        return node
+    return templatefilters.short(node)
+
+@templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
+              argspec='text width fillchar left')
+def pad(context, mapping, args):
+    """Pad text with a
+    fill character."""
+    if 'text' not in args or 'width' not in args:
+        # i18n: "pad" is a keyword
+        raise error.ParseError(_("pad() expects two to four arguments"))
+
+    width = evalinteger(context, mapping, args['width'],
+                        # i18n: "pad" is a keyword
+                        _("pad() expects an integer width"))
+
+    text = evalstring(context, mapping, args['text'])
+
+    left = False
+    fillchar = ' '
+    if 'fillchar' in args:
+        fillchar = evalstring(context, mapping, args['fillchar'])
+        if len(color.stripeffects(fillchar)) != 1:
+            # i18n: "pad" is a keyword
+            raise error.ParseError(_("pad() expects a single fill character"))
+    if 'left' in args:
+        left = evalboolean(context, mapping, args['left'])
+
+    fillwidth = width - encoding.colwidth(color.stripeffects(text))
+    if fillwidth <= 0:
+        return text
+    if left:
+        return fillchar * fillwidth + text
+    else:
+        return text + fillchar * fillwidth
+
+@templatefunc('indent(text, indentchars[, firstline])')
+def indent(context, mapping, args):
+    """Indents all non-empty lines
+    with the characters given in the indentchars string. An optional
+    third parameter will override the indent for the first line only
+    if present."""
+    if not (2 <= len(args) <= 3):
+        # i18n: "indent" is a keyword
+        raise error.ParseError(_("indent() expects two or three arguments"))
+
+    text = evalstring(context, mapping, args[0])
+    indent = evalstring(context, mapping, args[1])
+
+    if len(args) == 3:
+        firstline = evalstring(context, mapping, args[2])
+    else:
+        firstline = indent
+
+    # the indent function doesn't indent the first line, so we do it here
+    return templatefilters.indent(firstline + text, indent)
+
+@templatefunc('get(dict, key)')
+def get(context, mapping, args):
+    """Get an attribute/key from an object. Some keywords
+    are complex types. This function allows you to obtain the value of an
+    attribute on these types."""
+    if len(args) != 2:
+        # i18n: "get" is a keyword
+        raise error.ParseError(_("get() expects two arguments"))
+
+    dictarg = evalfuncarg(context, mapping, args[0])
+    if not util.safehasattr(dictarg, 'get'):
+        # i18n: "get" is a keyword
+        raise error.ParseError(_("get() expects a dict as first argument"))
+
+    key = evalfuncarg(context, mapping, args[1])
+    return templateutil.getdictitem(dictarg, key)
+
+@templatefunc('if(expr, then[, else])')
+def if_(context, mapping, args):
+    """Conditionally execute based on the result of
+    an expression."""
+    if not (2 <= len(args) <= 3):
+        # i18n: "if" is a keyword
+        raise error.ParseError(_("if expects two or three arguments"))
+
+    test = evalboolean(context, mapping, args[0])
+    if test:
+        yield evalrawexp(context, mapping, args[1])
+    elif len(args) == 3:
+        yield evalrawexp(context, mapping, args[2])
+
+@templatefunc('ifcontains(needle, haystack, then[, else])')
+def ifcontains(context, mapping, args):
+    """Conditionally execute based
+    on whether the item "needle" is in "haystack"."""
+    if not (3 <= len(args) <= 4):
+        # i18n: "ifcontains" is a keyword
+        raise error.ParseError(_("ifcontains expects three or four arguments"))
+
+    haystack = evalfuncarg(context, mapping, args[1])
+    try:
+        needle = evalastype(context, mapping, args[0],
+                            getattr(haystack, 'keytype', None) or bytes)
+        found = (needle in haystack)
+    except error.ParseError:
+        found = False
+
+    if found:
+        yield evalrawexp(context, mapping, args[2])
+    elif len(args) == 4:
+        yield evalrawexp(context, mapping, args[3])
+
+@templatefunc('ifeq(expr1, expr2, then[, else])')
+def ifeq(context, mapping, args):
+    """Conditionally execute based on
+    whether 2 items are equivalent."""
+    if not (3 <= len(args) <= 4):
+        # i18n: "ifeq" is a keyword
+        raise error.ParseError(_("ifeq expects three or four arguments"))
+
+    test = evalstring(context, mapping, args[0])
+    match = evalstring(context, mapping, args[1])
+    if test == match:
+        yield evalrawexp(context, mapping, args[2])
+    elif len(args) == 4:
+        yield evalrawexp(context, mapping, args[3])
+
+@templatefunc('join(list, sep)')
+def join(context, mapping, args):
+    """Join items in a list with a delimiter."""
+    if not (1 <= len(args) <= 2):
+        # i18n: "join" is a keyword
+        raise error.ParseError(_("join expects one or two arguments"))
+
+    # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
+    # abuses generator as a keyword that returns a list of dicts.
+    joinset = evalrawexp(context, mapping, args[0])
+    joinset = templateutil.unwrapvalue(joinset)
+    joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
+    joiner = " "
+    if len(args) > 1:
+        joiner = evalstring(context, mapping, args[1])
+
+    first = True
+    for x in pycompat.maybebytestr(joinset):
+        if first:
+            first = False
+        else:
+            yield joiner
+        yield joinfmt(x)
+
+@templatefunc('label(label, expr)')
+def label(context, mapping, args):
+    """Apply a label to generated content. Content with
+    a label applied can result in additional post-processing, such as
+    automatic colorization."""
+    if len(args) != 2:
+        # i18n: "label" is a keyword
+        raise error.ParseError(_("label expects two arguments"))
+
+    ui = context.resource(mapping, 'ui')
+    thing = evalstring(context, mapping, args[1])
+    # preserve unknown symbol as literal so effects like 'red', 'bold',
+    # etc. don't need to be quoted
+    label = evalstringliteral(context, mapping, args[0])
+
+    return ui.label(thing, label)
+
+@templatefunc('latesttag([pattern])')
+def latesttag(context, mapping, args):
+    """The global tags matching the given pattern on the
+    most recent globally tagged ancestor of this changeset.
+    If no such tags exist, the "{tag}" template resolves to
+    the string "null"."""
+    if len(args) > 1:
+        # i18n: "latesttag" is a keyword
+        raise error.ParseError(_("latesttag expects at most one argument"))
+
+    pattern = None
+    if len(args) == 1:
+        pattern = evalstring(context, mapping, args[0])
+    return templatekw.showlatesttags(context, mapping, pattern)
+
+@templatefunc('localdate(date[, tz])')
+def localdate(context, mapping, args):
+    """Converts a date to the specified timezone.
+    The default is local date."""
+    if not (1 <= len(args) <= 2):
+        # i18n: "localdate" is a keyword
+        raise error.ParseError(_("localdate expects one or two arguments"))
+
+    date = evalfuncarg(context, mapping, args[0])
+    try:
+        date = dateutil.parsedate(date)
+    except AttributeError:  # not str nor date tuple
+        # i18n: "localdate" is a keyword
+        raise error.ParseError(_("localdate expects a date information"))
+    if len(args) >= 2:
+        tzoffset = None
+        tz = evalfuncarg(context, mapping, args[1])
+        if isinstance(tz, bytes):
+            tzoffset, remainder = dateutil.parsetimezone(tz)
+            if remainder:
+                tzoffset = None
+        if tzoffset is None:
+            try:
+                tzoffset = int(tz)
+            except (TypeError, ValueError):
+                # i18n: "localdate" is a keyword
+                raise error.ParseError(_("localdate expects a timezone"))
+    else:
+        tzoffset = dateutil.makedate()[1]
+    return (date[0], tzoffset)
+
+@templatefunc('max(iterable)')
+def max_(context, mapping, args, **kwargs):
+    """Return the max of an iterable"""
+    if len(args) != 1:
+        # i18n: "max" is a keyword
+        raise error.ParseError(_("max expects one argument"))
+
+    iterable = evalfuncarg(context, mapping, args[0])
+    try:
+        x = max(pycompat.maybebytestr(iterable))
+    except (TypeError, ValueError):
+        # i18n: "max" is a keyword
+        raise error.ParseError(_("max first argument should be an iterable"))
+    return templateutil.wraphybridvalue(iterable, x, x)
+
+@templatefunc('min(iterable)')
+def min_(context, mapping, args, **kwargs):
+    """Return the min of an iterable"""
+    if len(args) != 1:
+        # i18n: "min" is a keyword
+        raise error.ParseError(_("min expects one argument"))
+
+    iterable = evalfuncarg(context, mapping, args[0])
+    try:
+        x = min(pycompat.maybebytestr(iterable))
+    except (TypeError, ValueError):
+        # i18n: "min" is a keyword
+        raise error.ParseError(_("min first argument should be an iterable"))
+    return templateutil.wraphybridvalue(iterable, x, x)
+
+@templatefunc('mod(a, b)')
+def mod(context, mapping, args):
+    """Calculate a mod b such that a / b + a mod b == a"""
+    if not len(args) == 2:
+        # i18n: "mod" is a keyword
+        raise error.ParseError(_("mod expects two arguments"))
+
+    func = lambda a, b: a % b
+    return templateutil.runarithmetic(context, mapping,
+                                      (func, args[0], args[1]))
+
+@templatefunc('obsfateoperations(markers)')
+def obsfateoperations(context, mapping, args):
+    """Compute obsfate related information based on markers (EXPERIMENTAL)"""
+    if len(args) != 1:
+        # i18n: "obsfateoperations" is a keyword
+        raise error.ParseError(_("obsfateoperations expects one argument"))
+
+    markers = evalfuncarg(context, mapping, args[0])
+
+    try:
+        data = obsutil.markersoperations(markers)
+        return templateutil.hybridlist(data, name='operation')
+    except (TypeError, KeyError):
+        # i18n: "obsfateoperations" is a keyword
+        errmsg = _("obsfateoperations first argument should be an iterable")
+        raise error.ParseError(errmsg)
+
+@templatefunc('obsfatedate(markers)')
+def obsfatedate(context, mapping, args):
+    """Compute obsfate related information based on markers (EXPERIMENTAL)"""
+    if len(args) != 1:
+        # i18n: "obsfatedate" is a keyword
+        raise error.ParseError(_("obsfatedate expects one argument"))
+
+    markers = evalfuncarg(context, mapping, args[0])
+
+    try:
+        data = obsutil.markersdates(markers)
+        return templateutil.hybridlist(data, name='date', fmt='%d %d')
+    except (TypeError, KeyError):
+        # i18n: "obsfatedate" is a keyword
+        errmsg = _("obsfatedate first argument should be an iterable")
+        raise error.ParseError(errmsg)
+
+@templatefunc('obsfateusers(markers)')
+def obsfateusers(context, mapping, args):
+    """Compute obsfate related information based on markers (EXPERIMENTAL)"""
+    if len(args) != 1:
+        # i18n: "obsfateusers" is a keyword
+        raise error.ParseError(_("obsfateusers expects one argument"))
+
+    markers = evalfuncarg(context, mapping, args[0])
+
+    try:
+        data = obsutil.markersusers(markers)
+        return templateutil.hybridlist(data, name='user')
+    except (TypeError, KeyError, ValueError):
+        # i18n: "obsfateusers" is a keyword
+        msg = _("obsfateusers first argument should be an iterable of "
+                "obsmakers")
+        raise error.ParseError(msg)
+
+@templatefunc('obsfateverb(successors, markers)')
+def obsfateverb(context, mapping, args):
+    """Compute obsfate related information based on successors (EXPERIMENTAL)"""
+    if len(args) != 2:
+        # i18n: "obsfateverb" is a keyword
+        raise error.ParseError(_("obsfateverb expects two arguments"))
+
+    successors = evalfuncarg(context, mapping, args[0])
+    markers = evalfuncarg(context, mapping, args[1])
+
+    try:
+        return obsutil.obsfateverb(successors, markers)
+    except TypeError:
+        # i18n: "obsfateverb" is a keyword
+        errmsg = _("obsfateverb first argument should be countable")
+        raise error.ParseError(errmsg)
+
+@templatefunc('relpath(path)')
+def relpath(context, mapping, args):
+    """Convert a repository-absolute path into a filesystem path relative to
+    the current working directory."""
+    if len(args) != 1:
+        # i18n: "relpath" is a keyword
+        raise error.ParseError(_("relpath expects one argument"))
+
+    repo = context.resource(mapping, 'ctx').repo()
+    path = evalstring(context, mapping, args[0])
+    return repo.pathto(path)
+
+@templatefunc('revset(query[, formatargs...])')
+def revset(context, mapping, args):
+    """Execute a revision set query. See
+    :hg:`help revset`."""
+    if not len(args) > 0:
+        # i18n: "revset" is a keyword
+        raise error.ParseError(_("revset expects one or more arguments"))
+
+    raw = evalstring(context, mapping, args[0])
+    ctx = context.resource(mapping, 'ctx')
+    repo = ctx.repo()
+
+    def query(expr):
+        m = revsetmod.match(repo.ui, expr, repo=repo)
+        return m(repo)
+
+    if len(args) > 1:
+        formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
+        revs = query(revsetlang.formatspec(raw, *formatargs))
+        revs = list(revs)
+    else:
+        cache = context.resource(mapping, 'cache')
+        revsetcache = cache.setdefault("revsetcache", {})
+        if raw in revsetcache:
+            revs = revsetcache[raw]
+        else:
+            revs = query(raw)
+            revs = list(revs)
+            revsetcache[raw] = revs
+    return templatekw.showrevslist(context, mapping, "revision", revs)
+
+@templatefunc('rstdoc(text, style)')
+def rstdoc(context, mapping, args):
+    """Format reStructuredText."""
+    if len(args) != 2:
+        # i18n: "rstdoc" is a keyword
+        raise error.ParseError(_("rstdoc expects two arguments"))
+
+    text = evalstring(context, mapping, args[0])
+    style = evalstring(context, mapping, args[1])
+
+    return minirst.format(text, style=style, keep=['verbose'])
+
+@templatefunc('separate(sep, args)', argspec='sep *args')
+def separate(context, mapping, args):
+    """Add a separator between non-empty arguments."""
+    if 'sep' not in args:
+        # i18n: "separate" is a keyword
+        raise error.ParseError(_("separate expects at least one argument"))
+
+    sep = evalstring(context, mapping, args['sep'])
+    first = True
+    for arg in args['args']:
+        argstr = evalstring(context, mapping, arg)
+        if not argstr:
+            continue
+        if first:
+            first = False
+        else:
+            yield sep
+        yield argstr
+
+@templatefunc('shortest(node, minlength=4)')
+def shortest(context, mapping, args):
+    """Obtain the shortest representation of
+    a node."""
+    if not (1 <= len(args) <= 2):
+        # i18n: "shortest" is a keyword
+        raise error.ParseError(_("shortest() expects one or two arguments"))
+
+    node = evalstring(context, mapping, args[0])
+
+    minlength = 4
+    if len(args) > 1:
+        minlength = evalinteger(context, mapping, args[1],
+                                # i18n: "shortest" is a keyword
+                                _("shortest() expects an integer minlength"))
+
+    # _partialmatch() of filtered changelog could take O(len(repo)) time,
+    # which would be unacceptably slow. so we look for hash collision in
+    # unfiltered space, which means some hashes may be slightly longer.
+    cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
+    return cl.shortest(node, minlength)
+
+@templatefunc('strip(text[, chars])')
+def strip(context, mapping, args):
+    """Strip characters from a string. By default,
+    strips all leading and trailing whitespace."""
+    if not (1 <= len(args) <= 2):
+        # i18n: "strip" is a keyword
+        raise error.ParseError(_("strip expects one or two arguments"))
+
+    text = evalstring(context, mapping, args[0])
+    if len(args) == 2:
+        chars = evalstring(context, mapping, args[1])
+        return text.strip(chars)
+    return text.strip()
+
+@templatefunc('sub(pattern, replacement, expression)')
+def sub(context, mapping, args):
+    """Perform text substitution
+    using regular expressions."""
+    if len(args) != 3:
+        # i18n: "sub" is a keyword
+        raise error.ParseError(_("sub expects three arguments"))
+
+    pat = evalstring(context, mapping, args[0])
+    rpl = evalstring(context, mapping, args[1])
+    src = evalstring(context, mapping, args[2])
+    try:
+        patre = re.compile(pat)
+    except re.error:
+        # i18n: "sub" is a keyword
+        raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
+    try:
+        yield patre.sub(rpl, src)
+    except re.error:
+        # i18n: "sub" is a keyword
+        raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
+
+@templatefunc('startswith(pattern, text)')
+def startswith(context, mapping, args):
+    """Returns the value from the "text" argument
+    if it begins with the content from the "pattern" argument."""
+    if len(args) != 2:
+        # i18n: "startswith" is a keyword
+        raise error.ParseError(_("startswith expects two arguments"))
+
+    patn = evalstring(context, mapping, args[0])
+    text = evalstring(context, mapping, args[1])
+    if text.startswith(patn):
+        return text
+    return ''
+
+@templatefunc('word(number, text[, separator])')
+def word(context, mapping, args):
+    """Return the nth word from a string."""
+    if not (2 <= len(args) <= 3):
+        # i18n: "word" is a keyword
+        raise error.ParseError(_("word expects two or three arguments, got %d")
+                               % len(args))
+
+    num = evalinteger(context, mapping, args[0],
+                      # i18n: "word" is a keyword
+                      _("word expects an integer index"))
+    text = evalstring(context, mapping, args[1])
+    if len(args) == 3:
+        splitter = evalstring(context, mapping, args[2])
+    else:
+        splitter = None
+
+    tokens = text.split(splitter)
+    if num >= len(tokens) or num < -len(tokens):
+        return ''
+    else:
+        return tokens[num]
+
+def loadfunction(ui, extname, registrarobj):
+    """Load template function from specified registrarobj
+    """
+    for name, func in registrarobj._table.iteritems():
+        funcs[name] = func
+
+# tell hggettext to extract docstrings from these functions:
+i18nfunctions = funcs.values()
--- a/mercurial/templater.py	Thu Mar 08 23:15:09 2018 +0900
+++ b/mercurial/templater.py	Thu Mar 08 22:23:02 2018 +0900
@@ -8,36 +8,19 @@
 from __future__ import absolute_import, print_function
 
 import os
-import re
 
 from .i18n import _
 from . import (
-    color,
     config,
     encoding,
     error,
-    minirst,
-    obsutil,
     parser,
     pycompat,
-    registrar,
-    revset as revsetmod,
-    revsetlang,
-    scmutil,
     templatefilters,
-    templatekw,
+    templatefuncs,
     templateutil,
     util,
 )
-from .utils import dateutil
-
-evalrawexp = templateutil.evalrawexp
-evalfuncarg = templateutil.evalfuncarg
-evalboolean = templateutil.evalboolean
-evalinteger = templateutil.evalinteger
-evalstring = templateutil.evalstring
-evalstringliteral = templateutil.evalstringliteral
-evalastype = templateutil.evalastype
 
 # template parsing
 
@@ -455,625 +438,6 @@
 def buildkeyvaluepair(exp, content):
     raise error.ParseError(_("can't use a key-value pair in this context"))
 
-# dict of template built-in functions
-funcs = {}
-
-templatefunc = registrar.templatefunc(funcs)
-
-@templatefunc('date(date[, fmt])')
-def date(context, mapping, args):
-    """Format a date. See :hg:`help dates` for formatting
-    strings. The default is a Unix date format, including the timezone:
-    "Mon Sep 04 15:13:13 2006 0700"."""
-    if not (1 <= len(args) <= 2):
-        # i18n: "date" is a keyword
-        raise error.ParseError(_("date expects one or two arguments"))
-
-    date = evalfuncarg(context, mapping, args[0])
-    fmt = None
-    if len(args) == 2:
-        fmt = evalstring(context, mapping, args[1])
-    try:
-        if fmt is None:
-            return dateutil.datestr(date)
-        else:
-            return dateutil.datestr(date, fmt)
-    except (TypeError, ValueError):
-        # i18n: "date" is a keyword
-        raise error.ParseError(_("date expects a date information"))
-
-@templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
-def dict_(context, mapping, args):
-    """Construct a dict from key-value pairs. A key may be omitted if
-    a value expression can provide an unambiguous name."""
-    data = util.sortdict()
-
-    for v in args['args']:
-        k = templateutil.findsymbolicname(v)
-        if not k:
-            raise error.ParseError(_('dict key cannot be inferred'))
-        if k in data or k in args['kwargs']:
-            raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
-        data[k] = evalfuncarg(context, mapping, v)
-
-    data.update((k, evalfuncarg(context, mapping, v))
-                for k, v in args['kwargs'].iteritems())
-    return templateutil.hybriddict(data)
-
-@templatefunc('diff([includepattern [, excludepattern]])')
-def diff(context, mapping, args):
-    """Show a diff, optionally
-    specifying files to include or exclude."""
-    if len(args) > 2:
-        # i18n: "diff" is a keyword
-        raise error.ParseError(_("diff expects zero, one, or two arguments"))
-
-    def getpatterns(i):
-        if i < len(args):
-            s = evalstring(context, mapping, args[i]).strip()
-            if s:
-                return [s]
-        return []
-
-    ctx = context.resource(mapping, 'ctx')
-    chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
-
-    return ''.join(chunks)
-
-@templatefunc('extdata(source)', argspec='source')
-def extdata(context, mapping, args):
-    """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
-    if 'source' not in args:
-        # i18n: "extdata" is a keyword
-        raise error.ParseError(_('extdata expects one argument'))
-
-    source = evalstring(context, mapping, args['source'])
-    cache = context.resource(mapping, 'cache').setdefault('extdata', {})
-    ctx = context.resource(mapping, 'ctx')
-    if source in cache:
-        data = cache[source]
-    else:
-        data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
-    return data.get(ctx.rev(), '')
-
-@templatefunc('files(pattern)')
-def files(context, mapping, args):
-    """All files of the current changeset matching the pattern. See
-    :hg:`help patterns`."""
-    if not len(args) == 1:
-        # i18n: "files" is a keyword
-        raise error.ParseError(_("files expects one argument"))
-
-    raw = evalstring(context, mapping, args[0])
-    ctx = context.resource(mapping, 'ctx')
-    m = ctx.match([raw])
-    files = list(ctx.matches(m))
-    return templateutil.compatlist(context, mapping, "file", files)
-
-@templatefunc('fill(text[, width[, initialident[, hangindent]]])')
-def fill(context, mapping, args):
-    """Fill many
-    paragraphs with optional indentation. See the "fill" filter."""
-    if not (1 <= len(args) <= 4):
-        # i18n: "fill" is a keyword
-        raise error.ParseError(_("fill expects one to four arguments"))
-
-    text = evalstring(context, mapping, args[0])
-    width = 76
-    initindent = ''
-    hangindent = ''
-    if 2 <= len(args) <= 4:
-        width = evalinteger(context, mapping, args[1],
-                            # i18n: "fill" is a keyword
-                            _("fill expects an integer width"))
-        try:
-            initindent = evalstring(context, mapping, args[2])
-            hangindent = evalstring(context, mapping, args[3])
-        except IndexError:
-            pass
-
-    return templatefilters.fill(text, width, initindent, hangindent)
-
-@templatefunc('formatnode(node)')
-def formatnode(context, mapping, args):
-    """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
-    if len(args) != 1:
-        # i18n: "formatnode" is a keyword
-        raise error.ParseError(_("formatnode expects one argument"))
-
-    ui = context.resource(mapping, 'ui')
-    node = evalstring(context, mapping, args[0])
-    if ui.debugflag:
-        return node
-    return templatefilters.short(node)
-
-@templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
-              argspec='text width fillchar left')
-def pad(context, mapping, args):
-    """Pad text with a
-    fill character."""
-    if 'text' not in args or 'width' not in args:
-        # i18n: "pad" is a keyword
-        raise error.ParseError(_("pad() expects two to four arguments"))
-
-    width = evalinteger(context, mapping, args['width'],
-                        # i18n: "pad" is a keyword
-                        _("pad() expects an integer width"))
-
-    text = evalstring(context, mapping, args['text'])
-
-    left = False
-    fillchar = ' '
-    if 'fillchar' in args:
-        fillchar = evalstring(context, mapping, args['fillchar'])
-        if len(color.stripeffects(fillchar)) != 1:
-            # i18n: "pad" is a keyword
-            raise error.ParseError(_("pad() expects a single fill character"))
-    if 'left' in args:
-        left = evalboolean(context, mapping, args['left'])
-
-    fillwidth = width - encoding.colwidth(color.stripeffects(text))
-    if fillwidth <= 0:
-        return text
-    if left:
-        return fillchar * fillwidth + text
-    else:
-        return text + fillchar * fillwidth
-
-@templatefunc('indent(text, indentchars[, firstline])')
-def indent(context, mapping, args):
-    """Indents all non-empty lines
-    with the characters given in the indentchars string. An optional
-    third parameter will override the indent for the first line only
-    if present."""
-    if not (2 <= len(args) <= 3):
-        # i18n: "indent" is a keyword
-        raise error.ParseError(_("indent() expects two or three arguments"))
-
-    text = evalstring(context, mapping, args[0])
-    indent = evalstring(context, mapping, args[1])
-
-    if len(args) == 3:
-        firstline = evalstring(context, mapping, args[2])
-    else:
-        firstline = indent
-
-    # the indent function doesn't indent the first line, so we do it here
-    return templatefilters.indent(firstline + text, indent)
-
-@templatefunc('get(dict, key)')
-def get(context, mapping, args):
-    """Get an attribute/key from an object. Some keywords
-    are complex types. This function allows you to obtain the value of an
-    attribute on these types."""
-    if len(args) != 2:
-        # i18n: "get" is a keyword
-        raise error.ParseError(_("get() expects two arguments"))
-
-    dictarg = evalfuncarg(context, mapping, args[0])
-    if not util.safehasattr(dictarg, 'get'):
-        # i18n: "get" is a keyword
-        raise error.ParseError(_("get() expects a dict as first argument"))
-
-    key = evalfuncarg(context, mapping, args[1])
-    return templateutil.getdictitem(dictarg, key)
-
-@templatefunc('if(expr, then[, else])')
-def if_(context, mapping, args):
-    """Conditionally execute based on the result of
-    an expression."""
-    if not (2 <= len(args) <= 3):
-        # i18n: "if" is a keyword
-        raise error.ParseError(_("if expects two or three arguments"))
-
-    test = evalboolean(context, mapping, args[0])
-    if test:
-        yield evalrawexp(context, mapping, args[1])
-    elif len(args) == 3:
-        yield evalrawexp(context, mapping, args[2])
-
-@templatefunc('ifcontains(needle, haystack, then[, else])')
-def ifcontains(context, mapping, args):
-    """Conditionally execute based
-    on whether the item "needle" is in "haystack"."""
-    if not (3 <= len(args) <= 4):
-        # i18n: "ifcontains" is a keyword
-        raise error.ParseError(_("ifcontains expects three or four arguments"))
-
-    haystack = evalfuncarg(context, mapping, args[1])
-    try:
-        needle = evalastype(context, mapping, args[0],
-                            getattr(haystack, 'keytype', None) or bytes)
-        found = (needle in haystack)
-    except error.ParseError:
-        found = False
-
-    if found:
-        yield evalrawexp(context, mapping, args[2])
-    elif len(args) == 4:
-        yield evalrawexp(context, mapping, args[3])
-
-@templatefunc('ifeq(expr1, expr2, then[, else])')
-def ifeq(context, mapping, args):
-    """Conditionally execute based on
-    whether 2 items are equivalent."""
-    if not (3 <= len(args) <= 4):
-        # i18n: "ifeq" is a keyword
-        raise error.ParseError(_("ifeq expects three or four arguments"))
-
-    test = evalstring(context, mapping, args[0])
-    match = evalstring(context, mapping, args[1])
-    if test == match:
-        yield evalrawexp(context, mapping, args[2])
-    elif len(args) == 4:
-        yield evalrawexp(context, mapping, args[3])
-
-@templatefunc('join(list, sep)')
-def join(context, mapping, args):
-    """Join items in a list with a delimiter."""
-    if not (1 <= len(args) <= 2):
-        # i18n: "join" is a keyword
-        raise error.ParseError(_("join expects one or two arguments"))
-
-    # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
-    # abuses generator as a keyword that returns a list of dicts.
-    joinset = evalrawexp(context, mapping, args[0])
-    joinset = templateutil.unwrapvalue(joinset)
-    joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
-    joiner = " "
-    if len(args) > 1:
-        joiner = evalstring(context, mapping, args[1])
-
-    first = True
-    for x in pycompat.maybebytestr(joinset):
-        if first:
-            first = False
-        else:
-            yield joiner
-        yield joinfmt(x)
-
-@templatefunc('label(label, expr)')
-def label(context, mapping, args):
-    """Apply a label to generated content. Content with
-    a label applied can result in additional post-processing, such as
-    automatic colorization."""
-    if len(args) != 2:
-        # i18n: "label" is a keyword
-        raise error.ParseError(_("label expects two arguments"))
-
-    ui = context.resource(mapping, 'ui')
-    thing = evalstring(context, mapping, args[1])
-    # preserve unknown symbol as literal so effects like 'red', 'bold',
-    # etc. don't need to be quoted
-    label = evalstringliteral(context, mapping, args[0])
-
-    return ui.label(thing, label)
-
-@templatefunc('latesttag([pattern])')
-def latesttag(context, mapping, args):
-    """The global tags matching the given pattern on the
-    most recent globally tagged ancestor of this changeset.
-    If no such tags exist, the "{tag}" template resolves to
-    the string "null"."""
-    if len(args) > 1:
-        # i18n: "latesttag" is a keyword
-        raise error.ParseError(_("latesttag expects at most one argument"))
-
-    pattern = None
-    if len(args) == 1:
-        pattern = evalstring(context, mapping, args[0])
-    return templatekw.showlatesttags(context, mapping, pattern)
-
-@templatefunc('localdate(date[, tz])')
-def localdate(context, mapping, args):
-    """Converts a date to the specified timezone.
-    The default is local date."""
-    if not (1 <= len(args) <= 2):
-        # i18n: "localdate" is a keyword
-        raise error.ParseError(_("localdate expects one or two arguments"))
-
-    date = evalfuncarg(context, mapping, args[0])
-    try:
-        date = dateutil.parsedate(date)
-    except AttributeError:  # not str nor date tuple
-        # i18n: "localdate" is a keyword
-        raise error.ParseError(_("localdate expects a date information"))
-    if len(args) >= 2:
-        tzoffset = None
-        tz = evalfuncarg(context, mapping, args[1])
-        if isinstance(tz, bytes):
-            tzoffset, remainder = dateutil.parsetimezone(tz)
-            if remainder:
-                tzoffset = None
-        if tzoffset is None:
-            try:
-                tzoffset = int(tz)
-            except (TypeError, ValueError):
-                # i18n: "localdate" is a keyword
-                raise error.ParseError(_("localdate expects a timezone"))
-    else:
-        tzoffset = dateutil.makedate()[1]
-    return (date[0], tzoffset)
-
-@templatefunc('max(iterable)')
-def max_(context, mapping, args, **kwargs):
-    """Return the max of an iterable"""
-    if len(args) != 1:
-        # i18n: "max" is a keyword
-        raise error.ParseError(_("max expects one argument"))
-
-    iterable = evalfuncarg(context, mapping, args[0])
-    try:
-        x = max(pycompat.maybebytestr(iterable))
-    except (TypeError, ValueError):
-        # i18n: "max" is a keyword
-        raise error.ParseError(_("max first argument should be an iterable"))
-    return templateutil.wraphybridvalue(iterable, x, x)
-
-@templatefunc('min(iterable)')
-def min_(context, mapping, args, **kwargs):
-    """Return the min of an iterable"""
-    if len(args) != 1:
-        # i18n: "min" is a keyword
-        raise error.ParseError(_("min expects one argument"))
-
-    iterable = evalfuncarg(context, mapping, args[0])
-    try:
-        x = min(pycompat.maybebytestr(iterable))
-    except (TypeError, ValueError):
-        # i18n: "min" is a keyword
-        raise error.ParseError(_("min first argument should be an iterable"))
-    return templateutil.wraphybridvalue(iterable, x, x)
-
-@templatefunc('mod(a, b)')
-def mod(context, mapping, args):
-    """Calculate a mod b such that a / b + a mod b == a"""
-    if not len(args) == 2:
-        # i18n: "mod" is a keyword
-        raise error.ParseError(_("mod expects two arguments"))
-
-    func = lambda a, b: a % b
-    return templateutil.runarithmetic(context, mapping,
-                                      (func, args[0], args[1]))
-
-@templatefunc('obsfateoperations(markers)')
-def obsfateoperations(context, mapping, args):
-    """Compute obsfate related information based on markers (EXPERIMENTAL)"""
-    if len(args) != 1:
-        # i18n: "obsfateoperations" is a keyword
-        raise error.ParseError(_("obsfateoperations expects one argument"))
-
-    markers = evalfuncarg(context, mapping, args[0])
-
-    try:
-        data = obsutil.markersoperations(markers)
-        return templateutil.hybridlist(data, name='operation')
-    except (TypeError, KeyError):
-        # i18n: "obsfateoperations" is a keyword
-        errmsg = _("obsfateoperations first argument should be an iterable")
-        raise error.ParseError(errmsg)
-
-@templatefunc('obsfatedate(markers)')
-def obsfatedate(context, mapping, args):
-    """Compute obsfate related information based on markers (EXPERIMENTAL)"""
-    if len(args) != 1:
-        # i18n: "obsfatedate" is a keyword
-        raise error.ParseError(_("obsfatedate expects one argument"))
-
-    markers = evalfuncarg(context, mapping, args[0])
-
-    try:
-        data = obsutil.markersdates(markers)
-        return templateutil.hybridlist(data, name='date', fmt='%d %d')
-    except (TypeError, KeyError):
-        # i18n: "obsfatedate" is a keyword
-        errmsg = _("obsfatedate first argument should be an iterable")
-        raise error.ParseError(errmsg)
-
-@templatefunc('obsfateusers(markers)')
-def obsfateusers(context, mapping, args):
-    """Compute obsfate related information based on markers (EXPERIMENTAL)"""
-    if len(args) != 1:
-        # i18n: "obsfateusers" is a keyword
-        raise error.ParseError(_("obsfateusers expects one argument"))
-
-    markers = evalfuncarg(context, mapping, args[0])
-
-    try:
-        data = obsutil.markersusers(markers)
-        return templateutil.hybridlist(data, name='user')
-    except (TypeError, KeyError, ValueError):
-        # i18n: "obsfateusers" is a keyword
-        msg = _("obsfateusers first argument should be an iterable of "
-                "obsmakers")
-        raise error.ParseError(msg)
-
-@templatefunc('obsfateverb(successors, markers)')
-def obsfateverb(context, mapping, args):
-    """Compute obsfate related information based on successors (EXPERIMENTAL)"""
-    if len(args) != 2:
-        # i18n: "obsfateverb" is a keyword
-        raise error.ParseError(_("obsfateverb expects two arguments"))
-
-    successors = evalfuncarg(context, mapping, args[0])
-    markers = evalfuncarg(context, mapping, args[1])
-
-    try:
-        return obsutil.obsfateverb(successors, markers)
-    except TypeError:
-        # i18n: "obsfateverb" is a keyword
-        errmsg = _("obsfateverb first argument should be countable")
-        raise error.ParseError(errmsg)
-
-@templatefunc('relpath(path)')
-def relpath(context, mapping, args):
-    """Convert a repository-absolute path into a filesystem path relative to
-    the current working directory."""
-    if len(args) != 1:
-        # i18n: "relpath" is a keyword
-        raise error.ParseError(_("relpath expects one argument"))
-
-    repo = context.resource(mapping, 'ctx').repo()
-    path = evalstring(context, mapping, args[0])
-    return repo.pathto(path)
-
-@templatefunc('revset(query[, formatargs...])')
-def revset(context, mapping, args):
-    """Execute a revision set query. See
-    :hg:`help revset`."""
-    if not len(args) > 0:
-        # i18n: "revset" is a keyword
-        raise error.ParseError(_("revset expects one or more arguments"))
-
-    raw = evalstring(context, mapping, args[0])
-    ctx = context.resource(mapping, 'ctx')
-    repo = ctx.repo()
-
-    def query(expr):
-        m = revsetmod.match(repo.ui, expr, repo=repo)
-        return m(repo)
-
-    if len(args) > 1:
-        formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
-        revs = query(revsetlang.formatspec(raw, *formatargs))
-        revs = list(revs)
-    else:
-        cache = context.resource(mapping, 'cache')
-        revsetcache = cache.setdefault("revsetcache", {})
-        if raw in revsetcache:
-            revs = revsetcache[raw]
-        else:
-            revs = query(raw)
-            revs = list(revs)
-            revsetcache[raw] = revs
-    return templatekw.showrevslist(context, mapping, "revision", revs)
-
-@templatefunc('rstdoc(text, style)')
-def rstdoc(context, mapping, args):
-    """Format reStructuredText."""
-    if len(args) != 2:
-        # i18n: "rstdoc" is a keyword
-        raise error.ParseError(_("rstdoc expects two arguments"))
-
-    text = evalstring(context, mapping, args[0])
-    style = evalstring(context, mapping, args[1])
-
-    return minirst.format(text, style=style, keep=['verbose'])
-
-@templatefunc('separate(sep, args)', argspec='sep *args')
-def separate(context, mapping, args):
-    """Add a separator between non-empty arguments."""
-    if 'sep' not in args:
-        # i18n: "separate" is a keyword
-        raise error.ParseError(_("separate expects at least one argument"))
-
-    sep = evalstring(context, mapping, args['sep'])
-    first = True
-    for arg in args['args']:
-        argstr = evalstring(context, mapping, arg)
-        if not argstr:
-            continue
-        if first:
-            first = False
-        else:
-            yield sep
-        yield argstr
-
-@templatefunc('shortest(node, minlength=4)')
-def shortest(context, mapping, args):
-    """Obtain the shortest representation of
-    a node."""
-    if not (1 <= len(args) <= 2):
-        # i18n: "shortest" is a keyword
-        raise error.ParseError(_("shortest() expects one or two arguments"))
-
-    node = evalstring(context, mapping, args[0])
-
-    minlength = 4
-    if len(args) > 1:
-        minlength = evalinteger(context, mapping, args[1],
-                                # i18n: "shortest" is a keyword
-                                _("shortest() expects an integer minlength"))
-
-    # _partialmatch() of filtered changelog could take O(len(repo)) time,
-    # which would be unacceptably slow. so we look for hash collision in
-    # unfiltered space, which means some hashes may be slightly longer.
-    cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
-    return cl.shortest(node, minlength)
-
-@templatefunc('strip(text[, chars])')
-def strip(context, mapping, args):
-    """Strip characters from a string. By default,
-    strips all leading and trailing whitespace."""
-    if not (1 <= len(args) <= 2):
-        # i18n: "strip" is a keyword
-        raise error.ParseError(_("strip expects one or two arguments"))
-
-    text = evalstring(context, mapping, args[0])
-    if len(args) == 2:
-        chars = evalstring(context, mapping, args[1])
-        return text.strip(chars)
-    return text.strip()
-
-@templatefunc('sub(pattern, replacement, expression)')
-def sub(context, mapping, args):
-    """Perform text substitution
-    using regular expressions."""
-    if len(args) != 3:
-        # i18n: "sub" is a keyword
-        raise error.ParseError(_("sub expects three arguments"))
-
-    pat = evalstring(context, mapping, args[0])
-    rpl = evalstring(context, mapping, args[1])
-    src = evalstring(context, mapping, args[2])
-    try:
-        patre = re.compile(pat)
-    except re.error:
-        # i18n: "sub" is a keyword
-        raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
-    try:
-        yield patre.sub(rpl, src)
-    except re.error:
-        # i18n: "sub" is a keyword
-        raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
-
-@templatefunc('startswith(pattern, text)')
-def startswith(context, mapping, args):
-    """Returns the value from the "text" argument
-    if it begins with the content from the "pattern" argument."""
-    if len(args) != 2:
-        # i18n: "startswith" is a keyword
-        raise error.ParseError(_("startswith expects two arguments"))
-
-    patn = evalstring(context, mapping, args[0])
-    text = evalstring(context, mapping, args[1])
-    if text.startswith(patn):
-        return text
-    return ''
-
-@templatefunc('word(number, text[, separator])')
-def word(context, mapping, args):
-    """Return the nth word from a string."""
-    if not (2 <= len(args) <= 3):
-        # i18n: "word" is a keyword
-        raise error.ParseError(_("word expects two or three arguments, got %d")
-                               % len(args))
-
-    num = evalinteger(context, mapping, args[0],
-                      # i18n: "word" is a keyword
-                      _("word expects an integer index"))
-    text = evalstring(context, mapping, args[1])
-    if len(args) == 3:
-        splitter = evalstring(context, mapping, args[2])
-    else:
-        splitter = None
-
-    tokens = text.split(splitter)
-    if num >= len(tokens) or num < -len(tokens):
-        return ''
-    else:
-        return tokens[num]
-
 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
 exprmethods = {
     "integer": lambda e, c: (templateutil.runinteger, e[1]),
@@ -1177,7 +541,7 @@
         if filters is None:
             filters = {}
         self._filters = filters
-        self._funcs = funcs  # make this a parameter if needed
+        self._funcs = templatefuncs.funcs  # make this a parameter if needed
         if defaults is None:
             defaults = {}
         if resources is None:
@@ -1433,12 +797,3 @@
                     return style, mapfile
 
     raise RuntimeError("No hgweb templates found in %r" % paths)
-
-def loadfunction(ui, extname, registrarobj):
-    """Load template function from specified registrarobj
-    """
-    for name, func in registrarobj._table.iteritems():
-        funcs[name] = func
-
-# tell hggettext to extract docstrings from these functions:
-i18nfunctions = funcs.values()