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