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