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