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