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