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