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