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