##// END OF EJS Templates
templater: switch to lower-level config.parse() in _readmapfile()...
Martin von Zweigbergk -
r45769:8ec3062b default
parent child Browse files
Show More
@@ -1,1093 +1,1099 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 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(includepaths=[templatedir()])
827 conf.read(mapfile, remap={b'': b'templates'})
827
828 def include(rel, abs, remap, sections):
829 data = util.posixfile(abs, b'rb').read()
830 conf.parse(abs, data, sections=sections, remap=remap, include=include)
831
832 data = util.posixfile(mapfile, b'rb').read()
833 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
828
834
829 cache = {}
835 cache = {}
830 tmap = {}
836 tmap = {}
831 aliases = []
837 aliases = []
832
838
833 val = conf.get(b'templates', b'__base__')
839 val = conf.get(b'templates', b'__base__')
834 if val and val[0] not in b"'\"":
840 if val and val[0] not in b"'\"":
835 # treat as a pointer to a base class for this style
841 # treat as a pointer to a base class for this style
836 path = util.normpath(os.path.join(base, val))
842 path = util.normpath(os.path.join(base, val))
837
843
838 # fallback check in template paths
844 # fallback check in template paths
839 if not os.path.exists(path):
845 if not os.path.exists(path):
840 p2 = util.normpath(os.path.join(templatedir(), val))
846 p2 = util.normpath(os.path.join(templatedir(), val))
841 if os.path.isfile(p2):
847 if os.path.isfile(p2):
842 path = p2
848 path = p2
843 else:
849 else:
844 p3 = util.normpath(os.path.join(p2, b"map"))
850 p3 = util.normpath(os.path.join(p2, b"map"))
845 if os.path.isfile(p3):
851 if os.path.isfile(p3):
846 path = p3
852 path = p3
847
853
848 cache, tmap, aliases = _readmapfile(path)
854 cache, tmap, aliases = _readmapfile(path)
849
855
850 for key, val in conf[b'templates'].items():
856 for key, val in conf[b'templates'].items():
851 if not val:
857 if not val:
852 raise error.ParseError(
858 raise error.ParseError(
853 _(b'missing value'), conf.source(b'templates', key)
859 _(b'missing value'), conf.source(b'templates', key)
854 )
860 )
855 if val[0] in b"'\"":
861 if val[0] in b"'\"":
856 if val[0] != val[-1]:
862 if val[0] != val[-1]:
857 raise error.ParseError(
863 raise error.ParseError(
858 _(b'unmatched quotes'), conf.source(b'templates', key)
864 _(b'unmatched quotes'), conf.source(b'templates', key)
859 )
865 )
860 cache[key] = unquotestring(val)
866 cache[key] = unquotestring(val)
861 elif key != b'__base__':
867 elif key != b'__base__':
862 tmap[key] = os.path.join(base, val)
868 tmap[key] = os.path.join(base, val)
863 aliases.extend(conf[b'templatealias'].items())
869 aliases.extend(conf[b'templatealias'].items())
864 return cache, tmap, aliases
870 return cache, tmap, aliases
865
871
866
872
867 class loader(object):
873 class loader(object):
868 """Load template fragments optionally from a map file"""
874 """Load template fragments optionally from a map file"""
869
875
870 def __init__(self, cache, aliases):
876 def __init__(self, cache, aliases):
871 if cache is None:
877 if cache is None:
872 cache = {}
878 cache = {}
873 self.cache = cache.copy()
879 self.cache = cache.copy()
874 self._map = {}
880 self._map = {}
875 self._aliasmap = _aliasrules.buildmap(aliases)
881 self._aliasmap = _aliasrules.buildmap(aliases)
876
882
877 def __contains__(self, key):
883 def __contains__(self, key):
878 return key in self.cache or key in self._map
884 return key in self.cache or key in self._map
879
885
880 def load(self, t):
886 def load(self, t):
881 """Get parsed tree for the given template name. Use a local cache."""
887 """Get parsed tree for the given template name. Use a local cache."""
882 if t not in self.cache:
888 if t not in self.cache:
883 try:
889 try:
884 self.cache[t] = util.readfile(self._map[t])
890 self.cache[t] = util.readfile(self._map[t])
885 except KeyError as inst:
891 except KeyError as inst:
886 raise templateutil.TemplateNotFound(
892 raise templateutil.TemplateNotFound(
887 _(b'"%s" not in template map') % inst.args[0]
893 _(b'"%s" not in template map') % inst.args[0]
888 )
894 )
889 except IOError as inst:
895 except IOError as inst:
890 reason = _(b'template file %s: %s') % (
896 reason = _(b'template file %s: %s') % (
891 self._map[t],
897 self._map[t],
892 stringutil.forcebytestr(inst.args[1]),
898 stringutil.forcebytestr(inst.args[1]),
893 )
899 )
894 raise IOError(inst.args[0], encoding.strfromlocal(reason))
900 raise IOError(inst.args[0], encoding.strfromlocal(reason))
895 return self._parse(self.cache[t])
901 return self._parse(self.cache[t])
896
902
897 def _parse(self, tmpl):
903 def _parse(self, tmpl):
898 x = parse(tmpl)
904 x = parse(tmpl)
899 if self._aliasmap:
905 if self._aliasmap:
900 x = _aliasrules.expand(self._aliasmap, x)
906 x = _aliasrules.expand(self._aliasmap, x)
901 return x
907 return x
902
908
903 def _findsymbolsused(self, tree, syms):
909 def _findsymbolsused(self, tree, syms):
904 if not tree:
910 if not tree:
905 return
911 return
906 op = tree[0]
912 op = tree[0]
907 if op == b'symbol':
913 if op == b'symbol':
908 s = tree[1]
914 s = tree[1]
909 if s in syms[0]:
915 if s in syms[0]:
910 return # avoid recursion: s -> cache[s] -> s
916 return # avoid recursion: s -> cache[s] -> s
911 syms[0].add(s)
917 syms[0].add(s)
912 if s in self.cache or s in self._map:
918 if s in self.cache or s in self._map:
913 # s may be a reference for named template
919 # s may be a reference for named template
914 self._findsymbolsused(self.load(s), syms)
920 self._findsymbolsused(self.load(s), syms)
915 return
921 return
916 if op in {b'integer', b'string'}:
922 if op in {b'integer', b'string'}:
917 return
923 return
918 # '{arg|func}' == '{func(arg)}'
924 # '{arg|func}' == '{func(arg)}'
919 if op == b'|':
925 if op == b'|':
920 syms[1].add(getsymbol(tree[2]))
926 syms[1].add(getsymbol(tree[2]))
921 self._findsymbolsused(tree[1], syms)
927 self._findsymbolsused(tree[1], syms)
922 return
928 return
923 if op == b'func':
929 if op == b'func':
924 syms[1].add(getsymbol(tree[1]))
930 syms[1].add(getsymbol(tree[1]))
925 self._findsymbolsused(tree[2], syms)
931 self._findsymbolsused(tree[2], syms)
926 return
932 return
927 for x in tree[1:]:
933 for x in tree[1:]:
928 self._findsymbolsused(x, syms)
934 self._findsymbolsused(x, syms)
929
935
930 def symbolsused(self, t):
936 def symbolsused(self, t):
931 """Look up (keywords, filters/functions) referenced from the name
937 """Look up (keywords, filters/functions) referenced from the name
932 template 't'
938 template 't'
933
939
934 This may load additional templates from the map file.
940 This may load additional templates from the map file.
935 """
941 """
936 syms = (set(), set())
942 syms = (set(), set())
937 self._findsymbolsused(self.load(t), syms)
943 self._findsymbolsused(self.load(t), syms)
938 return syms
944 return syms
939
945
940
946
941 class templater(object):
947 class templater(object):
942 def __init__(
948 def __init__(
943 self,
949 self,
944 filters=None,
950 filters=None,
945 defaults=None,
951 defaults=None,
946 resources=None,
952 resources=None,
947 cache=None,
953 cache=None,
948 aliases=(),
954 aliases=(),
949 minchunk=1024,
955 minchunk=1024,
950 maxchunk=65536,
956 maxchunk=65536,
951 ):
957 ):
952 """Create template engine optionally with preloaded template fragments
958 """Create template engine optionally with preloaded template fragments
953
959
954 - ``filters``: a dict of functions to transform a value into another.
960 - ``filters``: a dict of functions to transform a value into another.
955 - ``defaults``: a dict of symbol values/functions; may be overridden
961 - ``defaults``: a dict of symbol values/functions; may be overridden
956 by a ``mapping`` dict.
962 by a ``mapping`` dict.
957 - ``resources``: a resourcemapper object to look up internal data
963 - ``resources``: a resourcemapper object to look up internal data
958 (e.g. cache), inaccessible from user template.
964 (e.g. cache), inaccessible from user template.
959 - ``cache``: a dict of preloaded template fragments.
965 - ``cache``: a dict of preloaded template fragments.
960 - ``aliases``: a list of alias (name, replacement) pairs.
966 - ``aliases``: a list of alias (name, replacement) pairs.
961
967
962 self.cache may be updated later to register additional template
968 self.cache may be updated later to register additional template
963 fragments.
969 fragments.
964 """
970 """
965 allfilters = templatefilters.filters.copy()
971 allfilters = templatefilters.filters.copy()
966 if filters:
972 if filters:
967 allfilters.update(filters)
973 allfilters.update(filters)
968 self._loader = loader(cache, aliases)
974 self._loader = loader(cache, aliases)
969 self._proc = engine(self._loader.load, allfilters, defaults, resources)
975 self._proc = engine(self._loader.load, allfilters, defaults, resources)
970 self._minchunk, self._maxchunk = minchunk, maxchunk
976 self._minchunk, self._maxchunk = minchunk, maxchunk
971
977
972 @classmethod
978 @classmethod
973 def frommapfile(
979 def frommapfile(
974 cls,
980 cls,
975 mapfile,
981 mapfile,
976 filters=None,
982 filters=None,
977 defaults=None,
983 defaults=None,
978 resources=None,
984 resources=None,
979 cache=None,
985 cache=None,
980 minchunk=1024,
986 minchunk=1024,
981 maxchunk=65536,
987 maxchunk=65536,
982 ):
988 ):
983 """Create templater from the specified map file"""
989 """Create templater from the specified map file"""
984 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
990 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
985 cache, tmap, aliases = _readmapfile(mapfile)
991 cache, tmap, aliases = _readmapfile(mapfile)
986 t._loader.cache.update(cache)
992 t._loader.cache.update(cache)
987 t._loader._map = tmap
993 t._loader._map = tmap
988 t._loader._aliasmap = _aliasrules.buildmap(aliases)
994 t._loader._aliasmap = _aliasrules.buildmap(aliases)
989 return t
995 return t
990
996
991 def __contains__(self, key):
997 def __contains__(self, key):
992 return key in self._loader
998 return key in self._loader
993
999
994 @property
1000 @property
995 def cache(self):
1001 def cache(self):
996 return self._loader.cache
1002 return self._loader.cache
997
1003
998 # for highlight extension to insert one-time 'colorize' filter
1004 # for highlight extension to insert one-time 'colorize' filter
999 @property
1005 @property
1000 def _filters(self):
1006 def _filters(self):
1001 return self._proc._filters
1007 return self._proc._filters
1002
1008
1003 @property
1009 @property
1004 def defaults(self):
1010 def defaults(self):
1005 return self._proc._defaults
1011 return self._proc._defaults
1006
1012
1007 def load(self, t):
1013 def load(self, t):
1008 """Get parsed tree for the given template name. Use a local cache."""
1014 """Get parsed tree for the given template name. Use a local cache."""
1009 return self._loader.load(t)
1015 return self._loader.load(t)
1010
1016
1011 def symbolsuseddefault(self):
1017 def symbolsuseddefault(self):
1012 """Look up (keywords, filters/functions) referenced from the default
1018 """Look up (keywords, filters/functions) referenced from the default
1013 unnamed template
1019 unnamed template
1014
1020
1015 This may load additional templates from the map file.
1021 This may load additional templates from the map file.
1016 """
1022 """
1017 return self.symbolsused(b'')
1023 return self.symbolsused(b'')
1018
1024
1019 def symbolsused(self, t):
1025 def symbolsused(self, t):
1020 """Look up (keywords, filters/functions) referenced from the name
1026 """Look up (keywords, filters/functions) referenced from the name
1021 template 't'
1027 template 't'
1022
1028
1023 This may load additional templates from the map file.
1029 This may load additional templates from the map file.
1024 """
1030 """
1025 return self._loader.symbolsused(t)
1031 return self._loader.symbolsused(t)
1026
1032
1027 def renderdefault(self, mapping):
1033 def renderdefault(self, mapping):
1028 """Render the default unnamed template and return result as string"""
1034 """Render the default unnamed template and return result as string"""
1029 return self.render(b'', mapping)
1035 return self.render(b'', mapping)
1030
1036
1031 def render(self, t, mapping):
1037 def render(self, t, mapping):
1032 """Render the specified named template and return result as string"""
1038 """Render the specified named template and return result as string"""
1033 return b''.join(self.generate(t, mapping))
1039 return b''.join(self.generate(t, mapping))
1034
1040
1035 def generate(self, t, mapping):
1041 def generate(self, t, mapping):
1036 """Return a generator that renders the specified named template and
1042 """Return a generator that renders the specified named template and
1037 yields chunks"""
1043 yields chunks"""
1038 stream = self._proc.process(t, mapping)
1044 stream = self._proc.process(t, mapping)
1039 if self._minchunk:
1045 if self._minchunk:
1040 stream = util.increasingchunks(
1046 stream = util.increasingchunks(
1041 stream, min=self._minchunk, max=self._maxchunk
1047 stream, min=self._minchunk, max=self._maxchunk
1042 )
1048 )
1043 return stream
1049 return stream
1044
1050
1045
1051
1046 def templatedir():
1052 def templatedir():
1047 '''return the directory used for template files, or None.'''
1053 '''return the directory used for template files, or None.'''
1048 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1054 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1049 return path if os.path.isdir(path) else None
1055 return path if os.path.isdir(path) else None
1050
1056
1051
1057
1052 def templatepath(name):
1058 def templatepath(name):
1053 '''return location of template file. returns None if not found.'''
1059 '''return location of template file. returns None if not found.'''
1054 f = os.path.join(templatedir(), name)
1060 f = os.path.join(templatedir(), name)
1055 if f and os.path.exists(f):
1061 if f and os.path.exists(f):
1056 return f
1062 return f
1057 return None
1063 return None
1058
1064
1059
1065
1060 def stylemap(styles, path=None):
1066 def stylemap(styles, path=None):
1061 """Return path to mapfile for a given style.
1067 """Return path to mapfile for a given style.
1062
1068
1063 Searches mapfile in the following locations:
1069 Searches mapfile in the following locations:
1064 1. templatepath/style/map
1070 1. templatepath/style/map
1065 2. templatepath/map-style
1071 2. templatepath/map-style
1066 3. templatepath/map
1072 3. templatepath/map
1067 """
1073 """
1068
1074
1069 if path is None:
1075 if path is None:
1070 path = templatedir()
1076 path = templatedir()
1071
1077
1072 if isinstance(styles, bytes):
1078 if isinstance(styles, bytes):
1073 styles = [styles]
1079 styles = [styles]
1074
1080
1075 for style in styles:
1081 for style in styles:
1076 # only plain name is allowed to honor template paths
1082 # only plain name is allowed to honor template paths
1077 if (
1083 if (
1078 not style
1084 not style
1079 or style in (pycompat.oscurdir, pycompat.ospardir)
1085 or style in (pycompat.oscurdir, pycompat.ospardir)
1080 or pycompat.ossep in style
1086 or pycompat.ossep in style
1081 or pycompat.osaltsep
1087 or pycompat.osaltsep
1082 and pycompat.osaltsep in style
1088 and pycompat.osaltsep in style
1083 ):
1089 ):
1084 continue
1090 continue
1085 locations = [os.path.join(style, b'map'), b'map-' + style]
1091 locations = [os.path.join(style, b'map'), b'map-' + style]
1086 locations.append(b'map')
1092 locations.append(b'map')
1087
1093
1088 for location in locations:
1094 for location in locations:
1089 mapfile = os.path.join(path, location)
1095 mapfile = os.path.join(path, location)
1090 if os.path.isfile(mapfile):
1096 if os.path.isfile(mapfile):
1091 return style, mapfile
1097 return style, mapfile
1092
1098
1093 raise RuntimeError(b"No hgweb templates found in %r" % path)
1099 raise RuntimeError(b"No hgweb templates found in %r" % path)
General Comments 0
You need to be logged in to leave comments. Login now