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