##// END OF EJS Templates
templater: adjust binding strengths to make room for key-value operator...
Yuya Nishihara -
r31884:0926ca37 default
parent child Browse files
Show More
@@ -1,1295 +1,1295
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 parser,
21 parser,
22 pycompat,
22 pycompat,
23 registrar,
23 registrar,
24 revset as revsetmod,
24 revset as revsetmod,
25 revsetlang,
25 revsetlang,
26 templatefilters,
26 templatefilters,
27 templatekw,
27 templatekw,
28 util,
28 util,
29 )
29 )
30
30
31 # template parsing
31 # template parsing
32
32
33 elements = {
33 elements = {
34 # token-type: binding-strength, primary, prefix, infix, suffix
34 # token-type: binding-strength, primary, prefix, infix, suffix
35 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
35 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
36 "%": (6, None, None, ("%", 6), None),
36 "%": (16, None, None, ("%", 16), None),
37 "|": (5, None, None, ("|", 5), None),
37 "|": (15, None, None, ("|", 15), None),
38 "*": (4, None, None, ("*", 4), None),
38 "*": (5, None, None, ("*", 5), None),
39 "/": (4, None, None, ("/", 4), None),
39 "/": (5, None, None, ("/", 5), None),
40 "+": (3, None, None, ("+", 3), None),
40 "+": (4, None, None, ("+", 4), None),
41 "-": (3, None, ("negate", 10), ("-", 3), None),
41 "-": (4, None, ("negate", 19), ("-", 4), None),
42 ",": (2, None, None, ("list", 2), None),
42 ",": (2, None, None, ("list", 2), None),
43 ")": (0, None, None, None, None),
43 ")": (0, None, None, None, None),
44 "integer": (0, "integer", None, None, None),
44 "integer": (0, "integer", None, None, None),
45 "symbol": (0, "symbol", None, None, None),
45 "symbol": (0, "symbol", None, None, None),
46 "string": (0, "string", None, None, None),
46 "string": (0, "string", None, None, None),
47 "template": (0, "template", None, None, None),
47 "template": (0, "template", None, None, None),
48 "end": (0, None, None, None, None),
48 "end": (0, None, None, None, None),
49 }
49 }
50
50
51 def tokenize(program, start, end, term=None):
51 def tokenize(program, start, end, term=None):
52 """Parse a template expression into a stream of tokens, which must end
52 """Parse a template expression into a stream of tokens, which must end
53 with term if specified"""
53 with term if specified"""
54 pos = start
54 pos = start
55 while pos < end:
55 while pos < end:
56 c = program[pos]
56 c = program[pos]
57 if c.isspace(): # skip inter-token whitespace
57 if c.isspace(): # skip inter-token whitespace
58 pass
58 pass
59 elif c in "(,)%|+-*/": # handle simple operators
59 elif c in "(,)%|+-*/": # handle simple operators
60 yield (c, None, pos)
60 yield (c, None, pos)
61 elif c in '"\'': # handle quoted templates
61 elif c in '"\'': # handle quoted templates
62 s = pos + 1
62 s = pos + 1
63 data, pos = _parsetemplate(program, s, end, c)
63 data, pos = _parsetemplate(program, s, end, c)
64 yield ('template', data, s)
64 yield ('template', data, s)
65 pos -= 1
65 pos -= 1
66 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
66 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
67 # handle quoted strings
67 # handle quoted strings
68 c = program[pos + 1]
68 c = program[pos + 1]
69 s = pos = pos + 2
69 s = pos = pos + 2
70 while pos < end: # find closing quote
70 while pos < end: # find closing quote
71 d = program[pos]
71 d = program[pos]
72 if d == '\\': # skip over escaped characters
72 if d == '\\': # skip over escaped characters
73 pos += 2
73 pos += 2
74 continue
74 continue
75 if d == c:
75 if d == c:
76 yield ('string', program[s:pos], s)
76 yield ('string', program[s:pos], s)
77 break
77 break
78 pos += 1
78 pos += 1
79 else:
79 else:
80 raise error.ParseError(_("unterminated string"), s)
80 raise error.ParseError(_("unterminated string"), s)
81 elif c.isdigit():
81 elif c.isdigit():
82 s = pos
82 s = pos
83 while pos < end:
83 while pos < end:
84 d = program[pos]
84 d = program[pos]
85 if not d.isdigit():
85 if not d.isdigit():
86 break
86 break
87 pos += 1
87 pos += 1
88 yield ('integer', program[s:pos], s)
88 yield ('integer', program[s:pos], s)
89 pos -= 1
89 pos -= 1
90 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
90 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
91 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
91 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
92 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
92 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
93 # where some of nested templates were preprocessed as strings and
93 # where some of nested templates were preprocessed as strings and
94 # then compiled. therefore, \"...\" was allowed. (issue4733)
94 # then compiled. therefore, \"...\" was allowed. (issue4733)
95 #
95 #
96 # processing flow of _evalifliteral() at 5ab28a2e9962:
96 # processing flow of _evalifliteral() at 5ab28a2e9962:
97 # outer template string -> stringify() -> compiletemplate()
97 # outer template string -> stringify() -> compiletemplate()
98 # ------------------------ ------------ ------------------
98 # ------------------------ ------------ ------------------
99 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
99 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
100 # ~~~~~~~~
100 # ~~~~~~~~
101 # escaped quoted string
101 # escaped quoted string
102 if c == 'r':
102 if c == 'r':
103 pos += 1
103 pos += 1
104 token = 'string'
104 token = 'string'
105 else:
105 else:
106 token = 'template'
106 token = 'template'
107 quote = program[pos:pos + 2]
107 quote = program[pos:pos + 2]
108 s = pos = pos + 2
108 s = pos = pos + 2
109 while pos < end: # find closing escaped quote
109 while pos < end: # find closing escaped quote
110 if program.startswith('\\\\\\', pos, end):
110 if program.startswith('\\\\\\', pos, end):
111 pos += 4 # skip over double escaped characters
111 pos += 4 # skip over double escaped characters
112 continue
112 continue
113 if program.startswith(quote, pos, end):
113 if program.startswith(quote, pos, end):
114 # interpret as if it were a part of an outer string
114 # interpret as if it were a part of an outer string
115 data = parser.unescapestr(program[s:pos])
115 data = parser.unescapestr(program[s:pos])
116 if token == 'template':
116 if token == 'template':
117 data = _parsetemplate(data, 0, len(data))[0]
117 data = _parsetemplate(data, 0, len(data))[0]
118 yield (token, data, s)
118 yield (token, data, s)
119 pos += 1
119 pos += 1
120 break
120 break
121 pos += 1
121 pos += 1
122 else:
122 else:
123 raise error.ParseError(_("unterminated string"), s)
123 raise error.ParseError(_("unterminated string"), s)
124 elif c.isalnum() or c in '_':
124 elif c.isalnum() or c in '_':
125 s = pos
125 s = pos
126 pos += 1
126 pos += 1
127 while pos < end: # find end of symbol
127 while pos < end: # find end of symbol
128 d = program[pos]
128 d = program[pos]
129 if not (d.isalnum() or d == "_"):
129 if not (d.isalnum() or d == "_"):
130 break
130 break
131 pos += 1
131 pos += 1
132 sym = program[s:pos]
132 sym = program[s:pos]
133 yield ('symbol', sym, s)
133 yield ('symbol', sym, s)
134 pos -= 1
134 pos -= 1
135 elif c == term:
135 elif c == term:
136 yield ('end', None, pos + 1)
136 yield ('end', None, pos + 1)
137 return
137 return
138 else:
138 else:
139 raise error.ParseError(_("syntax error"), pos)
139 raise error.ParseError(_("syntax error"), pos)
140 pos += 1
140 pos += 1
141 if term:
141 if term:
142 raise error.ParseError(_("unterminated template expansion"), start)
142 raise error.ParseError(_("unterminated template expansion"), start)
143 yield ('end', None, pos)
143 yield ('end', None, pos)
144
144
145 def _parsetemplate(tmpl, start, stop, quote=''):
145 def _parsetemplate(tmpl, start, stop, quote=''):
146 r"""
146 r"""
147 >>> _parsetemplate('foo{bar}"baz', 0, 12)
147 >>> _parsetemplate('foo{bar}"baz', 0, 12)
148 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
148 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
149 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
149 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
150 ([('string', 'foo'), ('symbol', 'bar')], 9)
150 ([('string', 'foo'), ('symbol', 'bar')], 9)
151 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
151 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
152 ([('string', 'foo')], 4)
152 ([('string', 'foo')], 4)
153 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
153 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
154 ([('string', 'foo"'), ('string', 'bar')], 9)
154 ([('string', 'foo"'), ('string', 'bar')], 9)
155 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
155 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
156 ([('string', 'foo\\')], 6)
156 ([('string', 'foo\\')], 6)
157 """
157 """
158 parsed = []
158 parsed = []
159 sepchars = '{' + quote
159 sepchars = '{' + quote
160 pos = start
160 pos = start
161 p = parser.parser(elements)
161 p = parser.parser(elements)
162 while pos < stop:
162 while pos < stop:
163 n = min((tmpl.find(c, pos, stop) for c in sepchars),
163 n = min((tmpl.find(c, pos, stop) for c in sepchars),
164 key=lambda n: (n < 0, n))
164 key=lambda n: (n < 0, n))
165 if n < 0:
165 if n < 0:
166 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
166 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
167 pos = stop
167 pos = stop
168 break
168 break
169 c = tmpl[n]
169 c = tmpl[n]
170 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
170 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
171 if bs % 2 == 1:
171 if bs % 2 == 1:
172 # escaped (e.g. '\{', '\\\{', but not '\\{')
172 # escaped (e.g. '\{', '\\\{', but not '\\{')
173 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
173 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
174 pos = n + 1
174 pos = n + 1
175 continue
175 continue
176 if n > pos:
176 if n > pos:
177 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
177 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
178 if c == quote:
178 if c == quote:
179 return parsed, n + 1
179 return parsed, n + 1
180
180
181 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
181 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
182 parsed.append(parseres)
182 parsed.append(parseres)
183
183
184 if quote:
184 if quote:
185 raise error.ParseError(_("unterminated string"), start)
185 raise error.ParseError(_("unterminated string"), start)
186 return parsed, pos
186 return parsed, pos
187
187
188 def _unnesttemplatelist(tree):
188 def _unnesttemplatelist(tree):
189 """Expand list of templates to node tuple
189 """Expand list of templates to node tuple
190
190
191 >>> def f(tree):
191 >>> def f(tree):
192 ... print prettyformat(_unnesttemplatelist(tree))
192 ... print prettyformat(_unnesttemplatelist(tree))
193 >>> f(('template', []))
193 >>> f(('template', []))
194 ('string', '')
194 ('string', '')
195 >>> f(('template', [('string', 'foo')]))
195 >>> f(('template', [('string', 'foo')]))
196 ('string', 'foo')
196 ('string', 'foo')
197 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
197 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
198 (template
198 (template
199 ('string', 'foo')
199 ('string', 'foo')
200 ('symbol', 'rev'))
200 ('symbol', 'rev'))
201 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
201 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
202 (template
202 (template
203 ('symbol', 'rev'))
203 ('symbol', 'rev'))
204 >>> f(('template', [('template', [('string', 'foo')])]))
204 >>> f(('template', [('template', [('string', 'foo')])]))
205 ('string', 'foo')
205 ('string', 'foo')
206 """
206 """
207 if not isinstance(tree, tuple):
207 if not isinstance(tree, tuple):
208 return tree
208 return tree
209 op = tree[0]
209 op = tree[0]
210 if op != 'template':
210 if op != 'template':
211 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
211 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
212
212
213 assert len(tree) == 2
213 assert len(tree) == 2
214 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
214 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
215 if not xs:
215 if not xs:
216 return ('string', '') # empty template ""
216 return ('string', '') # empty template ""
217 elif len(xs) == 1 and xs[0][0] == 'string':
217 elif len(xs) == 1 and xs[0][0] == 'string':
218 return xs[0] # fast path for string with no template fragment "x"
218 return xs[0] # fast path for string with no template fragment "x"
219 else:
219 else:
220 return (op,) + xs
220 return (op,) + xs
221
221
222 def parse(tmpl):
222 def parse(tmpl):
223 """Parse template string into tree"""
223 """Parse template string into tree"""
224 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
224 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
225 assert pos == len(tmpl), 'unquoted template should be consumed'
225 assert pos == len(tmpl), 'unquoted template should be consumed'
226 return _unnesttemplatelist(('template', parsed))
226 return _unnesttemplatelist(('template', parsed))
227
227
228 def _parseexpr(expr):
228 def _parseexpr(expr):
229 """Parse a template expression into tree
229 """Parse a template expression into tree
230
230
231 >>> _parseexpr('"foo"')
231 >>> _parseexpr('"foo"')
232 ('string', 'foo')
232 ('string', 'foo')
233 >>> _parseexpr('foo(bar)')
233 >>> _parseexpr('foo(bar)')
234 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
234 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
235 >>> _parseexpr('foo(')
235 >>> _parseexpr('foo(')
236 Traceback (most recent call last):
236 Traceback (most recent call last):
237 ...
237 ...
238 ParseError: ('not a prefix: end', 4)
238 ParseError: ('not a prefix: end', 4)
239 >>> _parseexpr('"foo" "bar"')
239 >>> _parseexpr('"foo" "bar"')
240 Traceback (most recent call last):
240 Traceback (most recent call last):
241 ...
241 ...
242 ParseError: ('invalid token', 7)
242 ParseError: ('invalid token', 7)
243 """
243 """
244 p = parser.parser(elements)
244 p = parser.parser(elements)
245 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
245 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
246 if pos != len(expr):
246 if pos != len(expr):
247 raise error.ParseError(_('invalid token'), pos)
247 raise error.ParseError(_('invalid token'), pos)
248 return _unnesttemplatelist(tree)
248 return _unnesttemplatelist(tree)
249
249
250 def prettyformat(tree):
250 def prettyformat(tree):
251 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
251 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
252
252
253 def compileexp(exp, context, curmethods):
253 def compileexp(exp, context, curmethods):
254 """Compile parsed template tree to (func, data) pair"""
254 """Compile parsed template tree to (func, data) pair"""
255 t = exp[0]
255 t = exp[0]
256 if t in curmethods:
256 if t in curmethods:
257 return curmethods[t](exp, context)
257 return curmethods[t](exp, context)
258 raise error.ParseError(_("unknown method '%s'") % t)
258 raise error.ParseError(_("unknown method '%s'") % t)
259
259
260 # template evaluation
260 # template evaluation
261
261
262 def getsymbol(exp):
262 def getsymbol(exp):
263 if exp[0] == 'symbol':
263 if exp[0] == 'symbol':
264 return exp[1]
264 return exp[1]
265 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
265 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
266
266
267 def getlist(x):
267 def getlist(x):
268 if not x:
268 if not x:
269 return []
269 return []
270 if x[0] == 'list':
270 if x[0] == 'list':
271 return getlist(x[1]) + [x[2]]
271 return getlist(x[1]) + [x[2]]
272 return [x]
272 return [x]
273
273
274 def gettemplate(exp, context):
274 def gettemplate(exp, context):
275 """Compile given template tree or load named template from map file;
275 """Compile given template tree or load named template from map file;
276 returns (func, data) pair"""
276 returns (func, data) pair"""
277 if exp[0] in ('template', 'string'):
277 if exp[0] in ('template', 'string'):
278 return compileexp(exp, context, methods)
278 return compileexp(exp, context, methods)
279 if exp[0] == 'symbol':
279 if exp[0] == 'symbol':
280 # unlike runsymbol(), here 'symbol' is always taken as template name
280 # unlike runsymbol(), here 'symbol' is always taken as template name
281 # even if it exists in mapping. this allows us to override mapping
281 # even if it exists in mapping. this allows us to override mapping
282 # by web templates, e.g. 'changelogtag' is redefined in map file.
282 # by web templates, e.g. 'changelogtag' is redefined in map file.
283 return context._load(exp[1])
283 return context._load(exp[1])
284 raise error.ParseError(_("expected template specifier"))
284 raise error.ParseError(_("expected template specifier"))
285
285
286 def evalfuncarg(context, mapping, arg):
286 def evalfuncarg(context, mapping, arg):
287 func, data = arg
287 func, data = arg
288 # func() may return string, generator of strings or arbitrary object such
288 # func() may return string, generator of strings or arbitrary object such
289 # as date tuple, but filter does not want generator.
289 # as date tuple, but filter does not want generator.
290 thing = func(context, mapping, data)
290 thing = func(context, mapping, data)
291 if isinstance(thing, types.GeneratorType):
291 if isinstance(thing, types.GeneratorType):
292 thing = stringify(thing)
292 thing = stringify(thing)
293 return thing
293 return thing
294
294
295 def evalboolean(context, mapping, arg):
295 def evalboolean(context, mapping, arg):
296 """Evaluate given argument as boolean, but also takes boolean literals"""
296 """Evaluate given argument as boolean, but also takes boolean literals"""
297 func, data = arg
297 func, data = arg
298 if func is runsymbol:
298 if func is runsymbol:
299 thing = func(context, mapping, data, default=None)
299 thing = func(context, mapping, data, default=None)
300 if thing is None:
300 if thing is None:
301 # not a template keyword, takes as a boolean literal
301 # not a template keyword, takes as a boolean literal
302 thing = util.parsebool(data)
302 thing = util.parsebool(data)
303 else:
303 else:
304 thing = func(context, mapping, data)
304 thing = func(context, mapping, data)
305 if isinstance(thing, bool):
305 if isinstance(thing, bool):
306 return thing
306 return thing
307 # other objects are evaluated as strings, which means 0 is True, but
307 # other objects are evaluated as strings, which means 0 is True, but
308 # empty dict/list should be False as they are expected to be ''
308 # empty dict/list should be False as they are expected to be ''
309 return bool(stringify(thing))
309 return bool(stringify(thing))
310
310
311 def evalinteger(context, mapping, arg, err):
311 def evalinteger(context, mapping, arg, err):
312 v = evalfuncarg(context, mapping, arg)
312 v = evalfuncarg(context, mapping, arg)
313 try:
313 try:
314 return int(v)
314 return int(v)
315 except (TypeError, ValueError):
315 except (TypeError, ValueError):
316 raise error.ParseError(err)
316 raise error.ParseError(err)
317
317
318 def evalstring(context, mapping, arg):
318 def evalstring(context, mapping, arg):
319 func, data = arg
319 func, data = arg
320 return stringify(func(context, mapping, data))
320 return stringify(func(context, mapping, data))
321
321
322 def evalstringliteral(context, mapping, arg):
322 def evalstringliteral(context, mapping, arg):
323 """Evaluate given argument as string template, but returns symbol name
323 """Evaluate given argument as string template, but returns symbol name
324 if it is unknown"""
324 if it is unknown"""
325 func, data = arg
325 func, data = arg
326 if func is runsymbol:
326 if func is runsymbol:
327 thing = func(context, mapping, data, default=data)
327 thing = func(context, mapping, data, default=data)
328 else:
328 else:
329 thing = func(context, mapping, data)
329 thing = func(context, mapping, data)
330 return stringify(thing)
330 return stringify(thing)
331
331
332 def runinteger(context, mapping, data):
332 def runinteger(context, mapping, data):
333 return int(data)
333 return int(data)
334
334
335 def runstring(context, mapping, data):
335 def runstring(context, mapping, data):
336 return data
336 return data
337
337
338 def _recursivesymbolblocker(key):
338 def _recursivesymbolblocker(key):
339 def showrecursion(**args):
339 def showrecursion(**args):
340 raise error.Abort(_("recursive reference '%s' in template") % key)
340 raise error.Abort(_("recursive reference '%s' in template") % key)
341 return showrecursion
341 return showrecursion
342
342
343 def _runrecursivesymbol(context, mapping, key):
343 def _runrecursivesymbol(context, mapping, key):
344 raise error.Abort(_("recursive reference '%s' in template") % key)
344 raise error.Abort(_("recursive reference '%s' in template") % key)
345
345
346 def runsymbol(context, mapping, key, default=''):
346 def runsymbol(context, mapping, key, default=''):
347 v = mapping.get(key)
347 v = mapping.get(key)
348 if v is None:
348 if v is None:
349 v = context._defaults.get(key)
349 v = context._defaults.get(key)
350 if v is None:
350 if v is None:
351 # put poison to cut recursion. we can't move this to parsing phase
351 # put poison to cut recursion. we can't move this to parsing phase
352 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
352 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
353 safemapping = mapping.copy()
353 safemapping = mapping.copy()
354 safemapping[key] = _recursivesymbolblocker(key)
354 safemapping[key] = _recursivesymbolblocker(key)
355 try:
355 try:
356 v = context.process(key, safemapping)
356 v = context.process(key, safemapping)
357 except TemplateNotFound:
357 except TemplateNotFound:
358 v = default
358 v = default
359 if callable(v):
359 if callable(v):
360 return v(**mapping)
360 return v(**mapping)
361 return v
361 return v
362
362
363 def buildtemplate(exp, context):
363 def buildtemplate(exp, context):
364 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
364 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
365 return (runtemplate, ctmpl)
365 return (runtemplate, ctmpl)
366
366
367 def runtemplate(context, mapping, template):
367 def runtemplate(context, mapping, template):
368 for func, data in template:
368 for func, data in template:
369 yield func(context, mapping, data)
369 yield func(context, mapping, data)
370
370
371 def buildfilter(exp, context):
371 def buildfilter(exp, context):
372 arg = compileexp(exp[1], context, methods)
372 arg = compileexp(exp[1], context, methods)
373 n = getsymbol(exp[2])
373 n = getsymbol(exp[2])
374 if n in context._filters:
374 if n in context._filters:
375 filt = context._filters[n]
375 filt = context._filters[n]
376 return (runfilter, (arg, filt))
376 return (runfilter, (arg, filt))
377 if n in funcs:
377 if n in funcs:
378 f = funcs[n]
378 f = funcs[n]
379 return (f, [arg])
379 return (f, [arg])
380 raise error.ParseError(_("unknown function '%s'") % n)
380 raise error.ParseError(_("unknown function '%s'") % n)
381
381
382 def runfilter(context, mapping, data):
382 def runfilter(context, mapping, data):
383 arg, filt = data
383 arg, filt = data
384 thing = evalfuncarg(context, mapping, arg)
384 thing = evalfuncarg(context, mapping, arg)
385 try:
385 try:
386 return filt(thing)
386 return filt(thing)
387 except (ValueError, AttributeError, TypeError):
387 except (ValueError, AttributeError, TypeError):
388 if isinstance(arg[1], tuple):
388 if isinstance(arg[1], tuple):
389 dt = arg[1][1]
389 dt = arg[1][1]
390 else:
390 else:
391 dt = arg[1]
391 dt = arg[1]
392 raise error.Abort(_("template filter '%s' is not compatible with "
392 raise error.Abort(_("template filter '%s' is not compatible with "
393 "keyword '%s'") % (filt.func_name, dt))
393 "keyword '%s'") % (filt.func_name, dt))
394
394
395 def buildmap(exp, context):
395 def buildmap(exp, context):
396 func, data = compileexp(exp[1], context, methods)
396 func, data = compileexp(exp[1], context, methods)
397 tfunc, tdata = gettemplate(exp[2], context)
397 tfunc, tdata = gettemplate(exp[2], context)
398 return (runmap, (func, data, tfunc, tdata))
398 return (runmap, (func, data, tfunc, tdata))
399
399
400 def runmap(context, mapping, data):
400 def runmap(context, mapping, data):
401 func, data, tfunc, tdata = data
401 func, data, tfunc, tdata = data
402 d = func(context, mapping, data)
402 d = func(context, mapping, data)
403 if util.safehasattr(d, 'itermaps'):
403 if util.safehasattr(d, 'itermaps'):
404 diter = d.itermaps()
404 diter = d.itermaps()
405 else:
405 else:
406 try:
406 try:
407 diter = iter(d)
407 diter = iter(d)
408 except TypeError:
408 except TypeError:
409 if func is runsymbol:
409 if func is runsymbol:
410 raise error.ParseError(_("keyword '%s' is not iterable") % data)
410 raise error.ParseError(_("keyword '%s' is not iterable") % data)
411 else:
411 else:
412 raise error.ParseError(_("%r is not iterable") % d)
412 raise error.ParseError(_("%r is not iterable") % d)
413
413
414 for i, v in enumerate(diter):
414 for i, v in enumerate(diter):
415 lm = mapping.copy()
415 lm = mapping.copy()
416 lm['index'] = i
416 lm['index'] = i
417 if isinstance(v, dict):
417 if isinstance(v, dict):
418 lm.update(v)
418 lm.update(v)
419 lm['originalnode'] = mapping.get('node')
419 lm['originalnode'] = mapping.get('node')
420 yield tfunc(context, lm, tdata)
420 yield tfunc(context, lm, tdata)
421 else:
421 else:
422 # v is not an iterable of dicts, this happen when 'key'
422 # v is not an iterable of dicts, this happen when 'key'
423 # has been fully expanded already and format is useless.
423 # has been fully expanded already and format is useless.
424 # If so, return the expanded value.
424 # If so, return the expanded value.
425 yield v
425 yield v
426
426
427 def buildnegate(exp, context):
427 def buildnegate(exp, context):
428 arg = compileexp(exp[1], context, exprmethods)
428 arg = compileexp(exp[1], context, exprmethods)
429 return (runnegate, arg)
429 return (runnegate, arg)
430
430
431 def runnegate(context, mapping, data):
431 def runnegate(context, mapping, data):
432 data = evalinteger(context, mapping, data,
432 data = evalinteger(context, mapping, data,
433 _('negation needs an integer argument'))
433 _('negation needs an integer argument'))
434 return -data
434 return -data
435
435
436 def buildarithmetic(exp, context, func):
436 def buildarithmetic(exp, context, func):
437 left = compileexp(exp[1], context, exprmethods)
437 left = compileexp(exp[1], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
438 right = compileexp(exp[2], context, exprmethods)
439 return (runarithmetic, (func, left, right))
439 return (runarithmetic, (func, left, right))
440
440
441 def runarithmetic(context, mapping, data):
441 def runarithmetic(context, mapping, data):
442 func, left, right = data
442 func, left, right = data
443 left = evalinteger(context, mapping, left,
443 left = evalinteger(context, mapping, left,
444 _('arithmetic only defined on integers'))
444 _('arithmetic only defined on integers'))
445 right = evalinteger(context, mapping, right,
445 right = evalinteger(context, mapping, right,
446 _('arithmetic only defined on integers'))
446 _('arithmetic only defined on integers'))
447 try:
447 try:
448 return func(left, right)
448 return func(left, right)
449 except ZeroDivisionError:
449 except ZeroDivisionError:
450 raise error.Abort(_('division by zero is not defined'))
450 raise error.Abort(_('division by zero is not defined'))
451
451
452 def buildfunc(exp, context):
452 def buildfunc(exp, context):
453 n = getsymbol(exp[1])
453 n = getsymbol(exp[1])
454 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
454 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
455 if n in funcs:
455 if n in funcs:
456 f = funcs[n]
456 f = funcs[n]
457 return (f, args)
457 return (f, args)
458 if n in context._filters:
458 if n in context._filters:
459 if len(args) != 1:
459 if len(args) != 1:
460 raise error.ParseError(_("filter %s expects one argument") % n)
460 raise error.ParseError(_("filter %s expects one argument") % n)
461 f = context._filters[n]
461 f = context._filters[n]
462 return (runfilter, (args[0], f))
462 return (runfilter, (args[0], f))
463 raise error.ParseError(_("unknown function '%s'") % n)
463 raise error.ParseError(_("unknown function '%s'") % n)
464
464
465 # dict of template built-in functions
465 # dict of template built-in functions
466 funcs = {}
466 funcs = {}
467
467
468 templatefunc = registrar.templatefunc(funcs)
468 templatefunc = registrar.templatefunc(funcs)
469
469
470 @templatefunc('date(date[, fmt])')
470 @templatefunc('date(date[, fmt])')
471 def date(context, mapping, args):
471 def date(context, mapping, args):
472 """Format a date. See :hg:`help dates` for formatting
472 """Format a date. See :hg:`help dates` for formatting
473 strings. The default is a Unix date format, including the timezone:
473 strings. The default is a Unix date format, including the timezone:
474 "Mon Sep 04 15:13:13 2006 0700"."""
474 "Mon Sep 04 15:13:13 2006 0700"."""
475 if not (1 <= len(args) <= 2):
475 if not (1 <= len(args) <= 2):
476 # i18n: "date" is a keyword
476 # i18n: "date" is a keyword
477 raise error.ParseError(_("date expects one or two arguments"))
477 raise error.ParseError(_("date expects one or two arguments"))
478
478
479 date = evalfuncarg(context, mapping, args[0])
479 date = evalfuncarg(context, mapping, args[0])
480 fmt = None
480 fmt = None
481 if len(args) == 2:
481 if len(args) == 2:
482 fmt = evalstring(context, mapping, args[1])
482 fmt = evalstring(context, mapping, args[1])
483 try:
483 try:
484 if fmt is None:
484 if fmt is None:
485 return util.datestr(date)
485 return util.datestr(date)
486 else:
486 else:
487 return util.datestr(date, fmt)
487 return util.datestr(date, fmt)
488 except (TypeError, ValueError):
488 except (TypeError, ValueError):
489 # i18n: "date" is a keyword
489 # i18n: "date" is a keyword
490 raise error.ParseError(_("date expects a date information"))
490 raise error.ParseError(_("date expects a date information"))
491
491
492 @templatefunc('diff([includepattern [, excludepattern]])')
492 @templatefunc('diff([includepattern [, excludepattern]])')
493 def diff(context, mapping, args):
493 def diff(context, mapping, args):
494 """Show a diff, optionally
494 """Show a diff, optionally
495 specifying files to include or exclude."""
495 specifying files to include or exclude."""
496 if len(args) > 2:
496 if len(args) > 2:
497 # i18n: "diff" is a keyword
497 # i18n: "diff" is a keyword
498 raise error.ParseError(_("diff expects zero, one, or two arguments"))
498 raise error.ParseError(_("diff expects zero, one, or two arguments"))
499
499
500 def getpatterns(i):
500 def getpatterns(i):
501 if i < len(args):
501 if i < len(args):
502 s = evalstring(context, mapping, args[i]).strip()
502 s = evalstring(context, mapping, args[i]).strip()
503 if s:
503 if s:
504 return [s]
504 return [s]
505 return []
505 return []
506
506
507 ctx = mapping['ctx']
507 ctx = mapping['ctx']
508 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
508 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
509
509
510 return ''.join(chunks)
510 return ''.join(chunks)
511
511
512 @templatefunc('files(pattern)')
512 @templatefunc('files(pattern)')
513 def files(context, mapping, args):
513 def files(context, mapping, args):
514 """All files of the current changeset matching the pattern. See
514 """All files of the current changeset matching the pattern. See
515 :hg:`help patterns`."""
515 :hg:`help patterns`."""
516 if not len(args) == 1:
516 if not len(args) == 1:
517 # i18n: "files" is a keyword
517 # i18n: "files" is a keyword
518 raise error.ParseError(_("files expects one argument"))
518 raise error.ParseError(_("files expects one argument"))
519
519
520 raw = evalstring(context, mapping, args[0])
520 raw = evalstring(context, mapping, args[0])
521 ctx = mapping['ctx']
521 ctx = mapping['ctx']
522 m = ctx.match([raw])
522 m = ctx.match([raw])
523 files = list(ctx.matches(m))
523 files = list(ctx.matches(m))
524 return templatekw.showlist("file", files, **mapping)
524 return templatekw.showlist("file", files, **mapping)
525
525
526 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
526 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
527 def fill(context, mapping, args):
527 def fill(context, mapping, args):
528 """Fill many
528 """Fill many
529 paragraphs with optional indentation. See the "fill" filter."""
529 paragraphs with optional indentation. See the "fill" filter."""
530 if not (1 <= len(args) <= 4):
530 if not (1 <= len(args) <= 4):
531 # i18n: "fill" is a keyword
531 # i18n: "fill" is a keyword
532 raise error.ParseError(_("fill expects one to four arguments"))
532 raise error.ParseError(_("fill expects one to four arguments"))
533
533
534 text = evalstring(context, mapping, args[0])
534 text = evalstring(context, mapping, args[0])
535 width = 76
535 width = 76
536 initindent = ''
536 initindent = ''
537 hangindent = ''
537 hangindent = ''
538 if 2 <= len(args) <= 4:
538 if 2 <= len(args) <= 4:
539 width = evalinteger(context, mapping, args[1],
539 width = evalinteger(context, mapping, args[1],
540 # i18n: "fill" is a keyword
540 # i18n: "fill" is a keyword
541 _("fill expects an integer width"))
541 _("fill expects an integer width"))
542 try:
542 try:
543 initindent = evalstring(context, mapping, args[2])
543 initindent = evalstring(context, mapping, args[2])
544 hangindent = evalstring(context, mapping, args[3])
544 hangindent = evalstring(context, mapping, args[3])
545 except IndexError:
545 except IndexError:
546 pass
546 pass
547
547
548 return templatefilters.fill(text, width, initindent, hangindent)
548 return templatefilters.fill(text, width, initindent, hangindent)
549
549
550 @templatefunc('formatnode(node)')
550 @templatefunc('formatnode(node)')
551 def formatnode(context, mapping, args):
551 def formatnode(context, mapping, args):
552 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
552 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
553 if len(args) != 1:
553 if len(args) != 1:
554 # i18n: "formatnode" is a keyword
554 # i18n: "formatnode" is a keyword
555 raise error.ParseError(_("formatnode expects one argument"))
555 raise error.ParseError(_("formatnode expects one argument"))
556
556
557 ui = mapping['ui']
557 ui = mapping['ui']
558 node = evalstring(context, mapping, args[0])
558 node = evalstring(context, mapping, args[0])
559 if ui.debugflag:
559 if ui.debugflag:
560 return node
560 return node
561 return templatefilters.short(node)
561 return templatefilters.short(node)
562
562
563 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])')
563 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])')
564 def pad(context, mapping, args):
564 def pad(context, mapping, args):
565 """Pad text with a
565 """Pad text with a
566 fill character."""
566 fill character."""
567 if not (2 <= len(args) <= 4):
567 if not (2 <= len(args) <= 4):
568 # i18n: "pad" is a keyword
568 # i18n: "pad" is a keyword
569 raise error.ParseError(_("pad() expects two to four arguments"))
569 raise error.ParseError(_("pad() expects two to four arguments"))
570
570
571 width = evalinteger(context, mapping, args[1],
571 width = evalinteger(context, mapping, args[1],
572 # i18n: "pad" is a keyword
572 # i18n: "pad" is a keyword
573 _("pad() expects an integer width"))
573 _("pad() expects an integer width"))
574
574
575 text = evalstring(context, mapping, args[0])
575 text = evalstring(context, mapping, args[0])
576
576
577 left = False
577 left = False
578 fillchar = ' '
578 fillchar = ' '
579 if len(args) > 2:
579 if len(args) > 2:
580 fillchar = evalstring(context, mapping, args[2])
580 fillchar = evalstring(context, mapping, args[2])
581 if len(color.stripeffects(fillchar)) != 1:
581 if len(color.stripeffects(fillchar)) != 1:
582 # i18n: "pad" is a keyword
582 # i18n: "pad" is a keyword
583 raise error.ParseError(_("pad() expects a single fill character"))
583 raise error.ParseError(_("pad() expects a single fill character"))
584 if len(args) > 3:
584 if len(args) > 3:
585 left = evalboolean(context, mapping, args[3])
585 left = evalboolean(context, mapping, args[3])
586
586
587 fillwidth = width - encoding.colwidth(color.stripeffects(text))
587 fillwidth = width - encoding.colwidth(color.stripeffects(text))
588 if fillwidth <= 0:
588 if fillwidth <= 0:
589 return text
589 return text
590 if left:
590 if left:
591 return fillchar * fillwidth + text
591 return fillchar * fillwidth + text
592 else:
592 else:
593 return text + fillchar * fillwidth
593 return text + fillchar * fillwidth
594
594
595 @templatefunc('indent(text, indentchars[, firstline])')
595 @templatefunc('indent(text, indentchars[, firstline])')
596 def indent(context, mapping, args):
596 def indent(context, mapping, args):
597 """Indents all non-empty lines
597 """Indents all non-empty lines
598 with the characters given in the indentchars string. An optional
598 with the characters given in the indentchars string. An optional
599 third parameter will override the indent for the first line only
599 third parameter will override the indent for the first line only
600 if present."""
600 if present."""
601 if not (2 <= len(args) <= 3):
601 if not (2 <= len(args) <= 3):
602 # i18n: "indent" is a keyword
602 # i18n: "indent" is a keyword
603 raise error.ParseError(_("indent() expects two or three arguments"))
603 raise error.ParseError(_("indent() expects two or three arguments"))
604
604
605 text = evalstring(context, mapping, args[0])
605 text = evalstring(context, mapping, args[0])
606 indent = evalstring(context, mapping, args[1])
606 indent = evalstring(context, mapping, args[1])
607
607
608 if len(args) == 3:
608 if len(args) == 3:
609 firstline = evalstring(context, mapping, args[2])
609 firstline = evalstring(context, mapping, args[2])
610 else:
610 else:
611 firstline = indent
611 firstline = indent
612
612
613 # the indent function doesn't indent the first line, so we do it here
613 # the indent function doesn't indent the first line, so we do it here
614 return templatefilters.indent(firstline + text, indent)
614 return templatefilters.indent(firstline + text, indent)
615
615
616 @templatefunc('get(dict, key)')
616 @templatefunc('get(dict, key)')
617 def get(context, mapping, args):
617 def get(context, mapping, args):
618 """Get an attribute/key from an object. Some keywords
618 """Get an attribute/key from an object. Some keywords
619 are complex types. This function allows you to obtain the value of an
619 are complex types. This function allows you to obtain the value of an
620 attribute on these types."""
620 attribute on these types."""
621 if len(args) != 2:
621 if len(args) != 2:
622 # i18n: "get" is a keyword
622 # i18n: "get" is a keyword
623 raise error.ParseError(_("get() expects two arguments"))
623 raise error.ParseError(_("get() expects two arguments"))
624
624
625 dictarg = evalfuncarg(context, mapping, args[0])
625 dictarg = evalfuncarg(context, mapping, args[0])
626 if not util.safehasattr(dictarg, 'get'):
626 if not util.safehasattr(dictarg, 'get'):
627 # i18n: "get" is a keyword
627 # i18n: "get" is a keyword
628 raise error.ParseError(_("get() expects a dict as first argument"))
628 raise error.ParseError(_("get() expects a dict as first argument"))
629
629
630 key = evalfuncarg(context, mapping, args[1])
630 key = evalfuncarg(context, mapping, args[1])
631 return dictarg.get(key)
631 return dictarg.get(key)
632
632
633 @templatefunc('if(expr, then[, else])')
633 @templatefunc('if(expr, then[, else])')
634 def if_(context, mapping, args):
634 def if_(context, mapping, args):
635 """Conditionally execute based on the result of
635 """Conditionally execute based on the result of
636 an expression."""
636 an expression."""
637 if not (2 <= len(args) <= 3):
637 if not (2 <= len(args) <= 3):
638 # i18n: "if" is a keyword
638 # i18n: "if" is a keyword
639 raise error.ParseError(_("if expects two or three arguments"))
639 raise error.ParseError(_("if expects two or three arguments"))
640
640
641 test = evalboolean(context, mapping, args[0])
641 test = evalboolean(context, mapping, args[0])
642 if test:
642 if test:
643 yield args[1][0](context, mapping, args[1][1])
643 yield args[1][0](context, mapping, args[1][1])
644 elif len(args) == 3:
644 elif len(args) == 3:
645 yield args[2][0](context, mapping, args[2][1])
645 yield args[2][0](context, mapping, args[2][1])
646
646
647 @templatefunc('ifcontains(needle, haystack, then[, else])')
647 @templatefunc('ifcontains(needle, haystack, then[, else])')
648 def ifcontains(context, mapping, args):
648 def ifcontains(context, mapping, args):
649 """Conditionally execute based
649 """Conditionally execute based
650 on whether the item "needle" is in "haystack"."""
650 on whether the item "needle" is in "haystack"."""
651 if not (3 <= len(args) <= 4):
651 if not (3 <= len(args) <= 4):
652 # i18n: "ifcontains" is a keyword
652 # i18n: "ifcontains" is a keyword
653 raise error.ParseError(_("ifcontains expects three or four arguments"))
653 raise error.ParseError(_("ifcontains expects three or four arguments"))
654
654
655 needle = evalstring(context, mapping, args[0])
655 needle = evalstring(context, mapping, args[0])
656 haystack = evalfuncarg(context, mapping, args[1])
656 haystack = evalfuncarg(context, mapping, args[1])
657
657
658 if needle in haystack:
658 if needle in haystack:
659 yield args[2][0](context, mapping, args[2][1])
659 yield args[2][0](context, mapping, args[2][1])
660 elif len(args) == 4:
660 elif len(args) == 4:
661 yield args[3][0](context, mapping, args[3][1])
661 yield args[3][0](context, mapping, args[3][1])
662
662
663 @templatefunc('ifeq(expr1, expr2, then[, else])')
663 @templatefunc('ifeq(expr1, expr2, then[, else])')
664 def ifeq(context, mapping, args):
664 def ifeq(context, mapping, args):
665 """Conditionally execute based on
665 """Conditionally execute based on
666 whether 2 items are equivalent."""
666 whether 2 items are equivalent."""
667 if not (3 <= len(args) <= 4):
667 if not (3 <= len(args) <= 4):
668 # i18n: "ifeq" is a keyword
668 # i18n: "ifeq" is a keyword
669 raise error.ParseError(_("ifeq expects three or four arguments"))
669 raise error.ParseError(_("ifeq expects three or four arguments"))
670
670
671 test = evalstring(context, mapping, args[0])
671 test = evalstring(context, mapping, args[0])
672 match = evalstring(context, mapping, args[1])
672 match = evalstring(context, mapping, args[1])
673 if test == match:
673 if test == match:
674 yield args[2][0](context, mapping, args[2][1])
674 yield args[2][0](context, mapping, args[2][1])
675 elif len(args) == 4:
675 elif len(args) == 4:
676 yield args[3][0](context, mapping, args[3][1])
676 yield args[3][0](context, mapping, args[3][1])
677
677
678 @templatefunc('join(list, sep)')
678 @templatefunc('join(list, sep)')
679 def join(context, mapping, args):
679 def join(context, mapping, args):
680 """Join items in a list with a delimiter."""
680 """Join items in a list with a delimiter."""
681 if not (1 <= len(args) <= 2):
681 if not (1 <= len(args) <= 2):
682 # i18n: "join" is a keyword
682 # i18n: "join" is a keyword
683 raise error.ParseError(_("join expects one or two arguments"))
683 raise error.ParseError(_("join expects one or two arguments"))
684
684
685 joinset = args[0][0](context, mapping, args[0][1])
685 joinset = args[0][0](context, mapping, args[0][1])
686 if util.safehasattr(joinset, 'itermaps'):
686 if util.safehasattr(joinset, 'itermaps'):
687 jf = joinset.joinfmt
687 jf = joinset.joinfmt
688 joinset = [jf(x) for x in joinset.itermaps()]
688 joinset = [jf(x) for x in joinset.itermaps()]
689
689
690 joiner = " "
690 joiner = " "
691 if len(args) > 1:
691 if len(args) > 1:
692 joiner = evalstring(context, mapping, args[1])
692 joiner = evalstring(context, mapping, args[1])
693
693
694 first = True
694 first = True
695 for x in joinset:
695 for x in joinset:
696 if first:
696 if first:
697 first = False
697 first = False
698 else:
698 else:
699 yield joiner
699 yield joiner
700 yield x
700 yield x
701
701
702 @templatefunc('label(label, expr)')
702 @templatefunc('label(label, expr)')
703 def label(context, mapping, args):
703 def label(context, mapping, args):
704 """Apply a label to generated content. Content with
704 """Apply a label to generated content. Content with
705 a label applied can result in additional post-processing, such as
705 a label applied can result in additional post-processing, such as
706 automatic colorization."""
706 automatic colorization."""
707 if len(args) != 2:
707 if len(args) != 2:
708 # i18n: "label" is a keyword
708 # i18n: "label" is a keyword
709 raise error.ParseError(_("label expects two arguments"))
709 raise error.ParseError(_("label expects two arguments"))
710
710
711 ui = mapping['ui']
711 ui = mapping['ui']
712 thing = evalstring(context, mapping, args[1])
712 thing = evalstring(context, mapping, args[1])
713 # preserve unknown symbol as literal so effects like 'red', 'bold',
713 # preserve unknown symbol as literal so effects like 'red', 'bold',
714 # etc. don't need to be quoted
714 # etc. don't need to be quoted
715 label = evalstringliteral(context, mapping, args[0])
715 label = evalstringliteral(context, mapping, args[0])
716
716
717 return ui.label(thing, label)
717 return ui.label(thing, label)
718
718
719 @templatefunc('latesttag([pattern])')
719 @templatefunc('latesttag([pattern])')
720 def latesttag(context, mapping, args):
720 def latesttag(context, mapping, args):
721 """The global tags matching the given pattern on the
721 """The global tags matching the given pattern on the
722 most recent globally tagged ancestor of this changeset.
722 most recent globally tagged ancestor of this changeset.
723 If no such tags exist, the "{tag}" template resolves to
723 If no such tags exist, the "{tag}" template resolves to
724 the string "null"."""
724 the string "null"."""
725 if len(args) > 1:
725 if len(args) > 1:
726 # i18n: "latesttag" is a keyword
726 # i18n: "latesttag" is a keyword
727 raise error.ParseError(_("latesttag expects at most one argument"))
727 raise error.ParseError(_("latesttag expects at most one argument"))
728
728
729 pattern = None
729 pattern = None
730 if len(args) == 1:
730 if len(args) == 1:
731 pattern = evalstring(context, mapping, args[0])
731 pattern = evalstring(context, mapping, args[0])
732
732
733 return templatekw.showlatesttags(pattern, **mapping)
733 return templatekw.showlatesttags(pattern, **mapping)
734
734
735 @templatefunc('localdate(date[, tz])')
735 @templatefunc('localdate(date[, tz])')
736 def localdate(context, mapping, args):
736 def localdate(context, mapping, args):
737 """Converts a date to the specified timezone.
737 """Converts a date to the specified timezone.
738 The default is local date."""
738 The default is local date."""
739 if not (1 <= len(args) <= 2):
739 if not (1 <= len(args) <= 2):
740 # i18n: "localdate" is a keyword
740 # i18n: "localdate" is a keyword
741 raise error.ParseError(_("localdate expects one or two arguments"))
741 raise error.ParseError(_("localdate expects one or two arguments"))
742
742
743 date = evalfuncarg(context, mapping, args[0])
743 date = evalfuncarg(context, mapping, args[0])
744 try:
744 try:
745 date = util.parsedate(date)
745 date = util.parsedate(date)
746 except AttributeError: # not str nor date tuple
746 except AttributeError: # not str nor date tuple
747 # i18n: "localdate" is a keyword
747 # i18n: "localdate" is a keyword
748 raise error.ParseError(_("localdate expects a date information"))
748 raise error.ParseError(_("localdate expects a date information"))
749 if len(args) >= 2:
749 if len(args) >= 2:
750 tzoffset = None
750 tzoffset = None
751 tz = evalfuncarg(context, mapping, args[1])
751 tz = evalfuncarg(context, mapping, args[1])
752 if isinstance(tz, str):
752 if isinstance(tz, str):
753 tzoffset, remainder = util.parsetimezone(tz)
753 tzoffset, remainder = util.parsetimezone(tz)
754 if remainder:
754 if remainder:
755 tzoffset = None
755 tzoffset = None
756 if tzoffset is None:
756 if tzoffset is None:
757 try:
757 try:
758 tzoffset = int(tz)
758 tzoffset = int(tz)
759 except (TypeError, ValueError):
759 except (TypeError, ValueError):
760 # i18n: "localdate" is a keyword
760 # i18n: "localdate" is a keyword
761 raise error.ParseError(_("localdate expects a timezone"))
761 raise error.ParseError(_("localdate expects a timezone"))
762 else:
762 else:
763 tzoffset = util.makedate()[1]
763 tzoffset = util.makedate()[1]
764 return (date[0], tzoffset)
764 return (date[0], tzoffset)
765
765
766 @templatefunc('mod(a, b)')
766 @templatefunc('mod(a, b)')
767 def mod(context, mapping, args):
767 def mod(context, mapping, args):
768 """Calculate a mod b such that a / b + a mod b == a"""
768 """Calculate a mod b such that a / b + a mod b == a"""
769 if not len(args) == 2:
769 if not len(args) == 2:
770 # i18n: "mod" is a keyword
770 # i18n: "mod" is a keyword
771 raise error.ParseError(_("mod expects two arguments"))
771 raise error.ParseError(_("mod expects two arguments"))
772
772
773 func = lambda a, b: a % b
773 func = lambda a, b: a % b
774 return runarithmetic(context, mapping, (func, args[0], args[1]))
774 return runarithmetic(context, mapping, (func, args[0], args[1]))
775
775
776 @templatefunc('relpath(path)')
776 @templatefunc('relpath(path)')
777 def relpath(context, mapping, args):
777 def relpath(context, mapping, args):
778 """Convert a repository-absolute path into a filesystem path relative to
778 """Convert a repository-absolute path into a filesystem path relative to
779 the current working directory."""
779 the current working directory."""
780 if len(args) != 1:
780 if len(args) != 1:
781 # i18n: "relpath" is a keyword
781 # i18n: "relpath" is a keyword
782 raise error.ParseError(_("relpath expects one argument"))
782 raise error.ParseError(_("relpath expects one argument"))
783
783
784 repo = mapping['ctx'].repo()
784 repo = mapping['ctx'].repo()
785 path = evalstring(context, mapping, args[0])
785 path = evalstring(context, mapping, args[0])
786 return repo.pathto(path)
786 return repo.pathto(path)
787
787
788 @templatefunc('revset(query[, formatargs...])')
788 @templatefunc('revset(query[, formatargs...])')
789 def revset(context, mapping, args):
789 def revset(context, mapping, args):
790 """Execute a revision set query. See
790 """Execute a revision set query. See
791 :hg:`help revset`."""
791 :hg:`help revset`."""
792 if not len(args) > 0:
792 if not len(args) > 0:
793 # i18n: "revset" is a keyword
793 # i18n: "revset" is a keyword
794 raise error.ParseError(_("revset expects one or more arguments"))
794 raise error.ParseError(_("revset expects one or more arguments"))
795
795
796 raw = evalstring(context, mapping, args[0])
796 raw = evalstring(context, mapping, args[0])
797 ctx = mapping['ctx']
797 ctx = mapping['ctx']
798 repo = ctx.repo()
798 repo = ctx.repo()
799
799
800 def query(expr):
800 def query(expr):
801 m = revsetmod.match(repo.ui, expr)
801 m = revsetmod.match(repo.ui, expr)
802 return m(repo)
802 return m(repo)
803
803
804 if len(args) > 1:
804 if len(args) > 1:
805 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
805 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
806 revs = query(revsetlang.formatspec(raw, *formatargs))
806 revs = query(revsetlang.formatspec(raw, *formatargs))
807 revs = list(revs)
807 revs = list(revs)
808 else:
808 else:
809 revsetcache = mapping['cache'].setdefault("revsetcache", {})
809 revsetcache = mapping['cache'].setdefault("revsetcache", {})
810 if raw in revsetcache:
810 if raw in revsetcache:
811 revs = revsetcache[raw]
811 revs = revsetcache[raw]
812 else:
812 else:
813 revs = query(raw)
813 revs = query(raw)
814 revs = list(revs)
814 revs = list(revs)
815 revsetcache[raw] = revs
815 revsetcache[raw] = revs
816
816
817 return templatekw.showrevslist("revision", revs, **mapping)
817 return templatekw.showrevslist("revision", revs, **mapping)
818
818
819 @templatefunc('rstdoc(text, style)')
819 @templatefunc('rstdoc(text, style)')
820 def rstdoc(context, mapping, args):
820 def rstdoc(context, mapping, args):
821 """Format reStructuredText."""
821 """Format reStructuredText."""
822 if len(args) != 2:
822 if len(args) != 2:
823 # i18n: "rstdoc" is a keyword
823 # i18n: "rstdoc" is a keyword
824 raise error.ParseError(_("rstdoc expects two arguments"))
824 raise error.ParseError(_("rstdoc expects two arguments"))
825
825
826 text = evalstring(context, mapping, args[0])
826 text = evalstring(context, mapping, args[0])
827 style = evalstring(context, mapping, args[1])
827 style = evalstring(context, mapping, args[1])
828
828
829 return minirst.format(text, style=style, keep=['verbose'])
829 return minirst.format(text, style=style, keep=['verbose'])
830
830
831 @templatefunc('separate(sep, args)')
831 @templatefunc('separate(sep, args)')
832 def separate(context, mapping, args):
832 def separate(context, mapping, args):
833 """Add a separator between non-empty arguments."""
833 """Add a separator between non-empty arguments."""
834 if not args:
834 if not args:
835 # i18n: "separate" is a keyword
835 # i18n: "separate" is a keyword
836 raise error.ParseError(_("separate expects at least one argument"))
836 raise error.ParseError(_("separate expects at least one argument"))
837
837
838 sep = evalstring(context, mapping, args[0])
838 sep = evalstring(context, mapping, args[0])
839 first = True
839 first = True
840 for arg in args[1:]:
840 for arg in args[1:]:
841 argstr = evalstring(context, mapping, arg)
841 argstr = evalstring(context, mapping, arg)
842 if not argstr:
842 if not argstr:
843 continue
843 continue
844 if first:
844 if first:
845 first = False
845 first = False
846 else:
846 else:
847 yield sep
847 yield sep
848 yield argstr
848 yield argstr
849
849
850 @templatefunc('shortest(node, minlength=4)')
850 @templatefunc('shortest(node, minlength=4)')
851 def shortest(context, mapping, args):
851 def shortest(context, mapping, args):
852 """Obtain the shortest representation of
852 """Obtain the shortest representation of
853 a node."""
853 a node."""
854 if not (1 <= len(args) <= 2):
854 if not (1 <= len(args) <= 2):
855 # i18n: "shortest" is a keyword
855 # i18n: "shortest" is a keyword
856 raise error.ParseError(_("shortest() expects one or two arguments"))
856 raise error.ParseError(_("shortest() expects one or two arguments"))
857
857
858 node = evalstring(context, mapping, args[0])
858 node = evalstring(context, mapping, args[0])
859
859
860 minlength = 4
860 minlength = 4
861 if len(args) > 1:
861 if len(args) > 1:
862 minlength = evalinteger(context, mapping, args[1],
862 minlength = evalinteger(context, mapping, args[1],
863 # i18n: "shortest" is a keyword
863 # i18n: "shortest" is a keyword
864 _("shortest() expects an integer minlength"))
864 _("shortest() expects an integer minlength"))
865
865
866 # _partialmatch() of filtered changelog could take O(len(repo)) time,
866 # _partialmatch() of filtered changelog could take O(len(repo)) time,
867 # which would be unacceptably slow. so we look for hash collision in
867 # which would be unacceptably slow. so we look for hash collision in
868 # unfiltered space, which means some hashes may be slightly longer.
868 # unfiltered space, which means some hashes may be slightly longer.
869 cl = mapping['ctx']._repo.unfiltered().changelog
869 cl = mapping['ctx']._repo.unfiltered().changelog
870 def isvalid(test):
870 def isvalid(test):
871 try:
871 try:
872 if cl._partialmatch(test) is None:
872 if cl._partialmatch(test) is None:
873 return False
873 return False
874
874
875 try:
875 try:
876 i = int(test)
876 i = int(test)
877 # if we are a pure int, then starting with zero will not be
877 # if we are a pure int, then starting with zero will not be
878 # confused as a rev; or, obviously, if the int is larger than
878 # confused as a rev; or, obviously, if the int is larger than
879 # the value of the tip rev
879 # the value of the tip rev
880 if test[0] == '0' or i > len(cl):
880 if test[0] == '0' or i > len(cl):
881 return True
881 return True
882 return False
882 return False
883 except ValueError:
883 except ValueError:
884 return True
884 return True
885 except error.RevlogError:
885 except error.RevlogError:
886 return False
886 return False
887
887
888 shortest = node
888 shortest = node
889 startlength = max(6, minlength)
889 startlength = max(6, minlength)
890 length = startlength
890 length = startlength
891 while True:
891 while True:
892 test = node[:length]
892 test = node[:length]
893 if isvalid(test):
893 if isvalid(test):
894 shortest = test
894 shortest = test
895 if length == minlength or length > startlength:
895 if length == minlength or length > startlength:
896 return shortest
896 return shortest
897 length -= 1
897 length -= 1
898 else:
898 else:
899 length += 1
899 length += 1
900 if len(shortest) <= length:
900 if len(shortest) <= length:
901 return shortest
901 return shortest
902
902
903 @templatefunc('strip(text[, chars])')
903 @templatefunc('strip(text[, chars])')
904 def strip(context, mapping, args):
904 def strip(context, mapping, args):
905 """Strip characters from a string. By default,
905 """Strip characters from a string. By default,
906 strips all leading and trailing whitespace."""
906 strips all leading and trailing whitespace."""
907 if not (1 <= len(args) <= 2):
907 if not (1 <= len(args) <= 2):
908 # i18n: "strip" is a keyword
908 # i18n: "strip" is a keyword
909 raise error.ParseError(_("strip expects one or two arguments"))
909 raise error.ParseError(_("strip expects one or two arguments"))
910
910
911 text = evalstring(context, mapping, args[0])
911 text = evalstring(context, mapping, args[0])
912 if len(args) == 2:
912 if len(args) == 2:
913 chars = evalstring(context, mapping, args[1])
913 chars = evalstring(context, mapping, args[1])
914 return text.strip(chars)
914 return text.strip(chars)
915 return text.strip()
915 return text.strip()
916
916
917 @templatefunc('sub(pattern, replacement, expression)')
917 @templatefunc('sub(pattern, replacement, expression)')
918 def sub(context, mapping, args):
918 def sub(context, mapping, args):
919 """Perform text substitution
919 """Perform text substitution
920 using regular expressions."""
920 using regular expressions."""
921 if len(args) != 3:
921 if len(args) != 3:
922 # i18n: "sub" is a keyword
922 # i18n: "sub" is a keyword
923 raise error.ParseError(_("sub expects three arguments"))
923 raise error.ParseError(_("sub expects three arguments"))
924
924
925 pat = evalstring(context, mapping, args[0])
925 pat = evalstring(context, mapping, args[0])
926 rpl = evalstring(context, mapping, args[1])
926 rpl = evalstring(context, mapping, args[1])
927 src = evalstring(context, mapping, args[2])
927 src = evalstring(context, mapping, args[2])
928 try:
928 try:
929 patre = re.compile(pat)
929 patre = re.compile(pat)
930 except re.error:
930 except re.error:
931 # i18n: "sub" is a keyword
931 # i18n: "sub" is a keyword
932 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
932 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
933 try:
933 try:
934 yield patre.sub(rpl, src)
934 yield patre.sub(rpl, src)
935 except re.error:
935 except re.error:
936 # i18n: "sub" is a keyword
936 # i18n: "sub" is a keyword
937 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
937 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
938
938
939 @templatefunc('startswith(pattern, text)')
939 @templatefunc('startswith(pattern, text)')
940 def startswith(context, mapping, args):
940 def startswith(context, mapping, args):
941 """Returns the value from the "text" argument
941 """Returns the value from the "text" argument
942 if it begins with the content from the "pattern" argument."""
942 if it begins with the content from the "pattern" argument."""
943 if len(args) != 2:
943 if len(args) != 2:
944 # i18n: "startswith" is a keyword
944 # i18n: "startswith" is a keyword
945 raise error.ParseError(_("startswith expects two arguments"))
945 raise error.ParseError(_("startswith expects two arguments"))
946
946
947 patn = evalstring(context, mapping, args[0])
947 patn = evalstring(context, mapping, args[0])
948 text = evalstring(context, mapping, args[1])
948 text = evalstring(context, mapping, args[1])
949 if text.startswith(patn):
949 if text.startswith(patn):
950 return text
950 return text
951 return ''
951 return ''
952
952
953 @templatefunc('word(number, text[, separator])')
953 @templatefunc('word(number, text[, separator])')
954 def word(context, mapping, args):
954 def word(context, mapping, args):
955 """Return the nth word from a string."""
955 """Return the nth word from a string."""
956 if not (2 <= len(args) <= 3):
956 if not (2 <= len(args) <= 3):
957 # i18n: "word" is a keyword
957 # i18n: "word" is a keyword
958 raise error.ParseError(_("word expects two or three arguments, got %d")
958 raise error.ParseError(_("word expects two or three arguments, got %d")
959 % len(args))
959 % len(args))
960
960
961 num = evalinteger(context, mapping, args[0],
961 num = evalinteger(context, mapping, args[0],
962 # i18n: "word" is a keyword
962 # i18n: "word" is a keyword
963 _("word expects an integer index"))
963 _("word expects an integer index"))
964 text = evalstring(context, mapping, args[1])
964 text = evalstring(context, mapping, args[1])
965 if len(args) == 3:
965 if len(args) == 3:
966 splitter = evalstring(context, mapping, args[2])
966 splitter = evalstring(context, mapping, args[2])
967 else:
967 else:
968 splitter = None
968 splitter = None
969
969
970 tokens = text.split(splitter)
970 tokens = text.split(splitter)
971 if num >= len(tokens) or num < -len(tokens):
971 if num >= len(tokens) or num < -len(tokens):
972 return ''
972 return ''
973 else:
973 else:
974 return tokens[num]
974 return tokens[num]
975
975
976 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
976 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
977 exprmethods = {
977 exprmethods = {
978 "integer": lambda e, c: (runinteger, e[1]),
978 "integer": lambda e, c: (runinteger, e[1]),
979 "string": lambda e, c: (runstring, e[1]),
979 "string": lambda e, c: (runstring, e[1]),
980 "symbol": lambda e, c: (runsymbol, e[1]),
980 "symbol": lambda e, c: (runsymbol, e[1]),
981 "template": buildtemplate,
981 "template": buildtemplate,
982 "group": lambda e, c: compileexp(e[1], c, exprmethods),
982 "group": lambda e, c: compileexp(e[1], c, exprmethods),
983 # ".": buildmember,
983 # ".": buildmember,
984 "|": buildfilter,
984 "|": buildfilter,
985 "%": buildmap,
985 "%": buildmap,
986 "func": buildfunc,
986 "func": buildfunc,
987 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
987 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
988 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
988 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
989 "negate": buildnegate,
989 "negate": buildnegate,
990 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
990 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
991 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
991 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
992 }
992 }
993
993
994 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
994 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
995 methods = exprmethods.copy()
995 methods = exprmethods.copy()
996 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
996 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
997
997
998 class _aliasrules(parser.basealiasrules):
998 class _aliasrules(parser.basealiasrules):
999 """Parsing and expansion rule set of template aliases"""
999 """Parsing and expansion rule set of template aliases"""
1000 _section = _('template alias')
1000 _section = _('template alias')
1001 _parse = staticmethod(_parseexpr)
1001 _parse = staticmethod(_parseexpr)
1002
1002
1003 @staticmethod
1003 @staticmethod
1004 def _trygetfunc(tree):
1004 def _trygetfunc(tree):
1005 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1005 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1006 None"""
1006 None"""
1007 if tree[0] == 'func' and tree[1][0] == 'symbol':
1007 if tree[0] == 'func' and tree[1][0] == 'symbol':
1008 return tree[1][1], getlist(tree[2])
1008 return tree[1][1], getlist(tree[2])
1009 if tree[0] == '|' and tree[2][0] == 'symbol':
1009 if tree[0] == '|' and tree[2][0] == 'symbol':
1010 return tree[2][1], [tree[1]]
1010 return tree[2][1], [tree[1]]
1011
1011
1012 def expandaliases(tree, aliases):
1012 def expandaliases(tree, aliases):
1013 """Return new tree of aliases are expanded"""
1013 """Return new tree of aliases are expanded"""
1014 aliasmap = _aliasrules.buildmap(aliases)
1014 aliasmap = _aliasrules.buildmap(aliases)
1015 return _aliasrules.expand(aliasmap, tree)
1015 return _aliasrules.expand(aliasmap, tree)
1016
1016
1017 # template engine
1017 # template engine
1018
1018
1019 stringify = templatefilters.stringify
1019 stringify = templatefilters.stringify
1020
1020
1021 def _flatten(thing):
1021 def _flatten(thing):
1022 '''yield a single stream from a possibly nested set of iterators'''
1022 '''yield a single stream from a possibly nested set of iterators'''
1023 thing = templatekw.unwraphybrid(thing)
1023 thing = templatekw.unwraphybrid(thing)
1024 if isinstance(thing, str):
1024 if isinstance(thing, str):
1025 yield thing
1025 yield thing
1026 elif thing is None:
1026 elif thing is None:
1027 pass
1027 pass
1028 elif not util.safehasattr(thing, '__iter__'):
1028 elif not util.safehasattr(thing, '__iter__'):
1029 yield str(thing)
1029 yield str(thing)
1030 else:
1030 else:
1031 for i in thing:
1031 for i in thing:
1032 i = templatekw.unwraphybrid(i)
1032 i = templatekw.unwraphybrid(i)
1033 if isinstance(i, str):
1033 if isinstance(i, str):
1034 yield i
1034 yield i
1035 elif i is None:
1035 elif i is None:
1036 pass
1036 pass
1037 elif not util.safehasattr(i, '__iter__'):
1037 elif not util.safehasattr(i, '__iter__'):
1038 yield str(i)
1038 yield str(i)
1039 else:
1039 else:
1040 for j in _flatten(i):
1040 for j in _flatten(i):
1041 yield j
1041 yield j
1042
1042
1043 def unquotestring(s):
1043 def unquotestring(s):
1044 '''unwrap quotes if any; otherwise returns unmodified string'''
1044 '''unwrap quotes if any; otherwise returns unmodified string'''
1045 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1045 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1046 return s
1046 return s
1047 return s[1:-1]
1047 return s[1:-1]
1048
1048
1049 class engine(object):
1049 class engine(object):
1050 '''template expansion engine.
1050 '''template expansion engine.
1051
1051
1052 template expansion works like this. a map file contains key=value
1052 template expansion works like this. a map file contains key=value
1053 pairs. if value is quoted, it is treated as string. otherwise, it
1053 pairs. if value is quoted, it is treated as string. otherwise, it
1054 is treated as name of template file.
1054 is treated as name of template file.
1055
1055
1056 templater is asked to expand a key in map. it looks up key, and
1056 templater is asked to expand a key in map. it looks up key, and
1057 looks for strings like this: {foo}. it expands {foo} by looking up
1057 looks for strings like this: {foo}. it expands {foo} by looking up
1058 foo in map, and substituting it. expansion is recursive: it stops
1058 foo in map, and substituting it. expansion is recursive: it stops
1059 when there is no more {foo} to replace.
1059 when there is no more {foo} to replace.
1060
1060
1061 expansion also allows formatting and filtering.
1061 expansion also allows formatting and filtering.
1062
1062
1063 format uses key to expand each item in list. syntax is
1063 format uses key to expand each item in list. syntax is
1064 {key%format}.
1064 {key%format}.
1065
1065
1066 filter uses function to transform value. syntax is
1066 filter uses function to transform value. syntax is
1067 {key|filter1|filter2|...}.'''
1067 {key|filter1|filter2|...}.'''
1068
1068
1069 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1069 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1070 self._loader = loader
1070 self._loader = loader
1071 if filters is None:
1071 if filters is None:
1072 filters = {}
1072 filters = {}
1073 self._filters = filters
1073 self._filters = filters
1074 if defaults is None:
1074 if defaults is None:
1075 defaults = {}
1075 defaults = {}
1076 self._defaults = defaults
1076 self._defaults = defaults
1077 self._aliasmap = _aliasrules.buildmap(aliases)
1077 self._aliasmap = _aliasrules.buildmap(aliases)
1078 self._cache = {} # key: (func, data)
1078 self._cache = {} # key: (func, data)
1079
1079
1080 def _load(self, t):
1080 def _load(self, t):
1081 '''load, parse, and cache a template'''
1081 '''load, parse, and cache a template'''
1082 if t not in self._cache:
1082 if t not in self._cache:
1083 # put poison to cut recursion while compiling 't'
1083 # put poison to cut recursion while compiling 't'
1084 self._cache[t] = (_runrecursivesymbol, t)
1084 self._cache[t] = (_runrecursivesymbol, t)
1085 try:
1085 try:
1086 x = parse(self._loader(t))
1086 x = parse(self._loader(t))
1087 if self._aliasmap:
1087 if self._aliasmap:
1088 x = _aliasrules.expand(self._aliasmap, x)
1088 x = _aliasrules.expand(self._aliasmap, x)
1089 self._cache[t] = compileexp(x, self, methods)
1089 self._cache[t] = compileexp(x, self, methods)
1090 except: # re-raises
1090 except: # re-raises
1091 del self._cache[t]
1091 del self._cache[t]
1092 raise
1092 raise
1093 return self._cache[t]
1093 return self._cache[t]
1094
1094
1095 def process(self, t, mapping):
1095 def process(self, t, mapping):
1096 '''Perform expansion. t is name of map element to expand.
1096 '''Perform expansion. t is name of map element to expand.
1097 mapping contains added elements for use during expansion. Is a
1097 mapping contains added elements for use during expansion. Is a
1098 generator.'''
1098 generator.'''
1099 func, data = self._load(t)
1099 func, data = self._load(t)
1100 return _flatten(func(self, mapping, data))
1100 return _flatten(func(self, mapping, data))
1101
1101
1102 engines = {'default': engine}
1102 engines = {'default': engine}
1103
1103
1104 def stylelist():
1104 def stylelist():
1105 paths = templatepaths()
1105 paths = templatepaths()
1106 if not paths:
1106 if not paths:
1107 return _('no templates found, try `hg debuginstall` for more info')
1107 return _('no templates found, try `hg debuginstall` for more info')
1108 dirlist = os.listdir(paths[0])
1108 dirlist = os.listdir(paths[0])
1109 stylelist = []
1109 stylelist = []
1110 for file in dirlist:
1110 for file in dirlist:
1111 split = file.split(".")
1111 split = file.split(".")
1112 if split[-1] in ('orig', 'rej'):
1112 if split[-1] in ('orig', 'rej'):
1113 continue
1113 continue
1114 if split[0] == "map-cmdline":
1114 if split[0] == "map-cmdline":
1115 stylelist.append(split[1])
1115 stylelist.append(split[1])
1116 return ", ".join(sorted(stylelist))
1116 return ", ".join(sorted(stylelist))
1117
1117
1118 def _readmapfile(mapfile):
1118 def _readmapfile(mapfile):
1119 """Load template elements from the given map file"""
1119 """Load template elements from the given map file"""
1120 if not os.path.exists(mapfile):
1120 if not os.path.exists(mapfile):
1121 raise error.Abort(_("style '%s' not found") % mapfile,
1121 raise error.Abort(_("style '%s' not found") % mapfile,
1122 hint=_("available styles: %s") % stylelist())
1122 hint=_("available styles: %s") % stylelist())
1123
1123
1124 base = os.path.dirname(mapfile)
1124 base = os.path.dirname(mapfile)
1125 conf = config.config(includepaths=templatepaths())
1125 conf = config.config(includepaths=templatepaths())
1126 conf.read(mapfile)
1126 conf.read(mapfile)
1127
1127
1128 cache = {}
1128 cache = {}
1129 tmap = {}
1129 tmap = {}
1130 for key, val in conf[''].items():
1130 for key, val in conf[''].items():
1131 if not val:
1131 if not val:
1132 raise error.ParseError(_('missing value'), conf.source('', key))
1132 raise error.ParseError(_('missing value'), conf.source('', key))
1133 if val[0] in "'\"":
1133 if val[0] in "'\"":
1134 if val[0] != val[-1]:
1134 if val[0] != val[-1]:
1135 raise error.ParseError(_('unmatched quotes'),
1135 raise error.ParseError(_('unmatched quotes'),
1136 conf.source('', key))
1136 conf.source('', key))
1137 cache[key] = unquotestring(val)
1137 cache[key] = unquotestring(val)
1138 elif key == "__base__":
1138 elif key == "__base__":
1139 # treat as a pointer to a base class for this style
1139 # treat as a pointer to a base class for this style
1140 path = util.normpath(os.path.join(base, val))
1140 path = util.normpath(os.path.join(base, val))
1141
1141
1142 # fallback check in template paths
1142 # fallback check in template paths
1143 if not os.path.exists(path):
1143 if not os.path.exists(path):
1144 for p in templatepaths():
1144 for p in templatepaths():
1145 p2 = util.normpath(os.path.join(p, val))
1145 p2 = util.normpath(os.path.join(p, val))
1146 if os.path.isfile(p2):
1146 if os.path.isfile(p2):
1147 path = p2
1147 path = p2
1148 break
1148 break
1149 p3 = util.normpath(os.path.join(p2, "map"))
1149 p3 = util.normpath(os.path.join(p2, "map"))
1150 if os.path.isfile(p3):
1150 if os.path.isfile(p3):
1151 path = p3
1151 path = p3
1152 break
1152 break
1153
1153
1154 bcache, btmap = _readmapfile(path)
1154 bcache, btmap = _readmapfile(path)
1155 for k in bcache:
1155 for k in bcache:
1156 if k not in cache:
1156 if k not in cache:
1157 cache[k] = bcache[k]
1157 cache[k] = bcache[k]
1158 for k in btmap:
1158 for k in btmap:
1159 if k not in tmap:
1159 if k not in tmap:
1160 tmap[k] = btmap[k]
1160 tmap[k] = btmap[k]
1161 else:
1161 else:
1162 val = 'default', val
1162 val = 'default', val
1163 if ':' in val[1]:
1163 if ':' in val[1]:
1164 val = val[1].split(':', 1)
1164 val = val[1].split(':', 1)
1165 tmap[key] = val[0], os.path.join(base, val[1])
1165 tmap[key] = val[0], os.path.join(base, val[1])
1166 return cache, tmap
1166 return cache, tmap
1167
1167
1168 class TemplateNotFound(error.Abort):
1168 class TemplateNotFound(error.Abort):
1169 pass
1169 pass
1170
1170
1171 class templater(object):
1171 class templater(object):
1172
1172
1173 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1173 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1174 minchunk=1024, maxchunk=65536):
1174 minchunk=1024, maxchunk=65536):
1175 '''set up template engine.
1175 '''set up template engine.
1176 filters is dict of functions. each transforms a value into another.
1176 filters is dict of functions. each transforms a value into another.
1177 defaults is dict of default map definitions.
1177 defaults is dict of default map definitions.
1178 aliases is list of alias (name, replacement) pairs.
1178 aliases is list of alias (name, replacement) pairs.
1179 '''
1179 '''
1180 if filters is None:
1180 if filters is None:
1181 filters = {}
1181 filters = {}
1182 if defaults is None:
1182 if defaults is None:
1183 defaults = {}
1183 defaults = {}
1184 if cache is None:
1184 if cache is None:
1185 cache = {}
1185 cache = {}
1186 self.cache = cache.copy()
1186 self.cache = cache.copy()
1187 self.map = {}
1187 self.map = {}
1188 self.filters = templatefilters.filters.copy()
1188 self.filters = templatefilters.filters.copy()
1189 self.filters.update(filters)
1189 self.filters.update(filters)
1190 self.defaults = defaults
1190 self.defaults = defaults
1191 self._aliases = aliases
1191 self._aliases = aliases
1192 self.minchunk, self.maxchunk = minchunk, maxchunk
1192 self.minchunk, self.maxchunk = minchunk, maxchunk
1193 self.ecache = {}
1193 self.ecache = {}
1194
1194
1195 @classmethod
1195 @classmethod
1196 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1196 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1197 minchunk=1024, maxchunk=65536):
1197 minchunk=1024, maxchunk=65536):
1198 """Create templater from the specified map file"""
1198 """Create templater from the specified map file"""
1199 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1199 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1200 cache, tmap = _readmapfile(mapfile)
1200 cache, tmap = _readmapfile(mapfile)
1201 t.cache.update(cache)
1201 t.cache.update(cache)
1202 t.map = tmap
1202 t.map = tmap
1203 return t
1203 return t
1204
1204
1205 def __contains__(self, key):
1205 def __contains__(self, key):
1206 return key in self.cache or key in self.map
1206 return key in self.cache or key in self.map
1207
1207
1208 def load(self, t):
1208 def load(self, t):
1209 '''Get the template for the given template name. Use a local cache.'''
1209 '''Get the template for the given template name. Use a local cache.'''
1210 if t not in self.cache:
1210 if t not in self.cache:
1211 try:
1211 try:
1212 self.cache[t] = util.readfile(self.map[t][1])
1212 self.cache[t] = util.readfile(self.map[t][1])
1213 except KeyError as inst:
1213 except KeyError as inst:
1214 raise TemplateNotFound(_('"%s" not in template map') %
1214 raise TemplateNotFound(_('"%s" not in template map') %
1215 inst.args[0])
1215 inst.args[0])
1216 except IOError as inst:
1216 except IOError as inst:
1217 raise IOError(inst.args[0], _('template file %s: %s') %
1217 raise IOError(inst.args[0], _('template file %s: %s') %
1218 (self.map[t][1], inst.args[1]))
1218 (self.map[t][1], inst.args[1]))
1219 return self.cache[t]
1219 return self.cache[t]
1220
1220
1221 def __call__(self, t, **mapping):
1221 def __call__(self, t, **mapping):
1222 ttype = t in self.map and self.map[t][0] or 'default'
1222 ttype = t in self.map and self.map[t][0] or 'default'
1223 if ttype not in self.ecache:
1223 if ttype not in self.ecache:
1224 try:
1224 try:
1225 ecls = engines[ttype]
1225 ecls = engines[ttype]
1226 except KeyError:
1226 except KeyError:
1227 raise error.Abort(_('invalid template engine: %s') % ttype)
1227 raise error.Abort(_('invalid template engine: %s') % ttype)
1228 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1228 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1229 self._aliases)
1229 self._aliases)
1230 proc = self.ecache[ttype]
1230 proc = self.ecache[ttype]
1231
1231
1232 stream = proc.process(t, mapping)
1232 stream = proc.process(t, mapping)
1233 if self.minchunk:
1233 if self.minchunk:
1234 stream = util.increasingchunks(stream, min=self.minchunk,
1234 stream = util.increasingchunks(stream, min=self.minchunk,
1235 max=self.maxchunk)
1235 max=self.maxchunk)
1236 return stream
1236 return stream
1237
1237
1238 def templatepaths():
1238 def templatepaths():
1239 '''return locations used for template files.'''
1239 '''return locations used for template files.'''
1240 pathsrel = ['templates']
1240 pathsrel = ['templates']
1241 paths = [os.path.normpath(os.path.join(util.datapath, f))
1241 paths = [os.path.normpath(os.path.join(util.datapath, f))
1242 for f in pathsrel]
1242 for f in pathsrel]
1243 return [p for p in paths if os.path.isdir(p)]
1243 return [p for p in paths if os.path.isdir(p)]
1244
1244
1245 def templatepath(name):
1245 def templatepath(name):
1246 '''return location of template file. returns None if not found.'''
1246 '''return location of template file. returns None if not found.'''
1247 for p in templatepaths():
1247 for p in templatepaths():
1248 f = os.path.join(p, name)
1248 f = os.path.join(p, name)
1249 if os.path.exists(f):
1249 if os.path.exists(f):
1250 return f
1250 return f
1251 return None
1251 return None
1252
1252
1253 def stylemap(styles, paths=None):
1253 def stylemap(styles, paths=None):
1254 """Return path to mapfile for a given style.
1254 """Return path to mapfile for a given style.
1255
1255
1256 Searches mapfile in the following locations:
1256 Searches mapfile in the following locations:
1257 1. templatepath/style/map
1257 1. templatepath/style/map
1258 2. templatepath/map-style
1258 2. templatepath/map-style
1259 3. templatepath/map
1259 3. templatepath/map
1260 """
1260 """
1261
1261
1262 if paths is None:
1262 if paths is None:
1263 paths = templatepaths()
1263 paths = templatepaths()
1264 elif isinstance(paths, str):
1264 elif isinstance(paths, str):
1265 paths = [paths]
1265 paths = [paths]
1266
1266
1267 if isinstance(styles, str):
1267 if isinstance(styles, str):
1268 styles = [styles]
1268 styles = [styles]
1269
1269
1270 for style in styles:
1270 for style in styles:
1271 # only plain name is allowed to honor template paths
1271 # only plain name is allowed to honor template paths
1272 if (not style
1272 if (not style
1273 or style in (os.curdir, os.pardir)
1273 or style in (os.curdir, os.pardir)
1274 or pycompat.ossep in style
1274 or pycompat.ossep in style
1275 or pycompat.osaltsep and pycompat.osaltsep in style):
1275 or pycompat.osaltsep and pycompat.osaltsep in style):
1276 continue
1276 continue
1277 locations = [os.path.join(style, 'map'), 'map-' + style]
1277 locations = [os.path.join(style, 'map'), 'map-' + style]
1278 locations.append('map')
1278 locations.append('map')
1279
1279
1280 for path in paths:
1280 for path in paths:
1281 for location in locations:
1281 for location in locations:
1282 mapfile = os.path.join(path, location)
1282 mapfile = os.path.join(path, location)
1283 if os.path.isfile(mapfile):
1283 if os.path.isfile(mapfile):
1284 return style, mapfile
1284 return style, mapfile
1285
1285
1286 raise RuntimeError("No hgweb templates found in %r" % paths)
1286 raise RuntimeError("No hgweb templates found in %r" % paths)
1287
1287
1288 def loadfunction(ui, extname, registrarobj):
1288 def loadfunction(ui, extname, registrarobj):
1289 """Load template function from specified registrarobj
1289 """Load template function from specified registrarobj
1290 """
1290 """
1291 for name, func in registrarobj._table.iteritems():
1291 for name, func in registrarobj._table.iteritems():
1292 funcs[name] = func
1292 funcs[name] = func
1293
1293
1294 # tell hggettext to extract docstrings from these functions:
1294 # tell hggettext to extract docstrings from these functions:
1295 i18nfunctions = funcs.values()
1295 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now