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