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