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