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