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