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