##// END OF EJS Templates
templater: show repr of string we're rejecting...
Augie Fackler -
r36729:e79adc12 default
parent child Browse files
Show More
@@ -1,1642 +1,1642 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 obsutil,
21 obsutil,
22 parser,
22 parser,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 revset as revsetmod,
25 revset as revsetmod,
26 revsetlang,
26 revsetlang,
27 scmutil,
27 scmutil,
28 templatefilters,
28 templatefilters,
29 templatekw,
29 templatekw,
30 util,
30 util,
31 )
31 )
32 from .utils import dateutil
32 from .utils import dateutil
33
33
34 class ResourceUnavailable(error.Abort):
34 class ResourceUnavailable(error.Abort):
35 pass
35 pass
36
36
37 class TemplateNotFound(error.Abort):
37 class TemplateNotFound(error.Abort):
38 pass
38 pass
39
39
40 # template parsing
40 # template parsing
41
41
42 elements = {
42 elements = {
43 # token-type: binding-strength, primary, prefix, infix, suffix
43 # token-type: binding-strength, primary, prefix, infix, suffix
44 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
44 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
45 ".": (18, None, None, (".", 18), None),
45 ".": (18, None, None, (".", 18), None),
46 "%": (15, None, None, ("%", 15), None),
46 "%": (15, None, None, ("%", 15), None),
47 "|": (15, None, None, ("|", 15), None),
47 "|": (15, None, None, ("|", 15), None),
48 "*": (5, None, None, ("*", 5), None),
48 "*": (5, None, None, ("*", 5), None),
49 "/": (5, None, None, ("/", 5), None),
49 "/": (5, None, None, ("/", 5), None),
50 "+": (4, None, None, ("+", 4), None),
50 "+": (4, None, None, ("+", 4), None),
51 "-": (4, None, ("negate", 19), ("-", 4), None),
51 "-": (4, None, ("negate", 19), ("-", 4), None),
52 "=": (3, None, None, ("keyvalue", 3), None),
52 "=": (3, None, None, ("keyvalue", 3), None),
53 ",": (2, None, None, ("list", 2), None),
53 ",": (2, None, None, ("list", 2), None),
54 ")": (0, None, None, None, None),
54 ")": (0, None, None, None, None),
55 "integer": (0, "integer", None, None, None),
55 "integer": (0, "integer", None, None, None),
56 "symbol": (0, "symbol", None, None, None),
56 "symbol": (0, "symbol", None, None, None),
57 "string": (0, "string", None, None, None),
57 "string": (0, "string", None, None, None),
58 "template": (0, "template", None, None, None),
58 "template": (0, "template", None, None, None),
59 "end": (0, None, None, None, None),
59 "end": (0, None, None, None, None),
60 }
60 }
61
61
62 def tokenize(program, start, end, term=None):
62 def tokenize(program, start, end, term=None):
63 """Parse a template expression into a stream of tokens, which must end
63 """Parse a template expression into a stream of tokens, which must end
64 with term if specified"""
64 with term if specified"""
65 pos = start
65 pos = start
66 program = pycompat.bytestr(program)
66 program = pycompat.bytestr(program)
67 while pos < end:
67 while pos < end:
68 c = program[pos]
68 c = program[pos]
69 if c.isspace(): # skip inter-token whitespace
69 if c.isspace(): # skip inter-token whitespace
70 pass
70 pass
71 elif c in "(=,).%|+-*/": # handle simple operators
71 elif c in "(=,).%|+-*/": # handle simple operators
72 yield (c, None, pos)
72 yield (c, None, pos)
73 elif c in '"\'': # handle quoted templates
73 elif c in '"\'': # handle quoted templates
74 s = pos + 1
74 s = pos + 1
75 data, pos = _parsetemplate(program, s, end, c)
75 data, pos = _parsetemplate(program, s, end, c)
76 yield ('template', data, s)
76 yield ('template', data, s)
77 pos -= 1
77 pos -= 1
78 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
78 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
79 # handle quoted strings
79 # handle quoted strings
80 c = program[pos + 1]
80 c = program[pos + 1]
81 s = pos = pos + 2
81 s = pos = pos + 2
82 while pos < end: # find closing quote
82 while pos < end: # find closing quote
83 d = program[pos]
83 d = program[pos]
84 if d == '\\': # skip over escaped characters
84 if d == '\\': # skip over escaped characters
85 pos += 2
85 pos += 2
86 continue
86 continue
87 if d == c:
87 if d == c:
88 yield ('string', program[s:pos], s)
88 yield ('string', program[s:pos], s)
89 break
89 break
90 pos += 1
90 pos += 1
91 else:
91 else:
92 raise error.ParseError(_("unterminated string"), s)
92 raise error.ParseError(_("unterminated string"), s)
93 elif c.isdigit():
93 elif c.isdigit():
94 s = pos
94 s = pos
95 while pos < end:
95 while pos < end:
96 d = program[pos]
96 d = program[pos]
97 if not d.isdigit():
97 if not d.isdigit():
98 break
98 break
99 pos += 1
99 pos += 1
100 yield ('integer', program[s:pos], s)
100 yield ('integer', program[s:pos], s)
101 pos -= 1
101 pos -= 1
102 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
102 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
103 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
103 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
104 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
104 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
105 # where some of nested templates were preprocessed as strings and
105 # where some of nested templates were preprocessed as strings and
106 # then compiled. therefore, \"...\" was allowed. (issue4733)
106 # then compiled. therefore, \"...\" was allowed. (issue4733)
107 #
107 #
108 # processing flow of _evalifliteral() at 5ab28a2e9962:
108 # processing flow of _evalifliteral() at 5ab28a2e9962:
109 # outer template string -> stringify() -> compiletemplate()
109 # outer template string -> stringify() -> compiletemplate()
110 # ------------------------ ------------ ------------------
110 # ------------------------ ------------ ------------------
111 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
111 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
112 # ~~~~~~~~
112 # ~~~~~~~~
113 # escaped quoted string
113 # escaped quoted string
114 if c == 'r':
114 if c == 'r':
115 pos += 1
115 pos += 1
116 token = 'string'
116 token = 'string'
117 else:
117 else:
118 token = 'template'
118 token = 'template'
119 quote = program[pos:pos + 2]
119 quote = program[pos:pos + 2]
120 s = pos = pos + 2
120 s = pos = pos + 2
121 while pos < end: # find closing escaped quote
121 while pos < end: # find closing escaped quote
122 if program.startswith('\\\\\\', pos, end):
122 if program.startswith('\\\\\\', pos, end):
123 pos += 4 # skip over double escaped characters
123 pos += 4 # skip over double escaped characters
124 continue
124 continue
125 if program.startswith(quote, pos, end):
125 if program.startswith(quote, pos, end):
126 # interpret as if it were a part of an outer string
126 # interpret as if it were a part of an outer string
127 data = parser.unescapestr(program[s:pos])
127 data = parser.unescapestr(program[s:pos])
128 if token == 'template':
128 if token == 'template':
129 data = _parsetemplate(data, 0, len(data))[0]
129 data = _parsetemplate(data, 0, len(data))[0]
130 yield (token, data, s)
130 yield (token, data, s)
131 pos += 1
131 pos += 1
132 break
132 break
133 pos += 1
133 pos += 1
134 else:
134 else:
135 raise error.ParseError(_("unterminated string"), s)
135 raise error.ParseError(_("unterminated string"), s)
136 elif c.isalnum() or c in '_':
136 elif c.isalnum() or c in '_':
137 s = pos
137 s = pos
138 pos += 1
138 pos += 1
139 while pos < end: # find end of symbol
139 while pos < end: # find end of symbol
140 d = program[pos]
140 d = program[pos]
141 if not (d.isalnum() or d == "_"):
141 if not (d.isalnum() or d == "_"):
142 break
142 break
143 pos += 1
143 pos += 1
144 sym = program[s:pos]
144 sym = program[s:pos]
145 yield ('symbol', sym, s)
145 yield ('symbol', sym, s)
146 pos -= 1
146 pos -= 1
147 elif c == term:
147 elif c == term:
148 yield ('end', None, pos)
148 yield ('end', None, pos)
149 return
149 return
150 else:
150 else:
151 raise error.ParseError(_("syntax error"), pos)
151 raise error.ParseError(_("syntax error"), pos)
152 pos += 1
152 pos += 1
153 if term:
153 if term:
154 raise error.ParseError(_("unterminated template expansion"), start)
154 raise error.ParseError(_("unterminated template expansion"), start)
155 yield ('end', None, pos)
155 yield ('end', None, pos)
156
156
157 def _parsetemplate(tmpl, start, stop, quote=''):
157 def _parsetemplate(tmpl, start, stop, quote=''):
158 r"""
158 r"""
159 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
159 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
160 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
160 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
161 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
162 ([('string', 'foo'), ('symbol', 'bar')], 9)
162 ([('string', 'foo'), ('symbol', 'bar')], 9)
163 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
163 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
164 ([('string', 'foo')], 4)
164 ([('string', 'foo')], 4)
165 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
165 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
166 ([('string', 'foo"'), ('string', 'bar')], 9)
166 ([('string', 'foo"'), ('string', 'bar')], 9)
167 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
167 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
168 ([('string', 'foo\\')], 6)
168 ([('string', 'foo\\')], 6)
169 """
169 """
170 parsed = []
170 parsed = []
171 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
171 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
172 if typ == 'string':
172 if typ == 'string':
173 parsed.append((typ, val))
173 parsed.append((typ, val))
174 elif typ == 'template':
174 elif typ == 'template':
175 parsed.append(val)
175 parsed.append(val)
176 elif typ == 'end':
176 elif typ == 'end':
177 return parsed, pos
177 return parsed, pos
178 else:
178 else:
179 raise error.ProgrammingError('unexpected type: %s' % typ)
179 raise error.ProgrammingError('unexpected type: %s' % typ)
180 raise error.ProgrammingError('unterminated scanning of template')
180 raise error.ProgrammingError('unterminated scanning of template')
181
181
182 def scantemplate(tmpl, raw=False):
182 def scantemplate(tmpl, raw=False):
183 r"""Scan (type, start, end) positions of outermost elements in template
183 r"""Scan (type, start, end) positions of outermost elements in template
184
184
185 If raw=True, a backslash is not taken as an escape character just like
185 If raw=True, a backslash is not taken as an escape character just like
186 r'' string in Python. Note that this is different from r'' literal in
186 r'' string in Python. Note that this is different from r'' literal in
187 template in that no template fragment can appear in r'', e.g. r'{foo}'
187 template in that no template fragment can appear in r'', e.g. r'{foo}'
188 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
188 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
189 'foo'.
189 'foo'.
190
190
191 >>> list(scantemplate(b'foo{bar}"baz'))
191 >>> list(scantemplate(b'foo{bar}"baz'))
192 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
192 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
193 >>> list(scantemplate(b'outer{"inner"}outer'))
193 >>> list(scantemplate(b'outer{"inner"}outer'))
194 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
194 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
195 >>> list(scantemplate(b'foo\\{escaped}'))
195 >>> list(scantemplate(b'foo\\{escaped}'))
196 [('string', 0, 5), ('string', 5, 13)]
196 [('string', 0, 5), ('string', 5, 13)]
197 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
197 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
198 [('string', 0, 4), ('template', 4, 13)]
198 [('string', 0, 4), ('template', 4, 13)]
199 """
199 """
200 last = None
200 last = None
201 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
201 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
202 if last:
202 if last:
203 yield last + (pos,)
203 yield last + (pos,)
204 if typ == 'end':
204 if typ == 'end':
205 return
205 return
206 else:
206 else:
207 last = (typ, pos)
207 last = (typ, pos)
208 raise error.ProgrammingError('unterminated scanning of template')
208 raise error.ProgrammingError('unterminated scanning of template')
209
209
210 def _scantemplate(tmpl, start, stop, quote='', raw=False):
210 def _scantemplate(tmpl, start, stop, quote='', raw=False):
211 """Parse template string into chunks of strings and template expressions"""
211 """Parse template string into chunks of strings and template expressions"""
212 sepchars = '{' + quote
212 sepchars = '{' + quote
213 unescape = [parser.unescapestr, pycompat.identity][raw]
213 unescape = [parser.unescapestr, pycompat.identity][raw]
214 pos = start
214 pos = start
215 p = parser.parser(elements)
215 p = parser.parser(elements)
216 try:
216 try:
217 while pos < stop:
217 while pos < stop:
218 n = min((tmpl.find(c, pos, stop) for c in sepchars),
218 n = min((tmpl.find(c, pos, stop) for c in sepchars),
219 key=lambda n: (n < 0, n))
219 key=lambda n: (n < 0, n))
220 if n < 0:
220 if n < 0:
221 yield ('string', unescape(tmpl[pos:stop]), pos)
221 yield ('string', unescape(tmpl[pos:stop]), pos)
222 pos = stop
222 pos = stop
223 break
223 break
224 c = tmpl[n:n + 1]
224 c = tmpl[n:n + 1]
225 bs = 0 # count leading backslashes
225 bs = 0 # count leading backslashes
226 if not raw:
226 if not raw:
227 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
227 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
228 if bs % 2 == 1:
228 if bs % 2 == 1:
229 # escaped (e.g. '\{', '\\\{', but not '\\{')
229 # escaped (e.g. '\{', '\\\{', but not '\\{')
230 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
230 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
231 pos = n + 1
231 pos = n + 1
232 continue
232 continue
233 if n > pos:
233 if n > pos:
234 yield ('string', unescape(tmpl[pos:n]), pos)
234 yield ('string', unescape(tmpl[pos:n]), pos)
235 if c == quote:
235 if c == quote:
236 yield ('end', None, n + 1)
236 yield ('end', None, n + 1)
237 return
237 return
238
238
239 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
239 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
240 if not tmpl.startswith('}', pos):
240 if not tmpl.startswith('}', pos):
241 raise error.ParseError(_("invalid token"), pos)
241 raise error.ParseError(_("invalid token"), pos)
242 yield ('template', parseres, n)
242 yield ('template', parseres, n)
243 pos += 1
243 pos += 1
244
244
245 if quote:
245 if quote:
246 raise error.ParseError(_("unterminated string"), start)
246 raise error.ParseError(_("unterminated string"), start)
247 except error.ParseError as inst:
247 except error.ParseError as inst:
248 if len(inst.args) > 1: # has location
248 if len(inst.args) > 1: # has location
249 loc = inst.args[1]
249 loc = inst.args[1]
250 # Offset the caret location by the number of newlines before the
250 # Offset the caret location by the number of newlines before the
251 # location of the error, since we will replace one-char newlines
251 # location of the error, since we will replace one-char newlines
252 # with the two-char literal r'\n'.
252 # with the two-char literal r'\n'.
253 offset = tmpl[:loc].count('\n')
253 offset = tmpl[:loc].count('\n')
254 tmpl = tmpl.replace('\n', br'\n')
254 tmpl = tmpl.replace('\n', br'\n')
255 # We want the caret to point to the place in the template that
255 # We want the caret to point to the place in the template that
256 # failed to parse, but in a hint we get a open paren at the
256 # failed to parse, but in a hint we get a open paren at the
257 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
257 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
258 # to line up the caret with the location of the error.
258 # to line up the caret with the location of the error.
259 inst.hint = (tmpl + '\n'
259 inst.hint = (tmpl + '\n'
260 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
260 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
261 raise
261 raise
262 yield ('end', None, pos)
262 yield ('end', None, pos)
263
263
264 def _unnesttemplatelist(tree):
264 def _unnesttemplatelist(tree):
265 """Expand list of templates to node tuple
265 """Expand list of templates to node tuple
266
266
267 >>> def f(tree):
267 >>> def f(tree):
268 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
268 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
269 >>> f((b'template', []))
269 >>> f((b'template', []))
270 (string '')
270 (string '')
271 >>> f((b'template', [(b'string', b'foo')]))
271 >>> f((b'template', [(b'string', b'foo')]))
272 (string 'foo')
272 (string 'foo')
273 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
273 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
274 (template
274 (template
275 (string 'foo')
275 (string 'foo')
276 (symbol 'rev'))
276 (symbol 'rev'))
277 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
277 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
278 (template
278 (template
279 (symbol 'rev'))
279 (symbol 'rev'))
280 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
280 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
281 (string 'foo')
281 (string 'foo')
282 """
282 """
283 if not isinstance(tree, tuple):
283 if not isinstance(tree, tuple):
284 return tree
284 return tree
285 op = tree[0]
285 op = tree[0]
286 if op != 'template':
286 if op != 'template':
287 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
287 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
288
288
289 assert len(tree) == 2
289 assert len(tree) == 2
290 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
290 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
291 if not xs:
291 if not xs:
292 return ('string', '') # empty template ""
292 return ('string', '') # empty template ""
293 elif len(xs) == 1 and xs[0][0] == 'string':
293 elif len(xs) == 1 and xs[0][0] == 'string':
294 return xs[0] # fast path for string with no template fragment "x"
294 return xs[0] # fast path for string with no template fragment "x"
295 else:
295 else:
296 return (op,) + xs
296 return (op,) + xs
297
297
298 def parse(tmpl):
298 def parse(tmpl):
299 """Parse template string into tree"""
299 """Parse template string into tree"""
300 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
300 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
301 assert pos == len(tmpl), 'unquoted template should be consumed'
301 assert pos == len(tmpl), 'unquoted template should be consumed'
302 return _unnesttemplatelist(('template', parsed))
302 return _unnesttemplatelist(('template', parsed))
303
303
304 def _parseexpr(expr):
304 def _parseexpr(expr):
305 """Parse a template expression into tree
305 """Parse a template expression into tree
306
306
307 >>> _parseexpr(b'"foo"')
307 >>> _parseexpr(b'"foo"')
308 ('string', 'foo')
308 ('string', 'foo')
309 >>> _parseexpr(b'foo(bar)')
309 >>> _parseexpr(b'foo(bar)')
310 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
310 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
311 >>> _parseexpr(b'foo(')
311 >>> _parseexpr(b'foo(')
312 Traceback (most recent call last):
312 Traceback (most recent call last):
313 ...
313 ...
314 ParseError: ('not a prefix: end', 4)
314 ParseError: ('not a prefix: end', 4)
315 >>> _parseexpr(b'"foo" "bar"')
315 >>> _parseexpr(b'"foo" "bar"')
316 Traceback (most recent call last):
316 Traceback (most recent call last):
317 ...
317 ...
318 ParseError: ('invalid token', 7)
318 ParseError: ('invalid token', 7)
319 """
319 """
320 p = parser.parser(elements)
320 p = parser.parser(elements)
321 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
321 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
322 if pos != len(expr):
322 if pos != len(expr):
323 raise error.ParseError(_('invalid token'), pos)
323 raise error.ParseError(_('invalid token'), pos)
324 return _unnesttemplatelist(tree)
324 return _unnesttemplatelist(tree)
325
325
326 def prettyformat(tree):
326 def prettyformat(tree):
327 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
327 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
328
328
329 def compileexp(exp, context, curmethods):
329 def compileexp(exp, context, curmethods):
330 """Compile parsed template tree to (func, data) pair"""
330 """Compile parsed template tree to (func, data) pair"""
331 if not exp:
331 if not exp:
332 raise error.ParseError(_("missing argument"))
332 raise error.ParseError(_("missing argument"))
333 t = exp[0]
333 t = exp[0]
334 if t in curmethods:
334 if t in curmethods:
335 return curmethods[t](exp, context)
335 return curmethods[t](exp, context)
336 raise error.ParseError(_("unknown method '%s'") % t)
336 raise error.ParseError(_("unknown method '%s'") % t)
337
337
338 # template evaluation
338 # template evaluation
339
339
340 def getsymbol(exp):
340 def getsymbol(exp):
341 if exp[0] == 'symbol':
341 if exp[0] == 'symbol':
342 return exp[1]
342 return exp[1]
343 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
343 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
344
344
345 def getlist(x):
345 def getlist(x):
346 if not x:
346 if not x:
347 return []
347 return []
348 if x[0] == 'list':
348 if x[0] == 'list':
349 return getlist(x[1]) + [x[2]]
349 return getlist(x[1]) + [x[2]]
350 return [x]
350 return [x]
351
351
352 def gettemplate(exp, context):
352 def gettemplate(exp, context):
353 """Compile given template tree or load named template from map file;
353 """Compile given template tree or load named template from map file;
354 returns (func, data) pair"""
354 returns (func, data) pair"""
355 if exp[0] in ('template', 'string'):
355 if exp[0] in ('template', 'string'):
356 return compileexp(exp, context, methods)
356 return compileexp(exp, context, methods)
357 if exp[0] == 'symbol':
357 if exp[0] == 'symbol':
358 # unlike runsymbol(), here 'symbol' is always taken as template name
358 # unlike runsymbol(), here 'symbol' is always taken as template name
359 # even if it exists in mapping. this allows us to override mapping
359 # even if it exists in mapping. this allows us to override mapping
360 # by web templates, e.g. 'changelogtag' is redefined in map file.
360 # by web templates, e.g. 'changelogtag' is redefined in map file.
361 return context._load(exp[1])
361 return context._load(exp[1])
362 raise error.ParseError(_("expected template specifier"))
362 raise error.ParseError(_("expected template specifier"))
363
363
364 def findsymbolicname(arg):
364 def findsymbolicname(arg):
365 """Find symbolic name for the given compiled expression; returns None
365 """Find symbolic name for the given compiled expression; returns None
366 if nothing found reliably"""
366 if nothing found reliably"""
367 while True:
367 while True:
368 func, data = arg
368 func, data = arg
369 if func is runsymbol:
369 if func is runsymbol:
370 return data
370 return data
371 elif func is runfilter:
371 elif func is runfilter:
372 arg = data[0]
372 arg = data[0]
373 else:
373 else:
374 return None
374 return None
375
375
376 def evalrawexp(context, mapping, arg):
376 def evalrawexp(context, mapping, arg):
377 """Evaluate given argument as a bare template object which may require
377 """Evaluate given argument as a bare template object which may require
378 further processing (such as folding generator of strings)"""
378 further processing (such as folding generator of strings)"""
379 func, data = arg
379 func, data = arg
380 return func(context, mapping, data)
380 return func(context, mapping, data)
381
381
382 def evalfuncarg(context, mapping, arg):
382 def evalfuncarg(context, mapping, arg):
383 """Evaluate given argument as value type"""
383 """Evaluate given argument as value type"""
384 thing = evalrawexp(context, mapping, arg)
384 thing = evalrawexp(context, mapping, arg)
385 thing = templatekw.unwrapvalue(thing)
385 thing = templatekw.unwrapvalue(thing)
386 # evalrawexp() may return string, generator of strings or arbitrary object
386 # evalrawexp() may return string, generator of strings or arbitrary object
387 # such as date tuple, but filter does not want generator.
387 # such as date tuple, but filter does not want generator.
388 if isinstance(thing, types.GeneratorType):
388 if isinstance(thing, types.GeneratorType):
389 thing = stringify(thing)
389 thing = stringify(thing)
390 return thing
390 return thing
391
391
392 def evalboolean(context, mapping, arg):
392 def evalboolean(context, mapping, arg):
393 """Evaluate given argument as boolean, but also takes boolean literals"""
393 """Evaluate given argument as boolean, but also takes boolean literals"""
394 func, data = arg
394 func, data = arg
395 if func is runsymbol:
395 if func is runsymbol:
396 thing = func(context, mapping, data, default=None)
396 thing = func(context, mapping, data, default=None)
397 if thing is None:
397 if thing is None:
398 # not a template keyword, takes as a boolean literal
398 # not a template keyword, takes as a boolean literal
399 thing = util.parsebool(data)
399 thing = util.parsebool(data)
400 else:
400 else:
401 thing = func(context, mapping, data)
401 thing = func(context, mapping, data)
402 thing = templatekw.unwrapvalue(thing)
402 thing = templatekw.unwrapvalue(thing)
403 if isinstance(thing, bool):
403 if isinstance(thing, bool):
404 return thing
404 return thing
405 # other objects are evaluated as strings, which means 0 is True, but
405 # other objects are evaluated as strings, which means 0 is True, but
406 # empty dict/list should be False as they are expected to be ''
406 # empty dict/list should be False as they are expected to be ''
407 return bool(stringify(thing))
407 return bool(stringify(thing))
408
408
409 def evalinteger(context, mapping, arg, err=None):
409 def evalinteger(context, mapping, arg, err=None):
410 v = evalfuncarg(context, mapping, arg)
410 v = evalfuncarg(context, mapping, arg)
411 try:
411 try:
412 return int(v)
412 return int(v)
413 except (TypeError, ValueError):
413 except (TypeError, ValueError):
414 raise error.ParseError(err or _('not an integer'))
414 raise error.ParseError(err or _('not an integer'))
415
415
416 def evalstring(context, mapping, arg):
416 def evalstring(context, mapping, arg):
417 return stringify(evalrawexp(context, mapping, arg))
417 return stringify(evalrawexp(context, mapping, arg))
418
418
419 def evalstringliteral(context, mapping, arg):
419 def evalstringliteral(context, mapping, arg):
420 """Evaluate given argument as string template, but returns symbol name
420 """Evaluate given argument as string template, but returns symbol name
421 if it is unknown"""
421 if it is unknown"""
422 func, data = arg
422 func, data = arg
423 if func is runsymbol:
423 if func is runsymbol:
424 thing = func(context, mapping, data, default=data)
424 thing = func(context, mapping, data, default=data)
425 else:
425 else:
426 thing = func(context, mapping, data)
426 thing = func(context, mapping, data)
427 return stringify(thing)
427 return stringify(thing)
428
428
429 _evalfuncbytype = {
429 _evalfuncbytype = {
430 bool: evalboolean,
430 bool: evalboolean,
431 bytes: evalstring,
431 bytes: evalstring,
432 int: evalinteger,
432 int: evalinteger,
433 }
433 }
434
434
435 def evalastype(context, mapping, arg, typ):
435 def evalastype(context, mapping, arg, typ):
436 """Evaluate given argument and coerce its type"""
436 """Evaluate given argument and coerce its type"""
437 try:
437 try:
438 f = _evalfuncbytype[typ]
438 f = _evalfuncbytype[typ]
439 except KeyError:
439 except KeyError:
440 raise error.ProgrammingError('invalid type specified: %r' % typ)
440 raise error.ProgrammingError('invalid type specified: %r' % typ)
441 return f(context, mapping, arg)
441 return f(context, mapping, arg)
442
442
443 def runinteger(context, mapping, data):
443 def runinteger(context, mapping, data):
444 return int(data)
444 return int(data)
445
445
446 def runstring(context, mapping, data):
446 def runstring(context, mapping, data):
447 return data
447 return data
448
448
449 def _recursivesymbolblocker(key):
449 def _recursivesymbolblocker(key):
450 def showrecursion(**args):
450 def showrecursion(**args):
451 raise error.Abort(_("recursive reference '%s' in template") % key)
451 raise error.Abort(_("recursive reference '%s' in template") % key)
452 return showrecursion
452 return showrecursion
453
453
454 def _runrecursivesymbol(context, mapping, key):
454 def _runrecursivesymbol(context, mapping, key):
455 raise error.Abort(_("recursive reference '%s' in template") % key)
455 raise error.Abort(_("recursive reference '%s' in template") % key)
456
456
457 def runsymbol(context, mapping, key, default=''):
457 def runsymbol(context, mapping, key, default=''):
458 v = context.symbol(mapping, key)
458 v = context.symbol(mapping, key)
459 if v is None:
459 if v is None:
460 # put poison to cut recursion. we can't move this to parsing phase
460 # put poison to cut recursion. we can't move this to parsing phase
461 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
461 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
462 safemapping = mapping.copy()
462 safemapping = mapping.copy()
463 safemapping[key] = _recursivesymbolblocker(key)
463 safemapping[key] = _recursivesymbolblocker(key)
464 try:
464 try:
465 v = context.process(key, safemapping)
465 v = context.process(key, safemapping)
466 except TemplateNotFound:
466 except TemplateNotFound:
467 v = default
467 v = default
468 if callable(v) and getattr(v, '_requires', None) is None:
468 if callable(v) and getattr(v, '_requires', None) is None:
469 # old templatekw: expand all keywords and resources
469 # old templatekw: expand all keywords and resources
470 props = context._resources.copy()
470 props = context._resources.copy()
471 props.update(mapping)
471 props.update(mapping)
472 return v(**pycompat.strkwargs(props))
472 return v(**pycompat.strkwargs(props))
473 if callable(v):
473 if callable(v):
474 # new templatekw
474 # new templatekw
475 try:
475 try:
476 return v(context, mapping)
476 return v(context, mapping)
477 except ResourceUnavailable:
477 except ResourceUnavailable:
478 # unsupported keyword is mapped to empty just like unknown keyword
478 # unsupported keyword is mapped to empty just like unknown keyword
479 return None
479 return None
480 return v
480 return v
481
481
482 def buildtemplate(exp, context):
482 def buildtemplate(exp, context):
483 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
483 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
484 return (runtemplate, ctmpl)
484 return (runtemplate, ctmpl)
485
485
486 def runtemplate(context, mapping, template):
486 def runtemplate(context, mapping, template):
487 for arg in template:
487 for arg in template:
488 yield evalrawexp(context, mapping, arg)
488 yield evalrawexp(context, mapping, arg)
489
489
490 def buildfilter(exp, context):
490 def buildfilter(exp, context):
491 n = getsymbol(exp[2])
491 n = getsymbol(exp[2])
492 if n in context._filters:
492 if n in context._filters:
493 filt = context._filters[n]
493 filt = context._filters[n]
494 arg = compileexp(exp[1], context, methods)
494 arg = compileexp(exp[1], context, methods)
495 return (runfilter, (arg, filt))
495 return (runfilter, (arg, filt))
496 if n in funcs:
496 if n in funcs:
497 f = funcs[n]
497 f = funcs[n]
498 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
498 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
499 return (f, args)
499 return (f, args)
500 raise error.ParseError(_("unknown function '%s'") % n)
500 raise error.ParseError(_("unknown function '%s'") % n)
501
501
502 def runfilter(context, mapping, data):
502 def runfilter(context, mapping, data):
503 arg, filt = data
503 arg, filt = data
504 thing = evalfuncarg(context, mapping, arg)
504 thing = evalfuncarg(context, mapping, arg)
505 try:
505 try:
506 return filt(thing)
506 return filt(thing)
507 except (ValueError, AttributeError, TypeError):
507 except (ValueError, AttributeError, TypeError):
508 sym = findsymbolicname(arg)
508 sym = findsymbolicname(arg)
509 if sym:
509 if sym:
510 msg = (_("template filter '%s' is not compatible with keyword '%s'")
510 msg = (_("template filter '%s' is not compatible with keyword '%s'")
511 % (pycompat.sysbytes(filt.__name__), sym))
511 % (pycompat.sysbytes(filt.__name__), sym))
512 else:
512 else:
513 msg = (_("incompatible use of template filter '%s'")
513 msg = (_("incompatible use of template filter '%s'")
514 % pycompat.sysbytes(filt.__name__))
514 % pycompat.sysbytes(filt.__name__))
515 raise error.Abort(msg)
515 raise error.Abort(msg)
516
516
517 def buildmap(exp, context):
517 def buildmap(exp, context):
518 darg = compileexp(exp[1], context, methods)
518 darg = compileexp(exp[1], context, methods)
519 targ = gettemplate(exp[2], context)
519 targ = gettemplate(exp[2], context)
520 return (runmap, (darg, targ))
520 return (runmap, (darg, targ))
521
521
522 def runmap(context, mapping, data):
522 def runmap(context, mapping, data):
523 darg, targ = data
523 darg, targ = data
524 d = evalrawexp(context, mapping, darg)
524 d = evalrawexp(context, mapping, darg)
525 if util.safehasattr(d, 'itermaps'):
525 if util.safehasattr(d, 'itermaps'):
526 diter = d.itermaps()
526 diter = d.itermaps()
527 else:
527 else:
528 try:
528 try:
529 diter = iter(d)
529 diter = iter(d)
530 except TypeError:
530 except TypeError:
531 sym = findsymbolicname(darg)
531 sym = findsymbolicname(darg)
532 if sym:
532 if sym:
533 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
533 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
534 else:
534 else:
535 raise error.ParseError(_("%r is not iterable") % d)
535 raise error.ParseError(_("%r is not iterable") % d)
536
536
537 for i, v in enumerate(diter):
537 for i, v in enumerate(diter):
538 lm = mapping.copy()
538 lm = mapping.copy()
539 lm['index'] = i
539 lm['index'] = i
540 if isinstance(v, dict):
540 if isinstance(v, dict):
541 lm.update(v)
541 lm.update(v)
542 lm['originalnode'] = mapping.get('node')
542 lm['originalnode'] = mapping.get('node')
543 yield evalrawexp(context, lm, targ)
543 yield evalrawexp(context, lm, targ)
544 else:
544 else:
545 # v is not an iterable of dicts, this happen when 'key'
545 # v is not an iterable of dicts, this happen when 'key'
546 # has been fully expanded already and format is useless.
546 # has been fully expanded already and format is useless.
547 # If so, return the expanded value.
547 # If so, return the expanded value.
548 yield v
548 yield v
549
549
550 def buildmember(exp, context):
550 def buildmember(exp, context):
551 darg = compileexp(exp[1], context, methods)
551 darg = compileexp(exp[1], context, methods)
552 memb = getsymbol(exp[2])
552 memb = getsymbol(exp[2])
553 return (runmember, (darg, memb))
553 return (runmember, (darg, memb))
554
554
555 def runmember(context, mapping, data):
555 def runmember(context, mapping, data):
556 darg, memb = data
556 darg, memb = data
557 d = evalrawexp(context, mapping, darg)
557 d = evalrawexp(context, mapping, darg)
558 if util.safehasattr(d, 'tomap'):
558 if util.safehasattr(d, 'tomap'):
559 lm = mapping.copy()
559 lm = mapping.copy()
560 lm.update(d.tomap())
560 lm.update(d.tomap())
561 return runsymbol(context, lm, memb)
561 return runsymbol(context, lm, memb)
562 if util.safehasattr(d, 'get'):
562 if util.safehasattr(d, 'get'):
563 return _getdictitem(d, memb)
563 return _getdictitem(d, memb)
564
564
565 sym = findsymbolicname(darg)
565 sym = findsymbolicname(darg)
566 if sym:
566 if sym:
567 raise error.ParseError(_("keyword '%s' has no member") % sym)
567 raise error.ParseError(_("keyword '%s' has no member") % sym)
568 else:
568 else:
569 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
569 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
570
570
571 def buildnegate(exp, context):
571 def buildnegate(exp, context):
572 arg = compileexp(exp[1], context, exprmethods)
572 arg = compileexp(exp[1], context, exprmethods)
573 return (runnegate, arg)
573 return (runnegate, arg)
574
574
575 def runnegate(context, mapping, data):
575 def runnegate(context, mapping, data):
576 data = evalinteger(context, mapping, data,
576 data = evalinteger(context, mapping, data,
577 _('negation needs an integer argument'))
577 _('negation needs an integer argument'))
578 return -data
578 return -data
579
579
580 def buildarithmetic(exp, context, func):
580 def buildarithmetic(exp, context, func):
581 left = compileexp(exp[1], context, exprmethods)
581 left = compileexp(exp[1], context, exprmethods)
582 right = compileexp(exp[2], context, exprmethods)
582 right = compileexp(exp[2], context, exprmethods)
583 return (runarithmetic, (func, left, right))
583 return (runarithmetic, (func, left, right))
584
584
585 def runarithmetic(context, mapping, data):
585 def runarithmetic(context, mapping, data):
586 func, left, right = data
586 func, left, right = data
587 left = evalinteger(context, mapping, left,
587 left = evalinteger(context, mapping, left,
588 _('arithmetic only defined on integers'))
588 _('arithmetic only defined on integers'))
589 right = evalinteger(context, mapping, right,
589 right = evalinteger(context, mapping, right,
590 _('arithmetic only defined on integers'))
590 _('arithmetic only defined on integers'))
591 try:
591 try:
592 return func(left, right)
592 return func(left, right)
593 except ZeroDivisionError:
593 except ZeroDivisionError:
594 raise error.Abort(_('division by zero is not defined'))
594 raise error.Abort(_('division by zero is not defined'))
595
595
596 def buildfunc(exp, context):
596 def buildfunc(exp, context):
597 n = getsymbol(exp[1])
597 n = getsymbol(exp[1])
598 if n in funcs:
598 if n in funcs:
599 f = funcs[n]
599 f = funcs[n]
600 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
600 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
601 return (f, args)
601 return (f, args)
602 if n in context._filters:
602 if n in context._filters:
603 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
603 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
604 if len(args) != 1:
604 if len(args) != 1:
605 raise error.ParseError(_("filter %s expects one argument") % n)
605 raise error.ParseError(_("filter %s expects one argument") % n)
606 f = context._filters[n]
606 f = context._filters[n]
607 return (runfilter, (args[0], f))
607 return (runfilter, (args[0], f))
608 raise error.ParseError(_("unknown function '%s'") % n)
608 raise error.ParseError(_("unknown function '%s'") % n)
609
609
610 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
610 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
611 """Compile parsed tree of function arguments into list or dict of
611 """Compile parsed tree of function arguments into list or dict of
612 (func, data) pairs
612 (func, data) pairs
613
613
614 >>> context = engine(lambda t: (runsymbol, t))
614 >>> context = engine(lambda t: (runsymbol, t))
615 >>> def fargs(expr, argspec):
615 >>> def fargs(expr, argspec):
616 ... x = _parseexpr(expr)
616 ... x = _parseexpr(expr)
617 ... n = getsymbol(x[1])
617 ... n = getsymbol(x[1])
618 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
618 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
619 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
619 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
620 ['l', 'k']
620 ['l', 'k']
621 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
621 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
622 >>> list(args.keys()), list(args[b'opts'].keys())
622 >>> list(args.keys()), list(args[b'opts'].keys())
623 (['opts'], ['opts', 'k'])
623 (['opts'], ['opts', 'k'])
624 """
624 """
625 def compiledict(xs):
625 def compiledict(xs):
626 return util.sortdict((k, compileexp(x, context, curmethods))
626 return util.sortdict((k, compileexp(x, context, curmethods))
627 for k, x in xs.iteritems())
627 for k, x in xs.iteritems())
628 def compilelist(xs):
628 def compilelist(xs):
629 return [compileexp(x, context, curmethods) for x in xs]
629 return [compileexp(x, context, curmethods) for x in xs]
630
630
631 if not argspec:
631 if not argspec:
632 # filter or function with no argspec: return list of positional args
632 # filter or function with no argspec: return list of positional args
633 return compilelist(getlist(exp))
633 return compilelist(getlist(exp))
634
634
635 # function with argspec: return dict of named args
635 # function with argspec: return dict of named args
636 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
636 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
637 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
637 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
638 keyvaluenode='keyvalue', keynode='symbol')
638 keyvaluenode='keyvalue', keynode='symbol')
639 compargs = util.sortdict()
639 compargs = util.sortdict()
640 if varkey:
640 if varkey:
641 compargs[varkey] = compilelist(treeargs.pop(varkey))
641 compargs[varkey] = compilelist(treeargs.pop(varkey))
642 if optkey:
642 if optkey:
643 compargs[optkey] = compiledict(treeargs.pop(optkey))
643 compargs[optkey] = compiledict(treeargs.pop(optkey))
644 compargs.update(compiledict(treeargs))
644 compargs.update(compiledict(treeargs))
645 return compargs
645 return compargs
646
646
647 def buildkeyvaluepair(exp, content):
647 def buildkeyvaluepair(exp, content):
648 raise error.ParseError(_("can't use a key-value pair in this context"))
648 raise error.ParseError(_("can't use a key-value pair in this context"))
649
649
650 # dict of template built-in functions
650 # dict of template built-in functions
651 funcs = {}
651 funcs = {}
652
652
653 templatefunc = registrar.templatefunc(funcs)
653 templatefunc = registrar.templatefunc(funcs)
654
654
655 @templatefunc('date(date[, fmt])')
655 @templatefunc('date(date[, fmt])')
656 def date(context, mapping, args):
656 def date(context, mapping, args):
657 """Format a date. See :hg:`help dates` for formatting
657 """Format a date. See :hg:`help dates` for formatting
658 strings. The default is a Unix date format, including the timezone:
658 strings. The default is a Unix date format, including the timezone:
659 "Mon Sep 04 15:13:13 2006 0700"."""
659 "Mon Sep 04 15:13:13 2006 0700"."""
660 if not (1 <= len(args) <= 2):
660 if not (1 <= len(args) <= 2):
661 # i18n: "date" is a keyword
661 # i18n: "date" is a keyword
662 raise error.ParseError(_("date expects one or two arguments"))
662 raise error.ParseError(_("date expects one or two arguments"))
663
663
664 date = evalfuncarg(context, mapping, args[0])
664 date = evalfuncarg(context, mapping, args[0])
665 fmt = None
665 fmt = None
666 if len(args) == 2:
666 if len(args) == 2:
667 fmt = evalstring(context, mapping, args[1])
667 fmt = evalstring(context, mapping, args[1])
668 try:
668 try:
669 if fmt is None:
669 if fmt is None:
670 return dateutil.datestr(date)
670 return dateutil.datestr(date)
671 else:
671 else:
672 return dateutil.datestr(date, fmt)
672 return dateutil.datestr(date, fmt)
673 except (TypeError, ValueError):
673 except (TypeError, ValueError):
674 # i18n: "date" is a keyword
674 # i18n: "date" is a keyword
675 raise error.ParseError(_("date expects a date information"))
675 raise error.ParseError(_("date expects a date information"))
676
676
677 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
677 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
678 def dict_(context, mapping, args):
678 def dict_(context, mapping, args):
679 """Construct a dict from key-value pairs. A key may be omitted if
679 """Construct a dict from key-value pairs. A key may be omitted if
680 a value expression can provide an unambiguous name."""
680 a value expression can provide an unambiguous name."""
681 data = util.sortdict()
681 data = util.sortdict()
682
682
683 for v in args['args']:
683 for v in args['args']:
684 k = findsymbolicname(v)
684 k = findsymbolicname(v)
685 if not k:
685 if not k:
686 raise error.ParseError(_('dict key cannot be inferred'))
686 raise error.ParseError(_('dict key cannot be inferred'))
687 if k in data or k in args['kwargs']:
687 if k in data or k in args['kwargs']:
688 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
688 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
689 data[k] = evalfuncarg(context, mapping, v)
689 data[k] = evalfuncarg(context, mapping, v)
690
690
691 data.update((k, evalfuncarg(context, mapping, v))
691 data.update((k, evalfuncarg(context, mapping, v))
692 for k, v in args['kwargs'].iteritems())
692 for k, v in args['kwargs'].iteritems())
693 return templatekw.hybriddict(data)
693 return templatekw.hybriddict(data)
694
694
695 @templatefunc('diff([includepattern [, excludepattern]])')
695 @templatefunc('diff([includepattern [, excludepattern]])')
696 def diff(context, mapping, args):
696 def diff(context, mapping, args):
697 """Show a diff, optionally
697 """Show a diff, optionally
698 specifying files to include or exclude."""
698 specifying files to include or exclude."""
699 if len(args) > 2:
699 if len(args) > 2:
700 # i18n: "diff" is a keyword
700 # i18n: "diff" is a keyword
701 raise error.ParseError(_("diff expects zero, one, or two arguments"))
701 raise error.ParseError(_("diff expects zero, one, or two arguments"))
702
702
703 def getpatterns(i):
703 def getpatterns(i):
704 if i < len(args):
704 if i < len(args):
705 s = evalstring(context, mapping, args[i]).strip()
705 s = evalstring(context, mapping, args[i]).strip()
706 if s:
706 if s:
707 return [s]
707 return [s]
708 return []
708 return []
709
709
710 ctx = context.resource(mapping, 'ctx')
710 ctx = context.resource(mapping, 'ctx')
711 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
711 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
712
712
713 return ''.join(chunks)
713 return ''.join(chunks)
714
714
715 @templatefunc('extdata(source)', argspec='source')
715 @templatefunc('extdata(source)', argspec='source')
716 def extdata(context, mapping, args):
716 def extdata(context, mapping, args):
717 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
717 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
718 if 'source' not in args:
718 if 'source' not in args:
719 # i18n: "extdata" is a keyword
719 # i18n: "extdata" is a keyword
720 raise error.ParseError(_('extdata expects one argument'))
720 raise error.ParseError(_('extdata expects one argument'))
721
721
722 source = evalstring(context, mapping, args['source'])
722 source = evalstring(context, mapping, args['source'])
723 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
723 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
724 ctx = context.resource(mapping, 'ctx')
724 ctx = context.resource(mapping, 'ctx')
725 if source in cache:
725 if source in cache:
726 data = cache[source]
726 data = cache[source]
727 else:
727 else:
728 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
728 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
729 return data.get(ctx.rev(), '')
729 return data.get(ctx.rev(), '')
730
730
731 @templatefunc('files(pattern)')
731 @templatefunc('files(pattern)')
732 def files(context, mapping, args):
732 def files(context, mapping, args):
733 """All files of the current changeset matching the pattern. See
733 """All files of the current changeset matching the pattern. See
734 :hg:`help patterns`."""
734 :hg:`help patterns`."""
735 if not len(args) == 1:
735 if not len(args) == 1:
736 # i18n: "files" is a keyword
736 # i18n: "files" is a keyword
737 raise error.ParseError(_("files expects one argument"))
737 raise error.ParseError(_("files expects one argument"))
738
738
739 raw = evalstring(context, mapping, args[0])
739 raw = evalstring(context, mapping, args[0])
740 ctx = context.resource(mapping, 'ctx')
740 ctx = context.resource(mapping, 'ctx')
741 m = ctx.match([raw])
741 m = ctx.match([raw])
742 files = list(ctx.matches(m))
742 files = list(ctx.matches(m))
743 return templatekw.compatlist(context, mapping, "file", files)
743 return templatekw.compatlist(context, mapping, "file", files)
744
744
745 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
745 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
746 def fill(context, mapping, args):
746 def fill(context, mapping, args):
747 """Fill many
747 """Fill many
748 paragraphs with optional indentation. See the "fill" filter."""
748 paragraphs with optional indentation. See the "fill" filter."""
749 if not (1 <= len(args) <= 4):
749 if not (1 <= len(args) <= 4):
750 # i18n: "fill" is a keyword
750 # i18n: "fill" is a keyword
751 raise error.ParseError(_("fill expects one to four arguments"))
751 raise error.ParseError(_("fill expects one to four arguments"))
752
752
753 text = evalstring(context, mapping, args[0])
753 text = evalstring(context, mapping, args[0])
754 width = 76
754 width = 76
755 initindent = ''
755 initindent = ''
756 hangindent = ''
756 hangindent = ''
757 if 2 <= len(args) <= 4:
757 if 2 <= len(args) <= 4:
758 width = evalinteger(context, mapping, args[1],
758 width = evalinteger(context, mapping, args[1],
759 # i18n: "fill" is a keyword
759 # i18n: "fill" is a keyword
760 _("fill expects an integer width"))
760 _("fill expects an integer width"))
761 try:
761 try:
762 initindent = evalstring(context, mapping, args[2])
762 initindent = evalstring(context, mapping, args[2])
763 hangindent = evalstring(context, mapping, args[3])
763 hangindent = evalstring(context, mapping, args[3])
764 except IndexError:
764 except IndexError:
765 pass
765 pass
766
766
767 return templatefilters.fill(text, width, initindent, hangindent)
767 return templatefilters.fill(text, width, initindent, hangindent)
768
768
769 @templatefunc('formatnode(node)')
769 @templatefunc('formatnode(node)')
770 def formatnode(context, mapping, args):
770 def formatnode(context, mapping, args):
771 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
771 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
772 if len(args) != 1:
772 if len(args) != 1:
773 # i18n: "formatnode" is a keyword
773 # i18n: "formatnode" is a keyword
774 raise error.ParseError(_("formatnode expects one argument"))
774 raise error.ParseError(_("formatnode expects one argument"))
775
775
776 ui = context.resource(mapping, 'ui')
776 ui = context.resource(mapping, 'ui')
777 node = evalstring(context, mapping, args[0])
777 node = evalstring(context, mapping, args[0])
778 if ui.debugflag:
778 if ui.debugflag:
779 return node
779 return node
780 return templatefilters.short(node)
780 return templatefilters.short(node)
781
781
782 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
782 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
783 argspec='text width fillchar left')
783 argspec='text width fillchar left')
784 def pad(context, mapping, args):
784 def pad(context, mapping, args):
785 """Pad text with a
785 """Pad text with a
786 fill character."""
786 fill character."""
787 if 'text' not in args or 'width' not in args:
787 if 'text' not in args or 'width' not in args:
788 # i18n: "pad" is a keyword
788 # i18n: "pad" is a keyword
789 raise error.ParseError(_("pad() expects two to four arguments"))
789 raise error.ParseError(_("pad() expects two to four arguments"))
790
790
791 width = evalinteger(context, mapping, args['width'],
791 width = evalinteger(context, mapping, args['width'],
792 # i18n: "pad" is a keyword
792 # i18n: "pad" is a keyword
793 _("pad() expects an integer width"))
793 _("pad() expects an integer width"))
794
794
795 text = evalstring(context, mapping, args['text'])
795 text = evalstring(context, mapping, args['text'])
796
796
797 left = False
797 left = False
798 fillchar = ' '
798 fillchar = ' '
799 if 'fillchar' in args:
799 if 'fillchar' in args:
800 fillchar = evalstring(context, mapping, args['fillchar'])
800 fillchar = evalstring(context, mapping, args['fillchar'])
801 if len(color.stripeffects(fillchar)) != 1:
801 if len(color.stripeffects(fillchar)) != 1:
802 # i18n: "pad" is a keyword
802 # i18n: "pad" is a keyword
803 raise error.ParseError(_("pad() expects a single fill character"))
803 raise error.ParseError(_("pad() expects a single fill character"))
804 if 'left' in args:
804 if 'left' in args:
805 left = evalboolean(context, mapping, args['left'])
805 left = evalboolean(context, mapping, args['left'])
806
806
807 fillwidth = width - encoding.colwidth(color.stripeffects(text))
807 fillwidth = width - encoding.colwidth(color.stripeffects(text))
808 if fillwidth <= 0:
808 if fillwidth <= 0:
809 return text
809 return text
810 if left:
810 if left:
811 return fillchar * fillwidth + text
811 return fillchar * fillwidth + text
812 else:
812 else:
813 return text + fillchar * fillwidth
813 return text + fillchar * fillwidth
814
814
815 @templatefunc('indent(text, indentchars[, firstline])')
815 @templatefunc('indent(text, indentchars[, firstline])')
816 def indent(context, mapping, args):
816 def indent(context, mapping, args):
817 """Indents all non-empty lines
817 """Indents all non-empty lines
818 with the characters given in the indentchars string. An optional
818 with the characters given in the indentchars string. An optional
819 third parameter will override the indent for the first line only
819 third parameter will override the indent for the first line only
820 if present."""
820 if present."""
821 if not (2 <= len(args) <= 3):
821 if not (2 <= len(args) <= 3):
822 # i18n: "indent" is a keyword
822 # i18n: "indent" is a keyword
823 raise error.ParseError(_("indent() expects two or three arguments"))
823 raise error.ParseError(_("indent() expects two or three arguments"))
824
824
825 text = evalstring(context, mapping, args[0])
825 text = evalstring(context, mapping, args[0])
826 indent = evalstring(context, mapping, args[1])
826 indent = evalstring(context, mapping, args[1])
827
827
828 if len(args) == 3:
828 if len(args) == 3:
829 firstline = evalstring(context, mapping, args[2])
829 firstline = evalstring(context, mapping, args[2])
830 else:
830 else:
831 firstline = indent
831 firstline = indent
832
832
833 # the indent function doesn't indent the first line, so we do it here
833 # the indent function doesn't indent the first line, so we do it here
834 return templatefilters.indent(firstline + text, indent)
834 return templatefilters.indent(firstline + text, indent)
835
835
836 @templatefunc('get(dict, key)')
836 @templatefunc('get(dict, key)')
837 def get(context, mapping, args):
837 def get(context, mapping, args):
838 """Get an attribute/key from an object. Some keywords
838 """Get an attribute/key from an object. Some keywords
839 are complex types. This function allows you to obtain the value of an
839 are complex types. This function allows you to obtain the value of an
840 attribute on these types."""
840 attribute on these types."""
841 if len(args) != 2:
841 if len(args) != 2:
842 # i18n: "get" is a keyword
842 # i18n: "get" is a keyword
843 raise error.ParseError(_("get() expects two arguments"))
843 raise error.ParseError(_("get() expects two arguments"))
844
844
845 dictarg = evalfuncarg(context, mapping, args[0])
845 dictarg = evalfuncarg(context, mapping, args[0])
846 if not util.safehasattr(dictarg, 'get'):
846 if not util.safehasattr(dictarg, 'get'):
847 # i18n: "get" is a keyword
847 # i18n: "get" is a keyword
848 raise error.ParseError(_("get() expects a dict as first argument"))
848 raise error.ParseError(_("get() expects a dict as first argument"))
849
849
850 key = evalfuncarg(context, mapping, args[1])
850 key = evalfuncarg(context, mapping, args[1])
851 return _getdictitem(dictarg, key)
851 return _getdictitem(dictarg, key)
852
852
853 def _getdictitem(dictarg, key):
853 def _getdictitem(dictarg, key):
854 val = dictarg.get(key)
854 val = dictarg.get(key)
855 if val is None:
855 if val is None:
856 return
856 return
857 return templatekw.wraphybridvalue(dictarg, key, val)
857 return templatekw.wraphybridvalue(dictarg, key, val)
858
858
859 @templatefunc('if(expr, then[, else])')
859 @templatefunc('if(expr, then[, else])')
860 def if_(context, mapping, args):
860 def if_(context, mapping, args):
861 """Conditionally execute based on the result of
861 """Conditionally execute based on the result of
862 an expression."""
862 an expression."""
863 if not (2 <= len(args) <= 3):
863 if not (2 <= len(args) <= 3):
864 # i18n: "if" is a keyword
864 # i18n: "if" is a keyword
865 raise error.ParseError(_("if expects two or three arguments"))
865 raise error.ParseError(_("if expects two or three arguments"))
866
866
867 test = evalboolean(context, mapping, args[0])
867 test = evalboolean(context, mapping, args[0])
868 if test:
868 if test:
869 yield evalrawexp(context, mapping, args[1])
869 yield evalrawexp(context, mapping, args[1])
870 elif len(args) == 3:
870 elif len(args) == 3:
871 yield evalrawexp(context, mapping, args[2])
871 yield evalrawexp(context, mapping, args[2])
872
872
873 @templatefunc('ifcontains(needle, haystack, then[, else])')
873 @templatefunc('ifcontains(needle, haystack, then[, else])')
874 def ifcontains(context, mapping, args):
874 def ifcontains(context, mapping, args):
875 """Conditionally execute based
875 """Conditionally execute based
876 on whether the item "needle" is in "haystack"."""
876 on whether the item "needle" is in "haystack"."""
877 if not (3 <= len(args) <= 4):
877 if not (3 <= len(args) <= 4):
878 # i18n: "ifcontains" is a keyword
878 # i18n: "ifcontains" is a keyword
879 raise error.ParseError(_("ifcontains expects three or four arguments"))
879 raise error.ParseError(_("ifcontains expects three or four arguments"))
880
880
881 haystack = evalfuncarg(context, mapping, args[1])
881 haystack = evalfuncarg(context, mapping, args[1])
882 try:
882 try:
883 needle = evalastype(context, mapping, args[0],
883 needle = evalastype(context, mapping, args[0],
884 getattr(haystack, 'keytype', None) or bytes)
884 getattr(haystack, 'keytype', None) or bytes)
885 found = (needle in haystack)
885 found = (needle in haystack)
886 except error.ParseError:
886 except error.ParseError:
887 found = False
887 found = False
888
888
889 if found:
889 if found:
890 yield evalrawexp(context, mapping, args[2])
890 yield evalrawexp(context, mapping, args[2])
891 elif len(args) == 4:
891 elif len(args) == 4:
892 yield evalrawexp(context, mapping, args[3])
892 yield evalrawexp(context, mapping, args[3])
893
893
894 @templatefunc('ifeq(expr1, expr2, then[, else])')
894 @templatefunc('ifeq(expr1, expr2, then[, else])')
895 def ifeq(context, mapping, args):
895 def ifeq(context, mapping, args):
896 """Conditionally execute based on
896 """Conditionally execute based on
897 whether 2 items are equivalent."""
897 whether 2 items are equivalent."""
898 if not (3 <= len(args) <= 4):
898 if not (3 <= len(args) <= 4):
899 # i18n: "ifeq" is a keyword
899 # i18n: "ifeq" is a keyword
900 raise error.ParseError(_("ifeq expects three or four arguments"))
900 raise error.ParseError(_("ifeq expects three or four arguments"))
901
901
902 test = evalstring(context, mapping, args[0])
902 test = evalstring(context, mapping, args[0])
903 match = evalstring(context, mapping, args[1])
903 match = evalstring(context, mapping, args[1])
904 if test == match:
904 if test == match:
905 yield evalrawexp(context, mapping, args[2])
905 yield evalrawexp(context, mapping, args[2])
906 elif len(args) == 4:
906 elif len(args) == 4:
907 yield evalrawexp(context, mapping, args[3])
907 yield evalrawexp(context, mapping, args[3])
908
908
909 @templatefunc('join(list, sep)')
909 @templatefunc('join(list, sep)')
910 def join(context, mapping, args):
910 def join(context, mapping, args):
911 """Join items in a list with a delimiter."""
911 """Join items in a list with a delimiter."""
912 if not (1 <= len(args) <= 2):
912 if not (1 <= len(args) <= 2):
913 # i18n: "join" is a keyword
913 # i18n: "join" is a keyword
914 raise error.ParseError(_("join expects one or two arguments"))
914 raise error.ParseError(_("join expects one or two arguments"))
915
915
916 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
916 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
917 # abuses generator as a keyword that returns a list of dicts.
917 # abuses generator as a keyword that returns a list of dicts.
918 joinset = evalrawexp(context, mapping, args[0])
918 joinset = evalrawexp(context, mapping, args[0])
919 joinset = templatekw.unwrapvalue(joinset)
919 joinset = templatekw.unwrapvalue(joinset)
920 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
920 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
921 joiner = " "
921 joiner = " "
922 if len(args) > 1:
922 if len(args) > 1:
923 joiner = evalstring(context, mapping, args[1])
923 joiner = evalstring(context, mapping, args[1])
924
924
925 first = True
925 first = True
926 for x in pycompat.maybebytestr(joinset):
926 for x in pycompat.maybebytestr(joinset):
927 if first:
927 if first:
928 first = False
928 first = False
929 else:
929 else:
930 yield joiner
930 yield joiner
931 yield joinfmt(x)
931 yield joinfmt(x)
932
932
933 @templatefunc('label(label, expr)')
933 @templatefunc('label(label, expr)')
934 def label(context, mapping, args):
934 def label(context, mapping, args):
935 """Apply a label to generated content. Content with
935 """Apply a label to generated content. Content with
936 a label applied can result in additional post-processing, such as
936 a label applied can result in additional post-processing, such as
937 automatic colorization."""
937 automatic colorization."""
938 if len(args) != 2:
938 if len(args) != 2:
939 # i18n: "label" is a keyword
939 # i18n: "label" is a keyword
940 raise error.ParseError(_("label expects two arguments"))
940 raise error.ParseError(_("label expects two arguments"))
941
941
942 ui = context.resource(mapping, 'ui')
942 ui = context.resource(mapping, 'ui')
943 thing = evalstring(context, mapping, args[1])
943 thing = evalstring(context, mapping, args[1])
944 # preserve unknown symbol as literal so effects like 'red', 'bold',
944 # preserve unknown symbol as literal so effects like 'red', 'bold',
945 # etc. don't need to be quoted
945 # etc. don't need to be quoted
946 label = evalstringliteral(context, mapping, args[0])
946 label = evalstringliteral(context, mapping, args[0])
947
947
948 return ui.label(thing, label)
948 return ui.label(thing, label)
949
949
950 @templatefunc('latesttag([pattern])')
950 @templatefunc('latesttag([pattern])')
951 def latesttag(context, mapping, args):
951 def latesttag(context, mapping, args):
952 """The global tags matching the given pattern on the
952 """The global tags matching the given pattern on the
953 most recent globally tagged ancestor of this changeset.
953 most recent globally tagged ancestor of this changeset.
954 If no such tags exist, the "{tag}" template resolves to
954 If no such tags exist, the "{tag}" template resolves to
955 the string "null"."""
955 the string "null"."""
956 if len(args) > 1:
956 if len(args) > 1:
957 # i18n: "latesttag" is a keyword
957 # i18n: "latesttag" is a keyword
958 raise error.ParseError(_("latesttag expects at most one argument"))
958 raise error.ParseError(_("latesttag expects at most one argument"))
959
959
960 pattern = None
960 pattern = None
961 if len(args) == 1:
961 if len(args) == 1:
962 pattern = evalstring(context, mapping, args[0])
962 pattern = evalstring(context, mapping, args[0])
963 return templatekw.showlatesttags(context, mapping, pattern)
963 return templatekw.showlatesttags(context, mapping, pattern)
964
964
965 @templatefunc('localdate(date[, tz])')
965 @templatefunc('localdate(date[, tz])')
966 def localdate(context, mapping, args):
966 def localdate(context, mapping, args):
967 """Converts a date to the specified timezone.
967 """Converts a date to the specified timezone.
968 The default is local date."""
968 The default is local date."""
969 if not (1 <= len(args) <= 2):
969 if not (1 <= len(args) <= 2):
970 # i18n: "localdate" is a keyword
970 # i18n: "localdate" is a keyword
971 raise error.ParseError(_("localdate expects one or two arguments"))
971 raise error.ParseError(_("localdate expects one or two arguments"))
972
972
973 date = evalfuncarg(context, mapping, args[0])
973 date = evalfuncarg(context, mapping, args[0])
974 try:
974 try:
975 date = dateutil.parsedate(date)
975 date = dateutil.parsedate(date)
976 except AttributeError: # not str nor date tuple
976 except AttributeError: # not str nor date tuple
977 # i18n: "localdate" is a keyword
977 # i18n: "localdate" is a keyword
978 raise error.ParseError(_("localdate expects a date information"))
978 raise error.ParseError(_("localdate expects a date information"))
979 if len(args) >= 2:
979 if len(args) >= 2:
980 tzoffset = None
980 tzoffset = None
981 tz = evalfuncarg(context, mapping, args[1])
981 tz = evalfuncarg(context, mapping, args[1])
982 if isinstance(tz, bytes):
982 if isinstance(tz, bytes):
983 tzoffset, remainder = dateutil.parsetimezone(tz)
983 tzoffset, remainder = dateutil.parsetimezone(tz)
984 if remainder:
984 if remainder:
985 tzoffset = None
985 tzoffset = None
986 if tzoffset is None:
986 if tzoffset is None:
987 try:
987 try:
988 tzoffset = int(tz)
988 tzoffset = int(tz)
989 except (TypeError, ValueError):
989 except (TypeError, ValueError):
990 # i18n: "localdate" is a keyword
990 # i18n: "localdate" is a keyword
991 raise error.ParseError(_("localdate expects a timezone"))
991 raise error.ParseError(_("localdate expects a timezone"))
992 else:
992 else:
993 tzoffset = dateutil.makedate()[1]
993 tzoffset = dateutil.makedate()[1]
994 return (date[0], tzoffset)
994 return (date[0], tzoffset)
995
995
996 @templatefunc('max(iterable)')
996 @templatefunc('max(iterable)')
997 def max_(context, mapping, args, **kwargs):
997 def max_(context, mapping, args, **kwargs):
998 """Return the max of an iterable"""
998 """Return the max of an iterable"""
999 if len(args) != 1:
999 if len(args) != 1:
1000 # i18n: "max" is a keyword
1000 # i18n: "max" is a keyword
1001 raise error.ParseError(_("max expects one argument"))
1001 raise error.ParseError(_("max expects one argument"))
1002
1002
1003 iterable = evalfuncarg(context, mapping, args[0])
1003 iterable = evalfuncarg(context, mapping, args[0])
1004 try:
1004 try:
1005 x = max(pycompat.maybebytestr(iterable))
1005 x = max(pycompat.maybebytestr(iterable))
1006 except (TypeError, ValueError):
1006 except (TypeError, ValueError):
1007 # i18n: "max" is a keyword
1007 # i18n: "max" is a keyword
1008 raise error.ParseError(_("max first argument should be an iterable"))
1008 raise error.ParseError(_("max first argument should be an iterable"))
1009 return templatekw.wraphybridvalue(iterable, x, x)
1009 return templatekw.wraphybridvalue(iterable, x, x)
1010
1010
1011 @templatefunc('min(iterable)')
1011 @templatefunc('min(iterable)')
1012 def min_(context, mapping, args, **kwargs):
1012 def min_(context, mapping, args, **kwargs):
1013 """Return the min of an iterable"""
1013 """Return the min of an iterable"""
1014 if len(args) != 1:
1014 if len(args) != 1:
1015 # i18n: "min" is a keyword
1015 # i18n: "min" is a keyword
1016 raise error.ParseError(_("min expects one argument"))
1016 raise error.ParseError(_("min expects one argument"))
1017
1017
1018 iterable = evalfuncarg(context, mapping, args[0])
1018 iterable = evalfuncarg(context, mapping, args[0])
1019 try:
1019 try:
1020 x = min(pycompat.maybebytestr(iterable))
1020 x = min(pycompat.maybebytestr(iterable))
1021 except (TypeError, ValueError):
1021 except (TypeError, ValueError):
1022 # i18n: "min" is a keyword
1022 # i18n: "min" is a keyword
1023 raise error.ParseError(_("min first argument should be an iterable"))
1023 raise error.ParseError(_("min first argument should be an iterable"))
1024 return templatekw.wraphybridvalue(iterable, x, x)
1024 return templatekw.wraphybridvalue(iterable, x, x)
1025
1025
1026 @templatefunc('mod(a, b)')
1026 @templatefunc('mod(a, b)')
1027 def mod(context, mapping, args):
1027 def mod(context, mapping, args):
1028 """Calculate a mod b such that a / b + a mod b == a"""
1028 """Calculate a mod b such that a / b + a mod b == a"""
1029 if not len(args) == 2:
1029 if not len(args) == 2:
1030 # i18n: "mod" is a keyword
1030 # i18n: "mod" is a keyword
1031 raise error.ParseError(_("mod expects two arguments"))
1031 raise error.ParseError(_("mod expects two arguments"))
1032
1032
1033 func = lambda a, b: a % b
1033 func = lambda a, b: a % b
1034 return runarithmetic(context, mapping, (func, args[0], args[1]))
1034 return runarithmetic(context, mapping, (func, args[0], args[1]))
1035
1035
1036 @templatefunc('obsfateoperations(markers)')
1036 @templatefunc('obsfateoperations(markers)')
1037 def obsfateoperations(context, mapping, args):
1037 def obsfateoperations(context, mapping, args):
1038 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1038 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1039 if len(args) != 1:
1039 if len(args) != 1:
1040 # i18n: "obsfateoperations" is a keyword
1040 # i18n: "obsfateoperations" is a keyword
1041 raise error.ParseError(_("obsfateoperations expects one argument"))
1041 raise error.ParseError(_("obsfateoperations expects one argument"))
1042
1042
1043 markers = evalfuncarg(context, mapping, args[0])
1043 markers = evalfuncarg(context, mapping, args[0])
1044
1044
1045 try:
1045 try:
1046 data = obsutil.markersoperations(markers)
1046 data = obsutil.markersoperations(markers)
1047 return templatekw.hybridlist(data, name='operation')
1047 return templatekw.hybridlist(data, name='operation')
1048 except (TypeError, KeyError):
1048 except (TypeError, KeyError):
1049 # i18n: "obsfateoperations" is a keyword
1049 # i18n: "obsfateoperations" is a keyword
1050 errmsg = _("obsfateoperations first argument should be an iterable")
1050 errmsg = _("obsfateoperations first argument should be an iterable")
1051 raise error.ParseError(errmsg)
1051 raise error.ParseError(errmsg)
1052
1052
1053 @templatefunc('obsfatedate(markers)')
1053 @templatefunc('obsfatedate(markers)')
1054 def obsfatedate(context, mapping, args):
1054 def obsfatedate(context, mapping, args):
1055 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1055 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1056 if len(args) != 1:
1056 if len(args) != 1:
1057 # i18n: "obsfatedate" is a keyword
1057 # i18n: "obsfatedate" is a keyword
1058 raise error.ParseError(_("obsfatedate expects one argument"))
1058 raise error.ParseError(_("obsfatedate expects one argument"))
1059
1059
1060 markers = evalfuncarg(context, mapping, args[0])
1060 markers = evalfuncarg(context, mapping, args[0])
1061
1061
1062 try:
1062 try:
1063 data = obsutil.markersdates(markers)
1063 data = obsutil.markersdates(markers)
1064 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1064 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1065 except (TypeError, KeyError):
1065 except (TypeError, KeyError):
1066 # i18n: "obsfatedate" is a keyword
1066 # i18n: "obsfatedate" is a keyword
1067 errmsg = _("obsfatedate first argument should be an iterable")
1067 errmsg = _("obsfatedate first argument should be an iterable")
1068 raise error.ParseError(errmsg)
1068 raise error.ParseError(errmsg)
1069
1069
1070 @templatefunc('obsfateusers(markers)')
1070 @templatefunc('obsfateusers(markers)')
1071 def obsfateusers(context, mapping, args):
1071 def obsfateusers(context, mapping, args):
1072 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1072 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1073 if len(args) != 1:
1073 if len(args) != 1:
1074 # i18n: "obsfateusers" is a keyword
1074 # i18n: "obsfateusers" is a keyword
1075 raise error.ParseError(_("obsfateusers expects one argument"))
1075 raise error.ParseError(_("obsfateusers expects one argument"))
1076
1076
1077 markers = evalfuncarg(context, mapping, args[0])
1077 markers = evalfuncarg(context, mapping, args[0])
1078
1078
1079 try:
1079 try:
1080 data = obsutil.markersusers(markers)
1080 data = obsutil.markersusers(markers)
1081 return templatekw.hybridlist(data, name='user')
1081 return templatekw.hybridlist(data, name='user')
1082 except (TypeError, KeyError, ValueError):
1082 except (TypeError, KeyError, ValueError):
1083 # i18n: "obsfateusers" is a keyword
1083 # i18n: "obsfateusers" is a keyword
1084 msg = _("obsfateusers first argument should be an iterable of "
1084 msg = _("obsfateusers first argument should be an iterable of "
1085 "obsmakers")
1085 "obsmakers")
1086 raise error.ParseError(msg)
1086 raise error.ParseError(msg)
1087
1087
1088 @templatefunc('obsfateverb(successors, markers)')
1088 @templatefunc('obsfateverb(successors, markers)')
1089 def obsfateverb(context, mapping, args):
1089 def obsfateverb(context, mapping, args):
1090 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1090 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1091 if len(args) != 2:
1091 if len(args) != 2:
1092 # i18n: "obsfateverb" is a keyword
1092 # i18n: "obsfateverb" is a keyword
1093 raise error.ParseError(_("obsfateverb expects two arguments"))
1093 raise error.ParseError(_("obsfateverb expects two arguments"))
1094
1094
1095 successors = evalfuncarg(context, mapping, args[0])
1095 successors = evalfuncarg(context, mapping, args[0])
1096 markers = evalfuncarg(context, mapping, args[1])
1096 markers = evalfuncarg(context, mapping, args[1])
1097
1097
1098 try:
1098 try:
1099 return obsutil.obsfateverb(successors, markers)
1099 return obsutil.obsfateverb(successors, markers)
1100 except TypeError:
1100 except TypeError:
1101 # i18n: "obsfateverb" is a keyword
1101 # i18n: "obsfateverb" is a keyword
1102 errmsg = _("obsfateverb first argument should be countable")
1102 errmsg = _("obsfateverb first argument should be countable")
1103 raise error.ParseError(errmsg)
1103 raise error.ParseError(errmsg)
1104
1104
1105 @templatefunc('relpath(path)')
1105 @templatefunc('relpath(path)')
1106 def relpath(context, mapping, args):
1106 def relpath(context, mapping, args):
1107 """Convert a repository-absolute path into a filesystem path relative to
1107 """Convert a repository-absolute path into a filesystem path relative to
1108 the current working directory."""
1108 the current working directory."""
1109 if len(args) != 1:
1109 if len(args) != 1:
1110 # i18n: "relpath" is a keyword
1110 # i18n: "relpath" is a keyword
1111 raise error.ParseError(_("relpath expects one argument"))
1111 raise error.ParseError(_("relpath expects one argument"))
1112
1112
1113 repo = context.resource(mapping, 'ctx').repo()
1113 repo = context.resource(mapping, 'ctx').repo()
1114 path = evalstring(context, mapping, args[0])
1114 path = evalstring(context, mapping, args[0])
1115 return repo.pathto(path)
1115 return repo.pathto(path)
1116
1116
1117 @templatefunc('revset(query[, formatargs...])')
1117 @templatefunc('revset(query[, formatargs...])')
1118 def revset(context, mapping, args):
1118 def revset(context, mapping, args):
1119 """Execute a revision set query. See
1119 """Execute a revision set query. See
1120 :hg:`help revset`."""
1120 :hg:`help revset`."""
1121 if not len(args) > 0:
1121 if not len(args) > 0:
1122 # i18n: "revset" is a keyword
1122 # i18n: "revset" is a keyword
1123 raise error.ParseError(_("revset expects one or more arguments"))
1123 raise error.ParseError(_("revset expects one or more arguments"))
1124
1124
1125 raw = evalstring(context, mapping, args[0])
1125 raw = evalstring(context, mapping, args[0])
1126 ctx = context.resource(mapping, 'ctx')
1126 ctx = context.resource(mapping, 'ctx')
1127 repo = ctx.repo()
1127 repo = ctx.repo()
1128
1128
1129 def query(expr):
1129 def query(expr):
1130 m = revsetmod.match(repo.ui, expr, repo=repo)
1130 m = revsetmod.match(repo.ui, expr, repo=repo)
1131 return m(repo)
1131 return m(repo)
1132
1132
1133 if len(args) > 1:
1133 if len(args) > 1:
1134 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1134 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1135 revs = query(revsetlang.formatspec(raw, *formatargs))
1135 revs = query(revsetlang.formatspec(raw, *formatargs))
1136 revs = list(revs)
1136 revs = list(revs)
1137 else:
1137 else:
1138 cache = context.resource(mapping, 'cache')
1138 cache = context.resource(mapping, 'cache')
1139 revsetcache = cache.setdefault("revsetcache", {})
1139 revsetcache = cache.setdefault("revsetcache", {})
1140 if raw in revsetcache:
1140 if raw in revsetcache:
1141 revs = revsetcache[raw]
1141 revs = revsetcache[raw]
1142 else:
1142 else:
1143 revs = query(raw)
1143 revs = query(raw)
1144 revs = list(revs)
1144 revs = list(revs)
1145 revsetcache[raw] = revs
1145 revsetcache[raw] = revs
1146 return templatekw.showrevslist(context, mapping, "revision", revs)
1146 return templatekw.showrevslist(context, mapping, "revision", revs)
1147
1147
1148 @templatefunc('rstdoc(text, style)')
1148 @templatefunc('rstdoc(text, style)')
1149 def rstdoc(context, mapping, args):
1149 def rstdoc(context, mapping, args):
1150 """Format reStructuredText."""
1150 """Format reStructuredText."""
1151 if len(args) != 2:
1151 if len(args) != 2:
1152 # i18n: "rstdoc" is a keyword
1152 # i18n: "rstdoc" is a keyword
1153 raise error.ParseError(_("rstdoc expects two arguments"))
1153 raise error.ParseError(_("rstdoc expects two arguments"))
1154
1154
1155 text = evalstring(context, mapping, args[0])
1155 text = evalstring(context, mapping, args[0])
1156 style = evalstring(context, mapping, args[1])
1156 style = evalstring(context, mapping, args[1])
1157
1157
1158 return minirst.format(text, style=style, keep=['verbose'])
1158 return minirst.format(text, style=style, keep=['verbose'])
1159
1159
1160 @templatefunc('separate(sep, args)', argspec='sep *args')
1160 @templatefunc('separate(sep, args)', argspec='sep *args')
1161 def separate(context, mapping, args):
1161 def separate(context, mapping, args):
1162 """Add a separator between non-empty arguments."""
1162 """Add a separator between non-empty arguments."""
1163 if 'sep' not in args:
1163 if 'sep' not in args:
1164 # i18n: "separate" is a keyword
1164 # i18n: "separate" is a keyword
1165 raise error.ParseError(_("separate expects at least one argument"))
1165 raise error.ParseError(_("separate expects at least one argument"))
1166
1166
1167 sep = evalstring(context, mapping, args['sep'])
1167 sep = evalstring(context, mapping, args['sep'])
1168 first = True
1168 first = True
1169 for arg in args['args']:
1169 for arg in args['args']:
1170 argstr = evalstring(context, mapping, arg)
1170 argstr = evalstring(context, mapping, arg)
1171 if not argstr:
1171 if not argstr:
1172 continue
1172 continue
1173 if first:
1173 if first:
1174 first = False
1174 first = False
1175 else:
1175 else:
1176 yield sep
1176 yield sep
1177 yield argstr
1177 yield argstr
1178
1178
1179 @templatefunc('shortest(node, minlength=4)')
1179 @templatefunc('shortest(node, minlength=4)')
1180 def shortest(context, mapping, args):
1180 def shortest(context, mapping, args):
1181 """Obtain the shortest representation of
1181 """Obtain the shortest representation of
1182 a node."""
1182 a node."""
1183 if not (1 <= len(args) <= 2):
1183 if not (1 <= len(args) <= 2):
1184 # i18n: "shortest" is a keyword
1184 # i18n: "shortest" is a keyword
1185 raise error.ParseError(_("shortest() expects one or two arguments"))
1185 raise error.ParseError(_("shortest() expects one or two arguments"))
1186
1186
1187 node = evalstring(context, mapping, args[0])
1187 node = evalstring(context, mapping, args[0])
1188
1188
1189 minlength = 4
1189 minlength = 4
1190 if len(args) > 1:
1190 if len(args) > 1:
1191 minlength = evalinteger(context, mapping, args[1],
1191 minlength = evalinteger(context, mapping, args[1],
1192 # i18n: "shortest" is a keyword
1192 # i18n: "shortest" is a keyword
1193 _("shortest() expects an integer minlength"))
1193 _("shortest() expects an integer minlength"))
1194
1194
1195 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1195 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1196 # which would be unacceptably slow. so we look for hash collision in
1196 # which would be unacceptably slow. so we look for hash collision in
1197 # unfiltered space, which means some hashes may be slightly longer.
1197 # unfiltered space, which means some hashes may be slightly longer.
1198 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1198 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1199 return cl.shortest(node, minlength)
1199 return cl.shortest(node, minlength)
1200
1200
1201 @templatefunc('strip(text[, chars])')
1201 @templatefunc('strip(text[, chars])')
1202 def strip(context, mapping, args):
1202 def strip(context, mapping, args):
1203 """Strip characters from a string. By default,
1203 """Strip characters from a string. By default,
1204 strips all leading and trailing whitespace."""
1204 strips all leading and trailing whitespace."""
1205 if not (1 <= len(args) <= 2):
1205 if not (1 <= len(args) <= 2):
1206 # i18n: "strip" is a keyword
1206 # i18n: "strip" is a keyword
1207 raise error.ParseError(_("strip expects one or two arguments"))
1207 raise error.ParseError(_("strip expects one or two arguments"))
1208
1208
1209 text = evalstring(context, mapping, args[0])
1209 text = evalstring(context, mapping, args[0])
1210 if len(args) == 2:
1210 if len(args) == 2:
1211 chars = evalstring(context, mapping, args[1])
1211 chars = evalstring(context, mapping, args[1])
1212 return text.strip(chars)
1212 return text.strip(chars)
1213 return text.strip()
1213 return text.strip()
1214
1214
1215 @templatefunc('sub(pattern, replacement, expression)')
1215 @templatefunc('sub(pattern, replacement, expression)')
1216 def sub(context, mapping, args):
1216 def sub(context, mapping, args):
1217 """Perform text substitution
1217 """Perform text substitution
1218 using regular expressions."""
1218 using regular expressions."""
1219 if len(args) != 3:
1219 if len(args) != 3:
1220 # i18n: "sub" is a keyword
1220 # i18n: "sub" is a keyword
1221 raise error.ParseError(_("sub expects three arguments"))
1221 raise error.ParseError(_("sub expects three arguments"))
1222
1222
1223 pat = evalstring(context, mapping, args[0])
1223 pat = evalstring(context, mapping, args[0])
1224 rpl = evalstring(context, mapping, args[1])
1224 rpl = evalstring(context, mapping, args[1])
1225 src = evalstring(context, mapping, args[2])
1225 src = evalstring(context, mapping, args[2])
1226 try:
1226 try:
1227 patre = re.compile(pat)
1227 patre = re.compile(pat)
1228 except re.error:
1228 except re.error:
1229 # i18n: "sub" is a keyword
1229 # i18n: "sub" is a keyword
1230 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1230 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1231 try:
1231 try:
1232 yield patre.sub(rpl, src)
1232 yield patre.sub(rpl, src)
1233 except re.error:
1233 except re.error:
1234 # i18n: "sub" is a keyword
1234 # i18n: "sub" is a keyword
1235 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1235 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1236
1236
1237 @templatefunc('startswith(pattern, text)')
1237 @templatefunc('startswith(pattern, text)')
1238 def startswith(context, mapping, args):
1238 def startswith(context, mapping, args):
1239 """Returns the value from the "text" argument
1239 """Returns the value from the "text" argument
1240 if it begins with the content from the "pattern" argument."""
1240 if it begins with the content from the "pattern" argument."""
1241 if len(args) != 2:
1241 if len(args) != 2:
1242 # i18n: "startswith" is a keyword
1242 # i18n: "startswith" is a keyword
1243 raise error.ParseError(_("startswith expects two arguments"))
1243 raise error.ParseError(_("startswith expects two arguments"))
1244
1244
1245 patn = evalstring(context, mapping, args[0])
1245 patn = evalstring(context, mapping, args[0])
1246 text = evalstring(context, mapping, args[1])
1246 text = evalstring(context, mapping, args[1])
1247 if text.startswith(patn):
1247 if text.startswith(patn):
1248 return text
1248 return text
1249 return ''
1249 return ''
1250
1250
1251 @templatefunc('word(number, text[, separator])')
1251 @templatefunc('word(number, text[, separator])')
1252 def word(context, mapping, args):
1252 def word(context, mapping, args):
1253 """Return the nth word from a string."""
1253 """Return the nth word from a string."""
1254 if not (2 <= len(args) <= 3):
1254 if not (2 <= len(args) <= 3):
1255 # i18n: "word" is a keyword
1255 # i18n: "word" is a keyword
1256 raise error.ParseError(_("word expects two or three arguments, got %d")
1256 raise error.ParseError(_("word expects two or three arguments, got %d")
1257 % len(args))
1257 % len(args))
1258
1258
1259 num = evalinteger(context, mapping, args[0],
1259 num = evalinteger(context, mapping, args[0],
1260 # i18n: "word" is a keyword
1260 # i18n: "word" is a keyword
1261 _("word expects an integer index"))
1261 _("word expects an integer index"))
1262 text = evalstring(context, mapping, args[1])
1262 text = evalstring(context, mapping, args[1])
1263 if len(args) == 3:
1263 if len(args) == 3:
1264 splitter = evalstring(context, mapping, args[2])
1264 splitter = evalstring(context, mapping, args[2])
1265 else:
1265 else:
1266 splitter = None
1266 splitter = None
1267
1267
1268 tokens = text.split(splitter)
1268 tokens = text.split(splitter)
1269 if num >= len(tokens) or num < -len(tokens):
1269 if num >= len(tokens) or num < -len(tokens):
1270 return ''
1270 return ''
1271 else:
1271 else:
1272 return tokens[num]
1272 return tokens[num]
1273
1273
1274 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1274 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1275 exprmethods = {
1275 exprmethods = {
1276 "integer": lambda e, c: (runinteger, e[1]),
1276 "integer": lambda e, c: (runinteger, e[1]),
1277 "string": lambda e, c: (runstring, e[1]),
1277 "string": lambda e, c: (runstring, e[1]),
1278 "symbol": lambda e, c: (runsymbol, e[1]),
1278 "symbol": lambda e, c: (runsymbol, e[1]),
1279 "template": buildtemplate,
1279 "template": buildtemplate,
1280 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1280 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1281 ".": buildmember,
1281 ".": buildmember,
1282 "|": buildfilter,
1282 "|": buildfilter,
1283 "%": buildmap,
1283 "%": buildmap,
1284 "func": buildfunc,
1284 "func": buildfunc,
1285 "keyvalue": buildkeyvaluepair,
1285 "keyvalue": buildkeyvaluepair,
1286 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1286 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1287 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1287 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1288 "negate": buildnegate,
1288 "negate": buildnegate,
1289 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1289 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1290 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1290 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1291 }
1291 }
1292
1292
1293 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1293 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1294 methods = exprmethods.copy()
1294 methods = exprmethods.copy()
1295 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1295 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1296
1296
1297 class _aliasrules(parser.basealiasrules):
1297 class _aliasrules(parser.basealiasrules):
1298 """Parsing and expansion rule set of template aliases"""
1298 """Parsing and expansion rule set of template aliases"""
1299 _section = _('template alias')
1299 _section = _('template alias')
1300 _parse = staticmethod(_parseexpr)
1300 _parse = staticmethod(_parseexpr)
1301
1301
1302 @staticmethod
1302 @staticmethod
1303 def _trygetfunc(tree):
1303 def _trygetfunc(tree):
1304 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1304 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1305 None"""
1305 None"""
1306 if tree[0] == 'func' and tree[1][0] == 'symbol':
1306 if tree[0] == 'func' and tree[1][0] == 'symbol':
1307 return tree[1][1], getlist(tree[2])
1307 return tree[1][1], getlist(tree[2])
1308 if tree[0] == '|' and tree[2][0] == 'symbol':
1308 if tree[0] == '|' and tree[2][0] == 'symbol':
1309 return tree[2][1], [tree[1]]
1309 return tree[2][1], [tree[1]]
1310
1310
1311 def expandaliases(tree, aliases):
1311 def expandaliases(tree, aliases):
1312 """Return new tree of aliases are expanded"""
1312 """Return new tree of aliases are expanded"""
1313 aliasmap = _aliasrules.buildmap(aliases)
1313 aliasmap = _aliasrules.buildmap(aliases)
1314 return _aliasrules.expand(aliasmap, tree)
1314 return _aliasrules.expand(aliasmap, tree)
1315
1315
1316 # template engine
1316 # template engine
1317
1317
1318 stringify = templatefilters.stringify
1318 stringify = templatefilters.stringify
1319
1319
1320 def _flatten(thing):
1320 def _flatten(thing):
1321 '''yield a single stream from a possibly nested set of iterators'''
1321 '''yield a single stream from a possibly nested set of iterators'''
1322 thing = templatekw.unwraphybrid(thing)
1322 thing = templatekw.unwraphybrid(thing)
1323 if isinstance(thing, bytes):
1323 if isinstance(thing, bytes):
1324 yield thing
1324 yield thing
1325 elif isinstance(thing, str):
1325 elif isinstance(thing, str):
1326 # We can only hit this on Python 3, and it's here to guard
1326 # We can only hit this on Python 3, and it's here to guard
1327 # against infinite recursion.
1327 # against infinite recursion.
1328 raise error.ProgrammingError('Mercurial IO including templates is done'
1328 raise error.ProgrammingError('Mercurial IO including templates is done'
1329 ' with bytes, not strings')
1329 ' with bytes, not strings, got %r' % thing)
1330 elif thing is None:
1330 elif thing is None:
1331 pass
1331 pass
1332 elif not util.safehasattr(thing, '__iter__'):
1332 elif not util.safehasattr(thing, '__iter__'):
1333 yield pycompat.bytestr(thing)
1333 yield pycompat.bytestr(thing)
1334 else:
1334 else:
1335 for i in thing:
1335 for i in thing:
1336 i = templatekw.unwraphybrid(i)
1336 i = templatekw.unwraphybrid(i)
1337 if isinstance(i, bytes):
1337 if isinstance(i, bytes):
1338 yield i
1338 yield i
1339 elif i is None:
1339 elif i is None:
1340 pass
1340 pass
1341 elif not util.safehasattr(i, '__iter__'):
1341 elif not util.safehasattr(i, '__iter__'):
1342 yield pycompat.bytestr(i)
1342 yield pycompat.bytestr(i)
1343 else:
1343 else:
1344 for j in _flatten(i):
1344 for j in _flatten(i):
1345 yield j
1345 yield j
1346
1346
1347 def unquotestring(s):
1347 def unquotestring(s):
1348 '''unwrap quotes if any; otherwise returns unmodified string'''
1348 '''unwrap quotes if any; otherwise returns unmodified string'''
1349 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1349 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1350 return s
1350 return s
1351 return s[1:-1]
1351 return s[1:-1]
1352
1352
1353 class engine(object):
1353 class engine(object):
1354 '''template expansion engine.
1354 '''template expansion engine.
1355
1355
1356 template expansion works like this. a map file contains key=value
1356 template expansion works like this. a map file contains key=value
1357 pairs. if value is quoted, it is treated as string. otherwise, it
1357 pairs. if value is quoted, it is treated as string. otherwise, it
1358 is treated as name of template file.
1358 is treated as name of template file.
1359
1359
1360 templater is asked to expand a key in map. it looks up key, and
1360 templater is asked to expand a key in map. it looks up key, and
1361 looks for strings like this: {foo}. it expands {foo} by looking up
1361 looks for strings like this: {foo}. it expands {foo} by looking up
1362 foo in map, and substituting it. expansion is recursive: it stops
1362 foo in map, and substituting it. expansion is recursive: it stops
1363 when there is no more {foo} to replace.
1363 when there is no more {foo} to replace.
1364
1364
1365 expansion also allows formatting and filtering.
1365 expansion also allows formatting and filtering.
1366
1366
1367 format uses key to expand each item in list. syntax is
1367 format uses key to expand each item in list. syntax is
1368 {key%format}.
1368 {key%format}.
1369
1369
1370 filter uses function to transform value. syntax is
1370 filter uses function to transform value. syntax is
1371 {key|filter1|filter2|...}.'''
1371 {key|filter1|filter2|...}.'''
1372
1372
1373 def __init__(self, loader, filters=None, defaults=None, resources=None,
1373 def __init__(self, loader, filters=None, defaults=None, resources=None,
1374 aliases=()):
1374 aliases=()):
1375 self._loader = loader
1375 self._loader = loader
1376 if filters is None:
1376 if filters is None:
1377 filters = {}
1377 filters = {}
1378 self._filters = filters
1378 self._filters = filters
1379 if defaults is None:
1379 if defaults is None:
1380 defaults = {}
1380 defaults = {}
1381 if resources is None:
1381 if resources is None:
1382 resources = {}
1382 resources = {}
1383 self._defaults = defaults
1383 self._defaults = defaults
1384 self._resources = resources
1384 self._resources = resources
1385 self._aliasmap = _aliasrules.buildmap(aliases)
1385 self._aliasmap = _aliasrules.buildmap(aliases)
1386 self._cache = {} # key: (func, data)
1386 self._cache = {} # key: (func, data)
1387
1387
1388 def symbol(self, mapping, key):
1388 def symbol(self, mapping, key):
1389 """Resolve symbol to value or function; None if nothing found"""
1389 """Resolve symbol to value or function; None if nothing found"""
1390 v = None
1390 v = None
1391 if key not in self._resources:
1391 if key not in self._resources:
1392 v = mapping.get(key)
1392 v = mapping.get(key)
1393 if v is None:
1393 if v is None:
1394 v = self._defaults.get(key)
1394 v = self._defaults.get(key)
1395 return v
1395 return v
1396
1396
1397 def resource(self, mapping, key):
1397 def resource(self, mapping, key):
1398 """Return internal data (e.g. cache) used for keyword/function
1398 """Return internal data (e.g. cache) used for keyword/function
1399 evaluation"""
1399 evaluation"""
1400 v = None
1400 v = None
1401 if key in self._resources:
1401 if key in self._resources:
1402 v = mapping.get(key)
1402 v = mapping.get(key)
1403 if v is None:
1403 if v is None:
1404 v = self._resources.get(key)
1404 v = self._resources.get(key)
1405 if v is None:
1405 if v is None:
1406 raise ResourceUnavailable(_('template resource not available: %s')
1406 raise ResourceUnavailable(_('template resource not available: %s')
1407 % key)
1407 % key)
1408 return v
1408 return v
1409
1409
1410 def _load(self, t):
1410 def _load(self, t):
1411 '''load, parse, and cache a template'''
1411 '''load, parse, and cache a template'''
1412 if t not in self._cache:
1412 if t not in self._cache:
1413 # put poison to cut recursion while compiling 't'
1413 # put poison to cut recursion while compiling 't'
1414 self._cache[t] = (_runrecursivesymbol, t)
1414 self._cache[t] = (_runrecursivesymbol, t)
1415 try:
1415 try:
1416 x = parse(self._loader(t))
1416 x = parse(self._loader(t))
1417 if self._aliasmap:
1417 if self._aliasmap:
1418 x = _aliasrules.expand(self._aliasmap, x)
1418 x = _aliasrules.expand(self._aliasmap, x)
1419 self._cache[t] = compileexp(x, self, methods)
1419 self._cache[t] = compileexp(x, self, methods)
1420 except: # re-raises
1420 except: # re-raises
1421 del self._cache[t]
1421 del self._cache[t]
1422 raise
1422 raise
1423 return self._cache[t]
1423 return self._cache[t]
1424
1424
1425 def process(self, t, mapping):
1425 def process(self, t, mapping):
1426 '''Perform expansion. t is name of map element to expand.
1426 '''Perform expansion. t is name of map element to expand.
1427 mapping contains added elements for use during expansion. Is a
1427 mapping contains added elements for use during expansion. Is a
1428 generator.'''
1428 generator.'''
1429 func, data = self._load(t)
1429 func, data = self._load(t)
1430 return _flatten(func(self, mapping, data))
1430 return _flatten(func(self, mapping, data))
1431
1431
1432 engines = {'default': engine}
1432 engines = {'default': engine}
1433
1433
1434 def stylelist():
1434 def stylelist():
1435 paths = templatepaths()
1435 paths = templatepaths()
1436 if not paths:
1436 if not paths:
1437 return _('no templates found, try `hg debuginstall` for more info')
1437 return _('no templates found, try `hg debuginstall` for more info')
1438 dirlist = os.listdir(paths[0])
1438 dirlist = os.listdir(paths[0])
1439 stylelist = []
1439 stylelist = []
1440 for file in dirlist:
1440 for file in dirlist:
1441 split = file.split(".")
1441 split = file.split(".")
1442 if split[-1] in ('orig', 'rej'):
1442 if split[-1] in ('orig', 'rej'):
1443 continue
1443 continue
1444 if split[0] == "map-cmdline":
1444 if split[0] == "map-cmdline":
1445 stylelist.append(split[1])
1445 stylelist.append(split[1])
1446 return ", ".join(sorted(stylelist))
1446 return ", ".join(sorted(stylelist))
1447
1447
1448 def _readmapfile(mapfile):
1448 def _readmapfile(mapfile):
1449 """Load template elements from the given map file"""
1449 """Load template elements from the given map file"""
1450 if not os.path.exists(mapfile):
1450 if not os.path.exists(mapfile):
1451 raise error.Abort(_("style '%s' not found") % mapfile,
1451 raise error.Abort(_("style '%s' not found") % mapfile,
1452 hint=_("available styles: %s") % stylelist())
1452 hint=_("available styles: %s") % stylelist())
1453
1453
1454 base = os.path.dirname(mapfile)
1454 base = os.path.dirname(mapfile)
1455 conf = config.config(includepaths=templatepaths())
1455 conf = config.config(includepaths=templatepaths())
1456 conf.read(mapfile, remap={'': 'templates'})
1456 conf.read(mapfile, remap={'': 'templates'})
1457
1457
1458 cache = {}
1458 cache = {}
1459 tmap = {}
1459 tmap = {}
1460 aliases = []
1460 aliases = []
1461
1461
1462 val = conf.get('templates', '__base__')
1462 val = conf.get('templates', '__base__')
1463 if val and val[0] not in "'\"":
1463 if val and val[0] not in "'\"":
1464 # treat as a pointer to a base class for this style
1464 # treat as a pointer to a base class for this style
1465 path = util.normpath(os.path.join(base, val))
1465 path = util.normpath(os.path.join(base, val))
1466
1466
1467 # fallback check in template paths
1467 # fallback check in template paths
1468 if not os.path.exists(path):
1468 if not os.path.exists(path):
1469 for p in templatepaths():
1469 for p in templatepaths():
1470 p2 = util.normpath(os.path.join(p, val))
1470 p2 = util.normpath(os.path.join(p, val))
1471 if os.path.isfile(p2):
1471 if os.path.isfile(p2):
1472 path = p2
1472 path = p2
1473 break
1473 break
1474 p3 = util.normpath(os.path.join(p2, "map"))
1474 p3 = util.normpath(os.path.join(p2, "map"))
1475 if os.path.isfile(p3):
1475 if os.path.isfile(p3):
1476 path = p3
1476 path = p3
1477 break
1477 break
1478
1478
1479 cache, tmap, aliases = _readmapfile(path)
1479 cache, tmap, aliases = _readmapfile(path)
1480
1480
1481 for key, val in conf['templates'].items():
1481 for key, val in conf['templates'].items():
1482 if not val:
1482 if not val:
1483 raise error.ParseError(_('missing value'),
1483 raise error.ParseError(_('missing value'),
1484 conf.source('templates', key))
1484 conf.source('templates', key))
1485 if val[0] in "'\"":
1485 if val[0] in "'\"":
1486 if val[0] != val[-1]:
1486 if val[0] != val[-1]:
1487 raise error.ParseError(_('unmatched quotes'),
1487 raise error.ParseError(_('unmatched quotes'),
1488 conf.source('templates', key))
1488 conf.source('templates', key))
1489 cache[key] = unquotestring(val)
1489 cache[key] = unquotestring(val)
1490 elif key != '__base__':
1490 elif key != '__base__':
1491 val = 'default', val
1491 val = 'default', val
1492 if ':' in val[1]:
1492 if ':' in val[1]:
1493 val = val[1].split(':', 1)
1493 val = val[1].split(':', 1)
1494 tmap[key] = val[0], os.path.join(base, val[1])
1494 tmap[key] = val[0], os.path.join(base, val[1])
1495 aliases.extend(conf['templatealias'].items())
1495 aliases.extend(conf['templatealias'].items())
1496 return cache, tmap, aliases
1496 return cache, tmap, aliases
1497
1497
1498 class templater(object):
1498 class templater(object):
1499
1499
1500 def __init__(self, filters=None, defaults=None, resources=None,
1500 def __init__(self, filters=None, defaults=None, resources=None,
1501 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1501 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1502 """Create template engine optionally with preloaded template fragments
1502 """Create template engine optionally with preloaded template fragments
1503
1503
1504 - ``filters``: a dict of functions to transform a value into another.
1504 - ``filters``: a dict of functions to transform a value into another.
1505 - ``defaults``: a dict of symbol values/functions; may be overridden
1505 - ``defaults``: a dict of symbol values/functions; may be overridden
1506 by a ``mapping`` dict.
1506 by a ``mapping`` dict.
1507 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1507 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1508 from user template; may be overridden by a ``mapping`` dict.
1508 from user template; may be overridden by a ``mapping`` dict.
1509 - ``cache``: a dict of preloaded template fragments.
1509 - ``cache``: a dict of preloaded template fragments.
1510 - ``aliases``: a list of alias (name, replacement) pairs.
1510 - ``aliases``: a list of alias (name, replacement) pairs.
1511
1511
1512 self.cache may be updated later to register additional template
1512 self.cache may be updated later to register additional template
1513 fragments.
1513 fragments.
1514 """
1514 """
1515 if filters is None:
1515 if filters is None:
1516 filters = {}
1516 filters = {}
1517 if defaults is None:
1517 if defaults is None:
1518 defaults = {}
1518 defaults = {}
1519 if resources is None:
1519 if resources is None:
1520 resources = {}
1520 resources = {}
1521 if cache is None:
1521 if cache is None:
1522 cache = {}
1522 cache = {}
1523 self.cache = cache.copy()
1523 self.cache = cache.copy()
1524 self.map = {}
1524 self.map = {}
1525 self.filters = templatefilters.filters.copy()
1525 self.filters = templatefilters.filters.copy()
1526 self.filters.update(filters)
1526 self.filters.update(filters)
1527 self.defaults = defaults
1527 self.defaults = defaults
1528 self._resources = {'templ': self}
1528 self._resources = {'templ': self}
1529 self._resources.update(resources)
1529 self._resources.update(resources)
1530 self._aliases = aliases
1530 self._aliases = aliases
1531 self.minchunk, self.maxchunk = minchunk, maxchunk
1531 self.minchunk, self.maxchunk = minchunk, maxchunk
1532 self.ecache = {}
1532 self.ecache = {}
1533
1533
1534 @classmethod
1534 @classmethod
1535 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1535 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1536 cache=None, minchunk=1024, maxchunk=65536):
1536 cache=None, minchunk=1024, maxchunk=65536):
1537 """Create templater from the specified map file"""
1537 """Create templater from the specified map file"""
1538 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1538 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1539 cache, tmap, aliases = _readmapfile(mapfile)
1539 cache, tmap, aliases = _readmapfile(mapfile)
1540 t.cache.update(cache)
1540 t.cache.update(cache)
1541 t.map = tmap
1541 t.map = tmap
1542 t._aliases = aliases
1542 t._aliases = aliases
1543 return t
1543 return t
1544
1544
1545 def __contains__(self, key):
1545 def __contains__(self, key):
1546 return key in self.cache or key in self.map
1546 return key in self.cache or key in self.map
1547
1547
1548 def load(self, t):
1548 def load(self, t):
1549 '''Get the template for the given template name. Use a local cache.'''
1549 '''Get the template for the given template name. Use a local cache.'''
1550 if t not in self.cache:
1550 if t not in self.cache:
1551 try:
1551 try:
1552 self.cache[t] = util.readfile(self.map[t][1])
1552 self.cache[t] = util.readfile(self.map[t][1])
1553 except KeyError as inst:
1553 except KeyError as inst:
1554 raise TemplateNotFound(_('"%s" not in template map') %
1554 raise TemplateNotFound(_('"%s" not in template map') %
1555 inst.args[0])
1555 inst.args[0])
1556 except IOError as inst:
1556 except IOError as inst:
1557 reason = (_('template file %s: %s')
1557 reason = (_('template file %s: %s')
1558 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1558 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1559 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1559 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1560 return self.cache[t]
1560 return self.cache[t]
1561
1561
1562 def render(self, mapping):
1562 def render(self, mapping):
1563 """Render the default unnamed template and return result as string"""
1563 """Render the default unnamed template and return result as string"""
1564 mapping = pycompat.strkwargs(mapping)
1564 mapping = pycompat.strkwargs(mapping)
1565 return stringify(self('', **mapping))
1565 return stringify(self('', **mapping))
1566
1566
1567 def __call__(self, t, **mapping):
1567 def __call__(self, t, **mapping):
1568 mapping = pycompat.byteskwargs(mapping)
1568 mapping = pycompat.byteskwargs(mapping)
1569 ttype = t in self.map and self.map[t][0] or 'default'
1569 ttype = t in self.map and self.map[t][0] or 'default'
1570 if ttype not in self.ecache:
1570 if ttype not in self.ecache:
1571 try:
1571 try:
1572 ecls = engines[ttype]
1572 ecls = engines[ttype]
1573 except KeyError:
1573 except KeyError:
1574 raise error.Abort(_('invalid template engine: %s') % ttype)
1574 raise error.Abort(_('invalid template engine: %s') % ttype)
1575 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1575 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1576 self._resources, self._aliases)
1576 self._resources, self._aliases)
1577 proc = self.ecache[ttype]
1577 proc = self.ecache[ttype]
1578
1578
1579 stream = proc.process(t, mapping)
1579 stream = proc.process(t, mapping)
1580 if self.minchunk:
1580 if self.minchunk:
1581 stream = util.increasingchunks(stream, min=self.minchunk,
1581 stream = util.increasingchunks(stream, min=self.minchunk,
1582 max=self.maxchunk)
1582 max=self.maxchunk)
1583 return stream
1583 return stream
1584
1584
1585 def templatepaths():
1585 def templatepaths():
1586 '''return locations used for template files.'''
1586 '''return locations used for template files.'''
1587 pathsrel = ['templates']
1587 pathsrel = ['templates']
1588 paths = [os.path.normpath(os.path.join(util.datapath, f))
1588 paths = [os.path.normpath(os.path.join(util.datapath, f))
1589 for f in pathsrel]
1589 for f in pathsrel]
1590 return [p for p in paths if os.path.isdir(p)]
1590 return [p for p in paths if os.path.isdir(p)]
1591
1591
1592 def templatepath(name):
1592 def templatepath(name):
1593 '''return location of template file. returns None if not found.'''
1593 '''return location of template file. returns None if not found.'''
1594 for p in templatepaths():
1594 for p in templatepaths():
1595 f = os.path.join(p, name)
1595 f = os.path.join(p, name)
1596 if os.path.exists(f):
1596 if os.path.exists(f):
1597 return f
1597 return f
1598 return None
1598 return None
1599
1599
1600 def stylemap(styles, paths=None):
1600 def stylemap(styles, paths=None):
1601 """Return path to mapfile for a given style.
1601 """Return path to mapfile for a given style.
1602
1602
1603 Searches mapfile in the following locations:
1603 Searches mapfile in the following locations:
1604 1. templatepath/style/map
1604 1. templatepath/style/map
1605 2. templatepath/map-style
1605 2. templatepath/map-style
1606 3. templatepath/map
1606 3. templatepath/map
1607 """
1607 """
1608
1608
1609 if paths is None:
1609 if paths is None:
1610 paths = templatepaths()
1610 paths = templatepaths()
1611 elif isinstance(paths, bytes):
1611 elif isinstance(paths, bytes):
1612 paths = [paths]
1612 paths = [paths]
1613
1613
1614 if isinstance(styles, bytes):
1614 if isinstance(styles, bytes):
1615 styles = [styles]
1615 styles = [styles]
1616
1616
1617 for style in styles:
1617 for style in styles:
1618 # only plain name is allowed to honor template paths
1618 # only plain name is allowed to honor template paths
1619 if (not style
1619 if (not style
1620 or style in (pycompat.oscurdir, pycompat.ospardir)
1620 or style in (pycompat.oscurdir, pycompat.ospardir)
1621 or pycompat.ossep in style
1621 or pycompat.ossep in style
1622 or pycompat.osaltsep and pycompat.osaltsep in style):
1622 or pycompat.osaltsep and pycompat.osaltsep in style):
1623 continue
1623 continue
1624 locations = [os.path.join(style, 'map'), 'map-' + style]
1624 locations = [os.path.join(style, 'map'), 'map-' + style]
1625 locations.append('map')
1625 locations.append('map')
1626
1626
1627 for path in paths:
1627 for path in paths:
1628 for location in locations:
1628 for location in locations:
1629 mapfile = os.path.join(path, location)
1629 mapfile = os.path.join(path, location)
1630 if os.path.isfile(mapfile):
1630 if os.path.isfile(mapfile):
1631 return style, mapfile
1631 return style, mapfile
1632
1632
1633 raise RuntimeError("No hgweb templates found in %r" % paths)
1633 raise RuntimeError("No hgweb templates found in %r" % paths)
1634
1634
1635 def loadfunction(ui, extname, registrarobj):
1635 def loadfunction(ui, extname, registrarobj):
1636 """Load template function from specified registrarobj
1636 """Load template function from specified registrarobj
1637 """
1637 """
1638 for name, func in registrarobj._table.iteritems():
1638 for name, func in registrarobj._table.iteritems():
1639 funcs[name] = func
1639 funcs[name] = func
1640
1640
1641 # tell hggettext to extract docstrings from these functions:
1641 # tell hggettext to extract docstrings from these functions:
1642 i18nfunctions = funcs.values()
1642 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now