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