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