##// END OF EJS Templates
templater: resurrect cache of engine instance...
Yuya Nishihara -
r38458:25658148 default
parent child Browse files
Show More
@@ -1,981 +1,982 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 self._loader = loader
601 self._loader = loader
602 if filters is None:
602 if filters is None:
603 filters = {}
603 filters = {}
604 self._filters = filters
604 self._filters = filters
605 self._funcs = templatefuncs.funcs # make this a parameter if needed
605 self._funcs = templatefuncs.funcs # make this a parameter if needed
606 if defaults is None:
606 if defaults is None:
607 defaults = {}
607 defaults = {}
608 if resources is None:
608 if resources is None:
609 resources = nullresourcemapper()
609 resources = nullresourcemapper()
610 self._defaults = defaults
610 self._defaults = defaults
611 self._resources = resources
611 self._resources = resources
612 self._cache = {} # key: (func, data)
612 self._cache = {} # key: (func, data)
613 self._tmplcache = {} # literal template: (func, data)
613 self._tmplcache = {} # literal template: (func, data)
614
614
615 def overlaymap(self, origmapping, newmapping):
615 def overlaymap(self, origmapping, newmapping):
616 """Create combined mapping from the original mapping and partial
616 """Create combined mapping from the original mapping and partial
617 mapping to override the original"""
617 mapping to override the original"""
618 # do not copy symbols which overrides the defaults depending on
618 # do not copy symbols which overrides the defaults depending on
619 # new resources, so the defaults will be re-evaluated (issue5612)
619 # new resources, so the defaults will be re-evaluated (issue5612)
620 knownres = self._resources.knownkeys()
620 knownres = self._resources.knownkeys()
621 newres = self._resources.availablekeys(self, newmapping)
621 newres = self._resources.availablekeys(self, newmapping)
622 mapping = {k: v for k, v in origmapping.iteritems()
622 mapping = {k: v for k, v in origmapping.iteritems()
623 if (k in knownres # not a symbol per self.symbol()
623 if (k in knownres # not a symbol per self.symbol()
624 or newres.isdisjoint(self._defaultrequires(k)))}
624 or newres.isdisjoint(self._defaultrequires(k)))}
625 mapping.update(newmapping)
625 mapping.update(newmapping)
626 mapping.update(
626 mapping.update(
627 self._resources.populatemap(self, origmapping, newmapping))
627 self._resources.populatemap(self, origmapping, newmapping))
628 return mapping
628 return mapping
629
629
630 def _defaultrequires(self, key):
630 def _defaultrequires(self, key):
631 """Resource keys required by the specified default symbol function"""
631 """Resource keys required by the specified default symbol function"""
632 v = self._defaults.get(key)
632 v = self._defaults.get(key)
633 if v is None or not callable(v):
633 if v is None or not callable(v):
634 return ()
634 return ()
635 return getattr(v, '_requires', ())
635 return getattr(v, '_requires', ())
636
636
637 def symbol(self, mapping, key):
637 def symbol(self, mapping, key):
638 """Resolve symbol to value or function; None if nothing found"""
638 """Resolve symbol to value or function; None if nothing found"""
639 v = None
639 v = None
640 if key not in self._resources.knownkeys():
640 if key not in self._resources.knownkeys():
641 v = mapping.get(key)
641 v = mapping.get(key)
642 if v is None:
642 if v is None:
643 v = self._defaults.get(key)
643 v = self._defaults.get(key)
644 return v
644 return v
645
645
646 def availableresourcekeys(self, mapping):
646 def availableresourcekeys(self, mapping):
647 """Return a set of available resource keys based on the given mapping"""
647 """Return a set of available resource keys based on the given mapping"""
648 return self._resources.availablekeys(self, mapping)
648 return self._resources.availablekeys(self, mapping)
649
649
650 def knownresourcekeys(self):
650 def knownresourcekeys(self):
651 """Return a set of supported resource keys"""
651 """Return a set of supported resource keys"""
652 return self._resources.knownkeys()
652 return self._resources.knownkeys()
653
653
654 def resource(self, mapping, key):
654 def resource(self, mapping, key):
655 """Return internal data (e.g. cache) used for keyword/function
655 """Return internal data (e.g. cache) used for keyword/function
656 evaluation"""
656 evaluation"""
657 v = self._resources.lookup(self, mapping, key)
657 v = self._resources.lookup(self, mapping, key)
658 if v is None:
658 if v is None:
659 raise templateutil.ResourceUnavailable(
659 raise templateutil.ResourceUnavailable(
660 _('template resource not available: %s') % key)
660 _('template resource not available: %s') % key)
661 return v
661 return v
662
662
663 def _load(self, t):
663 def _load(self, t):
664 '''load, parse, and cache a template'''
664 '''load, parse, and cache a template'''
665 if t not in self._cache:
665 if t not in self._cache:
666 x = self._loader(t)
666 x = self._loader(t)
667 # put poison to cut recursion while compiling 't'
667 # put poison to cut recursion while compiling 't'
668 self._cache[t] = (_runrecursivesymbol, t)
668 self._cache[t] = (_runrecursivesymbol, t)
669 try:
669 try:
670 self._cache[t] = compileexp(x, self, methods)
670 self._cache[t] = compileexp(x, self, methods)
671 except: # re-raises
671 except: # re-raises
672 del self._cache[t]
672 del self._cache[t]
673 raise
673 raise
674 return self._cache[t]
674 return self._cache[t]
675
675
676 def _parse(self, tmpl):
676 def _parse(self, tmpl):
677 """Parse and cache a literal template"""
677 """Parse and cache a literal template"""
678 if tmpl not in self._tmplcache:
678 if tmpl not in self._tmplcache:
679 x = parse(tmpl)
679 x = parse(tmpl)
680 self._tmplcache[tmpl] = compileexp(x, self, methods)
680 self._tmplcache[tmpl] = compileexp(x, self, methods)
681 return self._tmplcache[tmpl]
681 return self._tmplcache[tmpl]
682
682
683 def preload(self, t):
683 def preload(self, t):
684 """Load, parse, and cache the specified template if available"""
684 """Load, parse, and cache the specified template if available"""
685 try:
685 try:
686 self._load(t)
686 self._load(t)
687 return True
687 return True
688 except templateutil.TemplateNotFound:
688 except templateutil.TemplateNotFound:
689 return False
689 return False
690
690
691 def process(self, t, mapping):
691 def process(self, t, mapping):
692 '''Perform expansion. t is name of map element to expand.
692 '''Perform expansion. t is name of map element to expand.
693 mapping contains added elements for use during expansion. Is a
693 mapping contains added elements for use during expansion. Is a
694 generator.'''
694 generator.'''
695 func, data = self._load(t)
695 func, data = self._load(t)
696 return self._expand(func, data, mapping)
696 return self._expand(func, data, mapping)
697
697
698 def expand(self, tmpl, mapping):
698 def expand(self, tmpl, mapping):
699 """Perform expansion over a literal template
699 """Perform expansion over a literal template
700
700
701 No user aliases will be expanded since this is supposed to be called
701 No user aliases will be expanded since this is supposed to be called
702 with an internal template string.
702 with an internal template string.
703 """
703 """
704 func, data = self._parse(tmpl)
704 func, data = self._parse(tmpl)
705 return self._expand(func, data, mapping)
705 return self._expand(func, data, mapping)
706
706
707 def _expand(self, func, data, mapping):
707 def _expand(self, func, data, mapping):
708 # populate additional items only if they don't exist in the given
708 # populate additional items only if they don't exist in the given
709 # mapping. this is slightly different from overlaymap() because the
709 # mapping. this is slightly different from overlaymap() because the
710 # initial 'revcache' may contain pre-computed items.
710 # initial 'revcache' may contain pre-computed items.
711 extramapping = self._resources.populatemap(self, {}, mapping)
711 extramapping = self._resources.populatemap(self, {}, mapping)
712 if extramapping:
712 if extramapping:
713 extramapping.update(mapping)
713 extramapping.update(mapping)
714 mapping = extramapping
714 mapping = extramapping
715 return templateutil.flatten(self, mapping, func(self, mapping, data))
715 return templateutil.flatten(self, mapping, func(self, mapping, data))
716
716
717 def stylelist():
717 def stylelist():
718 paths = templatepaths()
718 paths = templatepaths()
719 if not paths:
719 if not paths:
720 return _('no templates found, try `hg debuginstall` for more info')
720 return _('no templates found, try `hg debuginstall` for more info')
721 dirlist = os.listdir(paths[0])
721 dirlist = os.listdir(paths[0])
722 stylelist = []
722 stylelist = []
723 for file in dirlist:
723 for file in dirlist:
724 split = file.split(".")
724 split = file.split(".")
725 if split[-1] in ('orig', 'rej'):
725 if split[-1] in ('orig', 'rej'):
726 continue
726 continue
727 if split[0] == "map-cmdline":
727 if split[0] == "map-cmdline":
728 stylelist.append(split[1])
728 stylelist.append(split[1])
729 return ", ".join(sorted(stylelist))
729 return ", ".join(sorted(stylelist))
730
730
731 def _readmapfile(mapfile):
731 def _readmapfile(mapfile):
732 """Load template elements from the given map file"""
732 """Load template elements from the given map file"""
733 if not os.path.exists(mapfile):
733 if not os.path.exists(mapfile):
734 raise error.Abort(_("style '%s' not found") % mapfile,
734 raise error.Abort(_("style '%s' not found") % mapfile,
735 hint=_("available styles: %s") % stylelist())
735 hint=_("available styles: %s") % stylelist())
736
736
737 base = os.path.dirname(mapfile)
737 base = os.path.dirname(mapfile)
738 conf = config.config(includepaths=templatepaths())
738 conf = config.config(includepaths=templatepaths())
739 conf.read(mapfile, remap={'': 'templates'})
739 conf.read(mapfile, remap={'': 'templates'})
740
740
741 cache = {}
741 cache = {}
742 tmap = {}
742 tmap = {}
743 aliases = []
743 aliases = []
744
744
745 val = conf.get('templates', '__base__')
745 val = conf.get('templates', '__base__')
746 if val and val[0] not in "'\"":
746 if val and val[0] not in "'\"":
747 # treat as a pointer to a base class for this style
747 # treat as a pointer to a base class for this style
748 path = util.normpath(os.path.join(base, val))
748 path = util.normpath(os.path.join(base, val))
749
749
750 # fallback check in template paths
750 # fallback check in template paths
751 if not os.path.exists(path):
751 if not os.path.exists(path):
752 for p in templatepaths():
752 for p in templatepaths():
753 p2 = util.normpath(os.path.join(p, val))
753 p2 = util.normpath(os.path.join(p, val))
754 if os.path.isfile(p2):
754 if os.path.isfile(p2):
755 path = p2
755 path = p2
756 break
756 break
757 p3 = util.normpath(os.path.join(p2, "map"))
757 p3 = util.normpath(os.path.join(p2, "map"))
758 if os.path.isfile(p3):
758 if os.path.isfile(p3):
759 path = p3
759 path = p3
760 break
760 break
761
761
762 cache, tmap, aliases = _readmapfile(path)
762 cache, tmap, aliases = _readmapfile(path)
763
763
764 for key, val in conf['templates'].items():
764 for key, val in conf['templates'].items():
765 if not val:
765 if not val:
766 raise error.ParseError(_('missing value'),
766 raise error.ParseError(_('missing value'),
767 conf.source('templates', key))
767 conf.source('templates', key))
768 if val[0] in "'\"":
768 if val[0] in "'\"":
769 if val[0] != val[-1]:
769 if val[0] != val[-1]:
770 raise error.ParseError(_('unmatched quotes'),
770 raise error.ParseError(_('unmatched quotes'),
771 conf.source('templates', key))
771 conf.source('templates', key))
772 cache[key] = unquotestring(val)
772 cache[key] = unquotestring(val)
773 elif key != '__base__':
773 elif key != '__base__':
774 tmap[key] = os.path.join(base, val)
774 tmap[key] = os.path.join(base, val)
775 aliases.extend(conf['templatealias'].items())
775 aliases.extend(conf['templatealias'].items())
776 return cache, tmap, aliases
776 return cache, tmap, aliases
777
777
778 class loader(object):
778 class loader(object):
779 """Load template fragments optionally from a map file"""
779 """Load template fragments optionally from a map file"""
780
780
781 def __init__(self, cache, aliases):
781 def __init__(self, cache, aliases):
782 if cache is None:
782 if cache is None:
783 cache = {}
783 cache = {}
784 self.cache = cache.copy()
784 self.cache = cache.copy()
785 self._map = {}
785 self._map = {}
786 self._aliasmap = _aliasrules.buildmap(aliases)
786 self._aliasmap = _aliasrules.buildmap(aliases)
787
787
788 def __contains__(self, key):
788 def __contains__(self, key):
789 return key in self.cache or key in self._map
789 return key in self.cache or key in self._map
790
790
791 def load(self, t):
791 def load(self, t):
792 """Get parsed tree for the given template name. Use a local cache."""
792 """Get parsed tree for the given template name. Use a local cache."""
793 if t not in self.cache:
793 if t not in self.cache:
794 try:
794 try:
795 self.cache[t] = util.readfile(self._map[t])
795 self.cache[t] = util.readfile(self._map[t])
796 except KeyError as inst:
796 except KeyError as inst:
797 raise templateutil.TemplateNotFound(
797 raise templateutil.TemplateNotFound(
798 _('"%s" not in template map') % inst.args[0])
798 _('"%s" not in template map') % inst.args[0])
799 except IOError as inst:
799 except IOError as inst:
800 reason = (_('template file %s: %s')
800 reason = (_('template file %s: %s')
801 % (self._map[t],
801 % (self._map[t],
802 stringutil.forcebytestr(inst.args[1])))
802 stringutil.forcebytestr(inst.args[1])))
803 raise IOError(inst.args[0], encoding.strfromlocal(reason))
803 raise IOError(inst.args[0], encoding.strfromlocal(reason))
804 return self._parse(self.cache[t])
804 return self._parse(self.cache[t])
805
805
806 def _parse(self, tmpl):
806 def _parse(self, tmpl):
807 x = parse(tmpl)
807 x = parse(tmpl)
808 if self._aliasmap:
808 if self._aliasmap:
809 x = _aliasrules.expand(self._aliasmap, x)
809 x = _aliasrules.expand(self._aliasmap, x)
810 return x
810 return x
811
811
812 def _findsymbolsused(self, tree, syms):
812 def _findsymbolsused(self, tree, syms):
813 if not tree:
813 if not tree:
814 return
814 return
815 op = tree[0]
815 op = tree[0]
816 if op == 'symbol':
816 if op == 'symbol':
817 s = tree[1]
817 s = tree[1]
818 if s in syms[0]:
818 if s in syms[0]:
819 return # avoid recursion: s -> cache[s] -> s
819 return # avoid recursion: s -> cache[s] -> s
820 syms[0].add(s)
820 syms[0].add(s)
821 if s in self.cache or s in self._map:
821 if s in self.cache or s in self._map:
822 # s may be a reference for named template
822 # s may be a reference for named template
823 self._findsymbolsused(self.load(s), syms)
823 self._findsymbolsused(self.load(s), syms)
824 return
824 return
825 if op in {'integer', 'string'}:
825 if op in {'integer', 'string'}:
826 return
826 return
827 # '{arg|func}' == '{func(arg)}'
827 # '{arg|func}' == '{func(arg)}'
828 if op == '|':
828 if op == '|':
829 syms[1].add(getsymbol(tree[2]))
829 syms[1].add(getsymbol(tree[2]))
830 self._findsymbolsused(tree[1], syms)
830 self._findsymbolsused(tree[1], syms)
831 return
831 return
832 if op == 'func':
832 if op == 'func':
833 syms[1].add(getsymbol(tree[1]))
833 syms[1].add(getsymbol(tree[1]))
834 self._findsymbolsused(tree[2], syms)
834 self._findsymbolsused(tree[2], syms)
835 return
835 return
836 for x in tree[1:]:
836 for x in tree[1:]:
837 self._findsymbolsused(x, syms)
837 self._findsymbolsused(x, syms)
838
838
839 def symbolsused(self, t):
839 def symbolsused(self, t):
840 """Look up (keywords, filters/functions) referenced from the name
840 """Look up (keywords, filters/functions) referenced from the name
841 template 't'
841 template 't'
842
842
843 This may load additional templates from the map file.
843 This may load additional templates from the map file.
844 """
844 """
845 syms = (set(), set())
845 syms = (set(), set())
846 self._findsymbolsused(self.load(t), syms)
846 self._findsymbolsused(self.load(t), syms)
847 return syms
847 return syms
848
848
849 class templater(object):
849 class templater(object):
850
850
851 def __init__(self, filters=None, defaults=None, resources=None,
851 def __init__(self, filters=None, defaults=None, resources=None,
852 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
852 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
853 """Create template engine optionally with preloaded template fragments
853 """Create template engine optionally with preloaded template fragments
854
854
855 - ``filters``: a dict of functions to transform a value into another.
855 - ``filters``: a dict of functions to transform a value into another.
856 - ``defaults``: a dict of symbol values/functions; may be overridden
856 - ``defaults``: a dict of symbol values/functions; may be overridden
857 by a ``mapping`` dict.
857 by a ``mapping`` dict.
858 - ``resources``: a resourcemapper object to look up internal data
858 - ``resources``: a resourcemapper object to look up internal data
859 (e.g. cache), inaccessible from user template.
859 (e.g. cache), inaccessible from user template.
860 - ``cache``: a dict of preloaded template fragments.
860 - ``cache``: a dict of preloaded template fragments.
861 - ``aliases``: a list of alias (name, replacement) pairs.
861 - ``aliases``: a list of alias (name, replacement) pairs.
862
862
863 self.cache may be updated later to register additional template
863 self.cache may be updated later to register additional template
864 fragments.
864 fragments.
865 """
865 """
866 if filters is None:
866 if filters is None:
867 filters = {}
867 filters = {}
868 if defaults is None:
868 if defaults is None:
869 defaults = {}
869 defaults = {}
870 self._filters = templatefilters.filters.copy()
870 self._filters = templatefilters.filters.copy()
871 self._filters.update(filters)
871 self._filters.update(filters)
872 self.defaults = defaults
872 self.defaults = defaults
873 self._resources = resources
873 self._resources = resources
874 self._loader = loader(cache, aliases)
874 self._loader = loader(cache, aliases)
875 self._proc = engine(self._loader.load, self._filters, self.defaults,
876 self._resources)
875 self._minchunk, self._maxchunk = minchunk, maxchunk
877 self._minchunk, self._maxchunk = minchunk, maxchunk
876
878
877 @classmethod
879 @classmethod
878 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
880 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
879 cache=None, minchunk=1024, maxchunk=65536):
881 cache=None, minchunk=1024, maxchunk=65536):
880 """Create templater from the specified map file"""
882 """Create templater from the specified map file"""
881 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
883 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
882 cache, tmap, aliases = _readmapfile(mapfile)
884 cache, tmap, aliases = _readmapfile(mapfile)
883 t._loader.cache.update(cache)
885 t._loader.cache.update(cache)
884 t._loader._map = tmap
886 t._loader._map = tmap
885 t._loader._aliasmap = _aliasrules.buildmap(aliases)
887 t._loader._aliasmap = _aliasrules.buildmap(aliases)
886 return t
888 return t
887
889
888 def __contains__(self, key):
890 def __contains__(self, key):
889 return key in self._loader
891 return key in self._loader
890
892
891 @property
893 @property
892 def cache(self):
894 def cache(self):
893 return self._loader.cache
895 return self._loader.cache
894
896
895 def load(self, t):
897 def load(self, t):
896 """Get parsed tree for the given template name. Use a local cache."""
898 """Get parsed tree for the given template name. Use a local cache."""
897 return self._loader.load(t)
899 return self._loader.load(t)
898
900
899 def symbolsuseddefault(self):
901 def symbolsuseddefault(self):
900 """Look up (keywords, filters/functions) referenced from the default
902 """Look up (keywords, filters/functions) referenced from the default
901 unnamed template
903 unnamed template
902
904
903 This may load additional templates from the map file.
905 This may load additional templates from the map file.
904 """
906 """
905 return self.symbolsused('')
907 return self.symbolsused('')
906
908
907 def symbolsused(self, t):
909 def symbolsused(self, t):
908 """Look up (keywords, filters/functions) referenced from the name
910 """Look up (keywords, filters/functions) referenced from the name
909 template 't'
911 template 't'
910
912
911 This may load additional templates from the map file.
913 This may load additional templates from the map file.
912 """
914 """
913 return self._loader.symbolsused(t)
915 return self._loader.symbolsused(t)
914
916
915 def renderdefault(self, mapping):
917 def renderdefault(self, mapping):
916 """Render the default unnamed template and return result as string"""
918 """Render the default unnamed template and return result as string"""
917 return self.render('', mapping)
919 return self.render('', mapping)
918
920
919 def render(self, t, mapping):
921 def render(self, t, mapping):
920 """Render the specified named template and return result as string"""
922 """Render the specified named template and return result as string"""
921 return b''.join(self.generate(t, mapping))
923 return b''.join(self.generate(t, mapping))
922
924
923 def generate(self, t, mapping):
925 def generate(self, t, mapping):
924 """Return a generator that renders the specified named template and
926 """Return a generator that renders the specified named template and
925 yields chunks"""
927 yields chunks"""
926 proc = engine(self.load, self._filters, self.defaults, self._resources)
928 stream = self._proc.process(t, mapping)
927 stream = proc.process(t, mapping)
928 if self._minchunk:
929 if self._minchunk:
929 stream = util.increasingchunks(stream, min=self._minchunk,
930 stream = util.increasingchunks(stream, min=self._minchunk,
930 max=self._maxchunk)
931 max=self._maxchunk)
931 return stream
932 return stream
932
933
933 def templatepaths():
934 def templatepaths():
934 '''return locations used for template files.'''
935 '''return locations used for template files.'''
935 pathsrel = ['templates']
936 pathsrel = ['templates']
936 paths = [os.path.normpath(os.path.join(util.datapath, f))
937 paths = [os.path.normpath(os.path.join(util.datapath, f))
937 for f in pathsrel]
938 for f in pathsrel]
938 return [p for p in paths if os.path.isdir(p)]
939 return [p for p in paths if os.path.isdir(p)]
939
940
940 def templatepath(name):
941 def templatepath(name):
941 '''return location of template file. returns None if not found.'''
942 '''return location of template file. returns None if not found.'''
942 for p in templatepaths():
943 for p in templatepaths():
943 f = os.path.join(p, name)
944 f = os.path.join(p, name)
944 if os.path.exists(f):
945 if os.path.exists(f):
945 return f
946 return f
946 return None
947 return None
947
948
948 def stylemap(styles, paths=None):
949 def stylemap(styles, paths=None):
949 """Return path to mapfile for a given style.
950 """Return path to mapfile for a given style.
950
951
951 Searches mapfile in the following locations:
952 Searches mapfile in the following locations:
952 1. templatepath/style/map
953 1. templatepath/style/map
953 2. templatepath/map-style
954 2. templatepath/map-style
954 3. templatepath/map
955 3. templatepath/map
955 """
956 """
956
957
957 if paths is None:
958 if paths is None:
958 paths = templatepaths()
959 paths = templatepaths()
959 elif isinstance(paths, bytes):
960 elif isinstance(paths, bytes):
960 paths = [paths]
961 paths = [paths]
961
962
962 if isinstance(styles, bytes):
963 if isinstance(styles, bytes):
963 styles = [styles]
964 styles = [styles]
964
965
965 for style in styles:
966 for style in styles:
966 # only plain name is allowed to honor template paths
967 # only plain name is allowed to honor template paths
967 if (not style
968 if (not style
968 or style in (pycompat.oscurdir, pycompat.ospardir)
969 or style in (pycompat.oscurdir, pycompat.ospardir)
969 or pycompat.ossep in style
970 or pycompat.ossep in style
970 or pycompat.osaltsep and pycompat.osaltsep in style):
971 or pycompat.osaltsep and pycompat.osaltsep in style):
971 continue
972 continue
972 locations = [os.path.join(style, 'map'), 'map-' + style]
973 locations = [os.path.join(style, 'map'), 'map-' + style]
973 locations.append('map')
974 locations.append('map')
974
975
975 for path in paths:
976 for path in paths:
976 for location in locations:
977 for location in locations:
977 mapfile = os.path.join(path, location)
978 mapfile = os.path.join(path, location)
978 if os.path.isfile(mapfile):
979 if os.path.isfile(mapfile):
979 return style, mapfile
980 return style, mapfile
980
981
981 raise RuntimeError("No hgweb templates found in %r" % paths)
982 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now