1# templater.py - template expansion for output
2#
3# Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8"""Slightly complicated template engine for commands and hgweb
9
10This module provides low-level interface to the template engine. See the
11formatter and cmdutil modules if you are looking for high-level functions
12such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
14Internal Data Types
15-------------------
16
17Template keywords and functions take a dictionary of current symbols and
18resources (a "mapping") and return result. Inputs and outputs must be one
19of the following data types:
20
21bytes
22    a byte string, which is generally a human-readable text in local encoding.
23
24generator
25    a lazily-evaluated byte string, which is a possibly nested generator of
26    values of any printable types, and  will be folded by ``stringify()``
27    or ``flatten()``.
28
29None
30    sometimes represents an empty value, which can be stringified to ''.
31
32True, False, int, float
33    can be stringified as such.
34
35wrappedbytes, wrappedvalue
36    a wrapper for the above printable types.
37
38date
39    represents a (unixtime, offset) tuple.
40
41hybrid
42    represents a list/dict of printable values, which can also be converted
43    to mappings by % operator.
44
45hybriditem
46    represents a scalar printable value, also supports % operator.
47
48revslist
49    represents a list of revision numbers.
50
51mappinggenerator, mappinglist
52    represents mappings (i.e. a list of dicts), which may have default
53    output format.
54
55mappingdict
56    represents a single mapping (i.e. a dict), which may have default output
57    format.
58
59mappingnone
60    represents None of Optional[mappable], which will be mapped to an empty
61    string by % operation.
62
63mappedgenerator
64    a lazily-evaluated list of byte strings, which is e.g. a result of %
65    operation.
66"""
67
68from __future__ import absolute_import, print_function
69
70import abc
71import os
72
73from .i18n import _
74from .pycompat import (
75    FileNotFoundError,
76    getattr,
77)
78from . import (
79    config,
80    encoding,
81    error,
82    parser,
83    pycompat,
84    templatefilters,
85    templatefuncs,
86    templateutil,
87    util,
88)
89from .utils import (
90    resourceutil,
91    stringutil,
92)
93
94# template parsing
95
96elements = {
97    # token-type: binding-strength, primary, prefix, infix, suffix
98    b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
99    b".": (18, None, None, (b".", 18), None),
100    b"%": (15, None, None, (b"%", 15), None),
101    b"|": (15, None, None, (b"|", 15), None),
102    b"*": (5, None, None, (b"*", 5), None),
103    b"/": (5, None, None, (b"/", 5), None),
104    b"+": (4, None, None, (b"+", 4), None),
105    b"-": (4, None, (b"negate", 19), (b"-", 4), None),
106    b"=": (3, None, None, (b"keyvalue", 3), None),
107    b",": (2, None, None, (b"list", 2), None),
108    b")": (0, None, None, None, None),
109    b"integer": (0, b"integer", None, None, None),
110    b"symbol": (0, b"symbol", None, None, None),
111    b"string": (0, b"string", None, None, None),
112    b"template": (0, b"template", None, None, None),
113    b"end": (0, None, None, None, None),
114}
115
116
117def tokenize(program, start, end, term=None):
118    """Parse a template expression into a stream of tokens, which must end
119    with term if specified"""
120    pos = start
121    program = pycompat.bytestr(program)
122    while pos < end:
123        c = program[pos]
124        if c.isspace():  # skip inter-token whitespace
125            pass
126        elif c in b"(=,).%|+-*/":  # handle simple operators
127            yield (c, None, pos)
128        elif c in b'"\'':  # handle quoted templates
129            s = pos + 1
130            data, pos = _parsetemplate(program, s, end, c)
131            yield (b'template', data, s)
132            pos -= 1
133        elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
134            # handle quoted strings
135            c = program[pos + 1]
136            s = pos = pos + 2
137            while pos < end:  # find closing quote
138                d = program[pos]
139                if d == b'\\':  # skip over escaped characters
140                    pos += 2
141                    continue
142                if d == c:
143                    yield (b'string', program[s:pos], s)
144                    break
145                pos += 1
146            else:
147                raise error.ParseError(_(b"unterminated string"), s)
148        elif c.isdigit():
149            s = pos
150            while pos < end:
151                d = program[pos]
152                if not d.isdigit():
153                    break
154                pos += 1
155            yield (b'integer', program[s:pos], s)
156            pos -= 1
157        elif (
158            c == b'\\'
159            and program[pos : pos + 2] in (br"\'", br'\"')
160            or c == b'r'
161            and program[pos : pos + 3] in (br"r\'", br'r\"')
162        ):
163            # handle escaped quoted strings for compatibility with 2.9.2-3.4,
164            # where some of nested templates were preprocessed as strings and
165            # then compiled. therefore, \"...\" was allowed. (issue4733)
166            #
167            # processing flow of _evalifliteral() at 5ab28a2e9962:
168            # outer template string    -> stringify()  -> compiletemplate()
169            # ------------------------    ------------    ------------------
170            # {f("\\\\ {g(\"\\\"\")}"}    \\ {g("\"")}    [r'\\', {g("\"")}]
171            #             ~~~~~~~~
172            #             escaped quoted string
173            if c == b'r':
174                pos += 1
175                token = b'string'
176            else:
177                token = b'template'
178            quote = program[pos : pos + 2]
179            s = pos = pos + 2
180            while pos < end:  # find closing escaped quote
181                if program.startswith(b'\\\\\\', pos, end):
182                    pos += 4  # skip over double escaped characters
183                    continue
184                if program.startswith(quote, pos, end):
185                    # interpret as if it were a part of an outer string
186                    data = parser.unescapestr(program[s:pos])
187                    if token == b'template':
188                        data = _parsetemplate(data, 0, len(data))[0]
189                    yield (token, data, s)
190                    pos += 1
191                    break
192                pos += 1
193            else:
194                raise error.ParseError(_(b"unterminated string"), s)
195        elif c.isalnum() or c in b'_':
196            s = pos
197            pos += 1
198            while pos < end:  # find end of symbol
199                d = program[pos]
200                if not (d.isalnum() or d == b"_"):
201                    break
202                pos += 1
203            sym = program[s:pos]
204            yield (b'symbol', sym, s)
205            pos -= 1
206        elif c == term:
207            yield (b'end', None, pos)
208            return
209        else:
210            raise error.ParseError(_(b"syntax error"), pos)
211        pos += 1
212    if term:
213        raise error.ParseError(_(b"unterminated template expansion"), start)
214    yield (b'end', None, pos)
215
216
217def _parsetemplate(tmpl, start, stop, quote=b''):
218    r"""
219    >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
220    ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
221    >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
222    ([('string', 'foo'), ('symbol', 'bar')], 9)
223    >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
224    ([('string', 'foo')], 4)
225    >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
226    ([('string', 'foo"'), ('string', 'bar')], 9)
227    >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
228    ([('string', 'foo\\')], 6)
229    """
230    parsed = []
231    for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
232        if typ == b'string':
233            parsed.append((typ, val))
234        elif typ == b'template':
235            parsed.append(val)
236        elif typ == b'end':
237            return parsed, pos
238        else:
239            raise error.ProgrammingError(b'unexpected type: %s' % typ)
240    raise error.ProgrammingError(b'unterminated scanning of template')
241
242
243def scantemplate(tmpl, raw=False):
244    r"""Scan (type, start, end) positions of outermost elements in template
245
246    If raw=True, a backslash is not taken as an escape character just like
247    r'' string in Python. Note that this is different from r'' literal in
248    template in that no template fragment can appear in r'', e.g. r'{foo}'
249    is a literal '{foo}', but ('{foo}', raw=True) is a template expression
250    'foo'.
251
252    >>> list(scantemplate(b'foo{bar}"baz'))
253    [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
254    >>> list(scantemplate(b'outer{"inner"}outer'))
255    [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
256    >>> list(scantemplate(b'foo\\{escaped}'))
257    [('string', 0, 5), ('string', 5, 13)]
258    >>> list(scantemplate(b'foo\\{escaped}', raw=True))
259    [('string', 0, 4), ('template', 4, 13)]
260    """
261    last = None
262    for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
263        if last:
264            yield last + (pos,)
265        if typ == b'end':
266            return
267        else:
268            last = (typ, pos)
269    raise error.ProgrammingError(b'unterminated scanning of template')
270
271
272def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
273    """Parse template string into chunks of strings and template expressions"""
274    sepchars = b'{' + quote
275    unescape = [parser.unescapestr, pycompat.identity][raw]
276    pos = start
277    p = parser.parser(elements)
278    try:
279        while pos < stop:
280            n = min(
281                (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
282                key=lambda n: (n < 0, n),
283            )
284            if n < 0:
285                yield (b'string', unescape(tmpl[pos:stop]), pos)
286                pos = stop
287                break
288            c = tmpl[n : n + 1]
289            bs = 0  # count leading backslashes
290            if not raw:
291                bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
292            if bs % 2 == 1:
293                # escaped (e.g. '\{', '\\\{', but not '\\{')
294                yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
295                pos = n + 1
296                continue
297            if n > pos:
298                yield (b'string', unescape(tmpl[pos:n]), pos)
299            if c == quote:
300                yield (b'end', None, n + 1)
301                return
302
303            parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
304            if not tmpl.startswith(b'}', pos):
305                raise error.ParseError(_(b"invalid token"), pos)
306            yield (b'template', parseres, n)
307            pos += 1
308
309        if quote:
310            raise error.ParseError(_(b"unterminated string"), start)
311    except error.ParseError as inst:
312        _addparseerrorhint(inst, tmpl)
313        raise
314    yield (b'end', None, pos)
315
316
317def _addparseerrorhint(inst, tmpl):
318    if inst.location is None:
319        return
320    loc = inst.location
321    # Offset the caret location by the number of newlines before the
322    # location of the error, since we will replace one-char newlines
323    # with the two-char literal r'\n'.
324    offset = tmpl[:loc].count(b'\n')
325    tmpl = tmpl.replace(b'\n', br'\n')
326    # We want the caret to point to the place in the template that
327    # failed to parse, but in a hint we get a open paren at the
328    # start. Therefore, we print "loc + 1" spaces (instead of "loc")
329    # to line up the caret with the location of the error.
330    inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
331
332
333def _unnesttemplatelist(tree):
334    """Expand list of templates to node tuple
335
336    >>> def f(tree):
337    ...     print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
338    >>> f((b'template', []))
339    (string '')
340    >>> f((b'template', [(b'string', b'foo')]))
341    (string 'foo')
342    >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
343    (template
344      (string 'foo')
345      (symbol 'rev'))
346    >>> f((b'template', [(b'symbol', b'rev')]))  # template(rev) -> str
347    (template
348      (symbol 'rev'))
349    >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
350    (string 'foo')
351    """
352    if not isinstance(tree, tuple):
353        return tree
354    op = tree[0]
355    if op != b'template':
356        return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
357
358    assert len(tree) == 2
359    xs = tuple(_unnesttemplatelist(x) for x in tree[1])
360    if not xs:
361        return (b'string', b'')  # empty template ""
362    elif len(xs) == 1 and xs[0][0] == b'string':
363        return xs[0]  # fast path for string with no template fragment "x"
364    else:
365        return (op,) + xs
366
367
368def parse(tmpl):
369    """Parse template string into tree"""
370    parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
371    assert pos == len(tmpl), b'unquoted template should be consumed'
372    return _unnesttemplatelist((b'template', parsed))
373
374
375def parseexpr(expr):
376    """Parse a template expression into tree
377
378    >>> parseexpr(b'"foo"')
379    ('string', 'foo')
380    >>> parseexpr(b'foo(bar)')
381    ('func', ('symbol', 'foo'), ('symbol', 'bar'))
382    >>> from . import error
383    >>> from . import pycompat
384    >>> try:
385    ...   parseexpr(b'foo(')
386    ... except error.ParseError as e:
387    ...   pycompat.sysstr(e.message)
388    ...   e.location
389    'not a prefix: end'
390    4
391    >>> try:
392    ...   parseexpr(b'"foo" "bar"')
393    ... except error.ParseError as e:
394    ...   pycompat.sysstr(e.message)
395    ...   e.location
396    'invalid token'
397    7
398    """
399    try:
400        return _parseexpr(expr)
401    except error.ParseError as inst:
402        _addparseerrorhint(inst, expr)
403        raise
404
405
406def _parseexpr(expr):
407    p = parser.parser(elements)
408    tree, pos = p.parse(tokenize(expr, 0, len(expr)))
409    if pos != len(expr):
410        raise error.ParseError(_(b'invalid token'), pos)
411    return _unnesttemplatelist(tree)
412
413
414def prettyformat(tree):
415    return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
416
417
418def compileexp(exp, context, curmethods):
419    """Compile parsed template tree to (func, data) pair"""
420    if not exp:
421        raise error.ParseError(_(b"missing argument"))
422    t = exp[0]
423    return curmethods[t](exp, context)
424
425
426# template evaluation
427
428
429def getsymbol(exp):
430    if exp[0] == b'symbol':
431        return exp[1]
432    raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
433
434
435def getlist(x):
436    if not x:
437        return []
438    if x[0] == b'list':
439        return getlist(x[1]) + [x[2]]
440    return [x]
441
442
443def gettemplate(exp, context):
444    """Compile given template tree or load named template from map file;
445    returns (func, data) pair"""
446    if exp[0] in (b'template', b'string'):
447        return compileexp(exp, context, methods)
448    if exp[0] == b'symbol':
449        # unlike runsymbol(), here 'symbol' is always taken as template name
450        # even if it exists in mapping. this allows us to override mapping
451        # by web templates, e.g. 'changelogtag' is redefined in map file.
452        return context._load(exp[1])
453    raise error.ParseError(_(b"expected template specifier"))
454
455
456def _runrecursivesymbol(context, mapping, key):
457    raise error.InputError(_(b"recursive reference '%s' in template") % key)
458
459
460def buildtemplate(exp, context):
461    ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
462    return (templateutil.runtemplate, ctmpl)
463
464
465def buildfilter(exp, context):
466    n = getsymbol(exp[2])
467    if n in context._filters:
468        filt = context._filters[n]
469        arg = compileexp(exp[1], context, methods)
470        return (templateutil.runfilter, (arg, filt))
471    if n in context._funcs:
472        f = context._funcs[n]
473        args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
474        return (f, args)
475    raise error.ParseError(_(b"unknown function '%s'") % n)
476
477
478def buildmap(exp, context):
479    darg = compileexp(exp[1], context, methods)
480    targ = gettemplate(exp[2], context)
481    return (templateutil.runmap, (darg, targ))
482
483
484def buildmember(exp, context):
485    darg = compileexp(exp[1], context, methods)
486    memb = getsymbol(exp[2])
487    return (templateutil.runmember, (darg, memb))
488
489
490def buildnegate(exp, context):
491    arg = compileexp(exp[1], context, exprmethods)
492    return (templateutil.runnegate, arg)
493
494
495def buildarithmetic(exp, context, func):
496    left = compileexp(exp[1], context, exprmethods)
497    right = compileexp(exp[2], context, exprmethods)
498    return (templateutil.runarithmetic, (func, left, right))
499
500
501def buildfunc(exp, context):
502    n = getsymbol(exp[1])
503    if n in context._funcs:
504        f = context._funcs[n]
505        args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
506        return (f, args)
507    if n in context._filters:
508        args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
509        if len(args) != 1:
510            raise error.ParseError(_(b"filter %s expects one argument") % n)
511        f = context._filters[n]
512        return (templateutil.runfilter, (args[0], f))
513    raise error.ParseError(_(b"unknown function '%s'") % n)
514
515
516def _buildfuncargs(exp, context, curmethods, funcname, argspec):
517    """Compile parsed tree of function arguments into list or dict of
518    (func, data) pairs
519
520    >>> context = engine(lambda t: (templateutil.runsymbol, t))
521    >>> def fargs(expr, argspec):
522    ...     x = _parseexpr(expr)
523    ...     n = getsymbol(x[1])
524    ...     return _buildfuncargs(x[2], context, exprmethods, n, argspec)
525    >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
526    ['l', 'k']
527    >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
528    >>> list(args.keys()), list(args[b'opts'].keys())
529    (['opts'], ['opts', 'k'])
530    """
531
532    def compiledict(xs):
533        return util.sortdict(
534            (k, compileexp(x, context, curmethods))
535            for k, x in pycompat.iteritems(xs)
536        )
537
538    def compilelist(xs):
539        return [compileexp(x, context, curmethods) for x in xs]
540
541    if not argspec:
542        # filter or function with no argspec: return list of positional args
543        return compilelist(getlist(exp))
544
545    # function with argspec: return dict of named args
546    _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
547    treeargs = parser.buildargsdict(
548        getlist(exp),
549        funcname,
550        argspec,
551        keyvaluenode=b'keyvalue',
552        keynode=b'symbol',
553    )
554    compargs = util.sortdict()
555    if varkey:
556        compargs[varkey] = compilelist(treeargs.pop(varkey))
557    if optkey:
558        compargs[optkey] = compiledict(treeargs.pop(optkey))
559    compargs.update(compiledict(treeargs))
560    return compargs
561
562
563def buildkeyvaluepair(exp, content):
564    raise error.ParseError(_(b"can't use a key-value pair in this context"))
565
566
567def buildlist(exp, context):
568    raise error.ParseError(
569        _(b"can't use a list in this context"),
570        hint=_(b'check place of comma and parens'),
571    )
572
573
574# methods to interpret function arguments or inner expressions (e.g. {_(x)})
575exprmethods = {
576    b"integer": lambda e, c: (templateutil.runinteger, e[1]),
577    b"string": lambda e, c: (templateutil.runstring, e[1]),
578    b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
579    b"template": buildtemplate,
580    b"group": lambda e, c: compileexp(e[1], c, exprmethods),
581    b".": buildmember,
582    b"|": buildfilter,
583    b"%": buildmap,
584    b"func": buildfunc,
585    b"keyvalue": buildkeyvaluepair,
586    b"list": buildlist,
587    b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
588    b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
589    b"negate": buildnegate,
590    b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
591    b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
592}
593
594# methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
595methods = exprmethods.copy()
596methods[b"integer"] = exprmethods[b"symbol"]  # '{1}' as variable
597
598
599class _aliasrules(parser.basealiasrules):
600    """Parsing and expansion rule set of template aliases"""
601
602    _section = _(b'template alias')
603    _parse = staticmethod(_parseexpr)
604
605    @staticmethod
606    def _trygetfunc(tree):
607        """Return (name, args) if tree is func(...) or ...|filter; otherwise
608        None"""
609        if tree[0] == b'func' and tree[1][0] == b'symbol':
610            return tree[1][1], getlist(tree[2])
611        if tree[0] == b'|' and tree[2][0] == b'symbol':
612            return tree[2][1], [tree[1]]
613
614
615def expandaliases(tree, aliases):
616    """Return new tree of aliases are expanded"""
617    aliasmap = _aliasrules.buildmap(aliases)
618    return _aliasrules.expand(aliasmap, tree)
619
620
621# template engine
622
623
624def unquotestring(s):
625    '''unwrap quotes if any; otherwise returns unmodified string'''
626    if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
627        return s
628    return s[1:-1]
629
630
631class resourcemapper(object):  # pytype: disable=ignored-metaclass
632    """Mapper of internal template resources"""
633
634    __metaclass__ = abc.ABCMeta
635
636    @abc.abstractmethod
637    def availablekeys(self, mapping):
638        """Return a set of available resource keys based on the given mapping"""
639
640    @abc.abstractmethod
641    def knownkeys(self):
642        """Return a set of supported resource keys"""
643
644    @abc.abstractmethod
645    def lookup(self, mapping, key):
646        """Return a resource for the key if available; otherwise None"""
647
648    @abc.abstractmethod
649    def populatemap(self, context, origmapping, newmapping):
650        """Return a dict of additional mapping items which should be paired
651        with the given new mapping"""
652
653
654class nullresourcemapper(resourcemapper):
655    def availablekeys(self, mapping):
656        return set()
657
658    def knownkeys(self):
659        return set()
660
661    def lookup(self, mapping, key):
662        return None
663
664    def populatemap(self, context, origmapping, newmapping):
665        return {}
666
667
668class engine(object):
669    """template expansion engine.
670
671    template expansion works like this. a map file contains key=value
672    pairs. if value is quoted, it is treated as string. otherwise, it
673    is treated as name of template file.
674
675    templater is asked to expand a key in map. it looks up key, and
676    looks for strings like this: {foo}. it expands {foo} by looking up
677    foo in map, and substituting it. expansion is recursive: it stops
678    when there is no more {foo} to replace.
679
680    expansion also allows formatting and filtering.
681
682    format uses key to expand each item in list. syntax is
683    {key%format}.
684
685    filter uses function to transform value. syntax is
686    {key|filter1|filter2|...}."""
687
688    def __init__(self, loader, filters=None, defaults=None, resources=None):
689        self._loader = loader
690        if filters is None:
691            filters = {}
692        self._filters = filters
693        self._funcs = templatefuncs.funcs  # make this a parameter if needed
694        if defaults is None:
695            defaults = {}
696        if resources is None:
697            resources = nullresourcemapper()
698        self._defaults = defaults
699        self._resources = resources
700        self._cache = {}  # key: (func, data)
701        self._tmplcache = {}  # literal template: (func, data)
702
703    def overlaymap(self, origmapping, newmapping):
704        """Create combined mapping from the original mapping and partial
705        mapping to override the original"""
706        # do not copy symbols which overrides the defaults depending on
707        # new resources, so the defaults will be re-evaluated (issue5612)
708        knownres = self._resources.knownkeys()
709        newres = self._resources.availablekeys(newmapping)
710        mapping = {
711            k: v
712            for k, v in pycompat.iteritems(origmapping)
713            if (
714                k in knownres  # not a symbol per self.symbol()
715                or newres.isdisjoint(self._defaultrequires(k))
716            )
717        }
718        mapping.update(newmapping)
719        mapping.update(
720            self._resources.populatemap(self, origmapping, newmapping)
721        )
722        return mapping
723
724    def _defaultrequires(self, key):
725        """Resource keys required by the specified default symbol function"""
726        v = self._defaults.get(key)
727        if v is None or not callable(v):
728            return ()
729        return getattr(v, '_requires', ())
730
731    def symbol(self, mapping, key):
732        """Resolve symbol to value or function; None if nothing found"""
733        v = None
734        if key not in self._resources.knownkeys():
735            v = mapping.get(key)
736        if v is None:
737            v = self._defaults.get(key)
738        return v
739
740    def availableresourcekeys(self, mapping):
741        """Return a set of available resource keys based on the given mapping"""
742        return self._resources.availablekeys(mapping)
743
744    def knownresourcekeys(self):
745        """Return a set of supported resource keys"""
746        return self._resources.knownkeys()
747
748    def resource(self, mapping, key):
749        """Return internal data (e.g. cache) used for keyword/function
750        evaluation"""
751        v = self._resources.lookup(mapping, key)
752        if v is None:
753            raise templateutil.ResourceUnavailable(
754                _(b'template resource not available: %s') % key
755            )
756        return v
757
758    def _load(self, t):
759        '''load, parse, and cache a template'''
760        if t not in self._cache:
761            x = self._loader(t)
762            # put poison to cut recursion while compiling 't'
763            self._cache[t] = (_runrecursivesymbol, t)
764            try:
765                self._cache[t] = compileexp(x, self, methods)
766            except:  # re-raises
767                del self._cache[t]
768                raise
769        return self._cache[t]
770
771    def _parse(self, tmpl):
772        """Parse and cache a literal template"""
773        if tmpl not in self._tmplcache:
774            x = parse(tmpl)
775            self._tmplcache[tmpl] = compileexp(x, self, methods)
776        return self._tmplcache[tmpl]
777
778    def preload(self, t):
779        """Load, parse, and cache the specified template if available"""
780        try:
781            self._load(t)
782            return True
783        except templateutil.TemplateNotFound:
784            return False
785
786    def process(self, t, mapping):
787        """Perform expansion. t is name of map element to expand.
788        mapping contains added elements for use during expansion. Is a
789        generator."""
790        func, data = self._load(t)
791        return self._expand(func, data, mapping)
792
793    def expand(self, tmpl, mapping):
794        """Perform expansion over a literal template
795
796        No user aliases will be expanded since this is supposed to be called
797        with an internal template string.
798        """
799        func, data = self._parse(tmpl)
800        return self._expand(func, data, mapping)
801
802    def _expand(self, func, data, mapping):
803        # populate additional items only if they don't exist in the given
804        # mapping. this is slightly different from overlaymap() because the
805        # initial 'revcache' may contain pre-computed items.
806        extramapping = self._resources.populatemap(self, {}, mapping)
807        if extramapping:
808            extramapping.update(mapping)
809            mapping = extramapping
810        return templateutil.flatten(self, mapping, func(self, mapping, data))
811
812
813def stylelist():
814    path = templatedir()
815    if not path:
816        return _(b'no templates found, try `hg debuginstall` for more info')
817    dirlist = os.listdir(path)
818    stylelist = []
819    for file in dirlist:
820        split = file.split(b".")
821        if split[-1] in (b'orig', b'rej'):
822            continue
823        if split[0] == b"map-cmdline":
824            stylelist.append(split[1])
825    return b", ".join(sorted(stylelist))
826
827
828def _open_mapfile(mapfile):
829    if os.path.exists(mapfile):
830        return util.posixfile(mapfile, b'rb')
831    raise error.Abort(
832        _(b"style '%s' not found") % mapfile,
833        hint=_(b"available styles: %s") % stylelist(),
834    )
835
836
837def _readmapfile(fp, mapfile):
838    """Load template elements from the given map file"""
839    if pycompat.iswindows:
840        # quick hack to make sure we can process '/' in the code dealing with
841        # ressource. Ideally we would make sure we use `/` instead of `ossep`
842        # in the templater code, but that seems a bigger and less certain
843        # change that we better left for the default branch.
844        name_paths = mapfile.split(pycompat.ossep)
845        mapfile = b'/'.join(name_paths)
846    base = os.path.dirname(mapfile)
847    conf = config.config()
848
849    def include(rel, remap, sections):
850        subresource = None
851        if base:
852            abs = os.path.normpath(os.path.join(base, rel))
853            if os.path.isfile(abs):
854                subresource = util.posixfile(abs, b'rb')
855        if not subresource:
856            if pycompat.ossep not in rel:
857                abs = rel
858                try:
859                    subresource = resourceutil.open_resource(
860                        b'mercurial.templates', rel
861                    )
862                except FileNotFoundError:
863                    subresource = None
864            else:
865                dir = templatedir()
866                if dir:
867                    abs = os.path.normpath(os.path.join(dir, rel))
868                    if os.path.isfile(abs):
869                        subresource = util.posixfile(abs, b'rb')
870        if subresource:
871            data = subresource.read()
872            conf.parse(
873                abs,
874                data,
875                sections=sections,
876                remap=remap,
877                include=include,
878            )
879
880    data = fp.read()
881    conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
882
883    cache = {}
884    tmap = {}
885    aliases = []
886
887    val = conf.get(b'templates', b'__base__')
888    if val and val[0] not in b"'\"":
889        # treat as a pointer to a base class for this style
890        path = os.path.normpath(os.path.join(base, val))
891
892        # fallback check in template paths
893        if not os.path.exists(path):
894            dir = templatedir()
895            if dir is not None:
896                p2 = os.path.normpath(os.path.join(dir, val))
897                if os.path.isfile(p2):
898                    path = p2
899                else:
900                    p3 = os.path.normpath(os.path.join(p2, b"map"))
901                    if os.path.isfile(p3):
902                        path = p3
903
904        fp = _open_mapfile(path)
905        cache, tmap, aliases = _readmapfile(fp, path)
906
907    for key, val in conf.items(b'templates'):
908        if not val:
909            raise error.ParseError(
910                _(b'missing value'), conf.source(b'templates', key)
911            )
912        if val[0] in b"'\"":
913            if val[0] != val[-1]:
914                raise error.ParseError(
915                    _(b'unmatched quotes'), conf.source(b'templates', key)
916                )
917            cache[key] = unquotestring(val)
918        elif key != b'__base__':
919            tmap[key] = os.path.join(base, val)
920    aliases.extend(conf.items(b'templatealias'))
921    return cache, tmap, aliases
922
923
924class loader(object):
925    """Load template fragments optionally from a map file"""
926
927    def __init__(self, cache, aliases):
928        if cache is None:
929            cache = {}
930        self.cache = cache.copy()
931        self._map = {}
932        self._aliasmap = _aliasrules.buildmap(aliases)
933
934    def __contains__(self, key):
935        return key in self.cache or key in self._map
936
937    def load(self, t):
938        """Get parsed tree for the given template name. Use a local cache."""
939        if t not in self.cache:
940            try:
941                mapfile, fp = open_template(self._map[t])
942                self.cache[t] = fp.read()
943            except KeyError as inst:
944                raise templateutil.TemplateNotFound(
945                    _(b'"%s" not in template map') % inst.args[0]
946                )
947            except IOError as inst:
948                reason = _(b'template file %s: %s') % (
949                    self._map[t],
950                    stringutil.forcebytestr(inst.args[1]),
951                )
952                raise IOError(inst.args[0], encoding.strfromlocal(reason))
953        return self._parse(self.cache[t])
954
955    def _parse(self, tmpl):
956        x = parse(tmpl)
957        if self._aliasmap:
958            x = _aliasrules.expand(self._aliasmap, x)
959        return x
960
961    def _findsymbolsused(self, tree, syms):
962        if not tree:
963            return
964        op = tree[0]
965        if op == b'symbol':
966            s = tree[1]
967            if s in syms[0]:
968                return  # avoid recursion: s -> cache[s] -> s
969            syms[0].add(s)
970            if s in self.cache or s in self._map:
971                # s may be a reference for named template
972                self._findsymbolsused(self.load(s), syms)
973            return
974        if op in {b'integer', b'string'}:
975            return
976        # '{arg|func}' == '{func(arg)}'
977        if op == b'|':
978            syms[1].add(getsymbol(tree[2]))
979            self._findsymbolsused(tree[1], syms)
980            return
981        if op == b'func':
982            syms[1].add(getsymbol(tree[1]))
983            self._findsymbolsused(tree[2], syms)
984            return
985        for x in tree[1:]:
986            self._findsymbolsused(x, syms)
987
988    def symbolsused(self, t):
989        """Look up (keywords, filters/functions) referenced from the name
990        template 't'
991
992        This may load additional templates from the map file.
993        """
994        syms = (set(), set())
995        self._findsymbolsused(self.load(t), syms)
996        return syms
997
998
999class templater(object):
1000    def __init__(
1001        self,
1002        filters=None,
1003        defaults=None,
1004        resources=None,
1005        cache=None,
1006        aliases=(),
1007        minchunk=1024,
1008        maxchunk=65536,
1009    ):
1010        """Create template engine optionally with preloaded template fragments
1011
1012        - ``filters``: a dict of functions to transform a value into another.
1013        - ``defaults``: a dict of symbol values/functions; may be overridden
1014          by a ``mapping`` dict.
1015        - ``resources``: a resourcemapper object to look up internal data
1016          (e.g. cache), inaccessible from user template.
1017        - ``cache``: a dict of preloaded template fragments.
1018        - ``aliases``: a list of alias (name, replacement) pairs.
1019
1020        self.cache may be updated later to register additional template
1021        fragments.
1022        """
1023        allfilters = templatefilters.filters.copy()
1024        if filters:
1025            allfilters.update(filters)
1026        self._loader = loader(cache, aliases)
1027        self._proc = engine(self._loader.load, allfilters, defaults, resources)
1028        self._minchunk, self._maxchunk = minchunk, maxchunk
1029
1030    @classmethod
1031    def frommapfile(
1032        cls,
1033        mapfile,
1034        fp=None,
1035        filters=None,
1036        defaults=None,
1037        resources=None,
1038        cache=None,
1039        minchunk=1024,
1040        maxchunk=65536,
1041    ):
1042        """Create templater from the specified map file"""
1043        t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1044        if not fp:
1045            fp = _open_mapfile(mapfile)
1046        cache, tmap, aliases = _readmapfile(fp, mapfile)
1047        t._loader.cache.update(cache)
1048        t._loader._map = tmap
1049        t._loader._aliasmap = _aliasrules.buildmap(aliases)
1050        return t
1051
1052    def __contains__(self, key):
1053        return key in self._loader
1054
1055    @property
1056    def cache(self):
1057        return self._loader.cache
1058
1059    # for highlight extension to insert one-time 'colorize' filter
1060    @property
1061    def _filters(self):
1062        return self._proc._filters
1063
1064    @property
1065    def defaults(self):
1066        return self._proc._defaults
1067
1068    def load(self, t):
1069        """Get parsed tree for the given template name. Use a local cache."""
1070        return self._loader.load(t)
1071
1072    def symbolsuseddefault(self):
1073        """Look up (keywords, filters/functions) referenced from the default
1074        unnamed template
1075
1076        This may load additional templates from the map file.
1077        """
1078        return self.symbolsused(b'')
1079
1080    def symbolsused(self, t):
1081        """Look up (keywords, filters/functions) referenced from the name
1082        template 't'
1083
1084        This may load additional templates from the map file.
1085        """
1086        return self._loader.symbolsused(t)
1087
1088    def renderdefault(self, mapping):
1089        """Render the default unnamed template and return result as string"""
1090        return self.render(b'', mapping)
1091
1092    def render(self, t, mapping):
1093        """Render the specified named template and return result as string"""
1094        return b''.join(self.generate(t, mapping))
1095
1096    def generate(self, t, mapping):
1097        """Return a generator that renders the specified named template and
1098        yields chunks"""
1099        stream = self._proc.process(t, mapping)
1100        if self._minchunk:
1101            stream = util.increasingchunks(
1102                stream, min=self._minchunk, max=self._maxchunk
1103            )
1104        return stream
1105
1106
1107def templatedir():
1108    '''return the directory used for template files, or None.'''
1109    path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1110    return path if os.path.isdir(path) else None
1111
1112
1113def open_template(name, templatepath=None):
1114    """returns a file-like object for the given template, and its full path
1115
1116    If the name is a relative path and we're in a frozen binary, the template
1117    will be read from the mercurial.templates package instead. The returned path
1118    will then be the relative path.
1119    """
1120    # Does the name point directly to a map file?
1121    if os.path.isfile(name) or os.path.isabs(name):
1122        return name, open(name, mode='rb')
1123
1124    # Does the name point to a template in the provided templatepath, or
1125    # in mercurial/templates/ if no path was provided?
1126    if templatepath is None:
1127        templatepath = templatedir()
1128    if templatepath is not None:
1129        f = os.path.join(templatepath, name)
1130        return f, open(f, mode='rb')
1131
1132    # Otherwise try to read it using the resources API
1133    if pycompat.iswindows:
1134        # quick hack to make sure we can process '/' in the code dealing with
1135        # ressource. Ideally we would make sure we use `/` instead of `ossep`
1136        # in the templater code, but that seems a bigger and less certain
1137        # change that we better left for the default branch.
1138        name_paths = name.split(pycompat.ossep)
1139        name = b'/'.join(name_paths)
1140    name_parts = name.split(b'/')
1141    package_name = b'.'.join([b'mercurial', b'templates'] + name_parts[:-1])
1142    return (
1143        name,
1144        resourceutil.open_resource(package_name, name_parts[-1]),
1145    )
1146
1147
1148def try_open_template(name, templatepath=None):
1149    try:
1150        return open_template(name, templatepath)
1151    except (EnvironmentError, ImportError):
1152        return None, None
1153