##// END OF EJS Templates
templater: rename variable "i" to "v" in runmap()...
Yuya Nishihara -
r31806:8f203b49 default
parent child Browse files
Show More
@@ -1,1290 +1,1290 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 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 ",": (2, None, None, ("list", 2), None),
36 ",": (2, None, None, ("list", 2), None),
37 "|": (5, None, None, ("|", 5), None),
37 "|": (5, None, None, ("|", 5), None),
38 "%": (6, None, None, ("%", 6), None),
38 "%": (6, None, None, ("%", 6), None),
39 ")": (0, None, None, None, None),
39 ")": (0, None, None, None, None),
40 "+": (3, None, None, ("+", 3), None),
40 "+": (3, None, None, ("+", 3), None),
41 "-": (3, None, ("negate", 10), ("-", 3), None),
41 "-": (3, None, ("negate", 10), ("-", 3), None),
42 "*": (4, None, None, ("*", 4), None),
42 "*": (4, None, None, ("*", 4), None),
43 "/": (4, None, None, ("/", 4), None),
43 "/": (4, None, None, ("/", 4), 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 in diter:
414 for v in diter:
415 lm = mapping.copy()
415 lm = mapping.copy()
416 if isinstance(i, dict):
416 if isinstance(v, dict):
417 lm.update(i)
417 lm.update(v)
418 lm['originalnode'] = mapping.get('node')
418 lm['originalnode'] = mapping.get('node')
419 yield tfunc(context, lm, tdata)
419 yield tfunc(context, lm, tdata)
420 else:
420 else:
421 # v is not an iterable of dicts, this happen when 'key'
421 # v is not an iterable of dicts, this happen when 'key'
422 # has been fully expanded already and format is useless.
422 # has been fully expanded already and format is useless.
423 # If so, return the expanded value.
423 # If so, return the expanded value.
424 yield i
424 yield v
425
425
426 def buildnegate(exp, context):
426 def buildnegate(exp, context):
427 arg = compileexp(exp[1], context, exprmethods)
427 arg = compileexp(exp[1], context, exprmethods)
428 return (runnegate, arg)
428 return (runnegate, arg)
429
429
430 def runnegate(context, mapping, data):
430 def runnegate(context, mapping, data):
431 data = evalinteger(context, mapping, data,
431 data = evalinteger(context, mapping, data,
432 _('negation needs an integer argument'))
432 _('negation needs an integer argument'))
433 return -data
433 return -data
434
434
435 def buildarithmetic(exp, context, func):
435 def buildarithmetic(exp, context, func):
436 left = compileexp(exp[1], context, exprmethods)
436 left = compileexp(exp[1], context, exprmethods)
437 right = compileexp(exp[2], context, exprmethods)
437 right = compileexp(exp[2], context, exprmethods)
438 return (runarithmetic, (func, left, right))
438 return (runarithmetic, (func, left, right))
439
439
440 def runarithmetic(context, mapping, data):
440 def runarithmetic(context, mapping, data):
441 func, left, right = data
441 func, left, right = data
442 left = evalinteger(context, mapping, left,
442 left = evalinteger(context, mapping, left,
443 _('arithmetic only defined on integers'))
443 _('arithmetic only defined on integers'))
444 right = evalinteger(context, mapping, right,
444 right = evalinteger(context, mapping, right,
445 _('arithmetic only defined on integers'))
445 _('arithmetic only defined on integers'))
446 try:
446 try:
447 return func(left, right)
447 return func(left, right)
448 except ZeroDivisionError:
448 except ZeroDivisionError:
449 raise error.Abort(_('division by zero is not defined'))
449 raise error.Abort(_('division by zero is not defined'))
450
450
451 def buildfunc(exp, context):
451 def buildfunc(exp, context):
452 n = getsymbol(exp[1])
452 n = getsymbol(exp[1])
453 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
453 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
454 if n in funcs:
454 if n in funcs:
455 f = funcs[n]
455 f = funcs[n]
456 return (f, args)
456 return (f, args)
457 if n in context._filters:
457 if n in context._filters:
458 if len(args) != 1:
458 if len(args) != 1:
459 raise error.ParseError(_("filter %s expects one argument") % n)
459 raise error.ParseError(_("filter %s expects one argument") % n)
460 f = context._filters[n]
460 f = context._filters[n]
461 return (runfilter, (args[0], f))
461 return (runfilter, (args[0], f))
462 raise error.ParseError(_("unknown function '%s'") % n)
462 raise error.ParseError(_("unknown function '%s'") % n)
463
463
464 # dict of template built-in functions
464 # dict of template built-in functions
465 funcs = {}
465 funcs = {}
466
466
467 templatefunc = registrar.templatefunc(funcs)
467 templatefunc = registrar.templatefunc(funcs)
468
468
469 @templatefunc('date(date[, fmt])')
469 @templatefunc('date(date[, fmt])')
470 def date(context, mapping, args):
470 def date(context, mapping, args):
471 """Format a date. See :hg:`help dates` for formatting
471 """Format a date. See :hg:`help dates` for formatting
472 strings. The default is a Unix date format, including the timezone:
472 strings. The default is a Unix date format, including the timezone:
473 "Mon Sep 04 15:13:13 2006 0700"."""
473 "Mon Sep 04 15:13:13 2006 0700"."""
474 if not (1 <= len(args) <= 2):
474 if not (1 <= len(args) <= 2):
475 # i18n: "date" is a keyword
475 # i18n: "date" is a keyword
476 raise error.ParseError(_("date expects one or two arguments"))
476 raise error.ParseError(_("date expects one or two arguments"))
477
477
478 date = evalfuncarg(context, mapping, args[0])
478 date = evalfuncarg(context, mapping, args[0])
479 fmt = None
479 fmt = None
480 if len(args) == 2:
480 if len(args) == 2:
481 fmt = evalstring(context, mapping, args[1])
481 fmt = evalstring(context, mapping, args[1])
482 try:
482 try:
483 if fmt is None:
483 if fmt is None:
484 return util.datestr(date)
484 return util.datestr(date)
485 else:
485 else:
486 return util.datestr(date, fmt)
486 return util.datestr(date, fmt)
487 except (TypeError, ValueError):
487 except (TypeError, ValueError):
488 # i18n: "date" is a keyword
488 # i18n: "date" is a keyword
489 raise error.ParseError(_("date expects a date information"))
489 raise error.ParseError(_("date expects a date information"))
490
490
491 @templatefunc('diff([includepattern [, excludepattern]])')
491 @templatefunc('diff([includepattern [, excludepattern]])')
492 def diff(context, mapping, args):
492 def diff(context, mapping, args):
493 """Show a diff, optionally
493 """Show a diff, optionally
494 specifying files to include or exclude."""
494 specifying files to include or exclude."""
495 if len(args) > 2:
495 if len(args) > 2:
496 # i18n: "diff" is a keyword
496 # i18n: "diff" is a keyword
497 raise error.ParseError(_("diff expects zero, one, or two arguments"))
497 raise error.ParseError(_("diff expects zero, one, or two arguments"))
498
498
499 def getpatterns(i):
499 def getpatterns(i):
500 if i < len(args):
500 if i < len(args):
501 s = evalstring(context, mapping, args[i]).strip()
501 s = evalstring(context, mapping, args[i]).strip()
502 if s:
502 if s:
503 return [s]
503 return [s]
504 return []
504 return []
505
505
506 ctx = mapping['ctx']
506 ctx = mapping['ctx']
507 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
507 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
508
508
509 return ''.join(chunks)
509 return ''.join(chunks)
510
510
511 @templatefunc('files(pattern)')
511 @templatefunc('files(pattern)')
512 def files(context, mapping, args):
512 def files(context, mapping, args):
513 """All files of the current changeset matching the pattern. See
513 """All files of the current changeset matching the pattern. See
514 :hg:`help patterns`."""
514 :hg:`help patterns`."""
515 if not len(args) == 1:
515 if not len(args) == 1:
516 # i18n: "files" is a keyword
516 # i18n: "files" is a keyword
517 raise error.ParseError(_("files expects one argument"))
517 raise error.ParseError(_("files expects one argument"))
518
518
519 raw = evalstring(context, mapping, args[0])
519 raw = evalstring(context, mapping, args[0])
520 ctx = mapping['ctx']
520 ctx = mapping['ctx']
521 m = ctx.match([raw])
521 m = ctx.match([raw])
522 files = list(ctx.matches(m))
522 files = list(ctx.matches(m))
523 return templatekw.showlist("file", files, **mapping)
523 return templatekw.showlist("file", files, **mapping)
524
524
525 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
525 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
526 def fill(context, mapping, args):
526 def fill(context, mapping, args):
527 """Fill many
527 """Fill many
528 paragraphs with optional indentation. See the "fill" filter."""
528 paragraphs with optional indentation. See the "fill" filter."""
529 if not (1 <= len(args) <= 4):
529 if not (1 <= len(args) <= 4):
530 # i18n: "fill" is a keyword
530 # i18n: "fill" is a keyword
531 raise error.ParseError(_("fill expects one to four arguments"))
531 raise error.ParseError(_("fill expects one to four arguments"))
532
532
533 text = evalstring(context, mapping, args[0])
533 text = evalstring(context, mapping, args[0])
534 width = 76
534 width = 76
535 initindent = ''
535 initindent = ''
536 hangindent = ''
536 hangindent = ''
537 if 2 <= len(args) <= 4:
537 if 2 <= len(args) <= 4:
538 width = evalinteger(context, mapping, args[1],
538 width = evalinteger(context, mapping, args[1],
539 # i18n: "fill" is a keyword
539 # i18n: "fill" is a keyword
540 _("fill expects an integer width"))
540 _("fill expects an integer width"))
541 try:
541 try:
542 initindent = evalstring(context, mapping, args[2])
542 initindent = evalstring(context, mapping, args[2])
543 hangindent = evalstring(context, mapping, args[3])
543 hangindent = evalstring(context, mapping, args[3])
544 except IndexError:
544 except IndexError:
545 pass
545 pass
546
546
547 return templatefilters.fill(text, width, initindent, hangindent)
547 return templatefilters.fill(text, width, initindent, hangindent)
548
548
549 @templatefunc('formatnode(node)')
549 @templatefunc('formatnode(node)')
550 def formatnode(context, mapping, args):
550 def formatnode(context, mapping, args):
551 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
551 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
552 if len(args) != 1:
552 if len(args) != 1:
553 # i18n: "formatnode" is a keyword
553 # i18n: "formatnode" is a keyword
554 raise error.ParseError(_("formatnode expects one argument"))
554 raise error.ParseError(_("formatnode expects one argument"))
555
555
556 ui = mapping['ui']
556 ui = mapping['ui']
557 node = evalstring(context, mapping, args[0])
557 node = evalstring(context, mapping, args[0])
558 if ui.debugflag:
558 if ui.debugflag:
559 return node
559 return node
560 return templatefilters.short(node)
560 return templatefilters.short(node)
561
561
562 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])')
562 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])')
563 def pad(context, mapping, args):
563 def pad(context, mapping, args):
564 """Pad text with a
564 """Pad text with a
565 fill character."""
565 fill character."""
566 if not (2 <= len(args) <= 4):
566 if not (2 <= len(args) <= 4):
567 # i18n: "pad" is a keyword
567 # i18n: "pad" is a keyword
568 raise error.ParseError(_("pad() expects two to four arguments"))
568 raise error.ParseError(_("pad() expects two to four arguments"))
569
569
570 width = evalinteger(context, mapping, args[1],
570 width = evalinteger(context, mapping, args[1],
571 # i18n: "pad" is a keyword
571 # i18n: "pad" is a keyword
572 _("pad() expects an integer width"))
572 _("pad() expects an integer width"))
573
573
574 text = evalstring(context, mapping, args[0])
574 text = evalstring(context, mapping, args[0])
575
575
576 left = False
576 left = False
577 fillchar = ' '
577 fillchar = ' '
578 if len(args) > 2:
578 if len(args) > 2:
579 fillchar = evalstring(context, mapping, args[2])
579 fillchar = evalstring(context, mapping, args[2])
580 if len(color.stripeffects(fillchar)) != 1:
580 if len(color.stripeffects(fillchar)) != 1:
581 # i18n: "pad" is a keyword
581 # i18n: "pad" is a keyword
582 raise error.ParseError(_("pad() expects a single fill character"))
582 raise error.ParseError(_("pad() expects a single fill character"))
583 if len(args) > 3:
583 if len(args) > 3:
584 left = evalboolean(context, mapping, args[3])
584 left = evalboolean(context, mapping, args[3])
585
585
586 fillwidth = width - encoding.colwidth(color.stripeffects(text))
586 fillwidth = width - encoding.colwidth(color.stripeffects(text))
587 if fillwidth <= 0:
587 if fillwidth <= 0:
588 return text
588 return text
589 if left:
589 if left:
590 return fillchar * fillwidth + text
590 return fillchar * fillwidth + text
591 else:
591 else:
592 return text + fillchar * fillwidth
592 return text + fillchar * fillwidth
593
593
594 @templatefunc('indent(text, indentchars[, firstline])')
594 @templatefunc('indent(text, indentchars[, firstline])')
595 def indent(context, mapping, args):
595 def indent(context, mapping, args):
596 """Indents all non-empty lines
596 """Indents all non-empty lines
597 with the characters given in the indentchars string. An optional
597 with the characters given in the indentchars string. An optional
598 third parameter will override the indent for the first line only
598 third parameter will override the indent for the first line only
599 if present."""
599 if present."""
600 if not (2 <= len(args) <= 3):
600 if not (2 <= len(args) <= 3):
601 # i18n: "indent" is a keyword
601 # i18n: "indent" is a keyword
602 raise error.ParseError(_("indent() expects two or three arguments"))
602 raise error.ParseError(_("indent() expects two or three arguments"))
603
603
604 text = evalstring(context, mapping, args[0])
604 text = evalstring(context, mapping, args[0])
605 indent = evalstring(context, mapping, args[1])
605 indent = evalstring(context, mapping, args[1])
606
606
607 if len(args) == 3:
607 if len(args) == 3:
608 firstline = evalstring(context, mapping, args[2])
608 firstline = evalstring(context, mapping, args[2])
609 else:
609 else:
610 firstline = indent
610 firstline = indent
611
611
612 # the indent function doesn't indent the first line, so we do it here
612 # the indent function doesn't indent the first line, so we do it here
613 return templatefilters.indent(firstline + text, indent)
613 return templatefilters.indent(firstline + text, indent)
614
614
615 @templatefunc('get(dict, key)')
615 @templatefunc('get(dict, key)')
616 def get(context, mapping, args):
616 def get(context, mapping, args):
617 """Get an attribute/key from an object. Some keywords
617 """Get an attribute/key from an object. Some keywords
618 are complex types. This function allows you to obtain the value of an
618 are complex types. This function allows you to obtain the value of an
619 attribute on these types."""
619 attribute on these types."""
620 if len(args) != 2:
620 if len(args) != 2:
621 # i18n: "get" is a keyword
621 # i18n: "get" is a keyword
622 raise error.ParseError(_("get() expects two arguments"))
622 raise error.ParseError(_("get() expects two arguments"))
623
623
624 dictarg = evalfuncarg(context, mapping, args[0])
624 dictarg = evalfuncarg(context, mapping, args[0])
625 if not util.safehasattr(dictarg, 'get'):
625 if not util.safehasattr(dictarg, 'get'):
626 # i18n: "get" is a keyword
626 # i18n: "get" is a keyword
627 raise error.ParseError(_("get() expects a dict as first argument"))
627 raise error.ParseError(_("get() expects a dict as first argument"))
628
628
629 key = evalfuncarg(context, mapping, args[1])
629 key = evalfuncarg(context, mapping, args[1])
630 return dictarg.get(key)
630 return dictarg.get(key)
631
631
632 @templatefunc('if(expr, then[, else])')
632 @templatefunc('if(expr, then[, else])')
633 def if_(context, mapping, args):
633 def if_(context, mapping, args):
634 """Conditionally execute based on the result of
634 """Conditionally execute based on the result of
635 an expression."""
635 an expression."""
636 if not (2 <= len(args) <= 3):
636 if not (2 <= len(args) <= 3):
637 # i18n: "if" is a keyword
637 # i18n: "if" is a keyword
638 raise error.ParseError(_("if expects two or three arguments"))
638 raise error.ParseError(_("if expects two or three arguments"))
639
639
640 test = evalboolean(context, mapping, args[0])
640 test = evalboolean(context, mapping, args[0])
641 if test:
641 if test:
642 yield args[1][0](context, mapping, args[1][1])
642 yield args[1][0](context, mapping, args[1][1])
643 elif len(args) == 3:
643 elif len(args) == 3:
644 yield args[2][0](context, mapping, args[2][1])
644 yield args[2][0](context, mapping, args[2][1])
645
645
646 @templatefunc('ifcontains(needle, haystack, then[, else])')
646 @templatefunc('ifcontains(needle, haystack, then[, else])')
647 def ifcontains(context, mapping, args):
647 def ifcontains(context, mapping, args):
648 """Conditionally execute based
648 """Conditionally execute based
649 on whether the item "needle" is in "haystack"."""
649 on whether the item "needle" is in "haystack"."""
650 if not (3 <= len(args) <= 4):
650 if not (3 <= len(args) <= 4):
651 # i18n: "ifcontains" is a keyword
651 # i18n: "ifcontains" is a keyword
652 raise error.ParseError(_("ifcontains expects three or four arguments"))
652 raise error.ParseError(_("ifcontains expects three or four arguments"))
653
653
654 needle = evalstring(context, mapping, args[0])
654 needle = evalstring(context, mapping, args[0])
655 haystack = evalfuncarg(context, mapping, args[1])
655 haystack = evalfuncarg(context, mapping, args[1])
656
656
657 if needle in haystack:
657 if needle in haystack:
658 yield args[2][0](context, mapping, args[2][1])
658 yield args[2][0](context, mapping, args[2][1])
659 elif len(args) == 4:
659 elif len(args) == 4:
660 yield args[3][0](context, mapping, args[3][1])
660 yield args[3][0](context, mapping, args[3][1])
661
661
662 @templatefunc('ifeq(expr1, expr2, then[, else])')
662 @templatefunc('ifeq(expr1, expr2, then[, else])')
663 def ifeq(context, mapping, args):
663 def ifeq(context, mapping, args):
664 """Conditionally execute based on
664 """Conditionally execute based on
665 whether 2 items are equivalent."""
665 whether 2 items are equivalent."""
666 if not (3 <= len(args) <= 4):
666 if not (3 <= len(args) <= 4):
667 # i18n: "ifeq" is a keyword
667 # i18n: "ifeq" is a keyword
668 raise error.ParseError(_("ifeq expects three or four arguments"))
668 raise error.ParseError(_("ifeq expects three or four arguments"))
669
669
670 test = evalstring(context, mapping, args[0])
670 test = evalstring(context, mapping, args[0])
671 match = evalstring(context, mapping, args[1])
671 match = evalstring(context, mapping, args[1])
672 if test == match:
672 if test == match:
673 yield args[2][0](context, mapping, args[2][1])
673 yield args[2][0](context, mapping, args[2][1])
674 elif len(args) == 4:
674 elif len(args) == 4:
675 yield args[3][0](context, mapping, args[3][1])
675 yield args[3][0](context, mapping, args[3][1])
676
676
677 @templatefunc('join(list, sep)')
677 @templatefunc('join(list, sep)')
678 def join(context, mapping, args):
678 def join(context, mapping, args):
679 """Join items in a list with a delimiter."""
679 """Join items in a list with a delimiter."""
680 if not (1 <= len(args) <= 2):
680 if not (1 <= len(args) <= 2):
681 # i18n: "join" is a keyword
681 # i18n: "join" is a keyword
682 raise error.ParseError(_("join expects one or two arguments"))
682 raise error.ParseError(_("join expects one or two arguments"))
683
683
684 joinset = args[0][0](context, mapping, args[0][1])
684 joinset = args[0][0](context, mapping, args[0][1])
685 if util.safehasattr(joinset, 'itermaps'):
685 if util.safehasattr(joinset, 'itermaps'):
686 jf = joinset.joinfmt
686 jf = joinset.joinfmt
687 joinset = [jf(x) for x in joinset.itermaps()]
687 joinset = [jf(x) for x in joinset.itermaps()]
688
688
689 joiner = " "
689 joiner = " "
690 if len(args) > 1:
690 if len(args) > 1:
691 joiner = evalstring(context, mapping, args[1])
691 joiner = evalstring(context, mapping, args[1])
692
692
693 first = True
693 first = True
694 for x in joinset:
694 for x in joinset:
695 if first:
695 if first:
696 first = False
696 first = False
697 else:
697 else:
698 yield joiner
698 yield joiner
699 yield x
699 yield x
700
700
701 @templatefunc('label(label, expr)')
701 @templatefunc('label(label, expr)')
702 def label(context, mapping, args):
702 def label(context, mapping, args):
703 """Apply a label to generated content. Content with
703 """Apply a label to generated content. Content with
704 a label applied can result in additional post-processing, such as
704 a label applied can result in additional post-processing, such as
705 automatic colorization."""
705 automatic colorization."""
706 if len(args) != 2:
706 if len(args) != 2:
707 # i18n: "label" is a keyword
707 # i18n: "label" is a keyword
708 raise error.ParseError(_("label expects two arguments"))
708 raise error.ParseError(_("label expects two arguments"))
709
709
710 ui = mapping['ui']
710 ui = mapping['ui']
711 thing = evalstring(context, mapping, args[1])
711 thing = evalstring(context, mapping, args[1])
712 # preserve unknown symbol as literal so effects like 'red', 'bold',
712 # preserve unknown symbol as literal so effects like 'red', 'bold',
713 # etc. don't need to be quoted
713 # etc. don't need to be quoted
714 label = evalstringliteral(context, mapping, args[0])
714 label = evalstringliteral(context, mapping, args[0])
715
715
716 return ui.label(thing, label)
716 return ui.label(thing, label)
717
717
718 @templatefunc('latesttag([pattern])')
718 @templatefunc('latesttag([pattern])')
719 def latesttag(context, mapping, args):
719 def latesttag(context, mapping, args):
720 """The global tags matching the given pattern on the
720 """The global tags matching the given pattern on the
721 most recent globally tagged ancestor of this changeset."""
721 most recent globally tagged ancestor of this changeset."""
722 if len(args) > 1:
722 if len(args) > 1:
723 # i18n: "latesttag" is a keyword
723 # i18n: "latesttag" is a keyword
724 raise error.ParseError(_("latesttag expects at most one argument"))
724 raise error.ParseError(_("latesttag expects at most one argument"))
725
725
726 pattern = None
726 pattern = None
727 if len(args) == 1:
727 if len(args) == 1:
728 pattern = evalstring(context, mapping, args[0])
728 pattern = evalstring(context, mapping, args[0])
729
729
730 return templatekw.showlatesttags(pattern, **mapping)
730 return templatekw.showlatesttags(pattern, **mapping)
731
731
732 @templatefunc('localdate(date[, tz])')
732 @templatefunc('localdate(date[, tz])')
733 def localdate(context, mapping, args):
733 def localdate(context, mapping, args):
734 """Converts a date to the specified timezone.
734 """Converts a date to the specified timezone.
735 The default is local date."""
735 The default is local date."""
736 if not (1 <= len(args) <= 2):
736 if not (1 <= len(args) <= 2):
737 # i18n: "localdate" is a keyword
737 # i18n: "localdate" is a keyword
738 raise error.ParseError(_("localdate expects one or two arguments"))
738 raise error.ParseError(_("localdate expects one or two arguments"))
739
739
740 date = evalfuncarg(context, mapping, args[0])
740 date = evalfuncarg(context, mapping, args[0])
741 try:
741 try:
742 date = util.parsedate(date)
742 date = util.parsedate(date)
743 except AttributeError: # not str nor date tuple
743 except AttributeError: # not str nor date tuple
744 # i18n: "localdate" is a keyword
744 # i18n: "localdate" is a keyword
745 raise error.ParseError(_("localdate expects a date information"))
745 raise error.ParseError(_("localdate expects a date information"))
746 if len(args) >= 2:
746 if len(args) >= 2:
747 tzoffset = None
747 tzoffset = None
748 tz = evalfuncarg(context, mapping, args[1])
748 tz = evalfuncarg(context, mapping, args[1])
749 if isinstance(tz, str):
749 if isinstance(tz, str):
750 tzoffset, remainder = util.parsetimezone(tz)
750 tzoffset, remainder = util.parsetimezone(tz)
751 if remainder:
751 if remainder:
752 tzoffset = None
752 tzoffset = None
753 if tzoffset is None:
753 if tzoffset is None:
754 try:
754 try:
755 tzoffset = int(tz)
755 tzoffset = int(tz)
756 except (TypeError, ValueError):
756 except (TypeError, ValueError):
757 # i18n: "localdate" is a keyword
757 # i18n: "localdate" is a keyword
758 raise error.ParseError(_("localdate expects a timezone"))
758 raise error.ParseError(_("localdate expects a timezone"))
759 else:
759 else:
760 tzoffset = util.makedate()[1]
760 tzoffset = util.makedate()[1]
761 return (date[0], tzoffset)
761 return (date[0], tzoffset)
762
762
763 @templatefunc('mod(a, b)')
763 @templatefunc('mod(a, b)')
764 def mod(context, mapping, args):
764 def mod(context, mapping, args):
765 """Calculate a mod b such that a / b + a mod b == a"""
765 """Calculate a mod b such that a / b + a mod b == a"""
766 if not len(args) == 2:
766 if not len(args) == 2:
767 # i18n: "mod" is a keyword
767 # i18n: "mod" is a keyword
768 raise error.ParseError(_("mod expects two arguments"))
768 raise error.ParseError(_("mod expects two arguments"))
769
769
770 func = lambda a, b: a % b
770 func = lambda a, b: a % b
771 return runarithmetic(context, mapping, (func, args[0], args[1]))
771 return runarithmetic(context, mapping, (func, args[0], args[1]))
772
772
773 @templatefunc('relpath(path)')
773 @templatefunc('relpath(path)')
774 def relpath(context, mapping, args):
774 def relpath(context, mapping, args):
775 """Convert a repository-absolute path into a filesystem path relative to
775 """Convert a repository-absolute path into a filesystem path relative to
776 the current working directory."""
776 the current working directory."""
777 if len(args) != 1:
777 if len(args) != 1:
778 # i18n: "relpath" is a keyword
778 # i18n: "relpath" is a keyword
779 raise error.ParseError(_("relpath expects one argument"))
779 raise error.ParseError(_("relpath expects one argument"))
780
780
781 repo = mapping['ctx'].repo()
781 repo = mapping['ctx'].repo()
782 path = evalstring(context, mapping, args[0])
782 path = evalstring(context, mapping, args[0])
783 return repo.pathto(path)
783 return repo.pathto(path)
784
784
785 @templatefunc('revset(query[, formatargs...])')
785 @templatefunc('revset(query[, formatargs...])')
786 def revset(context, mapping, args):
786 def revset(context, mapping, args):
787 """Execute a revision set query. See
787 """Execute a revision set query. See
788 :hg:`help revset`."""
788 :hg:`help revset`."""
789 if not len(args) > 0:
789 if not len(args) > 0:
790 # i18n: "revset" is a keyword
790 # i18n: "revset" is a keyword
791 raise error.ParseError(_("revset expects one or more arguments"))
791 raise error.ParseError(_("revset expects one or more arguments"))
792
792
793 raw = evalstring(context, mapping, args[0])
793 raw = evalstring(context, mapping, args[0])
794 ctx = mapping['ctx']
794 ctx = mapping['ctx']
795 repo = ctx.repo()
795 repo = ctx.repo()
796
796
797 def query(expr):
797 def query(expr):
798 m = revsetmod.match(repo.ui, expr)
798 m = revsetmod.match(repo.ui, expr)
799 return m(repo)
799 return m(repo)
800
800
801 if len(args) > 1:
801 if len(args) > 1:
802 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
802 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
803 revs = query(revsetlang.formatspec(raw, *formatargs))
803 revs = query(revsetlang.formatspec(raw, *formatargs))
804 revs = list(revs)
804 revs = list(revs)
805 else:
805 else:
806 revsetcache = mapping['cache'].setdefault("revsetcache", {})
806 revsetcache = mapping['cache'].setdefault("revsetcache", {})
807 if raw in revsetcache:
807 if raw in revsetcache:
808 revs = revsetcache[raw]
808 revs = revsetcache[raw]
809 else:
809 else:
810 revs = query(raw)
810 revs = query(raw)
811 revs = list(revs)
811 revs = list(revs)
812 revsetcache[raw] = revs
812 revsetcache[raw] = revs
813
813
814 return templatekw.showrevslist("revision", revs, **mapping)
814 return templatekw.showrevslist("revision", revs, **mapping)
815
815
816 @templatefunc('rstdoc(text, style)')
816 @templatefunc('rstdoc(text, style)')
817 def rstdoc(context, mapping, args):
817 def rstdoc(context, mapping, args):
818 """Format reStructuredText."""
818 """Format reStructuredText."""
819 if len(args) != 2:
819 if len(args) != 2:
820 # i18n: "rstdoc" is a keyword
820 # i18n: "rstdoc" is a keyword
821 raise error.ParseError(_("rstdoc expects two arguments"))
821 raise error.ParseError(_("rstdoc expects two arguments"))
822
822
823 text = evalstring(context, mapping, args[0])
823 text = evalstring(context, mapping, args[0])
824 style = evalstring(context, mapping, args[1])
824 style = evalstring(context, mapping, args[1])
825
825
826 return minirst.format(text, style=style, keep=['verbose'])
826 return minirst.format(text, style=style, keep=['verbose'])
827
827
828 @templatefunc('separate(sep, args)')
828 @templatefunc('separate(sep, args)')
829 def separate(context, mapping, args):
829 def separate(context, mapping, args):
830 """Add a separator between non-empty arguments."""
830 """Add a separator between non-empty arguments."""
831 if not args:
831 if not args:
832 # i18n: "separate" is a keyword
832 # i18n: "separate" is a keyword
833 raise error.ParseError(_("separate expects at least one argument"))
833 raise error.ParseError(_("separate expects at least one argument"))
834
834
835 sep = evalstring(context, mapping, args[0])
835 sep = evalstring(context, mapping, args[0])
836 first = True
836 first = True
837 for arg in args[1:]:
837 for arg in args[1:]:
838 argstr = evalstring(context, mapping, arg)
838 argstr = evalstring(context, mapping, arg)
839 if not argstr:
839 if not argstr:
840 continue
840 continue
841 if first:
841 if first:
842 first = False
842 first = False
843 else:
843 else:
844 yield sep
844 yield sep
845 yield argstr
845 yield argstr
846
846
847 @templatefunc('shortest(node, minlength=4)')
847 @templatefunc('shortest(node, minlength=4)')
848 def shortest(context, mapping, args):
848 def shortest(context, mapping, args):
849 """Obtain the shortest representation of
849 """Obtain the shortest representation of
850 a node."""
850 a node."""
851 if not (1 <= len(args) <= 2):
851 if not (1 <= len(args) <= 2):
852 # i18n: "shortest" is a keyword
852 # i18n: "shortest" is a keyword
853 raise error.ParseError(_("shortest() expects one or two arguments"))
853 raise error.ParseError(_("shortest() expects one or two arguments"))
854
854
855 node = evalstring(context, mapping, args[0])
855 node = evalstring(context, mapping, args[0])
856
856
857 minlength = 4
857 minlength = 4
858 if len(args) > 1:
858 if len(args) > 1:
859 minlength = evalinteger(context, mapping, args[1],
859 minlength = evalinteger(context, mapping, args[1],
860 # i18n: "shortest" is a keyword
860 # i18n: "shortest" is a keyword
861 _("shortest() expects an integer minlength"))
861 _("shortest() expects an integer minlength"))
862
862
863 # _partialmatch() of filtered changelog could take O(len(repo)) time,
863 # _partialmatch() of filtered changelog could take O(len(repo)) time,
864 # which would be unacceptably slow. so we look for hash collision in
864 # which would be unacceptably slow. so we look for hash collision in
865 # unfiltered space, which means some hashes may be slightly longer.
865 # unfiltered space, which means some hashes may be slightly longer.
866 cl = mapping['ctx']._repo.unfiltered().changelog
866 cl = mapping['ctx']._repo.unfiltered().changelog
867 def isvalid(test):
867 def isvalid(test):
868 try:
868 try:
869 if cl._partialmatch(test) is None:
869 if cl._partialmatch(test) is None:
870 return False
870 return False
871
871
872 try:
872 try:
873 i = int(test)
873 i = int(test)
874 # if we are a pure int, then starting with zero will not be
874 # if we are a pure int, then starting with zero will not be
875 # confused as a rev; or, obviously, if the int is larger than
875 # confused as a rev; or, obviously, if the int is larger than
876 # the value of the tip rev
876 # the value of the tip rev
877 if test[0] == '0' or i > len(cl):
877 if test[0] == '0' or i > len(cl):
878 return True
878 return True
879 return False
879 return False
880 except ValueError:
880 except ValueError:
881 return True
881 return True
882 except error.RevlogError:
882 except error.RevlogError:
883 return False
883 return False
884
884
885 shortest = node
885 shortest = node
886 startlength = max(6, minlength)
886 startlength = max(6, minlength)
887 length = startlength
887 length = startlength
888 while True:
888 while True:
889 test = node[:length]
889 test = node[:length]
890 if isvalid(test):
890 if isvalid(test):
891 shortest = test
891 shortest = test
892 if length == minlength or length > startlength:
892 if length == minlength or length > startlength:
893 return shortest
893 return shortest
894 length -= 1
894 length -= 1
895 else:
895 else:
896 length += 1
896 length += 1
897 if len(shortest) <= length:
897 if len(shortest) <= length:
898 return shortest
898 return shortest
899
899
900 @templatefunc('strip(text[, chars])')
900 @templatefunc('strip(text[, chars])')
901 def strip(context, mapping, args):
901 def strip(context, mapping, args):
902 """Strip characters from a string. By default,
902 """Strip characters from a string. By default,
903 strips all leading and trailing whitespace."""
903 strips all leading and trailing whitespace."""
904 if not (1 <= len(args) <= 2):
904 if not (1 <= len(args) <= 2):
905 # i18n: "strip" is a keyword
905 # i18n: "strip" is a keyword
906 raise error.ParseError(_("strip expects one or two arguments"))
906 raise error.ParseError(_("strip expects one or two arguments"))
907
907
908 text = evalstring(context, mapping, args[0])
908 text = evalstring(context, mapping, args[0])
909 if len(args) == 2:
909 if len(args) == 2:
910 chars = evalstring(context, mapping, args[1])
910 chars = evalstring(context, mapping, args[1])
911 return text.strip(chars)
911 return text.strip(chars)
912 return text.strip()
912 return text.strip()
913
913
914 @templatefunc('sub(pattern, replacement, expression)')
914 @templatefunc('sub(pattern, replacement, expression)')
915 def sub(context, mapping, args):
915 def sub(context, mapping, args):
916 """Perform text substitution
916 """Perform text substitution
917 using regular expressions."""
917 using regular expressions."""
918 if len(args) != 3:
918 if len(args) != 3:
919 # i18n: "sub" is a keyword
919 # i18n: "sub" is a keyword
920 raise error.ParseError(_("sub expects three arguments"))
920 raise error.ParseError(_("sub expects three arguments"))
921
921
922 pat = evalstring(context, mapping, args[0])
922 pat = evalstring(context, mapping, args[0])
923 rpl = evalstring(context, mapping, args[1])
923 rpl = evalstring(context, mapping, args[1])
924 src = evalstring(context, mapping, args[2])
924 src = evalstring(context, mapping, args[2])
925 try:
925 try:
926 patre = re.compile(pat)
926 patre = re.compile(pat)
927 except re.error:
927 except re.error:
928 # i18n: "sub" is a keyword
928 # i18n: "sub" is a keyword
929 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
929 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
930 try:
930 try:
931 yield patre.sub(rpl, src)
931 yield patre.sub(rpl, src)
932 except re.error:
932 except re.error:
933 # i18n: "sub" is a keyword
933 # i18n: "sub" is a keyword
934 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
934 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
935
935
936 @templatefunc('startswith(pattern, text)')
936 @templatefunc('startswith(pattern, text)')
937 def startswith(context, mapping, args):
937 def startswith(context, mapping, args):
938 """Returns the value from the "text" argument
938 """Returns the value from the "text" argument
939 if it begins with the content from the "pattern" argument."""
939 if it begins with the content from the "pattern" argument."""
940 if len(args) != 2:
940 if len(args) != 2:
941 # i18n: "startswith" is a keyword
941 # i18n: "startswith" is a keyword
942 raise error.ParseError(_("startswith expects two arguments"))
942 raise error.ParseError(_("startswith expects two arguments"))
943
943
944 patn = evalstring(context, mapping, args[0])
944 patn = evalstring(context, mapping, args[0])
945 text = evalstring(context, mapping, args[1])
945 text = evalstring(context, mapping, args[1])
946 if text.startswith(patn):
946 if text.startswith(patn):
947 return text
947 return text
948 return ''
948 return ''
949
949
950 @templatefunc('word(number, text[, separator])')
950 @templatefunc('word(number, text[, separator])')
951 def word(context, mapping, args):
951 def word(context, mapping, args):
952 """Return the nth word from a string."""
952 """Return the nth word from a string."""
953 if not (2 <= len(args) <= 3):
953 if not (2 <= len(args) <= 3):
954 # i18n: "word" is a keyword
954 # i18n: "word" is a keyword
955 raise error.ParseError(_("word expects two or three arguments, got %d")
955 raise error.ParseError(_("word expects two or three arguments, got %d")
956 % len(args))
956 % len(args))
957
957
958 num = evalinteger(context, mapping, args[0],
958 num = evalinteger(context, mapping, args[0],
959 # i18n: "word" is a keyword
959 # i18n: "word" is a keyword
960 _("word expects an integer index"))
960 _("word expects an integer index"))
961 text = evalstring(context, mapping, args[1])
961 text = evalstring(context, mapping, args[1])
962 if len(args) == 3:
962 if len(args) == 3:
963 splitter = evalstring(context, mapping, args[2])
963 splitter = evalstring(context, mapping, args[2])
964 else:
964 else:
965 splitter = None
965 splitter = None
966
966
967 tokens = text.split(splitter)
967 tokens = text.split(splitter)
968 if num >= len(tokens) or num < -len(tokens):
968 if num >= len(tokens) or num < -len(tokens):
969 return ''
969 return ''
970 else:
970 else:
971 return tokens[num]
971 return tokens[num]
972
972
973 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
973 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
974 exprmethods = {
974 exprmethods = {
975 "integer": lambda e, c: (runinteger, e[1]),
975 "integer": lambda e, c: (runinteger, e[1]),
976 "string": lambda e, c: (runstring, e[1]),
976 "string": lambda e, c: (runstring, e[1]),
977 "symbol": lambda e, c: (runsymbol, e[1]),
977 "symbol": lambda e, c: (runsymbol, e[1]),
978 "template": buildtemplate,
978 "template": buildtemplate,
979 "group": lambda e, c: compileexp(e[1], c, exprmethods),
979 "group": lambda e, c: compileexp(e[1], c, exprmethods),
980 # ".": buildmember,
980 # ".": buildmember,
981 "|": buildfilter,
981 "|": buildfilter,
982 "%": buildmap,
982 "%": buildmap,
983 "func": buildfunc,
983 "func": buildfunc,
984 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
984 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
985 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
985 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
986 "negate": buildnegate,
986 "negate": buildnegate,
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 }
989 }
990
990
991 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
991 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
992 methods = exprmethods.copy()
992 methods = exprmethods.copy()
993 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
993 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
994
994
995 class _aliasrules(parser.basealiasrules):
995 class _aliasrules(parser.basealiasrules):
996 """Parsing and expansion rule set of template aliases"""
996 """Parsing and expansion rule set of template aliases"""
997 _section = _('template alias')
997 _section = _('template alias')
998 _parse = staticmethod(_parseexpr)
998 _parse = staticmethod(_parseexpr)
999
999
1000 @staticmethod
1000 @staticmethod
1001 def _trygetfunc(tree):
1001 def _trygetfunc(tree):
1002 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1002 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1003 None"""
1003 None"""
1004 if tree[0] == 'func' and tree[1][0] == 'symbol':
1004 if tree[0] == 'func' and tree[1][0] == 'symbol':
1005 return tree[1][1], getlist(tree[2])
1005 return tree[1][1], getlist(tree[2])
1006 if tree[0] == '|' and tree[2][0] == 'symbol':
1006 if tree[0] == '|' and tree[2][0] == 'symbol':
1007 return tree[2][1], [tree[1]]
1007 return tree[2][1], [tree[1]]
1008
1008
1009 def expandaliases(tree, aliases):
1009 def expandaliases(tree, aliases):
1010 """Return new tree of aliases are expanded"""
1010 """Return new tree of aliases are expanded"""
1011 aliasmap = _aliasrules.buildmap(aliases)
1011 aliasmap = _aliasrules.buildmap(aliases)
1012 return _aliasrules.expand(aliasmap, tree)
1012 return _aliasrules.expand(aliasmap, tree)
1013
1013
1014 # template engine
1014 # template engine
1015
1015
1016 stringify = templatefilters.stringify
1016 stringify = templatefilters.stringify
1017
1017
1018 def _flatten(thing):
1018 def _flatten(thing):
1019 '''yield a single stream from a possibly nested set of iterators'''
1019 '''yield a single stream from a possibly nested set of iterators'''
1020 if isinstance(thing, str):
1020 if isinstance(thing, str):
1021 yield thing
1021 yield thing
1022 elif thing is None:
1022 elif thing is None:
1023 pass
1023 pass
1024 elif not util.safehasattr(thing, '__iter__'):
1024 elif not util.safehasattr(thing, '__iter__'):
1025 yield str(thing)
1025 yield str(thing)
1026 else:
1026 else:
1027 for i in thing:
1027 for i in thing:
1028 if isinstance(i, str):
1028 if isinstance(i, str):
1029 yield i
1029 yield i
1030 elif i is None:
1030 elif i is None:
1031 pass
1031 pass
1032 elif not util.safehasattr(i, '__iter__'):
1032 elif not util.safehasattr(i, '__iter__'):
1033 yield str(i)
1033 yield str(i)
1034 else:
1034 else:
1035 for j in _flatten(i):
1035 for j in _flatten(i):
1036 yield j
1036 yield j
1037
1037
1038 def unquotestring(s):
1038 def unquotestring(s):
1039 '''unwrap quotes if any; otherwise returns unmodified string'''
1039 '''unwrap quotes if any; otherwise returns unmodified string'''
1040 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1040 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1041 return s
1041 return s
1042 return s[1:-1]
1042 return s[1:-1]
1043
1043
1044 class engine(object):
1044 class engine(object):
1045 '''template expansion engine.
1045 '''template expansion engine.
1046
1046
1047 template expansion works like this. a map file contains key=value
1047 template expansion works like this. a map file contains key=value
1048 pairs. if value is quoted, it is treated as string. otherwise, it
1048 pairs. if value is quoted, it is treated as string. otherwise, it
1049 is treated as name of template file.
1049 is treated as name of template file.
1050
1050
1051 templater is asked to expand a key in map. it looks up key, and
1051 templater is asked to expand a key in map. it looks up key, and
1052 looks for strings like this: {foo}. it expands {foo} by looking up
1052 looks for strings like this: {foo}. it expands {foo} by looking up
1053 foo in map, and substituting it. expansion is recursive: it stops
1053 foo in map, and substituting it. expansion is recursive: it stops
1054 when there is no more {foo} to replace.
1054 when there is no more {foo} to replace.
1055
1055
1056 expansion also allows formatting and filtering.
1056 expansion also allows formatting and filtering.
1057
1057
1058 format uses key to expand each item in list. syntax is
1058 format uses key to expand each item in list. syntax is
1059 {key%format}.
1059 {key%format}.
1060
1060
1061 filter uses function to transform value. syntax is
1061 filter uses function to transform value. syntax is
1062 {key|filter1|filter2|...}.'''
1062 {key|filter1|filter2|...}.'''
1063
1063
1064 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1064 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1065 self._loader = loader
1065 self._loader = loader
1066 if filters is None:
1066 if filters is None:
1067 filters = {}
1067 filters = {}
1068 self._filters = filters
1068 self._filters = filters
1069 if defaults is None:
1069 if defaults is None:
1070 defaults = {}
1070 defaults = {}
1071 self._defaults = defaults
1071 self._defaults = defaults
1072 self._aliasmap = _aliasrules.buildmap(aliases)
1072 self._aliasmap = _aliasrules.buildmap(aliases)
1073 self._cache = {} # key: (func, data)
1073 self._cache = {} # key: (func, data)
1074
1074
1075 def _load(self, t):
1075 def _load(self, t):
1076 '''load, parse, and cache a template'''
1076 '''load, parse, and cache a template'''
1077 if t not in self._cache:
1077 if t not in self._cache:
1078 # put poison to cut recursion while compiling 't'
1078 # put poison to cut recursion while compiling 't'
1079 self._cache[t] = (_runrecursivesymbol, t)
1079 self._cache[t] = (_runrecursivesymbol, t)
1080 try:
1080 try:
1081 x = parse(self._loader(t))
1081 x = parse(self._loader(t))
1082 if self._aliasmap:
1082 if self._aliasmap:
1083 x = _aliasrules.expand(self._aliasmap, x)
1083 x = _aliasrules.expand(self._aliasmap, x)
1084 self._cache[t] = compileexp(x, self, methods)
1084 self._cache[t] = compileexp(x, self, methods)
1085 except: # re-raises
1085 except: # re-raises
1086 del self._cache[t]
1086 del self._cache[t]
1087 raise
1087 raise
1088 return self._cache[t]
1088 return self._cache[t]
1089
1089
1090 def process(self, t, mapping):
1090 def process(self, t, mapping):
1091 '''Perform expansion. t is name of map element to expand.
1091 '''Perform expansion. t is name of map element to expand.
1092 mapping contains added elements for use during expansion. Is a
1092 mapping contains added elements for use during expansion. Is a
1093 generator.'''
1093 generator.'''
1094 func, data = self._load(t)
1094 func, data = self._load(t)
1095 return _flatten(func(self, mapping, data))
1095 return _flatten(func(self, mapping, data))
1096
1096
1097 engines = {'default': engine}
1097 engines = {'default': engine}
1098
1098
1099 def stylelist():
1099 def stylelist():
1100 paths = templatepaths()
1100 paths = templatepaths()
1101 if not paths:
1101 if not paths:
1102 return _('no templates found, try `hg debuginstall` for more info')
1102 return _('no templates found, try `hg debuginstall` for more info')
1103 dirlist = os.listdir(paths[0])
1103 dirlist = os.listdir(paths[0])
1104 stylelist = []
1104 stylelist = []
1105 for file in dirlist:
1105 for file in dirlist:
1106 split = file.split(".")
1106 split = file.split(".")
1107 if split[-1] in ('orig', 'rej'):
1107 if split[-1] in ('orig', 'rej'):
1108 continue
1108 continue
1109 if split[0] == "map-cmdline":
1109 if split[0] == "map-cmdline":
1110 stylelist.append(split[1])
1110 stylelist.append(split[1])
1111 return ", ".join(sorted(stylelist))
1111 return ", ".join(sorted(stylelist))
1112
1112
1113 def _readmapfile(mapfile):
1113 def _readmapfile(mapfile):
1114 """Load template elements from the given map file"""
1114 """Load template elements from the given map file"""
1115 if not os.path.exists(mapfile):
1115 if not os.path.exists(mapfile):
1116 raise error.Abort(_("style '%s' not found") % mapfile,
1116 raise error.Abort(_("style '%s' not found") % mapfile,
1117 hint=_("available styles: %s") % stylelist())
1117 hint=_("available styles: %s") % stylelist())
1118
1118
1119 base = os.path.dirname(mapfile)
1119 base = os.path.dirname(mapfile)
1120 conf = config.config(includepaths=templatepaths())
1120 conf = config.config(includepaths=templatepaths())
1121 conf.read(mapfile)
1121 conf.read(mapfile)
1122
1122
1123 cache = {}
1123 cache = {}
1124 tmap = {}
1124 tmap = {}
1125 for key, val in conf[''].items():
1125 for key, val in conf[''].items():
1126 if not val:
1126 if not val:
1127 raise error.ParseError(_('missing value'), conf.source('', key))
1127 raise error.ParseError(_('missing value'), conf.source('', key))
1128 if val[0] in "'\"":
1128 if val[0] in "'\"":
1129 if val[0] != val[-1]:
1129 if val[0] != val[-1]:
1130 raise error.ParseError(_('unmatched quotes'),
1130 raise error.ParseError(_('unmatched quotes'),
1131 conf.source('', key))
1131 conf.source('', key))
1132 cache[key] = unquotestring(val)
1132 cache[key] = unquotestring(val)
1133 elif key == "__base__":
1133 elif key == "__base__":
1134 # treat as a pointer to a base class for this style
1134 # treat as a pointer to a base class for this style
1135 path = util.normpath(os.path.join(base, val))
1135 path = util.normpath(os.path.join(base, val))
1136
1136
1137 # fallback check in template paths
1137 # fallback check in template paths
1138 if not os.path.exists(path):
1138 if not os.path.exists(path):
1139 for p in templatepaths():
1139 for p in templatepaths():
1140 p2 = util.normpath(os.path.join(p, val))
1140 p2 = util.normpath(os.path.join(p, val))
1141 if os.path.isfile(p2):
1141 if os.path.isfile(p2):
1142 path = p2
1142 path = p2
1143 break
1143 break
1144 p3 = util.normpath(os.path.join(p2, "map"))
1144 p3 = util.normpath(os.path.join(p2, "map"))
1145 if os.path.isfile(p3):
1145 if os.path.isfile(p3):
1146 path = p3
1146 path = p3
1147 break
1147 break
1148
1148
1149 bcache, btmap = _readmapfile(path)
1149 bcache, btmap = _readmapfile(path)
1150 for k in bcache:
1150 for k in bcache:
1151 if k not in cache:
1151 if k not in cache:
1152 cache[k] = bcache[k]
1152 cache[k] = bcache[k]
1153 for k in btmap:
1153 for k in btmap:
1154 if k not in tmap:
1154 if k not in tmap:
1155 tmap[k] = btmap[k]
1155 tmap[k] = btmap[k]
1156 else:
1156 else:
1157 val = 'default', val
1157 val = 'default', val
1158 if ':' in val[1]:
1158 if ':' in val[1]:
1159 val = val[1].split(':', 1)
1159 val = val[1].split(':', 1)
1160 tmap[key] = val[0], os.path.join(base, val[1])
1160 tmap[key] = val[0], os.path.join(base, val[1])
1161 return cache, tmap
1161 return cache, tmap
1162
1162
1163 class TemplateNotFound(error.Abort):
1163 class TemplateNotFound(error.Abort):
1164 pass
1164 pass
1165
1165
1166 class templater(object):
1166 class templater(object):
1167
1167
1168 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1168 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1169 minchunk=1024, maxchunk=65536):
1169 minchunk=1024, maxchunk=65536):
1170 '''set up template engine.
1170 '''set up template engine.
1171 filters is dict of functions. each transforms a value into another.
1171 filters is dict of functions. each transforms a value into another.
1172 defaults is dict of default map definitions.
1172 defaults is dict of default map definitions.
1173 aliases is list of alias (name, replacement) pairs.
1173 aliases is list of alias (name, replacement) pairs.
1174 '''
1174 '''
1175 if filters is None:
1175 if filters is None:
1176 filters = {}
1176 filters = {}
1177 if defaults is None:
1177 if defaults is None:
1178 defaults = {}
1178 defaults = {}
1179 if cache is None:
1179 if cache is None:
1180 cache = {}
1180 cache = {}
1181 self.cache = cache.copy()
1181 self.cache = cache.copy()
1182 self.map = {}
1182 self.map = {}
1183 self.filters = templatefilters.filters.copy()
1183 self.filters = templatefilters.filters.copy()
1184 self.filters.update(filters)
1184 self.filters.update(filters)
1185 self.defaults = defaults
1185 self.defaults = defaults
1186 self._aliases = aliases
1186 self._aliases = aliases
1187 self.minchunk, self.maxchunk = minchunk, maxchunk
1187 self.minchunk, self.maxchunk = minchunk, maxchunk
1188 self.ecache = {}
1188 self.ecache = {}
1189
1189
1190 @classmethod
1190 @classmethod
1191 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1191 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1192 minchunk=1024, maxchunk=65536):
1192 minchunk=1024, maxchunk=65536):
1193 """Create templater from the specified map file"""
1193 """Create templater from the specified map file"""
1194 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1194 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1195 cache, tmap = _readmapfile(mapfile)
1195 cache, tmap = _readmapfile(mapfile)
1196 t.cache.update(cache)
1196 t.cache.update(cache)
1197 t.map = tmap
1197 t.map = tmap
1198 return t
1198 return t
1199
1199
1200 def __contains__(self, key):
1200 def __contains__(self, key):
1201 return key in self.cache or key in self.map
1201 return key in self.cache or key in self.map
1202
1202
1203 def load(self, t):
1203 def load(self, t):
1204 '''Get the template for the given template name. Use a local cache.'''
1204 '''Get the template for the given template name. Use a local cache.'''
1205 if t not in self.cache:
1205 if t not in self.cache:
1206 try:
1206 try:
1207 self.cache[t] = util.readfile(self.map[t][1])
1207 self.cache[t] = util.readfile(self.map[t][1])
1208 except KeyError as inst:
1208 except KeyError as inst:
1209 raise TemplateNotFound(_('"%s" not in template map') %
1209 raise TemplateNotFound(_('"%s" not in template map') %
1210 inst.args[0])
1210 inst.args[0])
1211 except IOError as inst:
1211 except IOError as inst:
1212 raise IOError(inst.args[0], _('template file %s: %s') %
1212 raise IOError(inst.args[0], _('template file %s: %s') %
1213 (self.map[t][1], inst.args[1]))
1213 (self.map[t][1], inst.args[1]))
1214 return self.cache[t]
1214 return self.cache[t]
1215
1215
1216 def __call__(self, t, **mapping):
1216 def __call__(self, t, **mapping):
1217 ttype = t in self.map and self.map[t][0] or 'default'
1217 ttype = t in self.map and self.map[t][0] or 'default'
1218 if ttype not in self.ecache:
1218 if ttype not in self.ecache:
1219 try:
1219 try:
1220 ecls = engines[ttype]
1220 ecls = engines[ttype]
1221 except KeyError:
1221 except KeyError:
1222 raise error.Abort(_('invalid template engine: %s') % ttype)
1222 raise error.Abort(_('invalid template engine: %s') % ttype)
1223 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1223 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1224 self._aliases)
1224 self._aliases)
1225 proc = self.ecache[ttype]
1225 proc = self.ecache[ttype]
1226
1226
1227 stream = proc.process(t, mapping)
1227 stream = proc.process(t, mapping)
1228 if self.minchunk:
1228 if self.minchunk:
1229 stream = util.increasingchunks(stream, min=self.minchunk,
1229 stream = util.increasingchunks(stream, min=self.minchunk,
1230 max=self.maxchunk)
1230 max=self.maxchunk)
1231 return stream
1231 return stream
1232
1232
1233 def templatepaths():
1233 def templatepaths():
1234 '''return locations used for template files.'''
1234 '''return locations used for template files.'''
1235 pathsrel = ['templates']
1235 pathsrel = ['templates']
1236 paths = [os.path.normpath(os.path.join(util.datapath, f))
1236 paths = [os.path.normpath(os.path.join(util.datapath, f))
1237 for f in pathsrel]
1237 for f in pathsrel]
1238 return [p for p in paths if os.path.isdir(p)]
1238 return [p for p in paths if os.path.isdir(p)]
1239
1239
1240 def templatepath(name):
1240 def templatepath(name):
1241 '''return location of template file. returns None if not found.'''
1241 '''return location of template file. returns None if not found.'''
1242 for p in templatepaths():
1242 for p in templatepaths():
1243 f = os.path.join(p, name)
1243 f = os.path.join(p, name)
1244 if os.path.exists(f):
1244 if os.path.exists(f):
1245 return f
1245 return f
1246 return None
1246 return None
1247
1247
1248 def stylemap(styles, paths=None):
1248 def stylemap(styles, paths=None):
1249 """Return path to mapfile for a given style.
1249 """Return path to mapfile for a given style.
1250
1250
1251 Searches mapfile in the following locations:
1251 Searches mapfile in the following locations:
1252 1. templatepath/style/map
1252 1. templatepath/style/map
1253 2. templatepath/map-style
1253 2. templatepath/map-style
1254 3. templatepath/map
1254 3. templatepath/map
1255 """
1255 """
1256
1256
1257 if paths is None:
1257 if paths is None:
1258 paths = templatepaths()
1258 paths = templatepaths()
1259 elif isinstance(paths, str):
1259 elif isinstance(paths, str):
1260 paths = [paths]
1260 paths = [paths]
1261
1261
1262 if isinstance(styles, str):
1262 if isinstance(styles, str):
1263 styles = [styles]
1263 styles = [styles]
1264
1264
1265 for style in styles:
1265 for style in styles:
1266 # only plain name is allowed to honor template paths
1266 # only plain name is allowed to honor template paths
1267 if (not style
1267 if (not style
1268 or style in (os.curdir, os.pardir)
1268 or style in (os.curdir, os.pardir)
1269 or pycompat.ossep in style
1269 or pycompat.ossep in style
1270 or pycompat.osaltsep and pycompat.osaltsep in style):
1270 or pycompat.osaltsep and pycompat.osaltsep in style):
1271 continue
1271 continue
1272 locations = [os.path.join(style, 'map'), 'map-' + style]
1272 locations = [os.path.join(style, 'map'), 'map-' + style]
1273 locations.append('map')
1273 locations.append('map')
1274
1274
1275 for path in paths:
1275 for path in paths:
1276 for location in locations:
1276 for location in locations:
1277 mapfile = os.path.join(path, location)
1277 mapfile = os.path.join(path, location)
1278 if os.path.isfile(mapfile):
1278 if os.path.isfile(mapfile):
1279 return style, mapfile
1279 return style, mapfile
1280
1280
1281 raise RuntimeError("No hgweb templates found in %r" % paths)
1281 raise RuntimeError("No hgweb templates found in %r" % paths)
1282
1282
1283 def loadfunction(ui, extname, registrarobj):
1283 def loadfunction(ui, extname, registrarobj):
1284 """Load template function from specified registrarobj
1284 """Load template function from specified registrarobj
1285 """
1285 """
1286 for name, func in registrarobj._table.iteritems():
1286 for name, func in registrarobj._table.iteritems():
1287 funcs[name] = func
1287 funcs[name] = func
1288
1288
1289 # tell hggettext to extract docstrings from these functions:
1289 # tell hggettext to extract docstrings from these functions:
1290 i18nfunctions = funcs.values()
1290 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now