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