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