##// END OF EJS Templates
templater: swap `\` with `/` to allow the resource logic to kicks in...
marmoute -
r48637:7ab7f73a stable
parent child Browse files
Show More
@@ -1,1132 +1,1139 b''
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 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 inst.location is None:
315 if inst.location is None:
316 return
316 return
317 loc = inst.location
317 loc = inst.location
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 >>> from . import error
379 >>> from . import error
380 >>> from . import pycompat
380 >>> from . import pycompat
381 >>> try:
381 >>> try:
382 ... parseexpr(b'foo(')
382 ... parseexpr(b'foo(')
383 ... except error.ParseError as e:
383 ... except error.ParseError as e:
384 ... pycompat.sysstr(e.message)
384 ... pycompat.sysstr(e.message)
385 ... e.location
385 ... e.location
386 'not a prefix: end'
386 'not a prefix: end'
387 4
387 4
388 >>> try:
388 >>> try:
389 ... parseexpr(b'"foo" "bar"')
389 ... parseexpr(b'"foo" "bar"')
390 ... except error.ParseError as e:
390 ... except error.ParseError as e:
391 ... pycompat.sysstr(e.message)
391 ... pycompat.sysstr(e.message)
392 ... e.location
392 ... e.location
393 'invalid token'
393 'invalid token'
394 7
394 7
395 """
395 """
396 try:
396 try:
397 return _parseexpr(expr)
397 return _parseexpr(expr)
398 except error.ParseError as inst:
398 except error.ParseError as inst:
399 _addparseerrorhint(inst, expr)
399 _addparseerrorhint(inst, expr)
400 raise
400 raise
401
401
402
402
403 def _parseexpr(expr):
403 def _parseexpr(expr):
404 p = parser.parser(elements)
404 p = parser.parser(elements)
405 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
405 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
406 if pos != len(expr):
406 if pos != len(expr):
407 raise error.ParseError(_(b'invalid token'), pos)
407 raise error.ParseError(_(b'invalid token'), pos)
408 return _unnesttemplatelist(tree)
408 return _unnesttemplatelist(tree)
409
409
410
410
411 def prettyformat(tree):
411 def prettyformat(tree):
412 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
412 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
413
413
414
414
415 def compileexp(exp, context, curmethods):
415 def compileexp(exp, context, curmethods):
416 """Compile parsed template tree to (func, data) pair"""
416 """Compile parsed template tree to (func, data) pair"""
417 if not exp:
417 if not exp:
418 raise error.ParseError(_(b"missing argument"))
418 raise error.ParseError(_(b"missing argument"))
419 t = exp[0]
419 t = exp[0]
420 return curmethods[t](exp, context)
420 return curmethods[t](exp, context)
421
421
422
422
423 # template evaluation
423 # template evaluation
424
424
425
425
426 def getsymbol(exp):
426 def getsymbol(exp):
427 if exp[0] == b'symbol':
427 if exp[0] == b'symbol':
428 return exp[1]
428 return exp[1]
429 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
429 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
430
430
431
431
432 def getlist(x):
432 def getlist(x):
433 if not x:
433 if not x:
434 return []
434 return []
435 if x[0] == b'list':
435 if x[0] == b'list':
436 return getlist(x[1]) + [x[2]]
436 return getlist(x[1]) + [x[2]]
437 return [x]
437 return [x]
438
438
439
439
440 def gettemplate(exp, context):
440 def gettemplate(exp, context):
441 """Compile given template tree or load named template from map file;
441 """Compile given template tree or load named template from map file;
442 returns (func, data) pair"""
442 returns (func, data) pair"""
443 if exp[0] in (b'template', b'string'):
443 if exp[0] in (b'template', b'string'):
444 return compileexp(exp, context, methods)
444 return compileexp(exp, context, methods)
445 if exp[0] == b'symbol':
445 if exp[0] == b'symbol':
446 # unlike runsymbol(), here 'symbol' is always taken as template name
446 # unlike runsymbol(), here 'symbol' is always taken as template name
447 # even if it exists in mapping. this allows us to override mapping
447 # even if it exists in mapping. this allows us to override mapping
448 # by web templates, e.g. 'changelogtag' is redefined in map file.
448 # by web templates, e.g. 'changelogtag' is redefined in map file.
449 return context._load(exp[1])
449 return context._load(exp[1])
450 raise error.ParseError(_(b"expected template specifier"))
450 raise error.ParseError(_(b"expected template specifier"))
451
451
452
452
453 def _runrecursivesymbol(context, mapping, key):
453 def _runrecursivesymbol(context, mapping, key):
454 raise error.InputError(_(b"recursive reference '%s' in template") % key)
454 raise error.InputError(_(b"recursive reference '%s' in template") % key)
455
455
456
456
457 def buildtemplate(exp, context):
457 def buildtemplate(exp, context):
458 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
458 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
459 return (templateutil.runtemplate, ctmpl)
459 return (templateutil.runtemplate, ctmpl)
460
460
461
461
462 def buildfilter(exp, context):
462 def buildfilter(exp, context):
463 n = getsymbol(exp[2])
463 n = getsymbol(exp[2])
464 if n in context._filters:
464 if n in context._filters:
465 filt = context._filters[n]
465 filt = context._filters[n]
466 arg = compileexp(exp[1], context, methods)
466 arg = compileexp(exp[1], context, methods)
467 return (templateutil.runfilter, (arg, filt))
467 return (templateutil.runfilter, (arg, filt))
468 if n in context._funcs:
468 if n in context._funcs:
469 f = context._funcs[n]
469 f = context._funcs[n]
470 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
470 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
471 return (f, args)
471 return (f, args)
472 raise error.ParseError(_(b"unknown function '%s'") % n)
472 raise error.ParseError(_(b"unknown function '%s'") % n)
473
473
474
474
475 def buildmap(exp, context):
475 def buildmap(exp, context):
476 darg = compileexp(exp[1], context, methods)
476 darg = compileexp(exp[1], context, methods)
477 targ = gettemplate(exp[2], context)
477 targ = gettemplate(exp[2], context)
478 return (templateutil.runmap, (darg, targ))
478 return (templateutil.runmap, (darg, targ))
479
479
480
480
481 def buildmember(exp, context):
481 def buildmember(exp, context):
482 darg = compileexp(exp[1], context, methods)
482 darg = compileexp(exp[1], context, methods)
483 memb = getsymbol(exp[2])
483 memb = getsymbol(exp[2])
484 return (templateutil.runmember, (darg, memb))
484 return (templateutil.runmember, (darg, memb))
485
485
486
486
487 def buildnegate(exp, context):
487 def buildnegate(exp, context):
488 arg = compileexp(exp[1], context, exprmethods)
488 arg = compileexp(exp[1], context, exprmethods)
489 return (templateutil.runnegate, arg)
489 return (templateutil.runnegate, arg)
490
490
491
491
492 def buildarithmetic(exp, context, func):
492 def buildarithmetic(exp, context, func):
493 left = compileexp(exp[1], context, exprmethods)
493 left = compileexp(exp[1], context, exprmethods)
494 right = compileexp(exp[2], context, exprmethods)
494 right = compileexp(exp[2], context, exprmethods)
495 return (templateutil.runarithmetic, (func, left, right))
495 return (templateutil.runarithmetic, (func, left, right))
496
496
497
497
498 def buildfunc(exp, context):
498 def buildfunc(exp, context):
499 n = getsymbol(exp[1])
499 n = getsymbol(exp[1])
500 if n in context._funcs:
500 if n in context._funcs:
501 f = context._funcs[n]
501 f = context._funcs[n]
502 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
502 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
503 return (f, args)
503 return (f, args)
504 if n in context._filters:
504 if n in context._filters:
505 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
505 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
506 if len(args) != 1:
506 if len(args) != 1:
507 raise error.ParseError(_(b"filter %s expects one argument") % n)
507 raise error.ParseError(_(b"filter %s expects one argument") % n)
508 f = context._filters[n]
508 f = context._filters[n]
509 return (templateutil.runfilter, (args[0], f))
509 return (templateutil.runfilter, (args[0], f))
510 raise error.ParseError(_(b"unknown function '%s'") % n)
510 raise error.ParseError(_(b"unknown function '%s'") % n)
511
511
512
512
513 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
513 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
514 """Compile parsed tree of function arguments into list or dict of
514 """Compile parsed tree of function arguments into list or dict of
515 (func, data) pairs
515 (func, data) pairs
516
516
517 >>> context = engine(lambda t: (templateutil.runsymbol, t))
517 >>> context = engine(lambda t: (templateutil.runsymbol, t))
518 >>> def fargs(expr, argspec):
518 >>> def fargs(expr, argspec):
519 ... x = _parseexpr(expr)
519 ... x = _parseexpr(expr)
520 ... n = getsymbol(x[1])
520 ... n = getsymbol(x[1])
521 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
521 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
522 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
522 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
523 ['l', 'k']
523 ['l', 'k']
524 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
524 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
525 >>> list(args.keys()), list(args[b'opts'].keys())
525 >>> list(args.keys()), list(args[b'opts'].keys())
526 (['opts'], ['opts', 'k'])
526 (['opts'], ['opts', 'k'])
527 """
527 """
528
528
529 def compiledict(xs):
529 def compiledict(xs):
530 return util.sortdict(
530 return util.sortdict(
531 (k, compileexp(x, context, curmethods))
531 (k, compileexp(x, context, curmethods))
532 for k, x in pycompat.iteritems(xs)
532 for k, x in pycompat.iteritems(xs)
533 )
533 )
534
534
535 def compilelist(xs):
535 def compilelist(xs):
536 return [compileexp(x, context, curmethods) for x in xs]
536 return [compileexp(x, context, curmethods) for x in xs]
537
537
538 if not argspec:
538 if not argspec:
539 # filter or function with no argspec: return list of positional args
539 # filter or function with no argspec: return list of positional args
540 return compilelist(getlist(exp))
540 return compilelist(getlist(exp))
541
541
542 # function with argspec: return dict of named args
542 # function with argspec: return dict of named args
543 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
543 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
544 treeargs = parser.buildargsdict(
544 treeargs = parser.buildargsdict(
545 getlist(exp),
545 getlist(exp),
546 funcname,
546 funcname,
547 argspec,
547 argspec,
548 keyvaluenode=b'keyvalue',
548 keyvaluenode=b'keyvalue',
549 keynode=b'symbol',
549 keynode=b'symbol',
550 )
550 )
551 compargs = util.sortdict()
551 compargs = util.sortdict()
552 if varkey:
552 if varkey:
553 compargs[varkey] = compilelist(treeargs.pop(varkey))
553 compargs[varkey] = compilelist(treeargs.pop(varkey))
554 if optkey:
554 if optkey:
555 compargs[optkey] = compiledict(treeargs.pop(optkey))
555 compargs[optkey] = compiledict(treeargs.pop(optkey))
556 compargs.update(compiledict(treeargs))
556 compargs.update(compiledict(treeargs))
557 return compargs
557 return compargs
558
558
559
559
560 def buildkeyvaluepair(exp, content):
560 def buildkeyvaluepair(exp, content):
561 raise error.ParseError(_(b"can't use a key-value pair in this context"))
561 raise error.ParseError(_(b"can't use a key-value pair in this context"))
562
562
563
563
564 def buildlist(exp, context):
564 def buildlist(exp, context):
565 raise error.ParseError(
565 raise error.ParseError(
566 _(b"can't use a list in this context"),
566 _(b"can't use a list in this context"),
567 hint=_(b'check place of comma and parens'),
567 hint=_(b'check place of comma and parens'),
568 )
568 )
569
569
570
570
571 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
571 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
572 exprmethods = {
572 exprmethods = {
573 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
573 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
574 b"string": lambda e, c: (templateutil.runstring, e[1]),
574 b"string": lambda e, c: (templateutil.runstring, e[1]),
575 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
575 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
576 b"template": buildtemplate,
576 b"template": buildtemplate,
577 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
577 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
578 b".": buildmember,
578 b".": buildmember,
579 b"|": buildfilter,
579 b"|": buildfilter,
580 b"%": buildmap,
580 b"%": buildmap,
581 b"func": buildfunc,
581 b"func": buildfunc,
582 b"keyvalue": buildkeyvaluepair,
582 b"keyvalue": buildkeyvaluepair,
583 b"list": buildlist,
583 b"list": buildlist,
584 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
584 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
585 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
585 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
586 b"negate": buildnegate,
586 b"negate": buildnegate,
587 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
587 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
588 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
588 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
589 }
589 }
590
590
591 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
591 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
592 methods = exprmethods.copy()
592 methods = exprmethods.copy()
593 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
593 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
594
594
595
595
596 class _aliasrules(parser.basealiasrules):
596 class _aliasrules(parser.basealiasrules):
597 """Parsing and expansion rule set of template aliases"""
597 """Parsing and expansion rule set of template aliases"""
598
598
599 _section = _(b'template alias')
599 _section = _(b'template alias')
600 _parse = staticmethod(_parseexpr)
600 _parse = staticmethod(_parseexpr)
601
601
602 @staticmethod
602 @staticmethod
603 def _trygetfunc(tree):
603 def _trygetfunc(tree):
604 """Return (name, args) if tree is func(...) or ...|filter; otherwise
604 """Return (name, args) if tree is func(...) or ...|filter; otherwise
605 None"""
605 None"""
606 if tree[0] == b'func' and tree[1][0] == b'symbol':
606 if tree[0] == b'func' and tree[1][0] == b'symbol':
607 return tree[1][1], getlist(tree[2])
607 return tree[1][1], getlist(tree[2])
608 if tree[0] == b'|' and tree[2][0] == b'symbol':
608 if tree[0] == b'|' and tree[2][0] == b'symbol':
609 return tree[2][1], [tree[1]]
609 return tree[2][1], [tree[1]]
610
610
611
611
612 def expandaliases(tree, aliases):
612 def expandaliases(tree, aliases):
613 """Return new tree of aliases are expanded"""
613 """Return new tree of aliases are expanded"""
614 aliasmap = _aliasrules.buildmap(aliases)
614 aliasmap = _aliasrules.buildmap(aliases)
615 return _aliasrules.expand(aliasmap, tree)
615 return _aliasrules.expand(aliasmap, tree)
616
616
617
617
618 # template engine
618 # template engine
619
619
620
620
621 def unquotestring(s):
621 def unquotestring(s):
622 '''unwrap quotes if any; otherwise returns unmodified string'''
622 '''unwrap quotes if any; otherwise returns unmodified string'''
623 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
623 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
624 return s
624 return s
625 return s[1:-1]
625 return s[1:-1]
626
626
627
627
628 class resourcemapper(object): # pytype: disable=ignored-metaclass
628 class resourcemapper(object): # pytype: disable=ignored-metaclass
629 """Mapper of internal template resources"""
629 """Mapper of internal template resources"""
630
630
631 __metaclass__ = abc.ABCMeta
631 __metaclass__ = abc.ABCMeta
632
632
633 @abc.abstractmethod
633 @abc.abstractmethod
634 def availablekeys(self, mapping):
634 def availablekeys(self, mapping):
635 """Return a set of available resource keys based on the given mapping"""
635 """Return a set of available resource keys based on the given mapping"""
636
636
637 @abc.abstractmethod
637 @abc.abstractmethod
638 def knownkeys(self):
638 def knownkeys(self):
639 """Return a set of supported resource keys"""
639 """Return a set of supported resource keys"""
640
640
641 @abc.abstractmethod
641 @abc.abstractmethod
642 def lookup(self, mapping, key):
642 def lookup(self, mapping, key):
643 """Return a resource for the key if available; otherwise None"""
643 """Return a resource for the key if available; otherwise None"""
644
644
645 @abc.abstractmethod
645 @abc.abstractmethod
646 def populatemap(self, context, origmapping, newmapping):
646 def populatemap(self, context, origmapping, newmapping):
647 """Return a dict of additional mapping items which should be paired
647 """Return a dict of additional mapping items which should be paired
648 with the given new mapping"""
648 with the given new mapping"""
649
649
650
650
651 class nullresourcemapper(resourcemapper):
651 class nullresourcemapper(resourcemapper):
652 def availablekeys(self, mapping):
652 def availablekeys(self, mapping):
653 return set()
653 return set()
654
654
655 def knownkeys(self):
655 def knownkeys(self):
656 return set()
656 return set()
657
657
658 def lookup(self, mapping, key):
658 def lookup(self, mapping, key):
659 return None
659 return None
660
660
661 def populatemap(self, context, origmapping, newmapping):
661 def populatemap(self, context, origmapping, newmapping):
662 return {}
662 return {}
663
663
664
664
665 class engine(object):
665 class engine(object):
666 """template expansion engine.
666 """template expansion engine.
667
667
668 template expansion works like this. a map file contains key=value
668 template expansion works like this. a map file contains key=value
669 pairs. if value is quoted, it is treated as string. otherwise, it
669 pairs. if value is quoted, it is treated as string. otherwise, it
670 is treated as name of template file.
670 is treated as name of template file.
671
671
672 templater is asked to expand a key in map. it looks up key, and
672 templater is asked to expand a key in map. it looks up key, and
673 looks for strings like this: {foo}. it expands {foo} by looking up
673 looks for strings like this: {foo}. it expands {foo} by looking up
674 foo in map, and substituting it. expansion is recursive: it stops
674 foo in map, and substituting it. expansion is recursive: it stops
675 when there is no more {foo} to replace.
675 when there is no more {foo} to replace.
676
676
677 expansion also allows formatting and filtering.
677 expansion also allows formatting and filtering.
678
678
679 format uses key to expand each item in list. syntax is
679 format uses key to expand each item in list. syntax is
680 {key%format}.
680 {key%format}.
681
681
682 filter uses function to transform value. syntax is
682 filter uses function to transform value. syntax is
683 {key|filter1|filter2|...}."""
683 {key|filter1|filter2|...}."""
684
684
685 def __init__(self, loader, filters=None, defaults=None, resources=None):
685 def __init__(self, loader, filters=None, defaults=None, resources=None):
686 self._loader = loader
686 self._loader = loader
687 if filters is None:
687 if filters is None:
688 filters = {}
688 filters = {}
689 self._filters = filters
689 self._filters = filters
690 self._funcs = templatefuncs.funcs # make this a parameter if needed
690 self._funcs = templatefuncs.funcs # make this a parameter if needed
691 if defaults is None:
691 if defaults is None:
692 defaults = {}
692 defaults = {}
693 if resources is None:
693 if resources is None:
694 resources = nullresourcemapper()
694 resources = nullresourcemapper()
695 self._defaults = defaults
695 self._defaults = defaults
696 self._resources = resources
696 self._resources = resources
697 self._cache = {} # key: (func, data)
697 self._cache = {} # key: (func, data)
698 self._tmplcache = {} # literal template: (func, data)
698 self._tmplcache = {} # literal template: (func, data)
699
699
700 def overlaymap(self, origmapping, newmapping):
700 def overlaymap(self, origmapping, newmapping):
701 """Create combined mapping from the original mapping and partial
701 """Create combined mapping from the original mapping and partial
702 mapping to override the original"""
702 mapping to override the original"""
703 # do not copy symbols which overrides the defaults depending on
703 # do not copy symbols which overrides the defaults depending on
704 # new resources, so the defaults will be re-evaluated (issue5612)
704 # new resources, so the defaults will be re-evaluated (issue5612)
705 knownres = self._resources.knownkeys()
705 knownres = self._resources.knownkeys()
706 newres = self._resources.availablekeys(newmapping)
706 newres = self._resources.availablekeys(newmapping)
707 mapping = {
707 mapping = {
708 k: v
708 k: v
709 for k, v in pycompat.iteritems(origmapping)
709 for k, v in pycompat.iteritems(origmapping)
710 if (
710 if (
711 k in knownres # not a symbol per self.symbol()
711 k in knownres # not a symbol per self.symbol()
712 or newres.isdisjoint(self._defaultrequires(k))
712 or newres.isdisjoint(self._defaultrequires(k))
713 )
713 )
714 }
714 }
715 mapping.update(newmapping)
715 mapping.update(newmapping)
716 mapping.update(
716 mapping.update(
717 self._resources.populatemap(self, origmapping, newmapping)
717 self._resources.populatemap(self, origmapping, newmapping)
718 )
718 )
719 return mapping
719 return mapping
720
720
721 def _defaultrequires(self, key):
721 def _defaultrequires(self, key):
722 """Resource keys required by the specified default symbol function"""
722 """Resource keys required by the specified default symbol function"""
723 v = self._defaults.get(key)
723 v = self._defaults.get(key)
724 if v is None or not callable(v):
724 if v is None or not callable(v):
725 return ()
725 return ()
726 return getattr(v, '_requires', ())
726 return getattr(v, '_requires', ())
727
727
728 def symbol(self, mapping, key):
728 def symbol(self, mapping, key):
729 """Resolve symbol to value or function; None if nothing found"""
729 """Resolve symbol to value or function; None if nothing found"""
730 v = None
730 v = None
731 if key not in self._resources.knownkeys():
731 if key not in self._resources.knownkeys():
732 v = mapping.get(key)
732 v = mapping.get(key)
733 if v is None:
733 if v is None:
734 v = self._defaults.get(key)
734 v = self._defaults.get(key)
735 return v
735 return v
736
736
737 def availableresourcekeys(self, mapping):
737 def availableresourcekeys(self, mapping):
738 """Return a set of available resource keys based on the given mapping"""
738 """Return a set of available resource keys based on the given mapping"""
739 return self._resources.availablekeys(mapping)
739 return self._resources.availablekeys(mapping)
740
740
741 def knownresourcekeys(self):
741 def knownresourcekeys(self):
742 """Return a set of supported resource keys"""
742 """Return a set of supported resource keys"""
743 return self._resources.knownkeys()
743 return self._resources.knownkeys()
744
744
745 def resource(self, mapping, key):
745 def resource(self, mapping, key):
746 """Return internal data (e.g. cache) used for keyword/function
746 """Return internal data (e.g. cache) used for keyword/function
747 evaluation"""
747 evaluation"""
748 v = self._resources.lookup(mapping, key)
748 v = self._resources.lookup(mapping, key)
749 if v is None:
749 if v is None:
750 raise templateutil.ResourceUnavailable(
750 raise templateutil.ResourceUnavailable(
751 _(b'template resource not available: %s') % key
751 _(b'template resource not available: %s') % key
752 )
752 )
753 return v
753 return v
754
754
755 def _load(self, t):
755 def _load(self, t):
756 '''load, parse, and cache a template'''
756 '''load, parse, and cache a template'''
757 if t not in self._cache:
757 if t not in self._cache:
758 x = self._loader(t)
758 x = self._loader(t)
759 # put poison to cut recursion while compiling 't'
759 # put poison to cut recursion while compiling 't'
760 self._cache[t] = (_runrecursivesymbol, t)
760 self._cache[t] = (_runrecursivesymbol, t)
761 try:
761 try:
762 self._cache[t] = compileexp(x, self, methods)
762 self._cache[t] = compileexp(x, self, methods)
763 except: # re-raises
763 except: # re-raises
764 del self._cache[t]
764 del self._cache[t]
765 raise
765 raise
766 return self._cache[t]
766 return self._cache[t]
767
767
768 def _parse(self, tmpl):
768 def _parse(self, tmpl):
769 """Parse and cache a literal template"""
769 """Parse and cache a literal template"""
770 if tmpl not in self._tmplcache:
770 if tmpl not in self._tmplcache:
771 x = parse(tmpl)
771 x = parse(tmpl)
772 self._tmplcache[tmpl] = compileexp(x, self, methods)
772 self._tmplcache[tmpl] = compileexp(x, self, methods)
773 return self._tmplcache[tmpl]
773 return self._tmplcache[tmpl]
774
774
775 def preload(self, t):
775 def preload(self, t):
776 """Load, parse, and cache the specified template if available"""
776 """Load, parse, and cache the specified template if available"""
777 try:
777 try:
778 self._load(t)
778 self._load(t)
779 return True
779 return True
780 except templateutil.TemplateNotFound:
780 except templateutil.TemplateNotFound:
781 return False
781 return False
782
782
783 def process(self, t, mapping):
783 def process(self, t, mapping):
784 """Perform expansion. t is name of map element to expand.
784 """Perform expansion. t is name of map element to expand.
785 mapping contains added elements for use during expansion. Is a
785 mapping contains added elements for use during expansion. Is a
786 generator."""
786 generator."""
787 func, data = self._load(t)
787 func, data = self._load(t)
788 return self._expand(func, data, mapping)
788 return self._expand(func, data, mapping)
789
789
790 def expand(self, tmpl, mapping):
790 def expand(self, tmpl, mapping):
791 """Perform expansion over a literal template
791 """Perform expansion over a literal template
792
792
793 No user aliases will be expanded since this is supposed to be called
793 No user aliases will be expanded since this is supposed to be called
794 with an internal template string.
794 with an internal template string.
795 """
795 """
796 func, data = self._parse(tmpl)
796 func, data = self._parse(tmpl)
797 return self._expand(func, data, mapping)
797 return self._expand(func, data, mapping)
798
798
799 def _expand(self, func, data, mapping):
799 def _expand(self, func, data, mapping):
800 # populate additional items only if they don't exist in the given
800 # populate additional items only if they don't exist in the given
801 # mapping. this is slightly different from overlaymap() because the
801 # mapping. this is slightly different from overlaymap() because the
802 # initial 'revcache' may contain pre-computed items.
802 # initial 'revcache' may contain pre-computed items.
803 extramapping = self._resources.populatemap(self, {}, mapping)
803 extramapping = self._resources.populatemap(self, {}, mapping)
804 if extramapping:
804 if extramapping:
805 extramapping.update(mapping)
805 extramapping.update(mapping)
806 mapping = extramapping
806 mapping = extramapping
807 return templateutil.flatten(self, mapping, func(self, mapping, data))
807 return templateutil.flatten(self, mapping, func(self, mapping, data))
808
808
809
809
810 def stylelist():
810 def stylelist():
811 path = templatedir()
811 path = templatedir()
812 if not path:
812 if not path:
813 return _(b'no templates found, try `hg debuginstall` for more info')
813 return _(b'no templates found, try `hg debuginstall` for more info')
814 dirlist = os.listdir(path)
814 dirlist = os.listdir(path)
815 stylelist = []
815 stylelist = []
816 for file in dirlist:
816 for file in dirlist:
817 split = file.split(b".")
817 split = file.split(b".")
818 if split[-1] in (b'orig', b'rej'):
818 if split[-1] in (b'orig', b'rej'):
819 continue
819 continue
820 if split[0] == b"map-cmdline":
820 if split[0] == b"map-cmdline":
821 stylelist.append(split[1])
821 stylelist.append(split[1])
822 return b", ".join(sorted(stylelist))
822 return b", ".join(sorted(stylelist))
823
823
824
824
825 def _open_mapfile(mapfile):
825 def _open_mapfile(mapfile):
826 if os.path.exists(mapfile):
826 if os.path.exists(mapfile):
827 return util.posixfile(mapfile, b'rb')
827 return util.posixfile(mapfile, b'rb')
828 raise error.Abort(
828 raise error.Abort(
829 _(b"style '%s' not found") % mapfile,
829 _(b"style '%s' not found") % mapfile,
830 hint=_(b"available styles: %s") % stylelist(),
830 hint=_(b"available styles: %s") % stylelist(),
831 )
831 )
832
832
833
833
834 def _readmapfile(fp, mapfile):
834 def _readmapfile(fp, mapfile):
835 """Load template elements from the given map file"""
835 """Load template elements from the given map file"""
836 base = os.path.dirname(mapfile)
836 base = os.path.dirname(mapfile)
837 conf = config.config()
837 conf = config.config()
838
838
839 def include(rel, remap, sections):
839 def include(rel, remap, sections):
840 subresource = None
840 subresource = None
841 if base:
841 if base:
842 abs = os.path.normpath(os.path.join(base, rel))
842 abs = os.path.normpath(os.path.join(base, rel))
843 if os.path.isfile(abs):
843 if os.path.isfile(abs):
844 subresource = util.posixfile(abs, b'rb')
844 subresource = util.posixfile(abs, b'rb')
845 if not subresource:
845 if not subresource:
846 if pycompat.ossep not in rel:
846 if pycompat.ossep not in rel:
847 abs = rel
847 abs = rel
848 subresource = resourceutil.open_resource(
848 subresource = resourceutil.open_resource(
849 b'mercurial.templates', rel
849 b'mercurial.templates', rel
850 )
850 )
851 else:
851 else:
852 dir = templatedir()
852 dir = templatedir()
853 if dir:
853 if dir:
854 abs = os.path.normpath(os.path.join(dir, rel))
854 abs = os.path.normpath(os.path.join(dir, rel))
855 if os.path.isfile(abs):
855 if os.path.isfile(abs):
856 subresource = util.posixfile(abs, b'rb')
856 subresource = util.posixfile(abs, b'rb')
857 if subresource:
857 if subresource:
858 data = subresource.read()
858 data = subresource.read()
859 conf.parse(
859 conf.parse(
860 abs,
860 abs,
861 data,
861 data,
862 sections=sections,
862 sections=sections,
863 remap=remap,
863 remap=remap,
864 include=include,
864 include=include,
865 )
865 )
866
866
867 data = fp.read()
867 data = fp.read()
868 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
868 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
869
869
870 cache = {}
870 cache = {}
871 tmap = {}
871 tmap = {}
872 aliases = []
872 aliases = []
873
873
874 val = conf.get(b'templates', b'__base__')
874 val = conf.get(b'templates', b'__base__')
875 if val and val[0] not in b"'\"":
875 if val and val[0] not in b"'\"":
876 # treat as a pointer to a base class for this style
876 # treat as a pointer to a base class for this style
877 path = os.path.normpath(os.path.join(base, val))
877 path = os.path.normpath(os.path.join(base, val))
878
878
879 # fallback check in template paths
879 # fallback check in template paths
880 if not os.path.exists(path):
880 if not os.path.exists(path):
881 dir = templatedir()
881 dir = templatedir()
882 if dir is not None:
882 if dir is not None:
883 p2 = os.path.normpath(os.path.join(dir, val))
883 p2 = os.path.normpath(os.path.join(dir, val))
884 if os.path.isfile(p2):
884 if os.path.isfile(p2):
885 path = p2
885 path = p2
886 else:
886 else:
887 p3 = os.path.normpath(os.path.join(p2, b"map"))
887 p3 = os.path.normpath(os.path.join(p2, b"map"))
888 if os.path.isfile(p3):
888 if os.path.isfile(p3):
889 path = p3
889 path = p3
890
890
891 fp = _open_mapfile(path)
891 fp = _open_mapfile(path)
892 cache, tmap, aliases = _readmapfile(fp, path)
892 cache, tmap, aliases = _readmapfile(fp, path)
893
893
894 for key, val in conf.items(b'templates'):
894 for key, val in conf.items(b'templates'):
895 if not val:
895 if not val:
896 raise error.ParseError(
896 raise error.ParseError(
897 _(b'missing value'), conf.source(b'templates', key)
897 _(b'missing value'), conf.source(b'templates', key)
898 )
898 )
899 if val[0] in b"'\"":
899 if val[0] in b"'\"":
900 if val[0] != val[-1]:
900 if val[0] != val[-1]:
901 raise error.ParseError(
901 raise error.ParseError(
902 _(b'unmatched quotes'), conf.source(b'templates', key)
902 _(b'unmatched quotes'), conf.source(b'templates', key)
903 )
903 )
904 cache[key] = unquotestring(val)
904 cache[key] = unquotestring(val)
905 elif key != b'__base__':
905 elif key != b'__base__':
906 tmap[key] = os.path.join(base, val)
906 tmap[key] = os.path.join(base, val)
907 aliases.extend(conf.items(b'templatealias'))
907 aliases.extend(conf.items(b'templatealias'))
908 return cache, tmap, aliases
908 return cache, tmap, aliases
909
909
910
910
911 class loader(object):
911 class loader(object):
912 """Load template fragments optionally from a map file"""
912 """Load template fragments optionally from a map file"""
913
913
914 def __init__(self, cache, aliases):
914 def __init__(self, cache, aliases):
915 if cache is None:
915 if cache is None:
916 cache = {}
916 cache = {}
917 self.cache = cache.copy()
917 self.cache = cache.copy()
918 self._map = {}
918 self._map = {}
919 self._aliasmap = _aliasrules.buildmap(aliases)
919 self._aliasmap = _aliasrules.buildmap(aliases)
920
920
921 def __contains__(self, key):
921 def __contains__(self, key):
922 return key in self.cache or key in self._map
922 return key in self.cache or key in self._map
923
923
924 def load(self, t):
924 def load(self, t):
925 """Get parsed tree for the given template name. Use a local cache."""
925 """Get parsed tree for the given template name. Use a local cache."""
926 if t not in self.cache:
926 if t not in self.cache:
927 try:
927 try:
928 mapfile, fp = open_template(self._map[t])
928 mapfile, fp = open_template(self._map[t])
929 self.cache[t] = fp.read()
929 self.cache[t] = fp.read()
930 except KeyError as inst:
930 except KeyError as inst:
931 raise templateutil.TemplateNotFound(
931 raise templateutil.TemplateNotFound(
932 _(b'"%s" not in template map') % inst.args[0]
932 _(b'"%s" not in template map') % inst.args[0]
933 )
933 )
934 except IOError as inst:
934 except IOError as inst:
935 reason = _(b'template file %s: %s') % (
935 reason = _(b'template file %s: %s') % (
936 self._map[t],
936 self._map[t],
937 stringutil.forcebytestr(inst.args[1]),
937 stringutil.forcebytestr(inst.args[1]),
938 )
938 )
939 raise IOError(inst.args[0], encoding.strfromlocal(reason))
939 raise IOError(inst.args[0], encoding.strfromlocal(reason))
940 return self._parse(self.cache[t])
940 return self._parse(self.cache[t])
941
941
942 def _parse(self, tmpl):
942 def _parse(self, tmpl):
943 x = parse(tmpl)
943 x = parse(tmpl)
944 if self._aliasmap:
944 if self._aliasmap:
945 x = _aliasrules.expand(self._aliasmap, x)
945 x = _aliasrules.expand(self._aliasmap, x)
946 return x
946 return x
947
947
948 def _findsymbolsused(self, tree, syms):
948 def _findsymbolsused(self, tree, syms):
949 if not tree:
949 if not tree:
950 return
950 return
951 op = tree[0]
951 op = tree[0]
952 if op == b'symbol':
952 if op == b'symbol':
953 s = tree[1]
953 s = tree[1]
954 if s in syms[0]:
954 if s in syms[0]:
955 return # avoid recursion: s -> cache[s] -> s
955 return # avoid recursion: s -> cache[s] -> s
956 syms[0].add(s)
956 syms[0].add(s)
957 if s in self.cache or s in self._map:
957 if s in self.cache or s in self._map:
958 # s may be a reference for named template
958 # s may be a reference for named template
959 self._findsymbolsused(self.load(s), syms)
959 self._findsymbolsused(self.load(s), syms)
960 return
960 return
961 if op in {b'integer', b'string'}:
961 if op in {b'integer', b'string'}:
962 return
962 return
963 # '{arg|func}' == '{func(arg)}'
963 # '{arg|func}' == '{func(arg)}'
964 if op == b'|':
964 if op == b'|':
965 syms[1].add(getsymbol(tree[2]))
965 syms[1].add(getsymbol(tree[2]))
966 self._findsymbolsused(tree[1], syms)
966 self._findsymbolsused(tree[1], syms)
967 return
967 return
968 if op == b'func':
968 if op == b'func':
969 syms[1].add(getsymbol(tree[1]))
969 syms[1].add(getsymbol(tree[1]))
970 self._findsymbolsused(tree[2], syms)
970 self._findsymbolsused(tree[2], syms)
971 return
971 return
972 for x in tree[1:]:
972 for x in tree[1:]:
973 self._findsymbolsused(x, syms)
973 self._findsymbolsused(x, syms)
974
974
975 def symbolsused(self, t):
975 def symbolsused(self, t):
976 """Look up (keywords, filters/functions) referenced from the name
976 """Look up (keywords, filters/functions) referenced from the name
977 template 't'
977 template 't'
978
978
979 This may load additional templates from the map file.
979 This may load additional templates from the map file.
980 """
980 """
981 syms = (set(), set())
981 syms = (set(), set())
982 self._findsymbolsused(self.load(t), syms)
982 self._findsymbolsused(self.load(t), syms)
983 return syms
983 return syms
984
984
985
985
986 class templater(object):
986 class templater(object):
987 def __init__(
987 def __init__(
988 self,
988 self,
989 filters=None,
989 filters=None,
990 defaults=None,
990 defaults=None,
991 resources=None,
991 resources=None,
992 cache=None,
992 cache=None,
993 aliases=(),
993 aliases=(),
994 minchunk=1024,
994 minchunk=1024,
995 maxchunk=65536,
995 maxchunk=65536,
996 ):
996 ):
997 """Create template engine optionally with preloaded template fragments
997 """Create template engine optionally with preloaded template fragments
998
998
999 - ``filters``: a dict of functions to transform a value into another.
999 - ``filters``: a dict of functions to transform a value into another.
1000 - ``defaults``: a dict of symbol values/functions; may be overridden
1000 - ``defaults``: a dict of symbol values/functions; may be overridden
1001 by a ``mapping`` dict.
1001 by a ``mapping`` dict.
1002 - ``resources``: a resourcemapper object to look up internal data
1002 - ``resources``: a resourcemapper object to look up internal data
1003 (e.g. cache), inaccessible from user template.
1003 (e.g. cache), inaccessible from user template.
1004 - ``cache``: a dict of preloaded template fragments.
1004 - ``cache``: a dict of preloaded template fragments.
1005 - ``aliases``: a list of alias (name, replacement) pairs.
1005 - ``aliases``: a list of alias (name, replacement) pairs.
1006
1006
1007 self.cache may be updated later to register additional template
1007 self.cache may be updated later to register additional template
1008 fragments.
1008 fragments.
1009 """
1009 """
1010 allfilters = templatefilters.filters.copy()
1010 allfilters = templatefilters.filters.copy()
1011 if filters:
1011 if filters:
1012 allfilters.update(filters)
1012 allfilters.update(filters)
1013 self._loader = loader(cache, aliases)
1013 self._loader = loader(cache, aliases)
1014 self._proc = engine(self._loader.load, allfilters, defaults, resources)
1014 self._proc = engine(self._loader.load, allfilters, defaults, resources)
1015 self._minchunk, self._maxchunk = minchunk, maxchunk
1015 self._minchunk, self._maxchunk = minchunk, maxchunk
1016
1016
1017 @classmethod
1017 @classmethod
1018 def frommapfile(
1018 def frommapfile(
1019 cls,
1019 cls,
1020 mapfile,
1020 mapfile,
1021 fp=None,
1021 fp=None,
1022 filters=None,
1022 filters=None,
1023 defaults=None,
1023 defaults=None,
1024 resources=None,
1024 resources=None,
1025 cache=None,
1025 cache=None,
1026 minchunk=1024,
1026 minchunk=1024,
1027 maxchunk=65536,
1027 maxchunk=65536,
1028 ):
1028 ):
1029 """Create templater from the specified map file"""
1029 """Create templater from the specified map file"""
1030 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1030 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1031 if not fp:
1031 if not fp:
1032 fp = _open_mapfile(mapfile)
1032 fp = _open_mapfile(mapfile)
1033 cache, tmap, aliases = _readmapfile(fp, mapfile)
1033 cache, tmap, aliases = _readmapfile(fp, mapfile)
1034 t._loader.cache.update(cache)
1034 t._loader.cache.update(cache)
1035 t._loader._map = tmap
1035 t._loader._map = tmap
1036 t._loader._aliasmap = _aliasrules.buildmap(aliases)
1036 t._loader._aliasmap = _aliasrules.buildmap(aliases)
1037 return t
1037 return t
1038
1038
1039 def __contains__(self, key):
1039 def __contains__(self, key):
1040 return key in self._loader
1040 return key in self._loader
1041
1041
1042 @property
1042 @property
1043 def cache(self):
1043 def cache(self):
1044 return self._loader.cache
1044 return self._loader.cache
1045
1045
1046 # for highlight extension to insert one-time 'colorize' filter
1046 # for highlight extension to insert one-time 'colorize' filter
1047 @property
1047 @property
1048 def _filters(self):
1048 def _filters(self):
1049 return self._proc._filters
1049 return self._proc._filters
1050
1050
1051 @property
1051 @property
1052 def defaults(self):
1052 def defaults(self):
1053 return self._proc._defaults
1053 return self._proc._defaults
1054
1054
1055 def load(self, t):
1055 def load(self, t):
1056 """Get parsed tree for the given template name. Use a local cache."""
1056 """Get parsed tree for the given template name. Use a local cache."""
1057 return self._loader.load(t)
1057 return self._loader.load(t)
1058
1058
1059 def symbolsuseddefault(self):
1059 def symbolsuseddefault(self):
1060 """Look up (keywords, filters/functions) referenced from the default
1060 """Look up (keywords, filters/functions) referenced from the default
1061 unnamed template
1061 unnamed template
1062
1062
1063 This may load additional templates from the map file.
1063 This may load additional templates from the map file.
1064 """
1064 """
1065 return self.symbolsused(b'')
1065 return self.symbolsused(b'')
1066
1066
1067 def symbolsused(self, t):
1067 def symbolsused(self, t):
1068 """Look up (keywords, filters/functions) referenced from the name
1068 """Look up (keywords, filters/functions) referenced from the name
1069 template 't'
1069 template 't'
1070
1070
1071 This may load additional templates from the map file.
1071 This may load additional templates from the map file.
1072 """
1072 """
1073 return self._loader.symbolsused(t)
1073 return self._loader.symbolsused(t)
1074
1074
1075 def renderdefault(self, mapping):
1075 def renderdefault(self, mapping):
1076 """Render the default unnamed template and return result as string"""
1076 """Render the default unnamed template and return result as string"""
1077 return self.render(b'', mapping)
1077 return self.render(b'', mapping)
1078
1078
1079 def render(self, t, mapping):
1079 def render(self, t, mapping):
1080 """Render the specified named template and return result as string"""
1080 """Render the specified named template and return result as string"""
1081 return b''.join(self.generate(t, mapping))
1081 return b''.join(self.generate(t, mapping))
1082
1082
1083 def generate(self, t, mapping):
1083 def generate(self, t, mapping):
1084 """Return a generator that renders the specified named template and
1084 """Return a generator that renders the specified named template and
1085 yields chunks"""
1085 yields chunks"""
1086 stream = self._proc.process(t, mapping)
1086 stream = self._proc.process(t, mapping)
1087 if self._minchunk:
1087 if self._minchunk:
1088 stream = util.increasingchunks(
1088 stream = util.increasingchunks(
1089 stream, min=self._minchunk, max=self._maxchunk
1089 stream, min=self._minchunk, max=self._maxchunk
1090 )
1090 )
1091 return stream
1091 return stream
1092
1092
1093
1093
1094 def templatedir():
1094 def templatedir():
1095 '''return the directory used for template files, or None.'''
1095 '''return the directory used for template files, or None.'''
1096 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1096 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1097 return path if os.path.isdir(path) else None
1097 return path if os.path.isdir(path) else None
1098
1098
1099
1099
1100 def open_template(name, templatepath=None):
1100 def open_template(name, templatepath=None):
1101 """returns a file-like object for the given template, and its full path
1101 """returns a file-like object for the given template, and its full path
1102
1102
1103 If the name is a relative path and we're in a frozen binary, the template
1103 If the name is a relative path and we're in a frozen binary, the template
1104 will be read from the mercurial.templates package instead. The returned path
1104 will be read from the mercurial.templates package instead. The returned path
1105 will then be the relative path.
1105 will then be the relative path.
1106 """
1106 """
1107 # Does the name point directly to a map file?
1107 # Does the name point directly to a map file?
1108 if os.path.isfile(name) or os.path.isabs(name):
1108 if os.path.isfile(name) or os.path.isabs(name):
1109 return name, open(name, mode='rb')
1109 return name, open(name, mode='rb')
1110
1110
1111 # Does the name point to a template in the provided templatepath, or
1111 # Does the name point to a template in the provided templatepath, or
1112 # in mercurial/templates/ if no path was provided?
1112 # in mercurial/templates/ if no path was provided?
1113 if templatepath is None:
1113 if templatepath is None:
1114 templatepath = templatedir()
1114 templatepath = templatedir()
1115 if templatepath is not None:
1115 if templatepath is not None:
1116 f = os.path.join(templatepath, name)
1116 f = os.path.join(templatepath, name)
1117 return f, open(f, mode='rb')
1117 return f, open(f, mode='rb')
1118
1118
1119 # Otherwise try to read it using the resources API
1119 # Otherwise try to read it using the resources API
1120 if pycompat.iswindows:
1121 # quick hack to make sure we can process '/' in the code dealing with
1122 # ressource. Ideally we would make sure we use `/` instead of `ossep`
1123 # in the templater code, but that seems a bigger and less certain
1124 # change that we better left for the default branch.
1125 name_paths = name.split(pycompat.ossep)
1126 name = b'/'.join(name_paths)
1120 name_parts = name.split(b'/')
1127 name_parts = name.split(b'/')
1121 package_name = b'.'.join([b'mercurial', b'templates'] + name_parts[:-1])
1128 package_name = b'.'.join([b'mercurial', b'templates'] + name_parts[:-1])
1122 return (
1129 return (
1123 name,
1130 name,
1124 resourceutil.open_resource(package_name, name_parts[-1]),
1131 resourceutil.open_resource(package_name, name_parts[-1]),
1125 )
1132 )
1126
1133
1127
1134
1128 def try_open_template(name, templatepath=None):
1135 def try_open_template(name, templatepath=None):
1129 try:
1136 try:
1130 return open_template(name, templatepath)
1137 return open_template(name, templatepath)
1131 except (EnvironmentError, ImportError):
1138 except (EnvironmentError, ImportError):
1132 return None, None
1139 return None, None
General Comments 0
You need to be logged in to leave comments. Login now