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