--- 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()