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