mercurial/templater.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43089 c59eb1560c44
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    83 
    83 
    84 # template parsing
    84 # template parsing
    85 
    85 
    86 elements = {
    86 elements = {
    87     # token-type: binding-strength, primary, prefix, infix, suffix
    87     # token-type: binding-strength, primary, prefix, infix, suffix
    88     "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
    88     b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
    89     ".": (18, None, None, (".", 18), None),
    89     b".": (18, None, None, (b".", 18), None),
    90     "%": (15, None, None, ("%", 15), None),
    90     b"%": (15, None, None, (b"%", 15), None),
    91     "|": (15, None, None, ("|", 15), None),
    91     b"|": (15, None, None, (b"|", 15), None),
    92     "*": (5, None, None, ("*", 5), None),
    92     b"*": (5, None, None, (b"*", 5), None),
    93     "/": (5, None, None, ("/", 5), None),
    93     b"/": (5, None, None, (b"/", 5), None),
    94     "+": (4, None, None, ("+", 4), None),
    94     b"+": (4, None, None, (b"+", 4), None),
    95     "-": (4, None, ("negate", 19), ("-", 4), None),
    95     b"-": (4, None, (b"negate", 19), (b"-", 4), None),
    96     "=": (3, None, None, ("keyvalue", 3), None),
    96     b"=": (3, None, None, (b"keyvalue", 3), None),
    97     ",": (2, None, None, ("list", 2), None),
    97     b",": (2, None, None, (b"list", 2), None),
    98     ")": (0, None, None, None, None),
    98     b")": (0, None, None, None, None),
    99     "integer": (0, "integer", None, None, None),
    99     b"integer": (0, b"integer", None, None, None),
   100     "symbol": (0, "symbol", None, None, None),
   100     b"symbol": (0, b"symbol", None, None, None),
   101     "string": (0, "string", None, None, None),
   101     b"string": (0, b"string", None, None, None),
   102     "template": (0, "template", None, None, None),
   102     b"template": (0, b"template", None, None, None),
   103     "end": (0, None, None, None, None),
   103     b"end": (0, None, None, None, None),
   104 }
   104 }
   105 
   105 
   106 
   106 
   107 def tokenize(program, start, end, term=None):
   107 def tokenize(program, start, end, term=None):
   108     """Parse a template expression into a stream of tokens, which must end
   108     """Parse a template expression into a stream of tokens, which must end
   111     program = pycompat.bytestr(program)
   111     program = pycompat.bytestr(program)
   112     while pos < end:
   112     while pos < end:
   113         c = program[pos]
   113         c = program[pos]
   114         if c.isspace():  # skip inter-token whitespace
   114         if c.isspace():  # skip inter-token whitespace
   115             pass
   115             pass
   116         elif c in "(=,).%|+-*/":  # handle simple operators
   116         elif c in b"(=,).%|+-*/":  # handle simple operators
   117             yield (c, None, pos)
   117             yield (c, None, pos)
   118         elif c in '"\'':  # handle quoted templates
   118         elif c in b'"\'':  # handle quoted templates
   119             s = pos + 1
   119             s = pos + 1
   120             data, pos = _parsetemplate(program, s, end, c)
   120             data, pos = _parsetemplate(program, s, end, c)
   121             yield ('template', data, s)
   121             yield (b'template', data, s)
   122             pos -= 1
   122             pos -= 1
   123         elif c == 'r' and program[pos : pos + 2] in ("r'", 'r"'):
   123         elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
   124             # handle quoted strings
   124             # handle quoted strings
   125             c = program[pos + 1]
   125             c = program[pos + 1]
   126             s = pos = pos + 2
   126             s = pos = pos + 2
   127             while pos < end:  # find closing quote
   127             while pos < end:  # find closing quote
   128                 d = program[pos]
   128                 d = program[pos]
   129                 if d == '\\':  # skip over escaped characters
   129                 if d == b'\\':  # skip over escaped characters
   130                     pos += 2
   130                     pos += 2
   131                     continue
   131                     continue
   132                 if d == c:
   132                 if d == c:
   133                     yield ('string', program[s:pos], s)
   133                     yield (b'string', program[s:pos], s)
   134                     break
   134                     break
   135                 pos += 1
   135                 pos += 1
   136             else:
   136             else:
   137                 raise error.ParseError(_("unterminated string"), s)
   137                 raise error.ParseError(_(b"unterminated string"), s)
   138         elif c.isdigit():
   138         elif c.isdigit():
   139             s = pos
   139             s = pos
   140             while pos < end:
   140             while pos < end:
   141                 d = program[pos]
   141                 d = program[pos]
   142                 if not d.isdigit():
   142                 if not d.isdigit():
   143                     break
   143                     break
   144                 pos += 1
   144                 pos += 1
   145             yield ('integer', program[s:pos], s)
   145             yield (b'integer', program[s:pos], s)
   146             pos -= 1
   146             pos -= 1
   147         elif (
   147         elif (
   148             c == '\\'
   148             c == b'\\'
   149             and program[pos : pos + 2] in (br"\'", br'\"')
   149             and program[pos : pos + 2] in (br"\'", br'\"')
   150             or c == 'r'
   150             or c == b'r'
   151             and program[pos : pos + 3] in (br"r\'", br'r\"')
   151             and program[pos : pos + 3] in (br"r\'", br'r\"')
   152         ):
   152         ):
   153             # handle escaped quoted strings for compatibility with 2.9.2-3.4,
   153             # handle escaped quoted strings for compatibility with 2.9.2-3.4,
   154             # where some of nested templates were preprocessed as strings and
   154             # where some of nested templates were preprocessed as strings and
   155             # then compiled. therefore, \"...\" was allowed. (issue4733)
   155             # then compiled. therefore, \"...\" was allowed. (issue4733)
   158             # outer template string    -> stringify()  -> compiletemplate()
   158             # outer template string    -> stringify()  -> compiletemplate()
   159             # ------------------------    ------------    ------------------
   159             # ------------------------    ------------    ------------------
   160             # {f("\\\\ {g(\"\\\"\")}"}    \\ {g("\"")}    [r'\\', {g("\"")}]
   160             # {f("\\\\ {g(\"\\\"\")}"}    \\ {g("\"")}    [r'\\', {g("\"")}]
   161             #             ~~~~~~~~
   161             #             ~~~~~~~~
   162             #             escaped quoted string
   162             #             escaped quoted string
   163             if c == 'r':
   163             if c == b'r':
   164                 pos += 1
   164                 pos += 1
   165                 token = 'string'
   165                 token = b'string'
   166             else:
   166             else:
   167                 token = 'template'
   167                 token = b'template'
   168             quote = program[pos : pos + 2]
   168             quote = program[pos : pos + 2]
   169             s = pos = pos + 2
   169             s = pos = pos + 2
   170             while pos < end:  # find closing escaped quote
   170             while pos < end:  # find closing escaped quote
   171                 if program.startswith('\\\\\\', pos, end):
   171                 if program.startswith(b'\\\\\\', pos, end):
   172                     pos += 4  # skip over double escaped characters
   172                     pos += 4  # skip over double escaped characters
   173                     continue
   173                     continue
   174                 if program.startswith(quote, pos, end):
   174                 if program.startswith(quote, pos, end):
   175                     # interpret as if it were a part of an outer string
   175                     # interpret as if it were a part of an outer string
   176                     data = parser.unescapestr(program[s:pos])
   176                     data = parser.unescapestr(program[s:pos])
   177                     if token == 'template':
   177                     if token == b'template':
   178                         data = _parsetemplate(data, 0, len(data))[0]
   178                         data = _parsetemplate(data, 0, len(data))[0]
   179                     yield (token, data, s)
   179                     yield (token, data, s)
   180                     pos += 1
   180                     pos += 1
   181                     break
   181                     break
   182                 pos += 1
   182                 pos += 1
   183             else:
   183             else:
   184                 raise error.ParseError(_("unterminated string"), s)
   184                 raise error.ParseError(_(b"unterminated string"), s)
   185         elif c.isalnum() or c in '_':
   185         elif c.isalnum() or c in b'_':
   186             s = pos
   186             s = pos
   187             pos += 1
   187             pos += 1
   188             while pos < end:  # find end of symbol
   188             while pos < end:  # find end of symbol
   189                 d = program[pos]
   189                 d = program[pos]
   190                 if not (d.isalnum() or d == "_"):
   190                 if not (d.isalnum() or d == b"_"):
   191                     break
   191                     break
   192                 pos += 1
   192                 pos += 1
   193             sym = program[s:pos]
   193             sym = program[s:pos]
   194             yield ('symbol', sym, s)
   194             yield (b'symbol', sym, s)
   195             pos -= 1
   195             pos -= 1
   196         elif c == term:
   196         elif c == term:
   197             yield ('end', None, pos)
   197             yield (b'end', None, pos)
   198             return
   198             return
   199         else:
   199         else:
   200             raise error.ParseError(_("syntax error"), pos)
   200             raise error.ParseError(_(b"syntax error"), pos)
   201         pos += 1
   201         pos += 1
   202     if term:
   202     if term:
   203         raise error.ParseError(_("unterminated template expansion"), start)
   203         raise error.ParseError(_(b"unterminated template expansion"), start)
   204     yield ('end', None, pos)
   204     yield (b'end', None, pos)
   205 
   205 
   206 
   206 
   207 def _parsetemplate(tmpl, start, stop, quote=''):
   207 def _parsetemplate(tmpl, start, stop, quote=b''):
   208     r"""
   208     r"""
   209     >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
   209     >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
   210     ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
   210     ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
   211     >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
   211     >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
   212     ([('string', 'foo'), ('symbol', 'bar')], 9)
   212     ([('string', 'foo'), ('symbol', 'bar')], 9)
   217     >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
   217     >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
   218     ([('string', 'foo\\')], 6)
   218     ([('string', 'foo\\')], 6)
   219     """
   219     """
   220     parsed = []
   220     parsed = []
   221     for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
   221     for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
   222         if typ == 'string':
   222         if typ == b'string':
   223             parsed.append((typ, val))
   223             parsed.append((typ, val))
   224         elif typ == 'template':
   224         elif typ == b'template':
   225             parsed.append(val)
   225             parsed.append(val)
   226         elif typ == 'end':
   226         elif typ == b'end':
   227             return parsed, pos
   227             return parsed, pos
   228         else:
   228         else:
   229             raise error.ProgrammingError('unexpected type: %s' % typ)
   229             raise error.ProgrammingError(b'unexpected type: %s' % typ)
   230     raise error.ProgrammingError('unterminated scanning of template')
   230     raise error.ProgrammingError(b'unterminated scanning of template')
   231 
   231 
   232 
   232 
   233 def scantemplate(tmpl, raw=False):
   233 def scantemplate(tmpl, raw=False):
   234     r"""Scan (type, start, end) positions of outermost elements in template
   234     r"""Scan (type, start, end) positions of outermost elements in template
   235 
   235 
   250     """
   250     """
   251     last = None
   251     last = None
   252     for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
   252     for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
   253         if last:
   253         if last:
   254             yield last + (pos,)
   254             yield last + (pos,)
   255         if typ == 'end':
   255         if typ == b'end':
   256             return
   256             return
   257         else:
   257         else:
   258             last = (typ, pos)
   258             last = (typ, pos)
   259     raise error.ProgrammingError('unterminated scanning of template')
   259     raise error.ProgrammingError(b'unterminated scanning of template')
   260 
   260 
   261 
   261 
   262 def _scantemplate(tmpl, start, stop, quote='', raw=False):
   262 def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
   263     """Parse template string into chunks of strings and template expressions"""
   263     """Parse template string into chunks of strings and template expressions"""
   264     sepchars = '{' + quote
   264     sepchars = b'{' + quote
   265     unescape = [parser.unescapestr, pycompat.identity][raw]
   265     unescape = [parser.unescapestr, pycompat.identity][raw]
   266     pos = start
   266     pos = start
   267     p = parser.parser(elements)
   267     p = parser.parser(elements)
   268     try:
   268     try:
   269         while pos < stop:
   269         while pos < stop:
   270             n = min(
   270             n = min(
   271                 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
   271                 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
   272                 key=lambda n: (n < 0, n),
   272                 key=lambda n: (n < 0, n),
   273             )
   273             )
   274             if n < 0:
   274             if n < 0:
   275                 yield ('string', unescape(tmpl[pos:stop]), pos)
   275                 yield (b'string', unescape(tmpl[pos:stop]), pos)
   276                 pos = stop
   276                 pos = stop
   277                 break
   277                 break
   278             c = tmpl[n : n + 1]
   278             c = tmpl[n : n + 1]
   279             bs = 0  # count leading backslashes
   279             bs = 0  # count leading backslashes
   280             if not raw:
   280             if not raw:
   281                 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
   281                 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
   282             if bs % 2 == 1:
   282             if bs % 2 == 1:
   283                 # escaped (e.g. '\{', '\\\{', but not '\\{')
   283                 # escaped (e.g. '\{', '\\\{', but not '\\{')
   284                 yield ('string', unescape(tmpl[pos : n - 1]) + c, pos)
   284                 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
   285                 pos = n + 1
   285                 pos = n + 1
   286                 continue
   286                 continue
   287             if n > pos:
   287             if n > pos:
   288                 yield ('string', unescape(tmpl[pos:n]), pos)
   288                 yield (b'string', unescape(tmpl[pos:n]), pos)
   289             if c == quote:
   289             if c == quote:
   290                 yield ('end', None, n + 1)
   290                 yield (b'end', None, n + 1)
   291                 return
   291                 return
   292 
   292 
   293             parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
   293             parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
   294             if not tmpl.startswith('}', pos):
   294             if not tmpl.startswith(b'}', pos):
   295                 raise error.ParseError(_("invalid token"), pos)
   295                 raise error.ParseError(_(b"invalid token"), pos)
   296             yield ('template', parseres, n)
   296             yield (b'template', parseres, n)
   297             pos += 1
   297             pos += 1
   298 
   298 
   299         if quote:
   299         if quote:
   300             raise error.ParseError(_("unterminated string"), start)
   300             raise error.ParseError(_(b"unterminated string"), start)
   301     except error.ParseError as inst:
   301     except error.ParseError as inst:
   302         if len(inst.args) > 1:  # has location
   302         if len(inst.args) > 1:  # has location
   303             loc = inst.args[1]
   303             loc = inst.args[1]
   304             # Offset the caret location by the number of newlines before the
   304             # Offset the caret location by the number of newlines before the
   305             # location of the error, since we will replace one-char newlines
   305             # location of the error, since we will replace one-char newlines
   306             # with the two-char literal r'\n'.
   306             # with the two-char literal r'\n'.
   307             offset = tmpl[:loc].count('\n')
   307             offset = tmpl[:loc].count(b'\n')
   308             tmpl = tmpl.replace('\n', br'\n')
   308             tmpl = tmpl.replace(b'\n', br'\n')
   309             # We want the caret to point to the place in the template that
   309             # We want the caret to point to the place in the template that
   310             # failed to parse, but in a hint we get a open paren at the
   310             # failed to parse, but in a hint we get a open paren at the
   311             # start. Therefore, we print "loc + 1" spaces (instead of "loc")
   311             # start. Therefore, we print "loc + 1" spaces (instead of "loc")
   312             # to line up the caret with the location of the error.
   312             # to line up the caret with the location of the error.
   313             inst.hint = (
   313             inst.hint = (
   314                 tmpl + '\n' + ' ' * (loc + 1 + offset) + '^ ' + _('here')
   314                 tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
   315             )
   315             )
   316         raise
   316         raise
   317     yield ('end', None, pos)
   317     yield (b'end', None, pos)
   318 
   318 
   319 
   319 
   320 def _unnesttemplatelist(tree):
   320 def _unnesttemplatelist(tree):
   321     """Expand list of templates to node tuple
   321     """Expand list of templates to node tuple
   322 
   322 
   337     (string 'foo')
   337     (string 'foo')
   338     """
   338     """
   339     if not isinstance(tree, tuple):
   339     if not isinstance(tree, tuple):
   340         return tree
   340         return tree
   341     op = tree[0]
   341     op = tree[0]
   342     if op != 'template':
   342     if op != b'template':
   343         return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
   343         return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
   344 
   344 
   345     assert len(tree) == 2
   345     assert len(tree) == 2
   346     xs = tuple(_unnesttemplatelist(x) for x in tree[1])
   346     xs = tuple(_unnesttemplatelist(x) for x in tree[1])
   347     if not xs:
   347     if not xs:
   348         return ('string', '')  # empty template ""
   348         return (b'string', b'')  # empty template ""
   349     elif len(xs) == 1 and xs[0][0] == 'string':
   349     elif len(xs) == 1 and xs[0][0] == b'string':
   350         return xs[0]  # fast path for string with no template fragment "x"
   350         return xs[0]  # fast path for string with no template fragment "x"
   351     else:
   351     else:
   352         return (op,) + xs
   352         return (op,) + xs
   353 
   353 
   354 
   354 
   355 def parse(tmpl):
   355 def parse(tmpl):
   356     """Parse template string into tree"""
   356     """Parse template string into tree"""
   357     parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
   357     parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
   358     assert pos == len(tmpl), 'unquoted template should be consumed'
   358     assert pos == len(tmpl), b'unquoted template should be consumed'
   359     return _unnesttemplatelist(('template', parsed))
   359     return _unnesttemplatelist((b'template', parsed))
   360 
   360 
   361 
   361 
   362 def _parseexpr(expr):
   362 def _parseexpr(expr):
   363     """Parse a template expression into tree
   363     """Parse a template expression into tree
   364 
   364 
   376     ParseError: ('invalid token', 7)
   376     ParseError: ('invalid token', 7)
   377     """
   377     """
   378     p = parser.parser(elements)
   378     p = parser.parser(elements)
   379     tree, pos = p.parse(tokenize(expr, 0, len(expr)))
   379     tree, pos = p.parse(tokenize(expr, 0, len(expr)))
   380     if pos != len(expr):
   380     if pos != len(expr):
   381         raise error.ParseError(_('invalid token'), pos)
   381         raise error.ParseError(_(b'invalid token'), pos)
   382     return _unnesttemplatelist(tree)
   382     return _unnesttemplatelist(tree)
   383 
   383 
   384 
   384 
   385 def prettyformat(tree):
   385 def prettyformat(tree):
   386     return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
   386     return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
   387 
   387 
   388 
   388 
   389 def compileexp(exp, context, curmethods):
   389 def compileexp(exp, context, curmethods):
   390     """Compile parsed template tree to (func, data) pair"""
   390     """Compile parsed template tree to (func, data) pair"""
   391     if not exp:
   391     if not exp:
   392         raise error.ParseError(_("missing argument"))
   392         raise error.ParseError(_(b"missing argument"))
   393     t = exp[0]
   393     t = exp[0]
   394     return curmethods[t](exp, context)
   394     return curmethods[t](exp, context)
   395 
   395 
   396 
   396 
   397 # template evaluation
   397 # template evaluation
   398 
   398 
   399 
   399 
   400 def getsymbol(exp):
   400 def getsymbol(exp):
   401     if exp[0] == 'symbol':
   401     if exp[0] == b'symbol':
   402         return exp[1]
   402         return exp[1]
   403     raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
   403     raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
   404 
   404 
   405 
   405 
   406 def getlist(x):
   406 def getlist(x):
   407     if not x:
   407     if not x:
   408         return []
   408         return []
   409     if x[0] == 'list':
   409     if x[0] == b'list':
   410         return getlist(x[1]) + [x[2]]
   410         return getlist(x[1]) + [x[2]]
   411     return [x]
   411     return [x]
   412 
   412 
   413 
   413 
   414 def gettemplate(exp, context):
   414 def gettemplate(exp, context):
   415     """Compile given template tree or load named template from map file;
   415     """Compile given template tree or load named template from map file;
   416     returns (func, data) pair"""
   416     returns (func, data) pair"""
   417     if exp[0] in ('template', 'string'):
   417     if exp[0] in (b'template', b'string'):
   418         return compileexp(exp, context, methods)
   418         return compileexp(exp, context, methods)
   419     if exp[0] == 'symbol':
   419     if exp[0] == b'symbol':
   420         # unlike runsymbol(), here 'symbol' is always taken as template name
   420         # unlike runsymbol(), here 'symbol' is always taken as template name
   421         # even if it exists in mapping. this allows us to override mapping
   421         # even if it exists in mapping. this allows us to override mapping
   422         # by web templates, e.g. 'changelogtag' is redefined in map file.
   422         # by web templates, e.g. 'changelogtag' is redefined in map file.
   423         return context._load(exp[1])
   423         return context._load(exp[1])
   424     raise error.ParseError(_("expected template specifier"))
   424     raise error.ParseError(_(b"expected template specifier"))
   425 
   425 
   426 
   426 
   427 def _runrecursivesymbol(context, mapping, key):
   427 def _runrecursivesymbol(context, mapping, key):
   428     raise error.Abort(_("recursive reference '%s' in template") % key)
   428     raise error.Abort(_(b"recursive reference '%s' in template") % key)
   429 
   429 
   430 
   430 
   431 def buildtemplate(exp, context):
   431 def buildtemplate(exp, context):
   432     ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
   432     ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
   433     return (templateutil.runtemplate, ctmpl)
   433     return (templateutil.runtemplate, ctmpl)
   441         return (templateutil.runfilter, (arg, filt))
   441         return (templateutil.runfilter, (arg, filt))
   442     if n in context._funcs:
   442     if n in context._funcs:
   443         f = context._funcs[n]
   443         f = context._funcs[n]
   444         args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
   444         args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
   445         return (f, args)
   445         return (f, args)
   446     raise error.ParseError(_("unknown function '%s'") % n)
   446     raise error.ParseError(_(b"unknown function '%s'") % n)
   447 
   447 
   448 
   448 
   449 def buildmap(exp, context):
   449 def buildmap(exp, context):
   450     darg = compileexp(exp[1], context, methods)
   450     darg = compileexp(exp[1], context, methods)
   451     targ = gettemplate(exp[2], context)
   451     targ = gettemplate(exp[2], context)
   476         args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
   476         args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
   477         return (f, args)
   477         return (f, args)
   478     if n in context._filters:
   478     if n in context._filters:
   479         args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
   479         args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
   480         if len(args) != 1:
   480         if len(args) != 1:
   481             raise error.ParseError(_("filter %s expects one argument") % n)
   481             raise error.ParseError(_(b"filter %s expects one argument") % n)
   482         f = context._filters[n]
   482         f = context._filters[n]
   483         return (templateutil.runfilter, (args[0], f))
   483         return (templateutil.runfilter, (args[0], f))
   484     raise error.ParseError(_("unknown function '%s'") % n)
   484     raise error.ParseError(_(b"unknown function '%s'") % n)
   485 
   485 
   486 
   486 
   487 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
   487 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
   488     """Compile parsed tree of function arguments into list or dict of
   488     """Compile parsed tree of function arguments into list or dict of
   489     (func, data) pairs
   489     (func, data) pairs
   516     _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
   516     _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
   517     treeargs = parser.buildargsdict(
   517     treeargs = parser.buildargsdict(
   518         getlist(exp),
   518         getlist(exp),
   519         funcname,
   519         funcname,
   520         argspec,
   520         argspec,
   521         keyvaluenode='keyvalue',
   521         keyvaluenode=b'keyvalue',
   522         keynode='symbol',
   522         keynode=b'symbol',
   523     )
   523     )
   524     compargs = util.sortdict()
   524     compargs = util.sortdict()
   525     if varkey:
   525     if varkey:
   526         compargs[varkey] = compilelist(treeargs.pop(varkey))
   526         compargs[varkey] = compilelist(treeargs.pop(varkey))
   527     if optkey:
   527     if optkey:
   529     compargs.update(compiledict(treeargs))
   529     compargs.update(compiledict(treeargs))
   530     return compargs
   530     return compargs
   531 
   531 
   532 
   532 
   533 def buildkeyvaluepair(exp, content):
   533 def buildkeyvaluepair(exp, content):
   534     raise error.ParseError(_("can't use a key-value pair in this context"))
   534     raise error.ParseError(_(b"can't use a key-value pair in this context"))
   535 
   535 
   536 
   536 
   537 def buildlist(exp, context):
   537 def buildlist(exp, context):
   538     raise error.ParseError(
   538     raise error.ParseError(
   539         _("can't use a list in this context"),
   539         _(b"can't use a list in this context"),
   540         hint=_('check place of comma and parens'),
   540         hint=_(b'check place of comma and parens'),
   541     )
   541     )
   542 
   542 
   543 
   543 
   544 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
   544 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
   545 exprmethods = {
   545 exprmethods = {
   546     "integer": lambda e, c: (templateutil.runinteger, e[1]),
   546     b"integer": lambda e, c: (templateutil.runinteger, e[1]),
   547     "string": lambda e, c: (templateutil.runstring, e[1]),
   547     b"string": lambda e, c: (templateutil.runstring, e[1]),
   548     "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
   548     b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
   549     "template": buildtemplate,
   549     b"template": buildtemplate,
   550     "group": lambda e, c: compileexp(e[1], c, exprmethods),
   550     b"group": lambda e, c: compileexp(e[1], c, exprmethods),
   551     ".": buildmember,
   551     b".": buildmember,
   552     "|": buildfilter,
   552     b"|": buildfilter,
   553     "%": buildmap,
   553     b"%": buildmap,
   554     "func": buildfunc,
   554     b"func": buildfunc,
   555     "keyvalue": buildkeyvaluepair,
   555     b"keyvalue": buildkeyvaluepair,
   556     "list": buildlist,
   556     b"list": buildlist,
   557     "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
   557     b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
   558     "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
   558     b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
   559     "negate": buildnegate,
   559     b"negate": buildnegate,
   560     "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
   560     b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
   561     "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
   561     b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
   562 }
   562 }
   563 
   563 
   564 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
   564 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
   565 methods = exprmethods.copy()
   565 methods = exprmethods.copy()
   566 methods["integer"] = exprmethods["symbol"]  # '{1}' as variable
   566 methods[b"integer"] = exprmethods[b"symbol"]  # '{1}' as variable
   567 
   567 
   568 
   568 
   569 class _aliasrules(parser.basealiasrules):
   569 class _aliasrules(parser.basealiasrules):
   570     """Parsing and expansion rule set of template aliases"""
   570     """Parsing and expansion rule set of template aliases"""
   571 
   571 
   572     _section = _('template alias')
   572     _section = _(b'template alias')
   573     _parse = staticmethod(_parseexpr)
   573     _parse = staticmethod(_parseexpr)
   574 
   574 
   575     @staticmethod
   575     @staticmethod
   576     def _trygetfunc(tree):
   576     def _trygetfunc(tree):
   577         """Return (name, args) if tree is func(...) or ...|filter; otherwise
   577         """Return (name, args) if tree is func(...) or ...|filter; otherwise
   578         None"""
   578         None"""
   579         if tree[0] == 'func' and tree[1][0] == 'symbol':
   579         if tree[0] == b'func' and tree[1][0] == b'symbol':
   580             return tree[1][1], getlist(tree[2])
   580             return tree[1][1], getlist(tree[2])
   581         if tree[0] == '|' and tree[2][0] == 'symbol':
   581         if tree[0] == b'|' and tree[2][0] == b'symbol':
   582             return tree[2][1], [tree[1]]
   582             return tree[2][1], [tree[1]]
   583 
   583 
   584 
   584 
   585 def expandaliases(tree, aliases):
   585 def expandaliases(tree, aliases):
   586     """Return new tree of aliases are expanded"""
   586     """Return new tree of aliases are expanded"""
   591 # template engine
   591 # template engine
   592 
   592 
   593 
   593 
   594 def unquotestring(s):
   594 def unquotestring(s):
   595     '''unwrap quotes if any; otherwise returns unmodified string'''
   595     '''unwrap quotes if any; otherwise returns unmodified string'''
   596     if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
   596     if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
   597         return s
   597         return s
   598     return s[1:-1]
   598     return s[1:-1]
   599 
   599 
   600 
   600 
   601 class resourcemapper(object):
   601 class resourcemapper(object):
   719         """Return internal data (e.g. cache) used for keyword/function
   719         """Return internal data (e.g. cache) used for keyword/function
   720         evaluation"""
   720         evaluation"""
   721         v = self._resources.lookup(mapping, key)
   721         v = self._resources.lookup(mapping, key)
   722         if v is None:
   722         if v is None:
   723             raise templateutil.ResourceUnavailable(
   723             raise templateutil.ResourceUnavailable(
   724                 _('template resource not available: %s') % key
   724                 _(b'template resource not available: %s') % key
   725             )
   725             )
   726         return v
   726         return v
   727 
   727 
   728     def _load(self, t):
   728     def _load(self, t):
   729         '''load, parse, and cache a template'''
   729         '''load, parse, and cache a template'''
   781 
   781 
   782 
   782 
   783 def stylelist():
   783 def stylelist():
   784     paths = templatepaths()
   784     paths = templatepaths()
   785     if not paths:
   785     if not paths:
   786         return _('no templates found, try `hg debuginstall` for more info')
   786         return _(b'no templates found, try `hg debuginstall` for more info')
   787     dirlist = os.listdir(paths[0])
   787     dirlist = os.listdir(paths[0])
   788     stylelist = []
   788     stylelist = []
   789     for file in dirlist:
   789     for file in dirlist:
   790         split = file.split(".")
   790         split = file.split(b".")
   791         if split[-1] in ('orig', 'rej'):
   791         if split[-1] in (b'orig', b'rej'):
   792             continue
   792             continue
   793         if split[0] == "map-cmdline":
   793         if split[0] == b"map-cmdline":
   794             stylelist.append(split[1])
   794             stylelist.append(split[1])
   795     return ", ".join(sorted(stylelist))
   795     return b", ".join(sorted(stylelist))
   796 
   796 
   797 
   797 
   798 def _readmapfile(mapfile):
   798 def _readmapfile(mapfile):
   799     """Load template elements from the given map file"""
   799     """Load template elements from the given map file"""
   800     if not os.path.exists(mapfile):
   800     if not os.path.exists(mapfile):
   801         raise error.Abort(
   801         raise error.Abort(
   802             _("style '%s' not found") % mapfile,
   802             _(b"style '%s' not found") % mapfile,
   803             hint=_("available styles: %s") % stylelist(),
   803             hint=_(b"available styles: %s") % stylelist(),
   804         )
   804         )
   805 
   805 
   806     base = os.path.dirname(mapfile)
   806     base = os.path.dirname(mapfile)
   807     conf = config.config(includepaths=templatepaths())
   807     conf = config.config(includepaths=templatepaths())
   808     conf.read(mapfile, remap={'': 'templates'})
   808     conf.read(mapfile, remap={b'': b'templates'})
   809 
   809 
   810     cache = {}
   810     cache = {}
   811     tmap = {}
   811     tmap = {}
   812     aliases = []
   812     aliases = []
   813 
   813 
   814     val = conf.get('templates', '__base__')
   814     val = conf.get(b'templates', b'__base__')
   815     if val and val[0] not in "'\"":
   815     if val and val[0] not in b"'\"":
   816         # treat as a pointer to a base class for this style
   816         # treat as a pointer to a base class for this style
   817         path = util.normpath(os.path.join(base, val))
   817         path = util.normpath(os.path.join(base, val))
   818 
   818 
   819         # fallback check in template paths
   819         # fallback check in template paths
   820         if not os.path.exists(path):
   820         if not os.path.exists(path):
   821             for p in templatepaths():
   821             for p in templatepaths():
   822                 p2 = util.normpath(os.path.join(p, val))
   822                 p2 = util.normpath(os.path.join(p, val))
   823                 if os.path.isfile(p2):
   823                 if os.path.isfile(p2):
   824                     path = p2
   824                     path = p2
   825                     break
   825                     break
   826                 p3 = util.normpath(os.path.join(p2, "map"))
   826                 p3 = util.normpath(os.path.join(p2, b"map"))
   827                 if os.path.isfile(p3):
   827                 if os.path.isfile(p3):
   828                     path = p3
   828                     path = p3
   829                     break
   829                     break
   830 
   830 
   831         cache, tmap, aliases = _readmapfile(path)
   831         cache, tmap, aliases = _readmapfile(path)
   832 
   832 
   833     for key, val in conf['templates'].items():
   833     for key, val in conf[b'templates'].items():
   834         if not val:
   834         if not val:
   835             raise error.ParseError(
   835             raise error.ParseError(
   836                 _('missing value'), conf.source('templates', key)
   836                 _(b'missing value'), conf.source(b'templates', key)
   837             )
   837             )
   838         if val[0] in "'\"":
   838         if val[0] in b"'\"":
   839             if val[0] != val[-1]:
   839             if val[0] != val[-1]:
   840                 raise error.ParseError(
   840                 raise error.ParseError(
   841                     _('unmatched quotes'), conf.source('templates', key)
   841                     _(b'unmatched quotes'), conf.source(b'templates', key)
   842                 )
   842                 )
   843             cache[key] = unquotestring(val)
   843             cache[key] = unquotestring(val)
   844         elif key != '__base__':
   844         elif key != b'__base__':
   845             tmap[key] = os.path.join(base, val)
   845             tmap[key] = os.path.join(base, val)
   846     aliases.extend(conf['templatealias'].items())
   846     aliases.extend(conf[b'templatealias'].items())
   847     return cache, tmap, aliases
   847     return cache, tmap, aliases
   848 
   848 
   849 
   849 
   850 class loader(object):
   850 class loader(object):
   851     """Load template fragments optionally from a map file"""
   851     """Load template fragments optionally from a map file"""
   865         if t not in self.cache:
   865         if t not in self.cache:
   866             try:
   866             try:
   867                 self.cache[t] = util.readfile(self._map[t])
   867                 self.cache[t] = util.readfile(self._map[t])
   868             except KeyError as inst:
   868             except KeyError as inst:
   869                 raise templateutil.TemplateNotFound(
   869                 raise templateutil.TemplateNotFound(
   870                     _('"%s" not in template map') % inst.args[0]
   870                     _(b'"%s" not in template map') % inst.args[0]
   871                 )
   871                 )
   872             except IOError as inst:
   872             except IOError as inst:
   873                 reason = _('template file %s: %s') % (
   873                 reason = _(b'template file %s: %s') % (
   874                     self._map[t],
   874                     self._map[t],
   875                     stringutil.forcebytestr(inst.args[1]),
   875                     stringutil.forcebytestr(inst.args[1]),
   876                 )
   876                 )
   877                 raise IOError(inst.args[0], encoding.strfromlocal(reason))
   877                 raise IOError(inst.args[0], encoding.strfromlocal(reason))
   878         return self._parse(self.cache[t])
   878         return self._parse(self.cache[t])
   885 
   885 
   886     def _findsymbolsused(self, tree, syms):
   886     def _findsymbolsused(self, tree, syms):
   887         if not tree:
   887         if not tree:
   888             return
   888             return
   889         op = tree[0]
   889         op = tree[0]
   890         if op == 'symbol':
   890         if op == b'symbol':
   891             s = tree[1]
   891             s = tree[1]
   892             if s in syms[0]:
   892             if s in syms[0]:
   893                 return  # avoid recursion: s -> cache[s] -> s
   893                 return  # avoid recursion: s -> cache[s] -> s
   894             syms[0].add(s)
   894             syms[0].add(s)
   895             if s in self.cache or s in self._map:
   895             if s in self.cache or s in self._map:
   896                 # s may be a reference for named template
   896                 # s may be a reference for named template
   897                 self._findsymbolsused(self.load(s), syms)
   897                 self._findsymbolsused(self.load(s), syms)
   898             return
   898             return
   899         if op in {'integer', 'string'}:
   899         if op in {b'integer', b'string'}:
   900             return
   900             return
   901         # '{arg|func}' == '{func(arg)}'
   901         # '{arg|func}' == '{func(arg)}'
   902         if op == '|':
   902         if op == b'|':
   903             syms[1].add(getsymbol(tree[2]))
   903             syms[1].add(getsymbol(tree[2]))
   904             self._findsymbolsused(tree[1], syms)
   904             self._findsymbolsused(tree[1], syms)
   905             return
   905             return
   906         if op == 'func':
   906         if op == b'func':
   907             syms[1].add(getsymbol(tree[1]))
   907             syms[1].add(getsymbol(tree[1]))
   908             self._findsymbolsused(tree[2], syms)
   908             self._findsymbolsused(tree[2], syms)
   909             return
   909             return
   910         for x in tree[1:]:
   910         for x in tree[1:]:
   911             self._findsymbolsused(x, syms)
   911             self._findsymbolsused(x, syms)
   995         """Look up (keywords, filters/functions) referenced from the default
   995         """Look up (keywords, filters/functions) referenced from the default
   996         unnamed template
   996         unnamed template
   997 
   997 
   998         This may load additional templates from the map file.
   998         This may load additional templates from the map file.
   999         """
   999         """
  1000         return self.symbolsused('')
  1000         return self.symbolsused(b'')
  1001 
  1001 
  1002     def symbolsused(self, t):
  1002     def symbolsused(self, t):
  1003         """Look up (keywords, filters/functions) referenced from the name
  1003         """Look up (keywords, filters/functions) referenced from the name
  1004         template 't'
  1004         template 't'
  1005 
  1005 
  1007         """
  1007         """
  1008         return self._loader.symbolsused(t)
  1008         return self._loader.symbolsused(t)
  1009 
  1009 
  1010     def renderdefault(self, mapping):
  1010     def renderdefault(self, mapping):
  1011         """Render the default unnamed template and return result as string"""
  1011         """Render the default unnamed template and return result as string"""
  1012         return self.render('', mapping)
  1012         return self.render(b'', mapping)
  1013 
  1013 
  1014     def render(self, t, mapping):
  1014     def render(self, t, mapping):
  1015         """Render the specified named template and return result as string"""
  1015         """Render the specified named template and return result as string"""
  1016         return b''.join(self.generate(t, mapping))
  1016         return b''.join(self.generate(t, mapping))
  1017 
  1017 
  1026         return stream
  1026         return stream
  1027 
  1027 
  1028 
  1028 
  1029 def templatepaths():
  1029 def templatepaths():
  1030     '''return locations used for template files.'''
  1030     '''return locations used for template files.'''
  1031     pathsrel = ['templates']
  1031     pathsrel = [b'templates']
  1032     paths = [os.path.normpath(os.path.join(util.datapath, f)) for f in pathsrel]
  1032     paths = [os.path.normpath(os.path.join(util.datapath, f)) for f in pathsrel]
  1033     return [p for p in paths if os.path.isdir(p)]
  1033     return [p for p in paths if os.path.isdir(p)]
  1034 
  1034 
  1035 
  1035 
  1036 def templatepath(name):
  1036 def templatepath(name):
  1067             or pycompat.ossep in style
  1067             or pycompat.ossep in style
  1068             or pycompat.osaltsep
  1068             or pycompat.osaltsep
  1069             and pycompat.osaltsep in style
  1069             and pycompat.osaltsep in style
  1070         ):
  1070         ):
  1071             continue
  1071             continue
  1072         locations = [os.path.join(style, 'map'), 'map-' + style]
  1072         locations = [os.path.join(style, b'map'), b'map-' + style]
  1073         locations.append('map')
  1073         locations.append(b'map')
  1074 
  1074 
  1075         for path in paths:
  1075         for path in paths:
  1076             for location in locations:
  1076             for location in locations:
  1077                 mapfile = os.path.join(path, location)
  1077                 mapfile = os.path.join(path, location)
  1078                 if os.path.isfile(mapfile):
  1078                 if os.path.isfile(mapfile):
  1079                     return style, mapfile
  1079                     return style, mapfile
  1080 
  1080 
  1081     raise RuntimeError("No hgweb templates found in %r" % paths)
  1081     raise RuntimeError(b"No hgweb templates found in %r" % paths)