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