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