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