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