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