##// END OF EJS Templates
templater: add class representing a nested mappings...
Yuya Nishihara -
r37417:0b644162 default
parent child Browse files
Show More
@@ -1,907 +1,911 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
29 BUG: hgweb overloads this type for mappings (i.e. some hgweb keywords
30 returns a generator of dicts.)
30 returns a generator of dicts.)
31
31
32 None
32 None
33 sometimes represents an empty value, which can be stringified to ''.
33 sometimes represents an empty value, which can be stringified to ''.
34
34
35 True, False, int, float
35 True, False, int, float
36 can be stringified as such.
36 can be stringified as such.
37
37
38 date tuple
38 date tuple
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
39 a (unixtime, offset) tuple, which produces no meaningful output by itself.
40
40
41 hybrid
41 hybrid
42 represents a list/dict of printable values, which can also be converted
42 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
43 to mappings by % operator.
44
44
45 mappable
45 mappable
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47
48 mappinggenerator, mappinglist
49 represents mappings (i.e. a list of dicts), which may have default
50 output format.
47 """
51 """
48
52
49 from __future__ import absolute_import, print_function
53 from __future__ import absolute_import, print_function
50
54
51 import abc
55 import abc
52 import os
56 import os
53
57
54 from .i18n import _
58 from .i18n import _
55 from . import (
59 from . import (
56 config,
60 config,
57 encoding,
61 encoding,
58 error,
62 error,
59 parser,
63 parser,
60 pycompat,
64 pycompat,
61 templatefilters,
65 templatefilters,
62 templatefuncs,
66 templatefuncs,
63 templateutil,
67 templateutil,
64 util,
68 util,
65 )
69 )
66 from .utils import (
70 from .utils import (
67 stringutil,
71 stringutil,
68 )
72 )
69
73
70 # template parsing
74 # template parsing
71
75
72 elements = {
76 elements = {
73 # token-type: binding-strength, primary, prefix, infix, suffix
77 # token-type: binding-strength, primary, prefix, infix, suffix
74 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
78 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
75 ".": (18, None, None, (".", 18), None),
79 ".": (18, None, None, (".", 18), None),
76 "%": (15, None, None, ("%", 15), None),
80 "%": (15, None, None, ("%", 15), None),
77 "|": (15, None, None, ("|", 15), None),
81 "|": (15, None, None, ("|", 15), None),
78 "*": (5, None, None, ("*", 5), None),
82 "*": (5, None, None, ("*", 5), None),
79 "/": (5, None, None, ("/", 5), None),
83 "/": (5, None, None, ("/", 5), None),
80 "+": (4, None, None, ("+", 4), None),
84 "+": (4, None, None, ("+", 4), None),
81 "-": (4, None, ("negate", 19), ("-", 4), None),
85 "-": (4, None, ("negate", 19), ("-", 4), None),
82 "=": (3, None, None, ("keyvalue", 3), None),
86 "=": (3, None, None, ("keyvalue", 3), None),
83 ",": (2, None, None, ("list", 2), None),
87 ",": (2, None, None, ("list", 2), None),
84 ")": (0, None, None, None, None),
88 ")": (0, None, None, None, None),
85 "integer": (0, "integer", None, None, None),
89 "integer": (0, "integer", None, None, None),
86 "symbol": (0, "symbol", None, None, None),
90 "symbol": (0, "symbol", None, None, None),
87 "string": (0, "string", None, None, None),
91 "string": (0, "string", None, None, None),
88 "template": (0, "template", None, None, None),
92 "template": (0, "template", None, None, None),
89 "end": (0, None, None, None, None),
93 "end": (0, None, None, None, None),
90 }
94 }
91
95
92 def tokenize(program, start, end, term=None):
96 def tokenize(program, start, end, term=None):
93 """Parse a template expression into a stream of tokens, which must end
97 """Parse a template expression into a stream of tokens, which must end
94 with term if specified"""
98 with term if specified"""
95 pos = start
99 pos = start
96 program = pycompat.bytestr(program)
100 program = pycompat.bytestr(program)
97 while pos < end:
101 while pos < end:
98 c = program[pos]
102 c = program[pos]
99 if c.isspace(): # skip inter-token whitespace
103 if c.isspace(): # skip inter-token whitespace
100 pass
104 pass
101 elif c in "(=,).%|+-*/": # handle simple operators
105 elif c in "(=,).%|+-*/": # handle simple operators
102 yield (c, None, pos)
106 yield (c, None, pos)
103 elif c in '"\'': # handle quoted templates
107 elif c in '"\'': # handle quoted templates
104 s = pos + 1
108 s = pos + 1
105 data, pos = _parsetemplate(program, s, end, c)
109 data, pos = _parsetemplate(program, s, end, c)
106 yield ('template', data, s)
110 yield ('template', data, s)
107 pos -= 1
111 pos -= 1
108 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
112 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
109 # handle quoted strings
113 # handle quoted strings
110 c = program[pos + 1]
114 c = program[pos + 1]
111 s = pos = pos + 2
115 s = pos = pos + 2
112 while pos < end: # find closing quote
116 while pos < end: # find closing quote
113 d = program[pos]
117 d = program[pos]
114 if d == '\\': # skip over escaped characters
118 if d == '\\': # skip over escaped characters
115 pos += 2
119 pos += 2
116 continue
120 continue
117 if d == c:
121 if d == c:
118 yield ('string', program[s:pos], s)
122 yield ('string', program[s:pos], s)
119 break
123 break
120 pos += 1
124 pos += 1
121 else:
125 else:
122 raise error.ParseError(_("unterminated string"), s)
126 raise error.ParseError(_("unterminated string"), s)
123 elif c.isdigit():
127 elif c.isdigit():
124 s = pos
128 s = pos
125 while pos < end:
129 while pos < end:
126 d = program[pos]
130 d = program[pos]
127 if not d.isdigit():
131 if not d.isdigit():
128 break
132 break
129 pos += 1
133 pos += 1
130 yield ('integer', program[s:pos], s)
134 yield ('integer', program[s:pos], s)
131 pos -= 1
135 pos -= 1
132 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
136 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
133 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
137 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
134 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
138 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
135 # where some of nested templates were preprocessed as strings and
139 # where some of nested templates were preprocessed as strings and
136 # then compiled. therefore, \"...\" was allowed. (issue4733)
140 # then compiled. therefore, \"...\" was allowed. (issue4733)
137 #
141 #
138 # processing flow of _evalifliteral() at 5ab28a2e9962:
142 # processing flow of _evalifliteral() at 5ab28a2e9962:
139 # outer template string -> stringify() -> compiletemplate()
143 # outer template string -> stringify() -> compiletemplate()
140 # ------------------------ ------------ ------------------
144 # ------------------------ ------------ ------------------
141 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
145 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
142 # ~~~~~~~~
146 # ~~~~~~~~
143 # escaped quoted string
147 # escaped quoted string
144 if c == 'r':
148 if c == 'r':
145 pos += 1
149 pos += 1
146 token = 'string'
150 token = 'string'
147 else:
151 else:
148 token = 'template'
152 token = 'template'
149 quote = program[pos:pos + 2]
153 quote = program[pos:pos + 2]
150 s = pos = pos + 2
154 s = pos = pos + 2
151 while pos < end: # find closing escaped quote
155 while pos < end: # find closing escaped quote
152 if program.startswith('\\\\\\', pos, end):
156 if program.startswith('\\\\\\', pos, end):
153 pos += 4 # skip over double escaped characters
157 pos += 4 # skip over double escaped characters
154 continue
158 continue
155 if program.startswith(quote, pos, end):
159 if program.startswith(quote, pos, end):
156 # interpret as if it were a part of an outer string
160 # interpret as if it were a part of an outer string
157 data = parser.unescapestr(program[s:pos])
161 data = parser.unescapestr(program[s:pos])
158 if token == 'template':
162 if token == 'template':
159 data = _parsetemplate(data, 0, len(data))[0]
163 data = _parsetemplate(data, 0, len(data))[0]
160 yield (token, data, s)
164 yield (token, data, s)
161 pos += 1
165 pos += 1
162 break
166 break
163 pos += 1
167 pos += 1
164 else:
168 else:
165 raise error.ParseError(_("unterminated string"), s)
169 raise error.ParseError(_("unterminated string"), s)
166 elif c.isalnum() or c in '_':
170 elif c.isalnum() or c in '_':
167 s = pos
171 s = pos
168 pos += 1
172 pos += 1
169 while pos < end: # find end of symbol
173 while pos < end: # find end of symbol
170 d = program[pos]
174 d = program[pos]
171 if not (d.isalnum() or d == "_"):
175 if not (d.isalnum() or d == "_"):
172 break
176 break
173 pos += 1
177 pos += 1
174 sym = program[s:pos]
178 sym = program[s:pos]
175 yield ('symbol', sym, s)
179 yield ('symbol', sym, s)
176 pos -= 1
180 pos -= 1
177 elif c == term:
181 elif c == term:
178 yield ('end', None, pos)
182 yield ('end', None, pos)
179 return
183 return
180 else:
184 else:
181 raise error.ParseError(_("syntax error"), pos)
185 raise error.ParseError(_("syntax error"), pos)
182 pos += 1
186 pos += 1
183 if term:
187 if term:
184 raise error.ParseError(_("unterminated template expansion"), start)
188 raise error.ParseError(_("unterminated template expansion"), start)
185 yield ('end', None, pos)
189 yield ('end', None, pos)
186
190
187 def _parsetemplate(tmpl, start, stop, quote=''):
191 def _parsetemplate(tmpl, start, stop, quote=''):
188 r"""
192 r"""
189 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
193 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
190 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
194 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
191 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
195 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
192 ([('string', 'foo'), ('symbol', 'bar')], 9)
196 ([('string', 'foo'), ('symbol', 'bar')], 9)
193 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
197 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
194 ([('string', 'foo')], 4)
198 ([('string', 'foo')], 4)
195 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
199 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
196 ([('string', 'foo"'), ('string', 'bar')], 9)
200 ([('string', 'foo"'), ('string', 'bar')], 9)
197 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
201 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
198 ([('string', 'foo\\')], 6)
202 ([('string', 'foo\\')], 6)
199 """
203 """
200 parsed = []
204 parsed = []
201 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
205 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
202 if typ == 'string':
206 if typ == 'string':
203 parsed.append((typ, val))
207 parsed.append((typ, val))
204 elif typ == 'template':
208 elif typ == 'template':
205 parsed.append(val)
209 parsed.append(val)
206 elif typ == 'end':
210 elif typ == 'end':
207 return parsed, pos
211 return parsed, pos
208 else:
212 else:
209 raise error.ProgrammingError('unexpected type: %s' % typ)
213 raise error.ProgrammingError('unexpected type: %s' % typ)
210 raise error.ProgrammingError('unterminated scanning of template')
214 raise error.ProgrammingError('unterminated scanning of template')
211
215
212 def scantemplate(tmpl, raw=False):
216 def scantemplate(tmpl, raw=False):
213 r"""Scan (type, start, end) positions of outermost elements in template
217 r"""Scan (type, start, end) positions of outermost elements in template
214
218
215 If raw=True, a backslash is not taken as an escape character just like
219 If raw=True, a backslash is not taken as an escape character just like
216 r'' string in Python. Note that this is different from r'' literal in
220 r'' string in Python. Note that this is different from r'' literal in
217 template in that no template fragment can appear in r'', e.g. r'{foo}'
221 template in that no template fragment can appear in r'', e.g. r'{foo}'
218 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
222 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
219 'foo'.
223 'foo'.
220
224
221 >>> list(scantemplate(b'foo{bar}"baz'))
225 >>> list(scantemplate(b'foo{bar}"baz'))
222 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
226 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
223 >>> list(scantemplate(b'outer{"inner"}outer'))
227 >>> list(scantemplate(b'outer{"inner"}outer'))
224 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
228 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
225 >>> list(scantemplate(b'foo\\{escaped}'))
229 >>> list(scantemplate(b'foo\\{escaped}'))
226 [('string', 0, 5), ('string', 5, 13)]
230 [('string', 0, 5), ('string', 5, 13)]
227 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
231 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
228 [('string', 0, 4), ('template', 4, 13)]
232 [('string', 0, 4), ('template', 4, 13)]
229 """
233 """
230 last = None
234 last = None
231 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
235 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
232 if last:
236 if last:
233 yield last + (pos,)
237 yield last + (pos,)
234 if typ == 'end':
238 if typ == 'end':
235 return
239 return
236 else:
240 else:
237 last = (typ, pos)
241 last = (typ, pos)
238 raise error.ProgrammingError('unterminated scanning of template')
242 raise error.ProgrammingError('unterminated scanning of template')
239
243
240 def _scantemplate(tmpl, start, stop, quote='', raw=False):
244 def _scantemplate(tmpl, start, stop, quote='', raw=False):
241 """Parse template string into chunks of strings and template expressions"""
245 """Parse template string into chunks of strings and template expressions"""
242 sepchars = '{' + quote
246 sepchars = '{' + quote
243 unescape = [parser.unescapestr, pycompat.identity][raw]
247 unescape = [parser.unescapestr, pycompat.identity][raw]
244 pos = start
248 pos = start
245 p = parser.parser(elements)
249 p = parser.parser(elements)
246 try:
250 try:
247 while pos < stop:
251 while pos < stop:
248 n = min((tmpl.find(c, pos, stop) for c in sepchars),
252 n = min((tmpl.find(c, pos, stop) for c in sepchars),
249 key=lambda n: (n < 0, n))
253 key=lambda n: (n < 0, n))
250 if n < 0:
254 if n < 0:
251 yield ('string', unescape(tmpl[pos:stop]), pos)
255 yield ('string', unescape(tmpl[pos:stop]), pos)
252 pos = stop
256 pos = stop
253 break
257 break
254 c = tmpl[n:n + 1]
258 c = tmpl[n:n + 1]
255 bs = 0 # count leading backslashes
259 bs = 0 # count leading backslashes
256 if not raw:
260 if not raw:
257 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
261 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
258 if bs % 2 == 1:
262 if bs % 2 == 1:
259 # escaped (e.g. '\{', '\\\{', but not '\\{')
263 # escaped (e.g. '\{', '\\\{', but not '\\{')
260 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
264 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
261 pos = n + 1
265 pos = n + 1
262 continue
266 continue
263 if n > pos:
267 if n > pos:
264 yield ('string', unescape(tmpl[pos:n]), pos)
268 yield ('string', unescape(tmpl[pos:n]), pos)
265 if c == quote:
269 if c == quote:
266 yield ('end', None, n + 1)
270 yield ('end', None, n + 1)
267 return
271 return
268
272
269 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
273 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
270 if not tmpl.startswith('}', pos):
274 if not tmpl.startswith('}', pos):
271 raise error.ParseError(_("invalid token"), pos)
275 raise error.ParseError(_("invalid token"), pos)
272 yield ('template', parseres, n)
276 yield ('template', parseres, n)
273 pos += 1
277 pos += 1
274
278
275 if quote:
279 if quote:
276 raise error.ParseError(_("unterminated string"), start)
280 raise error.ParseError(_("unterminated string"), start)
277 except error.ParseError as inst:
281 except error.ParseError as inst:
278 if len(inst.args) > 1: # has location
282 if len(inst.args) > 1: # has location
279 loc = inst.args[1]
283 loc = inst.args[1]
280 # Offset the caret location by the number of newlines before the
284 # Offset the caret location by the number of newlines before the
281 # location of the error, since we will replace one-char newlines
285 # location of the error, since we will replace one-char newlines
282 # with the two-char literal r'\n'.
286 # with the two-char literal r'\n'.
283 offset = tmpl[:loc].count('\n')
287 offset = tmpl[:loc].count('\n')
284 tmpl = tmpl.replace('\n', br'\n')
288 tmpl = tmpl.replace('\n', br'\n')
285 # We want the caret to point to the place in the template that
289 # We want the caret to point to the place in the template that
286 # failed to parse, but in a hint we get a open paren at the
290 # failed to parse, but in a hint we get a open paren at the
287 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
291 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
288 # to line up the caret with the location of the error.
292 # to line up the caret with the location of the error.
289 inst.hint = (tmpl + '\n'
293 inst.hint = (tmpl + '\n'
290 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
294 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
291 raise
295 raise
292 yield ('end', None, pos)
296 yield ('end', None, pos)
293
297
294 def _unnesttemplatelist(tree):
298 def _unnesttemplatelist(tree):
295 """Expand list of templates to node tuple
299 """Expand list of templates to node tuple
296
300
297 >>> def f(tree):
301 >>> def f(tree):
298 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
302 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
299 >>> f((b'template', []))
303 >>> f((b'template', []))
300 (string '')
304 (string '')
301 >>> f((b'template', [(b'string', b'foo')]))
305 >>> f((b'template', [(b'string', b'foo')]))
302 (string 'foo')
306 (string 'foo')
303 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
307 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
304 (template
308 (template
305 (string 'foo')
309 (string 'foo')
306 (symbol 'rev'))
310 (symbol 'rev'))
307 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
311 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
308 (template
312 (template
309 (symbol 'rev'))
313 (symbol 'rev'))
310 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
314 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
311 (string 'foo')
315 (string 'foo')
312 """
316 """
313 if not isinstance(tree, tuple):
317 if not isinstance(tree, tuple):
314 return tree
318 return tree
315 op = tree[0]
319 op = tree[0]
316 if op != 'template':
320 if op != 'template':
317 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
321 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
318
322
319 assert len(tree) == 2
323 assert len(tree) == 2
320 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
324 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
321 if not xs:
325 if not xs:
322 return ('string', '') # empty template ""
326 return ('string', '') # empty template ""
323 elif len(xs) == 1 and xs[0][0] == 'string':
327 elif len(xs) == 1 and xs[0][0] == 'string':
324 return xs[0] # fast path for string with no template fragment "x"
328 return xs[0] # fast path for string with no template fragment "x"
325 else:
329 else:
326 return (op,) + xs
330 return (op,) + xs
327
331
328 def parse(tmpl):
332 def parse(tmpl):
329 """Parse template string into tree"""
333 """Parse template string into tree"""
330 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
334 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
331 assert pos == len(tmpl), 'unquoted template should be consumed'
335 assert pos == len(tmpl), 'unquoted template should be consumed'
332 return _unnesttemplatelist(('template', parsed))
336 return _unnesttemplatelist(('template', parsed))
333
337
334 def _parseexpr(expr):
338 def _parseexpr(expr):
335 """Parse a template expression into tree
339 """Parse a template expression into tree
336
340
337 >>> _parseexpr(b'"foo"')
341 >>> _parseexpr(b'"foo"')
338 ('string', 'foo')
342 ('string', 'foo')
339 >>> _parseexpr(b'foo(bar)')
343 >>> _parseexpr(b'foo(bar)')
340 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
344 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
341 >>> _parseexpr(b'foo(')
345 >>> _parseexpr(b'foo(')
342 Traceback (most recent call last):
346 Traceback (most recent call last):
343 ...
347 ...
344 ParseError: ('not a prefix: end', 4)
348 ParseError: ('not a prefix: end', 4)
345 >>> _parseexpr(b'"foo" "bar"')
349 >>> _parseexpr(b'"foo" "bar"')
346 Traceback (most recent call last):
350 Traceback (most recent call last):
347 ...
351 ...
348 ParseError: ('invalid token', 7)
352 ParseError: ('invalid token', 7)
349 """
353 """
350 p = parser.parser(elements)
354 p = parser.parser(elements)
351 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
355 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
352 if pos != len(expr):
356 if pos != len(expr):
353 raise error.ParseError(_('invalid token'), pos)
357 raise error.ParseError(_('invalid token'), pos)
354 return _unnesttemplatelist(tree)
358 return _unnesttemplatelist(tree)
355
359
356 def prettyformat(tree):
360 def prettyformat(tree):
357 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
361 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
358
362
359 def compileexp(exp, context, curmethods):
363 def compileexp(exp, context, curmethods):
360 """Compile parsed template tree to (func, data) pair"""
364 """Compile parsed template tree to (func, data) pair"""
361 if not exp:
365 if not exp:
362 raise error.ParseError(_("missing argument"))
366 raise error.ParseError(_("missing argument"))
363 t = exp[0]
367 t = exp[0]
364 if t in curmethods:
368 if t in curmethods:
365 return curmethods[t](exp, context)
369 return curmethods[t](exp, context)
366 raise error.ParseError(_("unknown method '%s'") % t)
370 raise error.ParseError(_("unknown method '%s'") % t)
367
371
368 # template evaluation
372 # template evaluation
369
373
370 def getsymbol(exp):
374 def getsymbol(exp):
371 if exp[0] == 'symbol':
375 if exp[0] == 'symbol':
372 return exp[1]
376 return exp[1]
373 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
377 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
374
378
375 def getlist(x):
379 def getlist(x):
376 if not x:
380 if not x:
377 return []
381 return []
378 if x[0] == 'list':
382 if x[0] == 'list':
379 return getlist(x[1]) + [x[2]]
383 return getlist(x[1]) + [x[2]]
380 return [x]
384 return [x]
381
385
382 def gettemplate(exp, context):
386 def gettemplate(exp, context):
383 """Compile given template tree or load named template from map file;
387 """Compile given template tree or load named template from map file;
384 returns (func, data) pair"""
388 returns (func, data) pair"""
385 if exp[0] in ('template', 'string'):
389 if exp[0] in ('template', 'string'):
386 return compileexp(exp, context, methods)
390 return compileexp(exp, context, methods)
387 if exp[0] == 'symbol':
391 if exp[0] == 'symbol':
388 # unlike runsymbol(), here 'symbol' is always taken as template name
392 # unlike runsymbol(), here 'symbol' is always taken as template name
389 # even if it exists in mapping. this allows us to override mapping
393 # even if it exists in mapping. this allows us to override mapping
390 # by web templates, e.g. 'changelogtag' is redefined in map file.
394 # by web templates, e.g. 'changelogtag' is redefined in map file.
391 return context._load(exp[1])
395 return context._load(exp[1])
392 raise error.ParseError(_("expected template specifier"))
396 raise error.ParseError(_("expected template specifier"))
393
397
394 def _runrecursivesymbol(context, mapping, key):
398 def _runrecursivesymbol(context, mapping, key):
395 raise error.Abort(_("recursive reference '%s' in template") % key)
399 raise error.Abort(_("recursive reference '%s' in template") % key)
396
400
397 def buildtemplate(exp, context):
401 def buildtemplate(exp, context):
398 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
402 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
399 return (templateutil.runtemplate, ctmpl)
403 return (templateutil.runtemplate, ctmpl)
400
404
401 def buildfilter(exp, context):
405 def buildfilter(exp, context):
402 n = getsymbol(exp[2])
406 n = getsymbol(exp[2])
403 if n in context._filters:
407 if n in context._filters:
404 filt = context._filters[n]
408 filt = context._filters[n]
405 arg = compileexp(exp[1], context, methods)
409 arg = compileexp(exp[1], context, methods)
406 return (templateutil.runfilter, (arg, filt))
410 return (templateutil.runfilter, (arg, filt))
407 if n in context._funcs:
411 if n in context._funcs:
408 f = context._funcs[n]
412 f = context._funcs[n]
409 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
413 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
410 return (f, args)
414 return (f, args)
411 raise error.ParseError(_("unknown function '%s'") % n)
415 raise error.ParseError(_("unknown function '%s'") % n)
412
416
413 def buildmap(exp, context):
417 def buildmap(exp, context):
414 darg = compileexp(exp[1], context, methods)
418 darg = compileexp(exp[1], context, methods)
415 targ = gettemplate(exp[2], context)
419 targ = gettemplate(exp[2], context)
416 return (templateutil.runmap, (darg, targ))
420 return (templateutil.runmap, (darg, targ))
417
421
418 def buildmember(exp, context):
422 def buildmember(exp, context):
419 darg = compileexp(exp[1], context, methods)
423 darg = compileexp(exp[1], context, methods)
420 memb = getsymbol(exp[2])
424 memb = getsymbol(exp[2])
421 return (templateutil.runmember, (darg, memb))
425 return (templateutil.runmember, (darg, memb))
422
426
423 def buildnegate(exp, context):
427 def buildnegate(exp, context):
424 arg = compileexp(exp[1], context, exprmethods)
428 arg = compileexp(exp[1], context, exprmethods)
425 return (templateutil.runnegate, arg)
429 return (templateutil.runnegate, arg)
426
430
427 def buildarithmetic(exp, context, func):
431 def buildarithmetic(exp, context, func):
428 left = compileexp(exp[1], context, exprmethods)
432 left = compileexp(exp[1], context, exprmethods)
429 right = compileexp(exp[2], context, exprmethods)
433 right = compileexp(exp[2], context, exprmethods)
430 return (templateutil.runarithmetic, (func, left, right))
434 return (templateutil.runarithmetic, (func, left, right))
431
435
432 def buildfunc(exp, context):
436 def buildfunc(exp, context):
433 n = getsymbol(exp[1])
437 n = getsymbol(exp[1])
434 if n in context._funcs:
438 if n in context._funcs:
435 f = context._funcs[n]
439 f = context._funcs[n]
436 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
440 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
437 return (f, args)
441 return (f, args)
438 if n in context._filters:
442 if n in context._filters:
439 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
443 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
440 if len(args) != 1:
444 if len(args) != 1:
441 raise error.ParseError(_("filter %s expects one argument") % n)
445 raise error.ParseError(_("filter %s expects one argument") % n)
442 f = context._filters[n]
446 f = context._filters[n]
443 return (templateutil.runfilter, (args[0], f))
447 return (templateutil.runfilter, (args[0], f))
444 raise error.ParseError(_("unknown function '%s'") % n)
448 raise error.ParseError(_("unknown function '%s'") % n)
445
449
446 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
450 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
447 """Compile parsed tree of function arguments into list or dict of
451 """Compile parsed tree of function arguments into list or dict of
448 (func, data) pairs
452 (func, data) pairs
449
453
450 >>> context = engine(lambda t: (templateutil.runsymbol, t))
454 >>> context = engine(lambda t: (templateutil.runsymbol, t))
451 >>> def fargs(expr, argspec):
455 >>> def fargs(expr, argspec):
452 ... x = _parseexpr(expr)
456 ... x = _parseexpr(expr)
453 ... n = getsymbol(x[1])
457 ... n = getsymbol(x[1])
454 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
458 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
455 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
459 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
456 ['l', 'k']
460 ['l', 'k']
457 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
461 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
458 >>> list(args.keys()), list(args[b'opts'].keys())
462 >>> list(args.keys()), list(args[b'opts'].keys())
459 (['opts'], ['opts', 'k'])
463 (['opts'], ['opts', 'k'])
460 """
464 """
461 def compiledict(xs):
465 def compiledict(xs):
462 return util.sortdict((k, compileexp(x, context, curmethods))
466 return util.sortdict((k, compileexp(x, context, curmethods))
463 for k, x in xs.iteritems())
467 for k, x in xs.iteritems())
464 def compilelist(xs):
468 def compilelist(xs):
465 return [compileexp(x, context, curmethods) for x in xs]
469 return [compileexp(x, context, curmethods) for x in xs]
466
470
467 if not argspec:
471 if not argspec:
468 # filter or function with no argspec: return list of positional args
472 # filter or function with no argspec: return list of positional args
469 return compilelist(getlist(exp))
473 return compilelist(getlist(exp))
470
474
471 # function with argspec: return dict of named args
475 # function with argspec: return dict of named args
472 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
476 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
473 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
477 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
474 keyvaluenode='keyvalue', keynode='symbol')
478 keyvaluenode='keyvalue', keynode='symbol')
475 compargs = util.sortdict()
479 compargs = util.sortdict()
476 if varkey:
480 if varkey:
477 compargs[varkey] = compilelist(treeargs.pop(varkey))
481 compargs[varkey] = compilelist(treeargs.pop(varkey))
478 if optkey:
482 if optkey:
479 compargs[optkey] = compiledict(treeargs.pop(optkey))
483 compargs[optkey] = compiledict(treeargs.pop(optkey))
480 compargs.update(compiledict(treeargs))
484 compargs.update(compiledict(treeargs))
481 return compargs
485 return compargs
482
486
483 def buildkeyvaluepair(exp, content):
487 def buildkeyvaluepair(exp, content):
484 raise error.ParseError(_("can't use a key-value pair in this context"))
488 raise error.ParseError(_("can't use a key-value pair in this context"))
485
489
486 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
490 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
487 exprmethods = {
491 exprmethods = {
488 "integer": lambda e, c: (templateutil.runinteger, e[1]),
492 "integer": lambda e, c: (templateutil.runinteger, e[1]),
489 "string": lambda e, c: (templateutil.runstring, e[1]),
493 "string": lambda e, c: (templateutil.runstring, e[1]),
490 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
494 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
491 "template": buildtemplate,
495 "template": buildtemplate,
492 "group": lambda e, c: compileexp(e[1], c, exprmethods),
496 "group": lambda e, c: compileexp(e[1], c, exprmethods),
493 ".": buildmember,
497 ".": buildmember,
494 "|": buildfilter,
498 "|": buildfilter,
495 "%": buildmap,
499 "%": buildmap,
496 "func": buildfunc,
500 "func": buildfunc,
497 "keyvalue": buildkeyvaluepair,
501 "keyvalue": buildkeyvaluepair,
498 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
502 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
499 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
503 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
500 "negate": buildnegate,
504 "negate": buildnegate,
501 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
505 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
502 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
506 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
503 }
507 }
504
508
505 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
509 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
506 methods = exprmethods.copy()
510 methods = exprmethods.copy()
507 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
511 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
508
512
509 class _aliasrules(parser.basealiasrules):
513 class _aliasrules(parser.basealiasrules):
510 """Parsing and expansion rule set of template aliases"""
514 """Parsing and expansion rule set of template aliases"""
511 _section = _('template alias')
515 _section = _('template alias')
512 _parse = staticmethod(_parseexpr)
516 _parse = staticmethod(_parseexpr)
513
517
514 @staticmethod
518 @staticmethod
515 def _trygetfunc(tree):
519 def _trygetfunc(tree):
516 """Return (name, args) if tree is func(...) or ...|filter; otherwise
520 """Return (name, args) if tree is func(...) or ...|filter; otherwise
517 None"""
521 None"""
518 if tree[0] == 'func' and tree[1][0] == 'symbol':
522 if tree[0] == 'func' and tree[1][0] == 'symbol':
519 return tree[1][1], getlist(tree[2])
523 return tree[1][1], getlist(tree[2])
520 if tree[0] == '|' and tree[2][0] == 'symbol':
524 if tree[0] == '|' and tree[2][0] == 'symbol':
521 return tree[2][1], [tree[1]]
525 return tree[2][1], [tree[1]]
522
526
523 def expandaliases(tree, aliases):
527 def expandaliases(tree, aliases):
524 """Return new tree of aliases are expanded"""
528 """Return new tree of aliases are expanded"""
525 aliasmap = _aliasrules.buildmap(aliases)
529 aliasmap = _aliasrules.buildmap(aliases)
526 return _aliasrules.expand(aliasmap, tree)
530 return _aliasrules.expand(aliasmap, tree)
527
531
528 # template engine
532 # template engine
529
533
530 def unquotestring(s):
534 def unquotestring(s):
531 '''unwrap quotes if any; otherwise returns unmodified string'''
535 '''unwrap quotes if any; otherwise returns unmodified string'''
532 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
536 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
533 return s
537 return s
534 return s[1:-1]
538 return s[1:-1]
535
539
536 class resourcemapper(object):
540 class resourcemapper(object):
537 """Mapper of internal template resources"""
541 """Mapper of internal template resources"""
538
542
539 __metaclass__ = abc.ABCMeta
543 __metaclass__ = abc.ABCMeta
540
544
541 @abc.abstractmethod
545 @abc.abstractmethod
542 def availablekeys(self, context, mapping):
546 def availablekeys(self, context, mapping):
543 """Return a set of available resource keys based on the given mapping"""
547 """Return a set of available resource keys based on the given mapping"""
544
548
545 @abc.abstractmethod
549 @abc.abstractmethod
546 def knownkeys(self):
550 def knownkeys(self):
547 """Return a set of supported resource keys"""
551 """Return a set of supported resource keys"""
548
552
549 @abc.abstractmethod
553 @abc.abstractmethod
550 def lookup(self, context, mapping, key):
554 def lookup(self, context, mapping, key):
551 """Return a resource for the key if available; otherwise None"""
555 """Return a resource for the key if available; otherwise None"""
552
556
553 @abc.abstractmethod
557 @abc.abstractmethod
554 def populatemap(self, context, origmapping, newmapping):
558 def populatemap(self, context, origmapping, newmapping):
555 """Return a dict of additional mapping items which should be paired
559 """Return a dict of additional mapping items which should be paired
556 with the given new mapping"""
560 with the given new mapping"""
557
561
558 class nullresourcemapper(resourcemapper):
562 class nullresourcemapper(resourcemapper):
559 def availablekeys(self, context, mapping):
563 def availablekeys(self, context, mapping):
560 return set()
564 return set()
561
565
562 def knownkeys(self):
566 def knownkeys(self):
563 return set()
567 return set()
564
568
565 def lookup(self, context, mapping, key):
569 def lookup(self, context, mapping, key):
566 return None
570 return None
567
571
568 def populatemap(self, context, origmapping, newmapping):
572 def populatemap(self, context, origmapping, newmapping):
569 return {}
573 return {}
570
574
571 class engine(object):
575 class engine(object):
572 '''template expansion engine.
576 '''template expansion engine.
573
577
574 template expansion works like this. a map file contains key=value
578 template expansion works like this. a map file contains key=value
575 pairs. if value is quoted, it is treated as string. otherwise, it
579 pairs. if value is quoted, it is treated as string. otherwise, it
576 is treated as name of template file.
580 is treated as name of template file.
577
581
578 templater is asked to expand a key in map. it looks up key, and
582 templater is asked to expand a key in map. it looks up key, and
579 looks for strings like this: {foo}. it expands {foo} by looking up
583 looks for strings like this: {foo}. it expands {foo} by looking up
580 foo in map, and substituting it. expansion is recursive: it stops
584 foo in map, and substituting it. expansion is recursive: it stops
581 when there is no more {foo} to replace.
585 when there is no more {foo} to replace.
582
586
583 expansion also allows formatting and filtering.
587 expansion also allows formatting and filtering.
584
588
585 format uses key to expand each item in list. syntax is
589 format uses key to expand each item in list. syntax is
586 {key%format}.
590 {key%format}.
587
591
588 filter uses function to transform value. syntax is
592 filter uses function to transform value. syntax is
589 {key|filter1|filter2|...}.'''
593 {key|filter1|filter2|...}.'''
590
594
591 def __init__(self, loader, filters=None, defaults=None, resources=None,
595 def __init__(self, loader, filters=None, defaults=None, resources=None,
592 aliases=()):
596 aliases=()):
593 self._loader = loader
597 self._loader = loader
594 if filters is None:
598 if filters is None:
595 filters = {}
599 filters = {}
596 self._filters = filters
600 self._filters = filters
597 self._funcs = templatefuncs.funcs # make this a parameter if needed
601 self._funcs = templatefuncs.funcs # make this a parameter if needed
598 if defaults is None:
602 if defaults is None:
599 defaults = {}
603 defaults = {}
600 if resources is None:
604 if resources is None:
601 resources = nullresourcemapper()
605 resources = nullresourcemapper()
602 self._defaults = defaults
606 self._defaults = defaults
603 self._resources = resources
607 self._resources = resources
604 self._aliasmap = _aliasrules.buildmap(aliases)
608 self._aliasmap = _aliasrules.buildmap(aliases)
605 self._cache = {} # key: (func, data)
609 self._cache = {} # key: (func, data)
606 self._tmplcache = {} # literal template: (func, data)
610 self._tmplcache = {} # literal template: (func, data)
607
611
608 def overlaymap(self, origmapping, newmapping):
612 def overlaymap(self, origmapping, newmapping):
609 """Create combined mapping from the original mapping and partial
613 """Create combined mapping from the original mapping and partial
610 mapping to override the original"""
614 mapping to override the original"""
611 # do not copy symbols which overrides the defaults depending on
615 # do not copy symbols which overrides the defaults depending on
612 # new resources, so the defaults will be re-evaluated (issue5612)
616 # new resources, so the defaults will be re-evaluated (issue5612)
613 knownres = self._resources.knownkeys()
617 knownres = self._resources.knownkeys()
614 newres = self._resources.availablekeys(self, newmapping)
618 newres = self._resources.availablekeys(self, newmapping)
615 mapping = {k: v for k, v in origmapping.iteritems()
619 mapping = {k: v for k, v in origmapping.iteritems()
616 if (k in knownres # not a symbol per self.symbol()
620 if (k in knownres # not a symbol per self.symbol()
617 or newres.isdisjoint(self._defaultrequires(k)))}
621 or newres.isdisjoint(self._defaultrequires(k)))}
618 mapping.update(newmapping)
622 mapping.update(newmapping)
619 mapping.update(
623 mapping.update(
620 self._resources.populatemap(self, origmapping, newmapping))
624 self._resources.populatemap(self, origmapping, newmapping))
621 return mapping
625 return mapping
622
626
623 def _defaultrequires(self, key):
627 def _defaultrequires(self, key):
624 """Resource keys required by the specified default symbol function"""
628 """Resource keys required by the specified default symbol function"""
625 v = self._defaults.get(key)
629 v = self._defaults.get(key)
626 if v is None or not callable(v):
630 if v is None or not callable(v):
627 return ()
631 return ()
628 return getattr(v, '_requires', ())
632 return getattr(v, '_requires', ())
629
633
630 def symbol(self, mapping, key):
634 def symbol(self, mapping, key):
631 """Resolve symbol to value or function; None if nothing found"""
635 """Resolve symbol to value or function; None if nothing found"""
632 v = None
636 v = None
633 if key not in self._resources.knownkeys():
637 if key not in self._resources.knownkeys():
634 v = mapping.get(key)
638 v = mapping.get(key)
635 if v is None:
639 if v is None:
636 v = self._defaults.get(key)
640 v = self._defaults.get(key)
637 return v
641 return v
638
642
639 def resource(self, mapping, key):
643 def resource(self, mapping, key):
640 """Return internal data (e.g. cache) used for keyword/function
644 """Return internal data (e.g. cache) used for keyword/function
641 evaluation"""
645 evaluation"""
642 v = self._resources.lookup(self, mapping, key)
646 v = self._resources.lookup(self, mapping, key)
643 if v is None:
647 if v is None:
644 raise templateutil.ResourceUnavailable(
648 raise templateutil.ResourceUnavailable(
645 _('template resource not available: %s') % key)
649 _('template resource not available: %s') % key)
646 return v
650 return v
647
651
648 def _load(self, t):
652 def _load(self, t):
649 '''load, parse, and cache a template'''
653 '''load, parse, and cache a template'''
650 if t not in self._cache:
654 if t not in self._cache:
651 # put poison to cut recursion while compiling 't'
655 # put poison to cut recursion while compiling 't'
652 self._cache[t] = (_runrecursivesymbol, t)
656 self._cache[t] = (_runrecursivesymbol, t)
653 try:
657 try:
654 x = parse(self._loader(t))
658 x = parse(self._loader(t))
655 if self._aliasmap:
659 if self._aliasmap:
656 x = _aliasrules.expand(self._aliasmap, x)
660 x = _aliasrules.expand(self._aliasmap, x)
657 self._cache[t] = compileexp(x, self, methods)
661 self._cache[t] = compileexp(x, self, methods)
658 except: # re-raises
662 except: # re-raises
659 del self._cache[t]
663 del self._cache[t]
660 raise
664 raise
661 return self._cache[t]
665 return self._cache[t]
662
666
663 def _parse(self, tmpl):
667 def _parse(self, tmpl):
664 """Parse and cache a literal template"""
668 """Parse and cache a literal template"""
665 if tmpl not in self._tmplcache:
669 if tmpl not in self._tmplcache:
666 x = parse(tmpl)
670 x = parse(tmpl)
667 self._tmplcache[tmpl] = compileexp(x, self, methods)
671 self._tmplcache[tmpl] = compileexp(x, self, methods)
668 return self._tmplcache[tmpl]
672 return self._tmplcache[tmpl]
669
673
670 def preload(self, t):
674 def preload(self, t):
671 """Load, parse, and cache the specified template if available"""
675 """Load, parse, and cache the specified template if available"""
672 try:
676 try:
673 self._load(t)
677 self._load(t)
674 return True
678 return True
675 except templateutil.TemplateNotFound:
679 except templateutil.TemplateNotFound:
676 return False
680 return False
677
681
678 def process(self, t, mapping):
682 def process(self, t, mapping):
679 '''Perform expansion. t is name of map element to expand.
683 '''Perform expansion. t is name of map element to expand.
680 mapping contains added elements for use during expansion. Is a
684 mapping contains added elements for use during expansion. Is a
681 generator.'''
685 generator.'''
682 func, data = self._load(t)
686 func, data = self._load(t)
683 return self._expand(func, data, mapping)
687 return self._expand(func, data, mapping)
684
688
685 def expand(self, tmpl, mapping):
689 def expand(self, tmpl, mapping):
686 """Perform expansion over a literal template
690 """Perform expansion over a literal template
687
691
688 No user aliases will be expanded since this is supposed to be called
692 No user aliases will be expanded since this is supposed to be called
689 with an internal template string.
693 with an internal template string.
690 """
694 """
691 func, data = self._parse(tmpl)
695 func, data = self._parse(tmpl)
692 return self._expand(func, data, mapping)
696 return self._expand(func, data, mapping)
693
697
694 def _expand(self, func, data, mapping):
698 def _expand(self, func, data, mapping):
695 # populate additional items only if they don't exist in the given
699 # populate additional items only if they don't exist in the given
696 # mapping. this is slightly different from overlaymap() because the
700 # mapping. this is slightly different from overlaymap() because the
697 # initial 'revcache' may contain pre-computed items.
701 # initial 'revcache' may contain pre-computed items.
698 extramapping = self._resources.populatemap(self, {}, mapping)
702 extramapping = self._resources.populatemap(self, {}, mapping)
699 if extramapping:
703 if extramapping:
700 extramapping.update(mapping)
704 extramapping.update(mapping)
701 mapping = extramapping
705 mapping = extramapping
702 return templateutil.flatten(self, mapping, func(self, mapping, data))
706 return templateutil.flatten(self, mapping, func(self, mapping, data))
703
707
704 engines = {'default': engine}
708 engines = {'default': engine}
705
709
706 def stylelist():
710 def stylelist():
707 paths = templatepaths()
711 paths = templatepaths()
708 if not paths:
712 if not paths:
709 return _('no templates found, try `hg debuginstall` for more info')
713 return _('no templates found, try `hg debuginstall` for more info')
710 dirlist = os.listdir(paths[0])
714 dirlist = os.listdir(paths[0])
711 stylelist = []
715 stylelist = []
712 for file in dirlist:
716 for file in dirlist:
713 split = file.split(".")
717 split = file.split(".")
714 if split[-1] in ('orig', 'rej'):
718 if split[-1] in ('orig', 'rej'):
715 continue
719 continue
716 if split[0] == "map-cmdline":
720 if split[0] == "map-cmdline":
717 stylelist.append(split[1])
721 stylelist.append(split[1])
718 return ", ".join(sorted(stylelist))
722 return ", ".join(sorted(stylelist))
719
723
720 def _readmapfile(mapfile):
724 def _readmapfile(mapfile):
721 """Load template elements from the given map file"""
725 """Load template elements from the given map file"""
722 if not os.path.exists(mapfile):
726 if not os.path.exists(mapfile):
723 raise error.Abort(_("style '%s' not found") % mapfile,
727 raise error.Abort(_("style '%s' not found") % mapfile,
724 hint=_("available styles: %s") % stylelist())
728 hint=_("available styles: %s") % stylelist())
725
729
726 base = os.path.dirname(mapfile)
730 base = os.path.dirname(mapfile)
727 conf = config.config(includepaths=templatepaths())
731 conf = config.config(includepaths=templatepaths())
728 conf.read(mapfile, remap={'': 'templates'})
732 conf.read(mapfile, remap={'': 'templates'})
729
733
730 cache = {}
734 cache = {}
731 tmap = {}
735 tmap = {}
732 aliases = []
736 aliases = []
733
737
734 val = conf.get('templates', '__base__')
738 val = conf.get('templates', '__base__')
735 if val and val[0] not in "'\"":
739 if val and val[0] not in "'\"":
736 # treat as a pointer to a base class for this style
740 # treat as a pointer to a base class for this style
737 path = util.normpath(os.path.join(base, val))
741 path = util.normpath(os.path.join(base, val))
738
742
739 # fallback check in template paths
743 # fallback check in template paths
740 if not os.path.exists(path):
744 if not os.path.exists(path):
741 for p in templatepaths():
745 for p in templatepaths():
742 p2 = util.normpath(os.path.join(p, val))
746 p2 = util.normpath(os.path.join(p, val))
743 if os.path.isfile(p2):
747 if os.path.isfile(p2):
744 path = p2
748 path = p2
745 break
749 break
746 p3 = util.normpath(os.path.join(p2, "map"))
750 p3 = util.normpath(os.path.join(p2, "map"))
747 if os.path.isfile(p3):
751 if os.path.isfile(p3):
748 path = p3
752 path = p3
749 break
753 break
750
754
751 cache, tmap, aliases = _readmapfile(path)
755 cache, tmap, aliases = _readmapfile(path)
752
756
753 for key, val in conf['templates'].items():
757 for key, val in conf['templates'].items():
754 if not val:
758 if not val:
755 raise error.ParseError(_('missing value'),
759 raise error.ParseError(_('missing value'),
756 conf.source('templates', key))
760 conf.source('templates', key))
757 if val[0] in "'\"":
761 if val[0] in "'\"":
758 if val[0] != val[-1]:
762 if val[0] != val[-1]:
759 raise error.ParseError(_('unmatched quotes'),
763 raise error.ParseError(_('unmatched quotes'),
760 conf.source('templates', key))
764 conf.source('templates', key))
761 cache[key] = unquotestring(val)
765 cache[key] = unquotestring(val)
762 elif key != '__base__':
766 elif key != '__base__':
763 val = 'default', val
767 val = 'default', val
764 if ':' in val[1]:
768 if ':' in val[1]:
765 val = val[1].split(':', 1)
769 val = val[1].split(':', 1)
766 tmap[key] = val[0], os.path.join(base, val[1])
770 tmap[key] = val[0], os.path.join(base, val[1])
767 aliases.extend(conf['templatealias'].items())
771 aliases.extend(conf['templatealias'].items())
768 return cache, tmap, aliases
772 return cache, tmap, aliases
769
773
770 class templater(object):
774 class templater(object):
771
775
772 def __init__(self, filters=None, defaults=None, resources=None,
776 def __init__(self, filters=None, defaults=None, resources=None,
773 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
777 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
774 """Create template engine optionally with preloaded template fragments
778 """Create template engine optionally with preloaded template fragments
775
779
776 - ``filters``: a dict of functions to transform a value into another.
780 - ``filters``: a dict of functions to transform a value into another.
777 - ``defaults``: a dict of symbol values/functions; may be overridden
781 - ``defaults``: a dict of symbol values/functions; may be overridden
778 by a ``mapping`` dict.
782 by a ``mapping`` dict.
779 - ``resources``: a resourcemapper object to look up internal data
783 - ``resources``: a resourcemapper object to look up internal data
780 (e.g. cache), inaccessible from user template.
784 (e.g. cache), inaccessible from user template.
781 - ``cache``: a dict of preloaded template fragments.
785 - ``cache``: a dict of preloaded template fragments.
782 - ``aliases``: a list of alias (name, replacement) pairs.
786 - ``aliases``: a list of alias (name, replacement) pairs.
783
787
784 self.cache may be updated later to register additional template
788 self.cache may be updated later to register additional template
785 fragments.
789 fragments.
786 """
790 """
787 if filters is None:
791 if filters is None:
788 filters = {}
792 filters = {}
789 if defaults is None:
793 if defaults is None:
790 defaults = {}
794 defaults = {}
791 if cache is None:
795 if cache is None:
792 cache = {}
796 cache = {}
793 self.cache = cache.copy()
797 self.cache = cache.copy()
794 self.map = {}
798 self.map = {}
795 self.filters = templatefilters.filters.copy()
799 self.filters = templatefilters.filters.copy()
796 self.filters.update(filters)
800 self.filters.update(filters)
797 self.defaults = defaults
801 self.defaults = defaults
798 self._resources = resources
802 self._resources = resources
799 self._aliases = aliases
803 self._aliases = aliases
800 self.minchunk, self.maxchunk = minchunk, maxchunk
804 self.minchunk, self.maxchunk = minchunk, maxchunk
801 self.ecache = {}
805 self.ecache = {}
802
806
803 @classmethod
807 @classmethod
804 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
808 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
805 cache=None, minchunk=1024, maxchunk=65536):
809 cache=None, minchunk=1024, maxchunk=65536):
806 """Create templater from the specified map file"""
810 """Create templater from the specified map file"""
807 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
811 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
808 cache, tmap, aliases = _readmapfile(mapfile)
812 cache, tmap, aliases = _readmapfile(mapfile)
809 t.cache.update(cache)
813 t.cache.update(cache)
810 t.map = tmap
814 t.map = tmap
811 t._aliases = aliases
815 t._aliases = aliases
812 return t
816 return t
813
817
814 def __contains__(self, key):
818 def __contains__(self, key):
815 return key in self.cache or key in self.map
819 return key in self.cache or key in self.map
816
820
817 def load(self, t):
821 def load(self, t):
818 '''Get the template for the given template name. Use a local cache.'''
822 '''Get the template for the given template name. Use a local cache.'''
819 if t not in self.cache:
823 if t not in self.cache:
820 try:
824 try:
821 self.cache[t] = util.readfile(self.map[t][1])
825 self.cache[t] = util.readfile(self.map[t][1])
822 except KeyError as inst:
826 except KeyError as inst:
823 raise templateutil.TemplateNotFound(
827 raise templateutil.TemplateNotFound(
824 _('"%s" not in template map') % inst.args[0])
828 _('"%s" not in template map') % inst.args[0])
825 except IOError as inst:
829 except IOError as inst:
826 reason = (_('template file %s: %s')
830 reason = (_('template file %s: %s')
827 % (self.map[t][1],
831 % (self.map[t][1],
828 stringutil.forcebytestr(inst.args[1])))
832 stringutil.forcebytestr(inst.args[1])))
829 raise IOError(inst.args[0], encoding.strfromlocal(reason))
833 raise IOError(inst.args[0], encoding.strfromlocal(reason))
830 return self.cache[t]
834 return self.cache[t]
831
835
832 def renderdefault(self, mapping):
836 def renderdefault(self, mapping):
833 """Render the default unnamed template and return result as string"""
837 """Render the default unnamed template and return result as string"""
834 return self.render('', mapping)
838 return self.render('', mapping)
835
839
836 def render(self, t, mapping):
840 def render(self, t, mapping):
837 """Render the specified named template and return result as string"""
841 """Render the specified named template and return result as string"""
838 return b''.join(self.generate(t, mapping))
842 return b''.join(self.generate(t, mapping))
839
843
840 def generate(self, t, mapping):
844 def generate(self, t, mapping):
841 """Return a generator that renders the specified named template and
845 """Return a generator that renders the specified named template and
842 yields chunks"""
846 yields chunks"""
843 ttype = t in self.map and self.map[t][0] or 'default'
847 ttype = t in self.map and self.map[t][0] or 'default'
844 if ttype not in self.ecache:
848 if ttype not in self.ecache:
845 try:
849 try:
846 ecls = engines[ttype]
850 ecls = engines[ttype]
847 except KeyError:
851 except KeyError:
848 raise error.Abort(_('invalid template engine: %s') % ttype)
852 raise error.Abort(_('invalid template engine: %s') % ttype)
849 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
853 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
850 self._resources, self._aliases)
854 self._resources, self._aliases)
851 proc = self.ecache[ttype]
855 proc = self.ecache[ttype]
852
856
853 stream = proc.process(t, mapping)
857 stream = proc.process(t, mapping)
854 if self.minchunk:
858 if self.minchunk:
855 stream = util.increasingchunks(stream, min=self.minchunk,
859 stream = util.increasingchunks(stream, min=self.minchunk,
856 max=self.maxchunk)
860 max=self.maxchunk)
857 return stream
861 return stream
858
862
859 def templatepaths():
863 def templatepaths():
860 '''return locations used for template files.'''
864 '''return locations used for template files.'''
861 pathsrel = ['templates']
865 pathsrel = ['templates']
862 paths = [os.path.normpath(os.path.join(util.datapath, f))
866 paths = [os.path.normpath(os.path.join(util.datapath, f))
863 for f in pathsrel]
867 for f in pathsrel]
864 return [p for p in paths if os.path.isdir(p)]
868 return [p for p in paths if os.path.isdir(p)]
865
869
866 def templatepath(name):
870 def templatepath(name):
867 '''return location of template file. returns None if not found.'''
871 '''return location of template file. returns None if not found.'''
868 for p in templatepaths():
872 for p in templatepaths():
869 f = os.path.join(p, name)
873 f = os.path.join(p, name)
870 if os.path.exists(f):
874 if os.path.exists(f):
871 return f
875 return f
872 return None
876 return None
873
877
874 def stylemap(styles, paths=None):
878 def stylemap(styles, paths=None):
875 """Return path to mapfile for a given style.
879 """Return path to mapfile for a given style.
876
880
877 Searches mapfile in the following locations:
881 Searches mapfile in the following locations:
878 1. templatepath/style/map
882 1. templatepath/style/map
879 2. templatepath/map-style
883 2. templatepath/map-style
880 3. templatepath/map
884 3. templatepath/map
881 """
885 """
882
886
883 if paths is None:
887 if paths is None:
884 paths = templatepaths()
888 paths = templatepaths()
885 elif isinstance(paths, bytes):
889 elif isinstance(paths, bytes):
886 paths = [paths]
890 paths = [paths]
887
891
888 if isinstance(styles, bytes):
892 if isinstance(styles, bytes):
889 styles = [styles]
893 styles = [styles]
890
894
891 for style in styles:
895 for style in styles:
892 # only plain name is allowed to honor template paths
896 # only plain name is allowed to honor template paths
893 if (not style
897 if (not style
894 or style in (pycompat.oscurdir, pycompat.ospardir)
898 or style in (pycompat.oscurdir, pycompat.ospardir)
895 or pycompat.ossep in style
899 or pycompat.ossep in style
896 or pycompat.osaltsep and pycompat.osaltsep in style):
900 or pycompat.osaltsep and pycompat.osaltsep in style):
897 continue
901 continue
898 locations = [os.path.join(style, 'map'), 'map-' + style]
902 locations = [os.path.join(style, 'map'), 'map-' + style]
899 locations.append('map')
903 locations.append('map')
900
904
901 for path in paths:
905 for path in paths:
902 for location in locations:
906 for location in locations:
903 mapfile = os.path.join(path, location)
907 mapfile = os.path.join(path, location)
904 if os.path.isfile(mapfile):
908 if os.path.isfile(mapfile):
905 return style, mapfile
909 return style, mapfile
906
910
907 raise RuntimeError("No hgweb templates found in %r" % paths)
911 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,584 +1,649 b''
1 # templateutil.py - utility for template evaluation
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
8 from __future__ import absolute_import
9
9
10 import abc
10 import abc
11 import types
11 import types
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 )
18 )
19 from .utils import (
19 from .utils import (
20 dateutil,
20 dateutil,
21 stringutil,
21 stringutil,
22 )
22 )
23
23
24 class ResourceUnavailable(error.Abort):
24 class ResourceUnavailable(error.Abort):
25 pass
25 pass
26
26
27 class TemplateNotFound(error.Abort):
27 class TemplateNotFound(error.Abort):
28 pass
28 pass
29
29
30 class wrapped(object):
30 class wrapped(object):
31 """Object requiring extra conversion prior to displaying or processing
31 """Object requiring extra conversion prior to displaying or processing
32 as value
32 as value
33
33
34 Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain the inner
34 Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain the inner
35 object.
35 object.
36 """
36 """
37
37
38 __metaclass__ = abc.ABCMeta
38 __metaclass__ = abc.ABCMeta
39
39
40 @abc.abstractmethod
40 @abc.abstractmethod
41 def itermaps(self, context):
41 def itermaps(self, context):
42 """Yield each template mapping"""
42 """Yield each template mapping"""
43
43
44 @abc.abstractmethod
44 @abc.abstractmethod
45 def join(self, context, mapping, sep):
45 def join(self, context, mapping, sep):
46 """Join items with the separator; Returns a bytes or (possibly nested)
46 """Join items with the separator; Returns a bytes or (possibly nested)
47 generator of bytes
47 generator of bytes
48
48
49 A pre-configured template may be rendered per item if this container
49 A pre-configured template may be rendered per item if this container
50 holds unprintable items.
50 holds unprintable items.
51 """
51 """
52
52
53 @abc.abstractmethod
53 @abc.abstractmethod
54 def show(self, context, mapping):
54 def show(self, context, mapping):
55 """Return a bytes or (possibly nested) generator of bytes representing
55 """Return a bytes or (possibly nested) generator of bytes representing
56 the underlying object
56 the underlying object
57
57
58 A pre-configured template may be rendered if the underlying object is
58 A pre-configured template may be rendered if the underlying object is
59 not printable.
59 not printable.
60 """
60 """
61
61
62 @abc.abstractmethod
62 @abc.abstractmethod
63 def tovalue(self, context, mapping):
63 def tovalue(self, context, mapping):
64 """Move the inner value object out or create a value representation
64 """Move the inner value object out or create a value representation
65
65
66 A returned value must be serializable by templaterfilters.json().
66 A returned value must be serializable by templaterfilters.json().
67 """
67 """
68
68
69 # stub for representing a date type; may be a real date type that can
69 # stub for representing a date type; may be a real date type that can
70 # provide a readable string value
70 # provide a readable string value
71 class date(object):
71 class date(object):
72 pass
72 pass
73
73
74 class hybrid(wrapped):
74 class hybrid(wrapped):
75 """Wrapper for list or dict to support legacy template
75 """Wrapper for list or dict to support legacy template
76
76
77 This class allows us to handle both:
77 This class allows us to handle both:
78 - "{files}" (legacy command-line-specific list hack) and
78 - "{files}" (legacy command-line-specific list hack) and
79 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
79 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
80 and to access raw values:
80 and to access raw values:
81 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
81 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
82 - "{get(extras, key)}"
82 - "{get(extras, key)}"
83 - "{files|json}"
83 - "{files|json}"
84 """
84 """
85
85
86 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
86 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
87 self._gen = gen # generator or function returning generator
87 self._gen = gen # generator or function returning generator
88 self._values = values
88 self._values = values
89 self._makemap = makemap
89 self._makemap = makemap
90 self._joinfmt = joinfmt
90 self._joinfmt = joinfmt
91 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
91 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
92
92
93 def itermaps(self, context):
93 def itermaps(self, context):
94 makemap = self._makemap
94 makemap = self._makemap
95 for x in self._values:
95 for x in self._values:
96 yield makemap(x)
96 yield makemap(x)
97
97
98 def join(self, context, mapping, sep):
98 def join(self, context, mapping, sep):
99 # TODO: switch gen to (context, mapping) API?
99 # TODO: switch gen to (context, mapping) API?
100 return joinitems((self._joinfmt(x) for x in self._values), sep)
100 return joinitems((self._joinfmt(x) for x in self._values), sep)
101
101
102 def show(self, context, mapping):
102 def show(self, context, mapping):
103 # TODO: switch gen to (context, mapping) API?
103 # TODO: switch gen to (context, mapping) API?
104 gen = self._gen
104 gen = self._gen
105 if gen is None:
105 if gen is None:
106 return self.join(context, mapping, ' ')
106 return self.join(context, mapping, ' ')
107 if callable(gen):
107 if callable(gen):
108 return gen()
108 return gen()
109 return gen
109 return gen
110
110
111 def tovalue(self, context, mapping):
111 def tovalue(self, context, mapping):
112 # TODO: return self._values and get rid of proxy methods
112 # TODO: return self._values and get rid of proxy methods
113 return self
113 return self
114
114
115 def __contains__(self, x):
115 def __contains__(self, x):
116 return x in self._values
116 return x in self._values
117 def __getitem__(self, key):
117 def __getitem__(self, key):
118 return self._values[key]
118 return self._values[key]
119 def __len__(self):
119 def __len__(self):
120 return len(self._values)
120 return len(self._values)
121 def __iter__(self):
121 def __iter__(self):
122 return iter(self._values)
122 return iter(self._values)
123 def __getattr__(self, name):
123 def __getattr__(self, name):
124 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
124 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
125 r'itervalues', r'keys', r'values'):
125 r'itervalues', r'keys', r'values'):
126 raise AttributeError(name)
126 raise AttributeError(name)
127 return getattr(self._values, name)
127 return getattr(self._values, name)
128
128
129 class mappable(wrapped):
129 class mappable(wrapped):
130 """Wrapper for non-list/dict object to support map operation
130 """Wrapper for non-list/dict object to support map operation
131
131
132 This class allows us to handle both:
132 This class allows us to handle both:
133 - "{manifest}"
133 - "{manifest}"
134 - "{manifest % '{rev}:{node}'}"
134 - "{manifest % '{rev}:{node}'}"
135 - "{manifest.rev}"
135 - "{manifest.rev}"
136
136
137 Unlike a hybrid, this does not simulate the behavior of the underling
137 Unlike a hybrid, this does not simulate the behavior of the underling
138 value.
138 value.
139 """
139 """
140
140
141 def __init__(self, gen, key, value, makemap):
141 def __init__(self, gen, key, value, makemap):
142 self._gen = gen # generator or function returning generator
142 self._gen = gen # generator or function returning generator
143 self._key = key
143 self._key = key
144 self._value = value # may be generator of strings
144 self._value = value # may be generator of strings
145 self._makemap = makemap
145 self._makemap = makemap
146
146
147 def tomap(self):
147 def tomap(self):
148 return self._makemap(self._key)
148 return self._makemap(self._key)
149
149
150 def itermaps(self, context):
150 def itermaps(self, context):
151 yield self.tomap()
151 yield self.tomap()
152
152
153 def join(self, context, mapping, sep):
153 def join(self, context, mapping, sep):
154 # TODO: just copies the old behavior where a value was a generator
154 # TODO: just copies the old behavior where a value was a generator
155 # yielding one item, but reconsider about it. join() over a string
155 # yielding one item, but reconsider about it. join() over a string
156 # has no consistent result because a string may be a bytes, or a
156 # has no consistent result because a string may be a bytes, or a
157 # generator yielding an item, or a generator yielding multiple items.
157 # generator yielding an item, or a generator yielding multiple items.
158 # Preserving all of the current behaviors wouldn't make any sense.
158 # Preserving all of the current behaviors wouldn't make any sense.
159 return self.show(context, mapping)
159 return self.show(context, mapping)
160
160
161 def show(self, context, mapping):
161 def show(self, context, mapping):
162 # TODO: switch gen to (context, mapping) API?
162 # TODO: switch gen to (context, mapping) API?
163 gen = self._gen
163 gen = self._gen
164 if gen is None:
164 if gen is None:
165 return pycompat.bytestr(self._value)
165 return pycompat.bytestr(self._value)
166 if callable(gen):
166 if callable(gen):
167 return gen()
167 return gen()
168 return gen
168 return gen
169
169
170 def tovalue(self, context, mapping):
170 def tovalue(self, context, mapping):
171 return _unthunk(context, mapping, self._value)
171 return _unthunk(context, mapping, self._value)
172
172
173 class _mappingsequence(wrapped):
174 """Wrapper for sequence of template mappings
175
176 This represents an inner template structure (i.e. a list of dicts),
177 which can also be rendered by the specified named/literal template.
178
179 Template mappings may be nested.
180 """
181
182 def __init__(self, name=None, tmpl=None, sep=''):
183 if name is not None and tmpl is not None:
184 raise error.ProgrammingError('name and tmpl are mutually exclusive')
185 self._name = name
186 self._tmpl = tmpl
187 self._defaultsep = sep
188
189 def join(self, context, mapping, sep):
190 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
191 if self._name:
192 itemiter = (context.process(self._name, m) for m in mapsiter)
193 elif self._tmpl:
194 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
195 else:
196 raise error.ParseError(_('not displayable without template'))
197 return joinitems(itemiter, sep)
198
199 def show(self, context, mapping):
200 return self.join(context, mapping, self._defaultsep)
201
202 def tovalue(self, context, mapping):
203 return list(self.itermaps(context))
204
205 class mappinggenerator(_mappingsequence):
206 """Wrapper for generator of template mappings
207
208 The function ``make(context, *args)`` should return a generator of
209 mapping dicts.
210 """
211
212 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
213 super(mappinggenerator, self).__init__(name, tmpl, sep)
214 self._make = make
215 self._args = args
216
217 def itermaps(self, context):
218 return self._make(context, *self._args)
219
220 class mappinglist(_mappingsequence):
221 """Wrapper for list of template mappings"""
222
223 def __init__(self, mappings, name=None, tmpl=None, sep=''):
224 super(mappinglist, self).__init__(name, tmpl, sep)
225 self._mappings = mappings
226
227 def itermaps(self, context):
228 return iter(self._mappings)
229
173 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
230 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
174 """Wrap data to support both dict-like and string-like operations"""
231 """Wrap data to support both dict-like and string-like operations"""
175 prefmt = pycompat.identity
232 prefmt = pycompat.identity
176 if fmt is None:
233 if fmt is None:
177 fmt = '%s=%s'
234 fmt = '%s=%s'
178 prefmt = pycompat.bytestr
235 prefmt = pycompat.bytestr
179 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
236 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
180 lambda k: fmt % (prefmt(k), prefmt(data[k])))
237 lambda k: fmt % (prefmt(k), prefmt(data[k])))
181
238
182 def hybridlist(data, name, fmt=None, gen=None):
239 def hybridlist(data, name, fmt=None, gen=None):
183 """Wrap data to support both list-like and string-like operations"""
240 """Wrap data to support both list-like and string-like operations"""
184 prefmt = pycompat.identity
241 prefmt = pycompat.identity
185 if fmt is None:
242 if fmt is None:
186 fmt = '%s'
243 fmt = '%s'
187 prefmt = pycompat.bytestr
244 prefmt = pycompat.bytestr
188 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
245 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
189
246
190 def unwraphybrid(context, mapping, thing):
247 def unwraphybrid(context, mapping, thing):
191 """Return an object which can be stringified possibly by using a legacy
248 """Return an object which can be stringified possibly by using a legacy
192 template"""
249 template"""
193 if not isinstance(thing, wrapped):
250 if not isinstance(thing, wrapped):
194 return thing
251 return thing
195 return thing.show(context, mapping)
252 return thing.show(context, mapping)
196
253
197 def unwrapvalue(context, mapping, thing):
254 def unwrapvalue(context, mapping, thing):
198 """Move the inner value object out of the wrapper"""
255 """Move the inner value object out of the wrapper"""
199 if not isinstance(thing, wrapped):
256 if not isinstance(thing, wrapped):
200 return thing
257 return thing
201 return thing.tovalue(context, mapping)
258 return thing.tovalue(context, mapping)
202
259
203 def wraphybridvalue(container, key, value):
260 def wraphybridvalue(container, key, value):
204 """Wrap an element of hybrid container to be mappable
261 """Wrap an element of hybrid container to be mappable
205
262
206 The key is passed to the makemap function of the given container, which
263 The key is passed to the makemap function of the given container, which
207 should be an item generated by iter(container).
264 should be an item generated by iter(container).
208 """
265 """
209 makemap = getattr(container, '_makemap', None)
266 makemap = getattr(container, '_makemap', None)
210 if makemap is None:
267 if makemap is None:
211 return value
268 return value
212 if util.safehasattr(value, '_makemap'):
269 if util.safehasattr(value, '_makemap'):
213 # a nested hybrid list/dict, which has its own way of map operation
270 # a nested hybrid list/dict, which has its own way of map operation
214 return value
271 return value
215 return mappable(None, key, value, makemap)
272 return mappable(None, key, value, makemap)
216
273
217 def compatdict(context, mapping, name, data, key='key', value='value',
274 def compatdict(context, mapping, name, data, key='key', value='value',
218 fmt=None, plural=None, separator=' '):
275 fmt=None, plural=None, separator=' '):
219 """Wrap data like hybriddict(), but also supports old-style list template
276 """Wrap data like hybriddict(), but also supports old-style list template
220
277
221 This exists for backward compatibility with the old-style template. Use
278 This exists for backward compatibility with the old-style template. Use
222 hybriddict() for new template keywords.
279 hybriddict() for new template keywords.
223 """
280 """
224 c = [{key: k, value: v} for k, v in data.iteritems()]
281 c = [{key: k, value: v} for k, v in data.iteritems()]
225 f = _showcompatlist(context, mapping, name, c, plural, separator)
282 f = _showcompatlist(context, mapping, name, c, plural, separator)
226 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
283 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
227
284
228 def compatlist(context, mapping, name, data, element=None, fmt=None,
285 def compatlist(context, mapping, name, data, element=None, fmt=None,
229 plural=None, separator=' '):
286 plural=None, separator=' '):
230 """Wrap data like hybridlist(), but also supports old-style list template
287 """Wrap data like hybridlist(), but also supports old-style list template
231
288
232 This exists for backward compatibility with the old-style template. Use
289 This exists for backward compatibility with the old-style template. Use
233 hybridlist() for new template keywords.
290 hybridlist() for new template keywords.
234 """
291 """
235 f = _showcompatlist(context, mapping, name, data, plural, separator)
292 f = _showcompatlist(context, mapping, name, data, plural, separator)
236 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
293 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
237
294
238 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
295 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
239 """Return a generator that renders old-style list template
296 """Return a generator that renders old-style list template
240
297
241 name is name of key in template map.
298 name is name of key in template map.
242 values is list of strings or dicts.
299 values is list of strings or dicts.
243 plural is plural of name, if not simply name + 's'.
300 plural is plural of name, if not simply name + 's'.
244 separator is used to join values as a string
301 separator is used to join values as a string
245
302
246 expansion works like this, given name 'foo'.
303 expansion works like this, given name 'foo'.
247
304
248 if values is empty, expand 'no_foos'.
305 if values is empty, expand 'no_foos'.
249
306
250 if 'foo' not in template map, return values as a string,
307 if 'foo' not in template map, return values as a string,
251 joined by 'separator'.
308 joined by 'separator'.
252
309
253 expand 'start_foos'.
310 expand 'start_foos'.
254
311
255 for each value, expand 'foo'. if 'last_foo' in template
312 for each value, expand 'foo'. if 'last_foo' in template
256 map, expand it instead of 'foo' for last key.
313 map, expand it instead of 'foo' for last key.
257
314
258 expand 'end_foos'.
315 expand 'end_foos'.
259 """
316 """
260 if not plural:
317 if not plural:
261 plural = name + 's'
318 plural = name + 's'
262 if not values:
319 if not values:
263 noname = 'no_' + plural
320 noname = 'no_' + plural
264 if context.preload(noname):
321 if context.preload(noname):
265 yield context.process(noname, mapping)
322 yield context.process(noname, mapping)
266 return
323 return
267 if not context.preload(name):
324 if not context.preload(name):
268 if isinstance(values[0], bytes):
325 if isinstance(values[0], bytes):
269 yield separator.join(values)
326 yield separator.join(values)
270 else:
327 else:
271 for v in values:
328 for v in values:
272 r = dict(v)
329 r = dict(v)
273 r.update(mapping)
330 r.update(mapping)
274 yield r
331 yield r
275 return
332 return
276 startname = 'start_' + plural
333 startname = 'start_' + plural
277 if context.preload(startname):
334 if context.preload(startname):
278 yield context.process(startname, mapping)
335 yield context.process(startname, mapping)
279 def one(v, tag=name):
336 def one(v, tag=name):
280 vmapping = {}
337 vmapping = {}
281 try:
338 try:
282 vmapping.update(v)
339 vmapping.update(v)
283 # Python 2 raises ValueError if the type of v is wrong. Python
340 # Python 2 raises ValueError if the type of v is wrong. Python
284 # 3 raises TypeError.
341 # 3 raises TypeError.
285 except (AttributeError, TypeError, ValueError):
342 except (AttributeError, TypeError, ValueError):
286 try:
343 try:
287 # Python 2 raises ValueError trying to destructure an e.g.
344 # Python 2 raises ValueError trying to destructure an e.g.
288 # bytes. Python 3 raises TypeError.
345 # bytes. Python 3 raises TypeError.
289 for a, b in v:
346 for a, b in v:
290 vmapping[a] = b
347 vmapping[a] = b
291 except (TypeError, ValueError):
348 except (TypeError, ValueError):
292 vmapping[name] = v
349 vmapping[name] = v
293 vmapping = context.overlaymap(mapping, vmapping)
350 vmapping = context.overlaymap(mapping, vmapping)
294 return context.process(tag, vmapping)
351 return context.process(tag, vmapping)
295 lastname = 'last_' + name
352 lastname = 'last_' + name
296 if context.preload(lastname):
353 if context.preload(lastname):
297 last = values.pop()
354 last = values.pop()
298 else:
355 else:
299 last = None
356 last = None
300 for v in values:
357 for v in values:
301 yield one(v)
358 yield one(v)
302 if last is not None:
359 if last is not None:
303 yield one(last, tag=lastname)
360 yield one(last, tag=lastname)
304 endname = 'end_' + plural
361 endname = 'end_' + plural
305 if context.preload(endname):
362 if context.preload(endname):
306 yield context.process(endname, mapping)
363 yield context.process(endname, mapping)
307
364
308 def flatten(context, mapping, thing):
365 def flatten(context, mapping, thing):
309 """Yield a single stream from a possibly nested set of iterators"""
366 """Yield a single stream from a possibly nested set of iterators"""
310 thing = unwraphybrid(context, mapping, thing)
367 thing = unwraphybrid(context, mapping, thing)
311 if isinstance(thing, bytes):
368 if isinstance(thing, bytes):
312 yield thing
369 yield thing
313 elif isinstance(thing, str):
370 elif isinstance(thing, str):
314 # We can only hit this on Python 3, and it's here to guard
371 # We can only hit this on Python 3, and it's here to guard
315 # against infinite recursion.
372 # against infinite recursion.
316 raise error.ProgrammingError('Mercurial IO including templates is done'
373 raise error.ProgrammingError('Mercurial IO including templates is done'
317 ' with bytes, not strings, got %r' % thing)
374 ' with bytes, not strings, got %r' % thing)
318 elif thing is None:
375 elif thing is None:
319 pass
376 pass
320 elif not util.safehasattr(thing, '__iter__'):
377 elif not util.safehasattr(thing, '__iter__'):
321 yield pycompat.bytestr(thing)
378 yield pycompat.bytestr(thing)
322 else:
379 else:
323 for i in thing:
380 for i in thing:
324 i = unwraphybrid(context, mapping, i)
381 i = unwraphybrid(context, mapping, i)
325 if isinstance(i, bytes):
382 if isinstance(i, bytes):
326 yield i
383 yield i
327 elif i is None:
384 elif i is None:
328 pass
385 pass
329 elif not util.safehasattr(i, '__iter__'):
386 elif not util.safehasattr(i, '__iter__'):
330 yield pycompat.bytestr(i)
387 yield pycompat.bytestr(i)
331 else:
388 else:
332 for j in flatten(context, mapping, i):
389 for j in flatten(context, mapping, i):
333 yield j
390 yield j
334
391
335 def stringify(context, mapping, thing):
392 def stringify(context, mapping, thing):
336 """Turn values into bytes by converting into text and concatenating them"""
393 """Turn values into bytes by converting into text and concatenating them"""
337 if isinstance(thing, bytes):
394 if isinstance(thing, bytes):
338 return thing # retain localstr to be round-tripped
395 return thing # retain localstr to be round-tripped
339 return b''.join(flatten(context, mapping, thing))
396 return b''.join(flatten(context, mapping, thing))
340
397
341 def findsymbolicname(arg):
398 def findsymbolicname(arg):
342 """Find symbolic name for the given compiled expression; returns None
399 """Find symbolic name for the given compiled expression; returns None
343 if nothing found reliably"""
400 if nothing found reliably"""
344 while True:
401 while True:
345 func, data = arg
402 func, data = arg
346 if func is runsymbol:
403 if func is runsymbol:
347 return data
404 return data
348 elif func is runfilter:
405 elif func is runfilter:
349 arg = data[0]
406 arg = data[0]
350 else:
407 else:
351 return None
408 return None
352
409
353 def _unthunk(context, mapping, thing):
410 def _unthunk(context, mapping, thing):
354 """Evaluate a lazy byte string into value"""
411 """Evaluate a lazy byte string into value"""
355 if not isinstance(thing, types.GeneratorType):
412 if not isinstance(thing, types.GeneratorType):
356 return thing
413 return thing
357 return stringify(context, mapping, thing)
414 return stringify(context, mapping, thing)
358
415
359 def evalrawexp(context, mapping, arg):
416 def evalrawexp(context, mapping, arg):
360 """Evaluate given argument as a bare template object which may require
417 """Evaluate given argument as a bare template object which may require
361 further processing (such as folding generator of strings)"""
418 further processing (such as folding generator of strings)"""
362 func, data = arg
419 func, data = arg
363 return func(context, mapping, data)
420 return func(context, mapping, data)
364
421
365 def evalfuncarg(context, mapping, arg):
422 def evalfuncarg(context, mapping, arg):
366 """Evaluate given argument as value type"""
423 """Evaluate given argument as value type"""
367 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
424 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
368
425
369 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
426 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
370 # is fixed. we can't do that right now because join() has to take a generator
427 # is fixed. we can't do that right now because join() has to take a generator
371 # of byte strings as it is, not a lazy byte string.
428 # of byte strings as it is, not a lazy byte string.
372 def _unwrapvalue(context, mapping, thing):
429 def _unwrapvalue(context, mapping, thing):
373 thing = unwrapvalue(context, mapping, thing)
430 thing = unwrapvalue(context, mapping, thing)
374 # evalrawexp() may return string, generator of strings or arbitrary object
431 # evalrawexp() may return string, generator of strings or arbitrary object
375 # such as date tuple, but filter does not want generator.
432 # such as date tuple, but filter does not want generator.
376 return _unthunk(context, mapping, thing)
433 return _unthunk(context, mapping, thing)
377
434
378 def evalboolean(context, mapping, arg):
435 def evalboolean(context, mapping, arg):
379 """Evaluate given argument as boolean, but also takes boolean literals"""
436 """Evaluate given argument as boolean, but also takes boolean literals"""
380 func, data = arg
437 func, data = arg
381 if func is runsymbol:
438 if func is runsymbol:
382 thing = func(context, mapping, data, default=None)
439 thing = func(context, mapping, data, default=None)
383 if thing is None:
440 if thing is None:
384 # not a template keyword, takes as a boolean literal
441 # not a template keyword, takes as a boolean literal
385 thing = stringutil.parsebool(data)
442 thing = stringutil.parsebool(data)
386 else:
443 else:
387 thing = func(context, mapping, data)
444 thing = func(context, mapping, data)
388 thing = unwrapvalue(context, mapping, thing)
445 thing = unwrapvalue(context, mapping, thing)
389 if isinstance(thing, bool):
446 if isinstance(thing, bool):
390 return thing
447 return thing
391 # other objects are evaluated as strings, which means 0 is True, but
448 # other objects are evaluated as strings, which means 0 is True, but
392 # empty dict/list should be False as they are expected to be ''
449 # empty dict/list should be False as they are expected to be ''
393 return bool(stringify(context, mapping, thing))
450 return bool(stringify(context, mapping, thing))
394
451
395 def evaldate(context, mapping, arg, err=None):
452 def evaldate(context, mapping, arg, err=None):
396 """Evaluate given argument as a date tuple or a date string; returns
453 """Evaluate given argument as a date tuple or a date string; returns
397 a (unixtime, offset) tuple"""
454 a (unixtime, offset) tuple"""
398 thing = evalrawexp(context, mapping, arg)
455 thing = evalrawexp(context, mapping, arg)
399 return unwrapdate(context, mapping, thing, err)
456 return unwrapdate(context, mapping, thing, err)
400
457
401 def unwrapdate(context, mapping, thing, err=None):
458 def unwrapdate(context, mapping, thing, err=None):
402 thing = _unwrapvalue(context, mapping, thing)
459 thing = _unwrapvalue(context, mapping, thing)
403 try:
460 try:
404 return dateutil.parsedate(thing)
461 return dateutil.parsedate(thing)
405 except AttributeError:
462 except AttributeError:
406 raise error.ParseError(err or _('not a date tuple nor a string'))
463 raise error.ParseError(err or _('not a date tuple nor a string'))
407 except error.ParseError:
464 except error.ParseError:
408 if not err:
465 if not err:
409 raise
466 raise
410 raise error.ParseError(err)
467 raise error.ParseError(err)
411
468
412 def evalinteger(context, mapping, arg, err=None):
469 def evalinteger(context, mapping, arg, err=None):
413 thing = evalrawexp(context, mapping, arg)
470 thing = evalrawexp(context, mapping, arg)
414 return unwrapinteger(context, mapping, thing, err)
471 return unwrapinteger(context, mapping, thing, err)
415
472
416 def unwrapinteger(context, mapping, thing, err=None):
473 def unwrapinteger(context, mapping, thing, err=None):
417 thing = _unwrapvalue(context, mapping, thing)
474 thing = _unwrapvalue(context, mapping, thing)
418 try:
475 try:
419 return int(thing)
476 return int(thing)
420 except (TypeError, ValueError):
477 except (TypeError, ValueError):
421 raise error.ParseError(err or _('not an integer'))
478 raise error.ParseError(err or _('not an integer'))
422
479
423 def evalstring(context, mapping, arg):
480 def evalstring(context, mapping, arg):
424 return stringify(context, mapping, evalrawexp(context, mapping, arg))
481 return stringify(context, mapping, evalrawexp(context, mapping, arg))
425
482
426 def evalstringliteral(context, mapping, arg):
483 def evalstringliteral(context, mapping, arg):
427 """Evaluate given argument as string template, but returns symbol name
484 """Evaluate given argument as string template, but returns symbol name
428 if it is unknown"""
485 if it is unknown"""
429 func, data = arg
486 func, data = arg
430 if func is runsymbol:
487 if func is runsymbol:
431 thing = func(context, mapping, data, default=data)
488 thing = func(context, mapping, data, default=data)
432 else:
489 else:
433 thing = func(context, mapping, data)
490 thing = func(context, mapping, data)
434 return stringify(context, mapping, thing)
491 return stringify(context, mapping, thing)
435
492
436 _unwrapfuncbytype = {
493 _unwrapfuncbytype = {
437 None: _unwrapvalue,
494 None: _unwrapvalue,
438 bytes: stringify,
495 bytes: stringify,
439 date: unwrapdate,
496 date: unwrapdate,
440 int: unwrapinteger,
497 int: unwrapinteger,
441 }
498 }
442
499
443 def unwrapastype(context, mapping, thing, typ):
500 def unwrapastype(context, mapping, thing, typ):
444 """Move the inner value object out of the wrapper and coerce its type"""
501 """Move the inner value object out of the wrapper and coerce its type"""
445 try:
502 try:
446 f = _unwrapfuncbytype[typ]
503 f = _unwrapfuncbytype[typ]
447 except KeyError:
504 except KeyError:
448 raise error.ProgrammingError('invalid type specified: %r' % typ)
505 raise error.ProgrammingError('invalid type specified: %r' % typ)
449 return f(context, mapping, thing)
506 return f(context, mapping, thing)
450
507
451 def runinteger(context, mapping, data):
508 def runinteger(context, mapping, data):
452 return int(data)
509 return int(data)
453
510
454 def runstring(context, mapping, data):
511 def runstring(context, mapping, data):
455 return data
512 return data
456
513
457 def _recursivesymbolblocker(key):
514 def _recursivesymbolblocker(key):
458 def showrecursion(**args):
515 def showrecursion(**args):
459 raise error.Abort(_("recursive reference '%s' in template") % key)
516 raise error.Abort(_("recursive reference '%s' in template") % key)
460 return showrecursion
517 return showrecursion
461
518
462 def runsymbol(context, mapping, key, default=''):
519 def runsymbol(context, mapping, key, default=''):
463 v = context.symbol(mapping, key)
520 v = context.symbol(mapping, key)
464 if v is None:
521 if v is None:
465 # put poison to cut recursion. we can't move this to parsing phase
522 # put poison to cut recursion. we can't move this to parsing phase
466 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
523 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
467 safemapping = mapping.copy()
524 safemapping = mapping.copy()
468 safemapping[key] = _recursivesymbolblocker(key)
525 safemapping[key] = _recursivesymbolblocker(key)
469 try:
526 try:
470 v = context.process(key, safemapping)
527 v = context.process(key, safemapping)
471 except TemplateNotFound:
528 except TemplateNotFound:
472 v = default
529 v = default
473 if callable(v) and getattr(v, '_requires', None) is None:
530 if callable(v) and getattr(v, '_requires', None) is None:
474 # old templatekw: expand all keywords and resources
531 # old templatekw: expand all keywords and resources
475 # (TODO: deprecate this after porting web template keywords to new API)
532 # (TODO: deprecate this after porting web template keywords to new API)
476 props = {k: context._resources.lookup(context, mapping, k)
533 props = {k: context._resources.lookup(context, mapping, k)
477 for k in context._resources.knownkeys()}
534 for k in context._resources.knownkeys()}
478 # pass context to _showcompatlist() through templatekw._showlist()
535 # pass context to _showcompatlist() through templatekw._showlist()
479 props['templ'] = context
536 props['templ'] = context
480 props.update(mapping)
537 props.update(mapping)
481 return v(**pycompat.strkwargs(props))
538 return v(**pycompat.strkwargs(props))
482 if callable(v):
539 if callable(v):
483 # new templatekw
540 # new templatekw
484 try:
541 try:
485 return v(context, mapping)
542 return v(context, mapping)
486 except ResourceUnavailable:
543 except ResourceUnavailable:
487 # unsupported keyword is mapped to empty just like unknown keyword
544 # unsupported keyword is mapped to empty just like unknown keyword
488 return None
545 return None
489 return v
546 return v
490
547
491 def runtemplate(context, mapping, template):
548 def runtemplate(context, mapping, template):
492 for arg in template:
549 for arg in template:
493 yield evalrawexp(context, mapping, arg)
550 yield evalrawexp(context, mapping, arg)
494
551
495 def runfilter(context, mapping, data):
552 def runfilter(context, mapping, data):
496 arg, filt = data
553 arg, filt = data
497 thing = evalrawexp(context, mapping, arg)
554 thing = evalrawexp(context, mapping, arg)
498 intype = getattr(filt, '_intype', None)
555 intype = getattr(filt, '_intype', None)
499 try:
556 try:
500 thing = unwrapastype(context, mapping, thing, intype)
557 thing = unwrapastype(context, mapping, thing, intype)
501 return filt(thing)
558 return filt(thing)
502 except error.ParseError as e:
559 except error.ParseError as e:
503 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
560 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
504
561
505 def _formatfiltererror(arg, filt):
562 def _formatfiltererror(arg, filt):
506 fn = pycompat.sysbytes(filt.__name__)
563 fn = pycompat.sysbytes(filt.__name__)
507 sym = findsymbolicname(arg)
564 sym = findsymbolicname(arg)
508 if not sym:
565 if not sym:
509 return _("incompatible use of template filter '%s'") % fn
566 return _("incompatible use of template filter '%s'") % fn
510 return (_("template filter '%s' is not compatible with keyword '%s'")
567 return (_("template filter '%s' is not compatible with keyword '%s'")
511 % (fn, sym))
568 % (fn, sym))
512
569
570 def _iteroverlaymaps(context, origmapping, newmappings):
571 """Generate combined mappings from the original mapping and an iterable
572 of partial mappings to override the original"""
573 for i, nm in enumerate(newmappings):
574 lm = context.overlaymap(origmapping, nm)
575 lm['index'] = i
576 yield lm
577
513 def runmap(context, mapping, data):
578 def runmap(context, mapping, data):
514 darg, targ = data
579 darg, targ = data
515 d = evalrawexp(context, mapping, darg)
580 d = evalrawexp(context, mapping, darg)
516 if isinstance(d, wrapped):
581 if isinstance(d, wrapped):
517 diter = d.itermaps(context)
582 diter = d.itermaps(context)
518 else:
583 else:
519 try:
584 try:
520 diter = iter(d)
585 diter = iter(d)
521 except TypeError:
586 except TypeError:
522 sym = findsymbolicname(darg)
587 sym = findsymbolicname(darg)
523 if sym:
588 if sym:
524 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
589 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
525 else:
590 else:
526 raise error.ParseError(_("%r is not iterable") % d)
591 raise error.ParseError(_("%r is not iterable") % d)
527
592
528 for i, v in enumerate(diter):
593 for i, v in enumerate(diter):
529 if isinstance(v, dict):
594 if isinstance(v, dict):
530 lm = context.overlaymap(mapping, v)
595 lm = context.overlaymap(mapping, v)
531 lm['index'] = i
596 lm['index'] = i
532 yield evalrawexp(context, lm, targ)
597 yield evalrawexp(context, lm, targ)
533 else:
598 else:
534 # v is not an iterable of dicts, this happen when 'key'
599 # v is not an iterable of dicts, this happen when 'key'
535 # has been fully expanded already and format is useless.
600 # has been fully expanded already and format is useless.
536 # If so, return the expanded value.
601 # If so, return the expanded value.
537 yield v
602 yield v
538
603
539 def runmember(context, mapping, data):
604 def runmember(context, mapping, data):
540 darg, memb = data
605 darg, memb = data
541 d = evalrawexp(context, mapping, darg)
606 d = evalrawexp(context, mapping, darg)
542 if util.safehasattr(d, 'tomap'):
607 if util.safehasattr(d, 'tomap'):
543 lm = context.overlaymap(mapping, d.tomap())
608 lm = context.overlaymap(mapping, d.tomap())
544 return runsymbol(context, lm, memb)
609 return runsymbol(context, lm, memb)
545 if util.safehasattr(d, 'get'):
610 if util.safehasattr(d, 'get'):
546 return getdictitem(d, memb)
611 return getdictitem(d, memb)
547
612
548 sym = findsymbolicname(darg)
613 sym = findsymbolicname(darg)
549 if sym:
614 if sym:
550 raise error.ParseError(_("keyword '%s' has no member") % sym)
615 raise error.ParseError(_("keyword '%s' has no member") % sym)
551 else:
616 else:
552 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
617 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
553
618
554 def runnegate(context, mapping, data):
619 def runnegate(context, mapping, data):
555 data = evalinteger(context, mapping, data,
620 data = evalinteger(context, mapping, data,
556 _('negation needs an integer argument'))
621 _('negation needs an integer argument'))
557 return -data
622 return -data
558
623
559 def runarithmetic(context, mapping, data):
624 def runarithmetic(context, mapping, data):
560 func, left, right = data
625 func, left, right = data
561 left = evalinteger(context, mapping, left,
626 left = evalinteger(context, mapping, left,
562 _('arithmetic only defined on integers'))
627 _('arithmetic only defined on integers'))
563 right = evalinteger(context, mapping, right,
628 right = evalinteger(context, mapping, right,
564 _('arithmetic only defined on integers'))
629 _('arithmetic only defined on integers'))
565 try:
630 try:
566 return func(left, right)
631 return func(left, right)
567 except ZeroDivisionError:
632 except ZeroDivisionError:
568 raise error.Abort(_('division by zero is not defined'))
633 raise error.Abort(_('division by zero is not defined'))
569
634
570 def getdictitem(dictarg, key):
635 def getdictitem(dictarg, key):
571 val = dictarg.get(key)
636 val = dictarg.get(key)
572 if val is None:
637 if val is None:
573 return
638 return
574 return wraphybridvalue(dictarg, key, val)
639 return wraphybridvalue(dictarg, key, val)
575
640
576 def joinitems(itemiter, sep):
641 def joinitems(itemiter, sep):
577 """Join items with the separator; Returns generator of bytes"""
642 """Join items with the separator; Returns generator of bytes"""
578 first = True
643 first = True
579 for x in itemiter:
644 for x in itemiter:
580 if first:
645 if first:
581 first = False
646 first = False
582 elif sep:
647 elif sep:
583 yield sep
648 yield sep
584 yield x
649 yield x
General Comments 0
You need to be logged in to leave comments. Login now