mercurial/templateutil.py
changeset 36921 32f9b7e3f056
parent 36920 6ff6e1d6b5b8
child 36982 255f635c3204
--- a/mercurial/templateutil.py	Thu Mar 08 23:10:46 2018 +0900
+++ b/mercurial/templateutil.py	Thu Mar 08 23:15:09 2018 +0900
@@ -13,7 +13,6 @@
 from . import (
     error,
     pycompat,
-    templatekw,
     util,
 )
 
@@ -23,9 +22,219 @@
 class TemplateNotFound(error.Abort):
     pass
 
+class hybrid(object):
+    """Wrapper for list or dict to support legacy template
+
+    This class allows us to handle both:
+    - "{files}" (legacy command-line-specific list hack) and
+    - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
+    and to access raw values:
+    - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
+    - "{get(extras, key)}"
+    - "{files|json}"
+    """
+
+    def __init__(self, gen, values, makemap, joinfmt, keytype=None):
+        if gen is not None:
+            self.gen = gen  # generator or function returning generator
+        self._values = values
+        self._makemap = makemap
+        self.joinfmt = joinfmt
+        self.keytype = keytype  # hint for 'x in y' where type(x) is unresolved
+    def gen(self):
+        """Default generator to stringify this as {join(self, ' ')}"""
+        for i, x in enumerate(self._values):
+            if i > 0:
+                yield ' '
+            yield self.joinfmt(x)
+    def itermaps(self):
+        makemap = self._makemap
+        for x in self._values:
+            yield makemap(x)
+    def __contains__(self, x):
+        return x in self._values
+    def __getitem__(self, key):
+        return self._values[key]
+    def __len__(self):
+        return len(self._values)
+    def __iter__(self):
+        return iter(self._values)
+    def __getattr__(self, name):
+        if name not in (r'get', r'items', r'iteritems', r'iterkeys',
+                        r'itervalues', r'keys', r'values'):
+            raise AttributeError(name)
+        return getattr(self._values, name)
+
+class mappable(object):
+    """Wrapper for non-list/dict object to support map operation
+
+    This class allows us to handle both:
+    - "{manifest}"
+    - "{manifest % '{rev}:{node}'}"
+    - "{manifest.rev}"
+
+    Unlike a hybrid, this does not simulate the behavior of the underling
+    value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
+    """
+
+    def __init__(self, gen, key, value, makemap):
+        if gen is not None:
+            self.gen = gen  # generator or function returning generator
+        self._key = key
+        self._value = value  # may be generator of strings
+        self._makemap = makemap
+
+    def gen(self):
+        yield pycompat.bytestr(self._value)
+
+    def tomap(self):
+        return self._makemap(self._key)
+
+    def itermaps(self):
+        yield self.tomap()
+
+def hybriddict(data, key='key', value='value', fmt=None, gen=None):
+    """Wrap data to support both dict-like and string-like operations"""
+    prefmt = pycompat.identity
+    if fmt is None:
+        fmt = '%s=%s'
+        prefmt = pycompat.bytestr
+    return hybrid(gen, data, lambda k: {key: k, value: data[k]},
+                  lambda k: fmt % (prefmt(k), prefmt(data[k])))
+
+def hybridlist(data, name, fmt=None, gen=None):
+    """Wrap data to support both list-like and string-like operations"""
+    prefmt = pycompat.identity
+    if fmt is None:
+        fmt = '%s'
+        prefmt = pycompat.bytestr
+    return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
+
+def unwraphybrid(thing):
+    """Return an object which can be stringified possibly by using a legacy
+    template"""
+    gen = getattr(thing, 'gen', None)
+    if gen is None:
+        return thing
+    if callable(gen):
+        return gen()
+    return gen
+
+def unwrapvalue(thing):
+    """Move the inner value object out of the wrapper"""
+    if not util.safehasattr(thing, '_value'):
+        return thing
+    return thing._value
+
+def wraphybridvalue(container, key, value):
+    """Wrap an element of hybrid container to be mappable
+
+    The key is passed to the makemap function of the given container, which
+    should be an item generated by iter(container).
+    """
+    makemap = getattr(container, '_makemap', None)
+    if makemap is None:
+        return value
+    if util.safehasattr(value, '_makemap'):
+        # a nested hybrid list/dict, which has its own way of map operation
+        return value
+    return mappable(None, key, value, makemap)
+
+def compatdict(context, mapping, name, data, key='key', value='value',
+               fmt=None, plural=None, separator=' '):
+    """Wrap data like hybriddict(), but also supports old-style list template
+
+    This exists for backward compatibility with the old-style template. Use
+    hybriddict() for new template keywords.
+    """
+    c = [{key: k, value: v} for k, v in data.iteritems()]
+    t = context.resource(mapping, 'templ')
+    f = _showlist(name, c, t, mapping, plural, separator)
+    return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
+
+def compatlist(context, mapping, name, data, element=None, fmt=None,
+               plural=None, separator=' '):
+    """Wrap data like hybridlist(), but also supports old-style list template
+
+    This exists for backward compatibility with the old-style template. Use
+    hybridlist() for new template keywords.
+    """
+    t = context.resource(mapping, 'templ')
+    f = _showlist(name, data, t, mapping, plural, separator)
+    return hybridlist(data, name=element or name, fmt=fmt, gen=f)
+
+def _showlist(name, values, templ, mapping, plural=None, separator=' '):
+    '''expand set of values.
+    name is name of key in template map.
+    values is list of strings or dicts.
+    plural is plural of name, if not simply name + 's'.
+    separator is used to join values as a string
+
+    expansion works like this, given name 'foo'.
+
+    if values is empty, expand 'no_foos'.
+
+    if 'foo' not in template map, return values as a string,
+    joined by 'separator'.
+
+    expand 'start_foos'.
+
+    for each value, expand 'foo'. if 'last_foo' in template
+    map, expand it instead of 'foo' for last key.
+
+    expand 'end_foos'.
+    '''
+    strmapping = pycompat.strkwargs(mapping)
+    if not plural:
+        plural = name + 's'
+    if not values:
+        noname = 'no_' + plural
+        if noname in templ:
+            yield templ(noname, **strmapping)
+        return
+    if name not in templ:
+        if isinstance(values[0], bytes):
+            yield separator.join(values)
+        else:
+            for v in values:
+                r = dict(v)
+                r.update(mapping)
+                yield r
+        return
+    startname = 'start_' + plural
+    if startname in templ:
+        yield templ(startname, **strmapping)
+    vmapping = mapping.copy()
+    def one(v, tag=name):
+        try:
+            vmapping.update(v)
+        # Python 2 raises ValueError if the type of v is wrong. Python
+        # 3 raises TypeError.
+        except (AttributeError, TypeError, ValueError):
+            try:
+                # Python 2 raises ValueError trying to destructure an e.g.
+                # bytes. Python 3 raises TypeError.
+                for a, b in v:
+                    vmapping[a] = b
+            except (TypeError, ValueError):
+                vmapping[name] = v
+        return templ(tag, **pycompat.strkwargs(vmapping))
+    lastname = 'last_' + name
+    if lastname in templ:
+        last = values.pop()
+    else:
+        last = None
+    for v in values:
+        yield one(v)
+    if last is not None:
+        yield one(last, tag=lastname)
+    endname = 'end_' + plural
+    if endname in templ:
+        yield templ(endname, **strmapping)
+
 def stringify(thing):
     """Turn values into bytes by converting into text and concatenating them"""
-    thing = templatekw.unwraphybrid(thing)
+    thing = unwraphybrid(thing)
     if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
         if isinstance(thing, str):
             # This is only reachable on Python 3 (otherwise
@@ -59,7 +268,7 @@
 def evalfuncarg(context, mapping, arg):
     """Evaluate given argument as value type"""
     thing = evalrawexp(context, mapping, arg)
-    thing = templatekw.unwrapvalue(thing)
+    thing = unwrapvalue(thing)
     # evalrawexp() may return string, generator of strings or arbitrary object
     # such as date tuple, but filter does not want generator.
     if isinstance(thing, types.GeneratorType):
@@ -76,7 +285,7 @@
             thing = util.parsebool(data)
     else:
         thing = func(context, mapping, data)
-    thing = templatekw.unwrapvalue(thing)
+    thing = unwrapvalue(thing)
     if isinstance(thing, bool):
         return thing
     # other objects are evaluated as strings, which means 0 is True, but
@@ -236,4 +445,4 @@
     val = dictarg.get(key)
     if val is None:
         return
-    return templatekw.wraphybridvalue(dictarg, key, val)
+    return wraphybridvalue(dictarg, key, val)