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