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