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