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