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