##// END OF EJS Templates
templatekw: have showlist() take mapping dict with no **kwargs expansion (API)...
Yuya Nishihara -
r32037:e5eab0fe default
parent child Browse files
Show More
@@ -1,432 +1,432
1 # template-filters.py - common template expansion filters
1 # template-filters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 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 cgi
10 import cgi
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 hbisect,
17 hbisect,
18 node,
18 node,
19 registrar,
19 registrar,
20 templatekw,
20 templatekw,
21 util,
21 util,
22 )
22 )
23
23
24 urlerr = util.urlerr
24 urlerr = util.urlerr
25 urlreq = util.urlreq
25 urlreq = util.urlreq
26
26
27 # filters are callables like:
27 # filters are callables like:
28 # fn(obj)
28 # fn(obj)
29 # with:
29 # with:
30 # obj - object to be filtered (text, date, list and so on)
30 # obj - object to be filtered (text, date, list and so on)
31 filters = {}
31 filters = {}
32
32
33 templatefilter = registrar.templatefilter(filters)
33 templatefilter = registrar.templatefilter(filters)
34
34
35 @templatefilter('addbreaks')
35 @templatefilter('addbreaks')
36 def addbreaks(text):
36 def addbreaks(text):
37 """Any text. Add an XHTML "<br />" tag before the end of
37 """Any text. Add an XHTML "<br />" tag before the end of
38 every line except the last.
38 every line except the last.
39 """
39 """
40 return text.replace('\n', '<br/>\n')
40 return text.replace('\n', '<br/>\n')
41
41
42 agescales = [("year", 3600 * 24 * 365, 'Y'),
42 agescales = [("year", 3600 * 24 * 365, 'Y'),
43 ("month", 3600 * 24 * 30, 'M'),
43 ("month", 3600 * 24 * 30, 'M'),
44 ("week", 3600 * 24 * 7, 'W'),
44 ("week", 3600 * 24 * 7, 'W'),
45 ("day", 3600 * 24, 'd'),
45 ("day", 3600 * 24, 'd'),
46 ("hour", 3600, 'h'),
46 ("hour", 3600, 'h'),
47 ("minute", 60, 'm'),
47 ("minute", 60, 'm'),
48 ("second", 1, 's')]
48 ("second", 1, 's')]
49
49
50 @templatefilter('age')
50 @templatefilter('age')
51 def age(date, abbrev=False):
51 def age(date, abbrev=False):
52 """Date. Returns a human-readable date/time difference between the
52 """Date. Returns a human-readable date/time difference between the
53 given date/time and the current date/time.
53 given date/time and the current date/time.
54 """
54 """
55
55
56 def plural(t, c):
56 def plural(t, c):
57 if c == 1:
57 if c == 1:
58 return t
58 return t
59 return t + "s"
59 return t + "s"
60 def fmt(t, c, a):
60 def fmt(t, c, a):
61 if abbrev:
61 if abbrev:
62 return "%d%s" % (c, a)
62 return "%d%s" % (c, a)
63 return "%d %s" % (c, plural(t, c))
63 return "%d %s" % (c, plural(t, c))
64
64
65 now = time.time()
65 now = time.time()
66 then = date[0]
66 then = date[0]
67 future = False
67 future = False
68 if then > now:
68 if then > now:
69 future = True
69 future = True
70 delta = max(1, int(then - now))
70 delta = max(1, int(then - now))
71 if delta > agescales[0][1] * 30:
71 if delta > agescales[0][1] * 30:
72 return 'in the distant future'
72 return 'in the distant future'
73 else:
73 else:
74 delta = max(1, int(now - then))
74 delta = max(1, int(now - then))
75 if delta > agescales[0][1] * 2:
75 if delta > agescales[0][1] * 2:
76 return util.shortdate(date)
76 return util.shortdate(date)
77
77
78 for t, s, a in agescales:
78 for t, s, a in agescales:
79 n = delta // s
79 n = delta // s
80 if n >= 2 or s == 1:
80 if n >= 2 or s == 1:
81 if future:
81 if future:
82 return '%s from now' % fmt(t, n, a)
82 return '%s from now' % fmt(t, n, a)
83 return '%s ago' % fmt(t, n, a)
83 return '%s ago' % fmt(t, n, a)
84
84
85 @templatefilter('basename')
85 @templatefilter('basename')
86 def basename(path):
86 def basename(path):
87 """Any text. Treats the text as a path, and returns the last
87 """Any text. Treats the text as a path, and returns the last
88 component of the path after splitting by the path separator
88 component of the path after splitting by the path separator
89 (ignoring trailing separators). For example, "foo/bar/baz" becomes
89 (ignoring trailing separators). For example, "foo/bar/baz" becomes
90 "baz" and "foo/bar//" becomes "bar".
90 "baz" and "foo/bar//" becomes "bar".
91 """
91 """
92 return os.path.basename(path)
92 return os.path.basename(path)
93
93
94 @templatefilter('count')
94 @templatefilter('count')
95 def count(i):
95 def count(i):
96 """List or text. Returns the length as an integer."""
96 """List or text. Returns the length as an integer."""
97 return len(i)
97 return len(i)
98
98
99 @templatefilter('domain')
99 @templatefilter('domain')
100 def domain(author):
100 def domain(author):
101 """Any text. Finds the first string that looks like an email
101 """Any text. Finds the first string that looks like an email
102 address, and extracts just the domain component. Example: ``User
102 address, and extracts just the domain component. Example: ``User
103 <user@example.com>`` becomes ``example.com``.
103 <user@example.com>`` becomes ``example.com``.
104 """
104 """
105 f = author.find('@')
105 f = author.find('@')
106 if f == -1:
106 if f == -1:
107 return ''
107 return ''
108 author = author[f + 1:]
108 author = author[f + 1:]
109 f = author.find('>')
109 f = author.find('>')
110 if f >= 0:
110 if f >= 0:
111 author = author[:f]
111 author = author[:f]
112 return author
112 return author
113
113
114 @templatefilter('email')
114 @templatefilter('email')
115 def email(text):
115 def email(text):
116 """Any text. Extracts the first string that looks like an email
116 """Any text. Extracts the first string that looks like an email
117 address. Example: ``User <user@example.com>`` becomes
117 address. Example: ``User <user@example.com>`` becomes
118 ``user@example.com``.
118 ``user@example.com``.
119 """
119 """
120 return util.email(text)
120 return util.email(text)
121
121
122 @templatefilter('escape')
122 @templatefilter('escape')
123 def escape(text):
123 def escape(text):
124 """Any text. Replaces the special XML/XHTML characters "&", "<"
124 """Any text. Replaces the special XML/XHTML characters "&", "<"
125 and ">" with XML entities, and filters out NUL characters.
125 and ">" with XML entities, and filters out NUL characters.
126 """
126 """
127 return cgi.escape(text.replace('\0', ''), True)
127 return cgi.escape(text.replace('\0', ''), True)
128
128
129 para_re = None
129 para_re = None
130 space_re = None
130 space_re = None
131
131
132 def fill(text, width, initindent='', hangindent=''):
132 def fill(text, width, initindent='', hangindent=''):
133 '''fill many paragraphs with optional indentation.'''
133 '''fill many paragraphs with optional indentation.'''
134 global para_re, space_re
134 global para_re, space_re
135 if para_re is None:
135 if para_re is None:
136 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
136 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
137 space_re = re.compile(r' +')
137 space_re = re.compile(r' +')
138
138
139 def findparas():
139 def findparas():
140 start = 0
140 start = 0
141 while True:
141 while True:
142 m = para_re.search(text, start)
142 m = para_re.search(text, start)
143 if not m:
143 if not m:
144 uctext = unicode(text[start:], encoding.encoding)
144 uctext = unicode(text[start:], encoding.encoding)
145 w = len(uctext)
145 w = len(uctext)
146 while 0 < w and uctext[w - 1].isspace():
146 while 0 < w and uctext[w - 1].isspace():
147 w -= 1
147 w -= 1
148 yield (uctext[:w].encode(encoding.encoding),
148 yield (uctext[:w].encode(encoding.encoding),
149 uctext[w:].encode(encoding.encoding))
149 uctext[w:].encode(encoding.encoding))
150 break
150 break
151 yield text[start:m.start(0)], m.group(1)
151 yield text[start:m.start(0)], m.group(1)
152 start = m.end(1)
152 start = m.end(1)
153
153
154 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
154 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
155 width, initindent, hangindent) + rest
155 width, initindent, hangindent) + rest
156 for para, rest in findparas()])
156 for para, rest in findparas()])
157
157
158 @templatefilter('fill68')
158 @templatefilter('fill68')
159 def fill68(text):
159 def fill68(text):
160 """Any text. Wraps the text to fit in 68 columns."""
160 """Any text. Wraps the text to fit in 68 columns."""
161 return fill(text, 68)
161 return fill(text, 68)
162
162
163 @templatefilter('fill76')
163 @templatefilter('fill76')
164 def fill76(text):
164 def fill76(text):
165 """Any text. Wraps the text to fit in 76 columns."""
165 """Any text. Wraps the text to fit in 76 columns."""
166 return fill(text, 76)
166 return fill(text, 76)
167
167
168 @templatefilter('firstline')
168 @templatefilter('firstline')
169 def firstline(text):
169 def firstline(text):
170 """Any text. Returns the first line of text."""
170 """Any text. Returns the first line of text."""
171 try:
171 try:
172 return text.splitlines(True)[0].rstrip('\r\n')
172 return text.splitlines(True)[0].rstrip('\r\n')
173 except IndexError:
173 except IndexError:
174 return ''
174 return ''
175
175
176 @templatefilter('hex')
176 @templatefilter('hex')
177 def hexfilter(text):
177 def hexfilter(text):
178 """Any text. Convert a binary Mercurial node identifier into
178 """Any text. Convert a binary Mercurial node identifier into
179 its long hexadecimal representation.
179 its long hexadecimal representation.
180 """
180 """
181 return node.hex(text)
181 return node.hex(text)
182
182
183 @templatefilter('hgdate')
183 @templatefilter('hgdate')
184 def hgdate(text):
184 def hgdate(text):
185 """Date. Returns the date as a pair of numbers: "1157407993
185 """Date. Returns the date as a pair of numbers: "1157407993
186 25200" (Unix timestamp, timezone offset).
186 25200" (Unix timestamp, timezone offset).
187 """
187 """
188 return "%d %d" % text
188 return "%d %d" % text
189
189
190 @templatefilter('isodate')
190 @templatefilter('isodate')
191 def isodate(text):
191 def isodate(text):
192 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
192 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
193 +0200".
193 +0200".
194 """
194 """
195 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
195 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
196
196
197 @templatefilter('isodatesec')
197 @templatefilter('isodatesec')
198 def isodatesec(text):
198 def isodatesec(text):
199 """Date. Returns the date in ISO 8601 format, including
199 """Date. Returns the date in ISO 8601 format, including
200 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
200 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
201 filter.
201 filter.
202 """
202 """
203 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
203 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
204
204
205 def indent(text, prefix):
205 def indent(text, prefix):
206 '''indent each non-empty line of text after first with prefix.'''
206 '''indent each non-empty line of text after first with prefix.'''
207 lines = text.splitlines()
207 lines = text.splitlines()
208 num_lines = len(lines)
208 num_lines = len(lines)
209 endswithnewline = text[-1:] == '\n'
209 endswithnewline = text[-1:] == '\n'
210 def indenter():
210 def indenter():
211 for i in xrange(num_lines):
211 for i in xrange(num_lines):
212 l = lines[i]
212 l = lines[i]
213 if i and l.strip():
213 if i and l.strip():
214 yield prefix
214 yield prefix
215 yield l
215 yield l
216 if i < num_lines - 1 or endswithnewline:
216 if i < num_lines - 1 or endswithnewline:
217 yield '\n'
217 yield '\n'
218 return "".join(indenter())
218 return "".join(indenter())
219
219
220 @templatefilter('json')
220 @templatefilter('json')
221 def json(obj, paranoid=True):
221 def json(obj, paranoid=True):
222 if obj is None:
222 if obj is None:
223 return 'null'
223 return 'null'
224 elif obj is False:
224 elif obj is False:
225 return 'false'
225 return 'false'
226 elif obj is True:
226 elif obj is True:
227 return 'true'
227 return 'true'
228 elif isinstance(obj, (int, long, float)):
228 elif isinstance(obj, (int, long, float)):
229 return str(obj)
229 return str(obj)
230 elif isinstance(obj, str):
230 elif isinstance(obj, str):
231 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
231 return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
232 elif util.safehasattr(obj, 'keys'):
232 elif util.safehasattr(obj, 'keys'):
233 out = ['%s: %s' % (json(k), json(v))
233 out = ['%s: %s' % (json(k), json(v))
234 for k, v in sorted(obj.iteritems())]
234 for k, v in sorted(obj.iteritems())]
235 return '{' + ', '.join(out) + '}'
235 return '{' + ', '.join(out) + '}'
236 elif util.safehasattr(obj, '__iter__'):
236 elif util.safehasattr(obj, '__iter__'):
237 out = [json(i) for i in obj]
237 out = [json(i) for i in obj]
238 return '[' + ', '.join(out) + ']'
238 return '[' + ', '.join(out) + ']'
239 else:
239 else:
240 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
240 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
241
241
242 @templatefilter('lower')
242 @templatefilter('lower')
243 def lower(text):
243 def lower(text):
244 """Any text. Converts the text to lowercase."""
244 """Any text. Converts the text to lowercase."""
245 return encoding.lower(text)
245 return encoding.lower(text)
246
246
247 @templatefilter('nonempty')
247 @templatefilter('nonempty')
248 def nonempty(str):
248 def nonempty(str):
249 """Any text. Returns '(none)' if the string is empty."""
249 """Any text. Returns '(none)' if the string is empty."""
250 return str or "(none)"
250 return str or "(none)"
251
251
252 @templatefilter('obfuscate')
252 @templatefilter('obfuscate')
253 def obfuscate(text):
253 def obfuscate(text):
254 """Any text. Returns the input text rendered as a sequence of
254 """Any text. Returns the input text rendered as a sequence of
255 XML entities.
255 XML entities.
256 """
256 """
257 text = unicode(text, encoding.encoding, 'replace')
257 text = unicode(text, encoding.encoding, 'replace')
258 return ''.join(['&#%d;' % ord(c) for c in text])
258 return ''.join(['&#%d;' % ord(c) for c in text])
259
259
260 @templatefilter('permissions')
260 @templatefilter('permissions')
261 def permissions(flags):
261 def permissions(flags):
262 if "l" in flags:
262 if "l" in flags:
263 return "lrwxrwxrwx"
263 return "lrwxrwxrwx"
264 if "x" in flags:
264 if "x" in flags:
265 return "-rwxr-xr-x"
265 return "-rwxr-xr-x"
266 return "-rw-r--r--"
266 return "-rw-r--r--"
267
267
268 @templatefilter('person')
268 @templatefilter('person')
269 def person(author):
269 def person(author):
270 """Any text. Returns the name before an email address,
270 """Any text. Returns the name before an email address,
271 interpreting it as per RFC 5322.
271 interpreting it as per RFC 5322.
272
272
273 >>> person('foo@bar')
273 >>> person('foo@bar')
274 'foo'
274 'foo'
275 >>> person('Foo Bar <foo@bar>')
275 >>> person('Foo Bar <foo@bar>')
276 'Foo Bar'
276 'Foo Bar'
277 >>> person('"Foo Bar" <foo@bar>')
277 >>> person('"Foo Bar" <foo@bar>')
278 'Foo Bar'
278 'Foo Bar'
279 >>> person('"Foo \"buz\" Bar" <foo@bar>')
279 >>> person('"Foo \"buz\" Bar" <foo@bar>')
280 'Foo "buz" Bar'
280 'Foo "buz" Bar'
281 >>> # The following are invalid, but do exist in real-life
281 >>> # The following are invalid, but do exist in real-life
282 ...
282 ...
283 >>> person('Foo "buz" Bar <foo@bar>')
283 >>> person('Foo "buz" Bar <foo@bar>')
284 'Foo "buz" Bar'
284 'Foo "buz" Bar'
285 >>> person('"Foo Bar <foo@bar>')
285 >>> person('"Foo Bar <foo@bar>')
286 'Foo Bar'
286 'Foo Bar'
287 """
287 """
288 if '@' not in author:
288 if '@' not in author:
289 return author
289 return author
290 f = author.find('<')
290 f = author.find('<')
291 if f != -1:
291 if f != -1:
292 return author[:f].strip(' "').replace('\\"', '"')
292 return author[:f].strip(' "').replace('\\"', '"')
293 f = author.find('@')
293 f = author.find('@')
294 return author[:f].replace('.', ' ')
294 return author[:f].replace('.', ' ')
295
295
296 @templatefilter('revescape')
296 @templatefilter('revescape')
297 def revescape(text):
297 def revescape(text):
298 """Any text. Escapes all "special" characters, except @.
298 """Any text. Escapes all "special" characters, except @.
299 Forward slashes are escaped twice to prevent web servers from prematurely
299 Forward slashes are escaped twice to prevent web servers from prematurely
300 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
300 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
301 """
301 """
302 return urlreq.quote(text, safe='/@').replace('/', '%252F')
302 return urlreq.quote(text, safe='/@').replace('/', '%252F')
303
303
304 @templatefilter('rfc3339date')
304 @templatefilter('rfc3339date')
305 def rfc3339date(text):
305 def rfc3339date(text):
306 """Date. Returns a date using the Internet date format
306 """Date. Returns a date using the Internet date format
307 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
307 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
308 """
308 """
309 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
309 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
310
310
311 @templatefilter('rfc822date')
311 @templatefilter('rfc822date')
312 def rfc822date(text):
312 def rfc822date(text):
313 """Date. Returns a date using the same format used in email
313 """Date. Returns a date using the same format used in email
314 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
314 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
315 """
315 """
316 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
316 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
317
317
318 @templatefilter('short')
318 @templatefilter('short')
319 def short(text):
319 def short(text):
320 """Changeset hash. Returns the short form of a changeset hash,
320 """Changeset hash. Returns the short form of a changeset hash,
321 i.e. a 12 hexadecimal digit string.
321 i.e. a 12 hexadecimal digit string.
322 """
322 """
323 return text[:12]
323 return text[:12]
324
324
325 @templatefilter('shortbisect')
325 @templatefilter('shortbisect')
326 def shortbisect(text):
326 def shortbisect(text):
327 """Any text. Treats `text` as a bisection status, and
327 """Any text. Treats `text` as a bisection status, and
328 returns a single-character representing the status (G: good, B: bad,
328 returns a single-character representing the status (G: good, B: bad,
329 S: skipped, U: untested, I: ignored). Returns single space if `text`
329 S: skipped, U: untested, I: ignored). Returns single space if `text`
330 is not a valid bisection status.
330 is not a valid bisection status.
331 """
331 """
332 return hbisect.shortlabel(text) or ' '
332 return hbisect.shortlabel(text) or ' '
333
333
334 @templatefilter('shortdate')
334 @templatefilter('shortdate')
335 def shortdate(text):
335 def shortdate(text):
336 """Date. Returns a date like "2006-09-18"."""
336 """Date. Returns a date like "2006-09-18"."""
337 return util.shortdate(text)
337 return util.shortdate(text)
338
338
339 @templatefilter('splitlines')
339 @templatefilter('splitlines')
340 def splitlines(text):
340 def splitlines(text):
341 """Any text. Split text into a list of lines."""
341 """Any text. Split text into a list of lines."""
342 return templatekw.showlist('line', text.splitlines(), 'lines')
342 return templatekw.showlist('line', text.splitlines(), {}, plural='lines')
343
343
344 @templatefilter('stringescape')
344 @templatefilter('stringescape')
345 def stringescape(text):
345 def stringescape(text):
346 return util.escapestr(text)
346 return util.escapestr(text)
347
347
348 @templatefilter('stringify')
348 @templatefilter('stringify')
349 def stringify(thing):
349 def stringify(thing):
350 """Any type. Turns the value into text by converting values into
350 """Any type. Turns the value into text by converting values into
351 text and concatenating them.
351 text and concatenating them.
352 """
352 """
353 thing = templatekw.unwraphybrid(thing)
353 thing = templatekw.unwraphybrid(thing)
354 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
354 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
355 return "".join([stringify(t) for t in thing if t is not None])
355 return "".join([stringify(t) for t in thing if t is not None])
356 if thing is None:
356 if thing is None:
357 return ""
357 return ""
358 return str(thing)
358 return str(thing)
359
359
360 @templatefilter('stripdir')
360 @templatefilter('stripdir')
361 def stripdir(text):
361 def stripdir(text):
362 """Treat the text as path and strip a directory level, if
362 """Treat the text as path and strip a directory level, if
363 possible. For example, "foo" and "foo/bar" becomes "foo".
363 possible. For example, "foo" and "foo/bar" becomes "foo".
364 """
364 """
365 dir = os.path.dirname(text)
365 dir = os.path.dirname(text)
366 if dir == "":
366 if dir == "":
367 return os.path.basename(text)
367 return os.path.basename(text)
368 else:
368 else:
369 return dir
369 return dir
370
370
371 @templatefilter('tabindent')
371 @templatefilter('tabindent')
372 def tabindent(text):
372 def tabindent(text):
373 """Any text. Returns the text, with every non-empty line
373 """Any text. Returns the text, with every non-empty line
374 except the first starting with a tab character.
374 except the first starting with a tab character.
375 """
375 """
376 return indent(text, '\t')
376 return indent(text, '\t')
377
377
378 @templatefilter('upper')
378 @templatefilter('upper')
379 def upper(text):
379 def upper(text):
380 """Any text. Converts the text to uppercase."""
380 """Any text. Converts the text to uppercase."""
381 return encoding.upper(text)
381 return encoding.upper(text)
382
382
383 @templatefilter('urlescape')
383 @templatefilter('urlescape')
384 def urlescape(text):
384 def urlescape(text):
385 """Any text. Escapes all "special" characters. For example,
385 """Any text. Escapes all "special" characters. For example,
386 "foo bar" becomes "foo%20bar".
386 "foo bar" becomes "foo%20bar".
387 """
387 """
388 return urlreq.quote(text)
388 return urlreq.quote(text)
389
389
390 @templatefilter('user')
390 @templatefilter('user')
391 def userfilter(text):
391 def userfilter(text):
392 """Any text. Returns a short representation of a user name or email
392 """Any text. Returns a short representation of a user name or email
393 address."""
393 address."""
394 return util.shortuser(text)
394 return util.shortuser(text)
395
395
396 @templatefilter('emailuser')
396 @templatefilter('emailuser')
397 def emailuser(text):
397 def emailuser(text):
398 """Any text. Returns the user portion of an email address."""
398 """Any text. Returns the user portion of an email address."""
399 return util.emailuser(text)
399 return util.emailuser(text)
400
400
401 @templatefilter('utf8')
401 @templatefilter('utf8')
402 def utf8(text):
402 def utf8(text):
403 """Any text. Converts from the local character encoding to UTF-8."""
403 """Any text. Converts from the local character encoding to UTF-8."""
404 return encoding.fromlocal(text)
404 return encoding.fromlocal(text)
405
405
406 @templatefilter('xmlescape')
406 @templatefilter('xmlescape')
407 def xmlescape(text):
407 def xmlescape(text):
408 text = (text
408 text = (text
409 .replace('&', '&amp;')
409 .replace('&', '&amp;')
410 .replace('<', '&lt;')
410 .replace('<', '&lt;')
411 .replace('>', '&gt;')
411 .replace('>', '&gt;')
412 .replace('"', '&quot;')
412 .replace('"', '&quot;')
413 .replace("'", '&#39;')) # &apos; invalid in HTML
413 .replace("'", '&#39;')) # &apos; invalid in HTML
414 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
414 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
415
415
416 def websub(text, websubtable):
416 def websub(text, websubtable):
417 """:websub: Any text. Only applies to hgweb. Applies the regular
417 """:websub: Any text. Only applies to hgweb. Applies the regular
418 expression replacements defined in the websub section.
418 expression replacements defined in the websub section.
419 """
419 """
420 if websubtable:
420 if websubtable:
421 for regexp, format in websubtable:
421 for regexp, format in websubtable:
422 text = regexp.sub(format, text)
422 text = regexp.sub(format, text)
423 return text
423 return text
424
424
425 def loadfilter(ui, extname, registrarobj):
425 def loadfilter(ui, extname, registrarobj):
426 """Load template filter from specified registrarobj
426 """Load template filter from specified registrarobj
427 """
427 """
428 for name, func in registrarobj._table.iteritems():
428 for name, func in registrarobj._table.iteritems():
429 filters[name] = func
429 filters[name] = func
430
430
431 # tell hggettext to extract docstrings from these functions:
431 # tell hggettext to extract docstrings from these functions:
432 i18nfunctions = filters.values()
432 i18nfunctions = filters.values()
@@ -1,676 +1,676
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 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 from .i18n import _
10 from .i18n import _
11 from .node import hex, nullid
11 from .node import hex, nullid
12 from . import (
12 from . import (
13 encoding,
13 encoding,
14 error,
14 error,
15 hbisect,
15 hbisect,
16 patch,
16 patch,
17 registrar,
17 registrar,
18 scmutil,
18 scmutil,
19 util,
19 util,
20 )
20 )
21
21
22 class _hybrid(object):
22 class _hybrid(object):
23 """Wrapper for list or dict to support legacy template
23 """Wrapper for list or dict to support legacy template
24
24
25 This class allows us to handle both:
25 This class allows us to handle both:
26 - "{files}" (legacy command-line-specific list hack) and
26 - "{files}" (legacy command-line-specific list hack) and
27 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
27 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
28 and to access raw values:
28 and to access raw values:
29 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
29 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
30 - "{get(extras, key)}"
30 - "{get(extras, key)}"
31 - "{files|json}"
31 - "{files|json}"
32 """
32 """
33
33
34 def __init__(self, gen, values, makemap, joinfmt):
34 def __init__(self, gen, values, makemap, joinfmt):
35 if gen is not None:
35 if gen is not None:
36 self.gen = gen
36 self.gen = gen
37 self._values = values
37 self._values = values
38 self._makemap = makemap
38 self._makemap = makemap
39 self.joinfmt = joinfmt
39 self.joinfmt = joinfmt
40 @util.propertycache
40 @util.propertycache
41 def gen(self):
41 def gen(self):
42 return self._defaultgen()
42 return self._defaultgen()
43 def _defaultgen(self):
43 def _defaultgen(self):
44 """Generator to stringify this as {join(self, ' ')}"""
44 """Generator to stringify this as {join(self, ' ')}"""
45 for i, d in enumerate(self.itermaps()):
45 for i, d in enumerate(self.itermaps()):
46 if i > 0:
46 if i > 0:
47 yield ' '
47 yield ' '
48 yield self.joinfmt(d)
48 yield self.joinfmt(d)
49 def itermaps(self):
49 def itermaps(self):
50 makemap = self._makemap
50 makemap = self._makemap
51 for x in self._values:
51 for x in self._values:
52 yield makemap(x)
52 yield makemap(x)
53 def __contains__(self, x):
53 def __contains__(self, x):
54 return x in self._values
54 return x in self._values
55 def __len__(self):
55 def __len__(self):
56 return len(self._values)
56 return len(self._values)
57 def __iter__(self):
57 def __iter__(self):
58 return iter(self._values)
58 return iter(self._values)
59 def __getattr__(self, name):
59 def __getattr__(self, name):
60 if name not in ('get', 'items', 'iteritems', 'iterkeys', 'itervalues',
60 if name not in ('get', 'items', 'iteritems', 'iterkeys', 'itervalues',
61 'keys', 'values'):
61 'keys', 'values'):
62 raise AttributeError(name)
62 raise AttributeError(name)
63 return getattr(self._values, name)
63 return getattr(self._values, name)
64
64
65 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
65 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
66 """Wrap data to support both dict-like and string-like operations"""
66 """Wrap data to support both dict-like and string-like operations"""
67 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
67 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
68 lambda d: fmt % (d[key], d[value]))
68 lambda d: fmt % (d[key], d[value]))
69
69
70 def hybridlist(data, name, fmt='%s', gen=None):
70 def hybridlist(data, name, fmt='%s', gen=None):
71 """Wrap data to support both list-like and string-like operations"""
71 """Wrap data to support both list-like and string-like operations"""
72 return _hybrid(gen, data, lambda x: {name: x}, lambda d: fmt % d[name])
72 return _hybrid(gen, data, lambda x: {name: x}, lambda d: fmt % d[name])
73
73
74 def unwraphybrid(thing):
74 def unwraphybrid(thing):
75 """Return an object which can be stringified possibly by using a legacy
75 """Return an object which can be stringified possibly by using a legacy
76 template"""
76 template"""
77 if not util.safehasattr(thing, 'gen'):
77 if not util.safehasattr(thing, 'gen'):
78 return thing
78 return thing
79 return thing.gen
79 return thing.gen
80
80
81 def showlist(name, values, plural=None, element=None, separator=' ', **mapping):
81 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
82 if not element:
82 if not element:
83 element = name
83 element = name
84 f = _showlist(name, values, mapping, plural, separator)
84 f = _showlist(name, values, mapping, plural, separator)
85 return hybridlist(values, name=element, gen=f)
85 return hybridlist(values, name=element, gen=f)
86
86
87 def _showlist(name, values, mapping, plural=None, separator=' '):
87 def _showlist(name, values, mapping, plural=None, separator=' '):
88 '''expand set of values.
88 '''expand set of values.
89 name is name of key in template map.
89 name is name of key in template map.
90 values is list of strings or dicts.
90 values is list of strings or dicts.
91 plural is plural of name, if not simply name + 's'.
91 plural is plural of name, if not simply name + 's'.
92 separator is used to join values as a string
92 separator is used to join values as a string
93
93
94 expansion works like this, given name 'foo'.
94 expansion works like this, given name 'foo'.
95
95
96 if values is empty, expand 'no_foos'.
96 if values is empty, expand 'no_foos'.
97
97
98 if 'foo' not in template map, return values as a string,
98 if 'foo' not in template map, return values as a string,
99 joined by 'separator'.
99 joined by 'separator'.
100
100
101 expand 'start_foos'.
101 expand 'start_foos'.
102
102
103 for each value, expand 'foo'. if 'last_foo' in template
103 for each value, expand 'foo'. if 'last_foo' in template
104 map, expand it instead of 'foo' for last key.
104 map, expand it instead of 'foo' for last key.
105
105
106 expand 'end_foos'.
106 expand 'end_foos'.
107 '''
107 '''
108 templ = mapping['templ']
108 templ = mapping['templ']
109 if not plural:
109 if not plural:
110 plural = name + 's'
110 plural = name + 's'
111 if not values:
111 if not values:
112 noname = 'no_' + plural
112 noname = 'no_' + plural
113 if noname in templ:
113 if noname in templ:
114 yield templ(noname, **mapping)
114 yield templ(noname, **mapping)
115 return
115 return
116 if name not in templ:
116 if name not in templ:
117 if isinstance(values[0], str):
117 if isinstance(values[0], str):
118 yield separator.join(values)
118 yield separator.join(values)
119 else:
119 else:
120 for v in values:
120 for v in values:
121 yield dict(v, **mapping)
121 yield dict(v, **mapping)
122 return
122 return
123 startname = 'start_' + plural
123 startname = 'start_' + plural
124 if startname in templ:
124 if startname in templ:
125 yield templ(startname, **mapping)
125 yield templ(startname, **mapping)
126 vmapping = mapping.copy()
126 vmapping = mapping.copy()
127 def one(v, tag=name):
127 def one(v, tag=name):
128 try:
128 try:
129 vmapping.update(v)
129 vmapping.update(v)
130 except (AttributeError, ValueError):
130 except (AttributeError, ValueError):
131 try:
131 try:
132 for a, b in v:
132 for a, b in v:
133 vmapping[a] = b
133 vmapping[a] = b
134 except ValueError:
134 except ValueError:
135 vmapping[name] = v
135 vmapping[name] = v
136 return templ(tag, **vmapping)
136 return templ(tag, **vmapping)
137 lastname = 'last_' + name
137 lastname = 'last_' + name
138 if lastname in templ:
138 if lastname in templ:
139 last = values.pop()
139 last = values.pop()
140 else:
140 else:
141 last = None
141 last = None
142 for v in values:
142 for v in values:
143 yield one(v)
143 yield one(v)
144 if last is not None:
144 if last is not None:
145 yield one(last, tag=lastname)
145 yield one(last, tag=lastname)
146 endname = 'end_' + plural
146 endname = 'end_' + plural
147 if endname in templ:
147 if endname in templ:
148 yield templ(endname, **mapping)
148 yield templ(endname, **mapping)
149
149
150 def _formatrevnode(ctx):
150 def _formatrevnode(ctx):
151 """Format changeset as '{rev}:{node|formatnode}', which is the default
151 """Format changeset as '{rev}:{node|formatnode}', which is the default
152 template provided by cmdutil.changeset_templater"""
152 template provided by cmdutil.changeset_templater"""
153 repo = ctx.repo()
153 repo = ctx.repo()
154 if repo.ui.debugflag:
154 if repo.ui.debugflag:
155 hexnode = ctx.hex()
155 hexnode = ctx.hex()
156 else:
156 else:
157 hexnode = ctx.hex()[:12]
157 hexnode = ctx.hex()[:12]
158 return '%d:%s' % (scmutil.intrev(ctx.rev()), hexnode)
158 return '%d:%s' % (scmutil.intrev(ctx.rev()), hexnode)
159
159
160 def getfiles(repo, ctx, revcache):
160 def getfiles(repo, ctx, revcache):
161 if 'files' not in revcache:
161 if 'files' not in revcache:
162 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
162 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
163 return revcache['files']
163 return revcache['files']
164
164
165 def getlatesttags(repo, ctx, cache, pattern=None):
165 def getlatesttags(repo, ctx, cache, pattern=None):
166 '''return date, distance and name for the latest tag of rev'''
166 '''return date, distance and name for the latest tag of rev'''
167
167
168 cachename = 'latesttags'
168 cachename = 'latesttags'
169 if pattern is not None:
169 if pattern is not None:
170 cachename += '-' + pattern
170 cachename += '-' + pattern
171 match = util.stringmatcher(pattern)[2]
171 match = util.stringmatcher(pattern)[2]
172 else:
172 else:
173 match = util.always
173 match = util.always
174
174
175 if cachename not in cache:
175 if cachename not in cache:
176 # Cache mapping from rev to a tuple with tag date, tag
176 # Cache mapping from rev to a tuple with tag date, tag
177 # distance and tag name
177 # distance and tag name
178 cache[cachename] = {-1: (0, 0, ['null'])}
178 cache[cachename] = {-1: (0, 0, ['null'])}
179 latesttags = cache[cachename]
179 latesttags = cache[cachename]
180
180
181 rev = ctx.rev()
181 rev = ctx.rev()
182 todo = [rev]
182 todo = [rev]
183 while todo:
183 while todo:
184 rev = todo.pop()
184 rev = todo.pop()
185 if rev in latesttags:
185 if rev in latesttags:
186 continue
186 continue
187 ctx = repo[rev]
187 ctx = repo[rev]
188 tags = [t for t in ctx.tags()
188 tags = [t for t in ctx.tags()
189 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
189 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
190 and match(t))]
190 and match(t))]
191 if tags:
191 if tags:
192 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
192 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
193 continue
193 continue
194 try:
194 try:
195 # The tuples are laid out so the right one can be found by
195 # The tuples are laid out so the right one can be found by
196 # comparison.
196 # comparison.
197 pdate, pdist, ptag = max(
197 pdate, pdist, ptag = max(
198 latesttags[p.rev()] for p in ctx.parents())
198 latesttags[p.rev()] for p in ctx.parents())
199 except KeyError:
199 except KeyError:
200 # Cache miss - recurse
200 # Cache miss - recurse
201 todo.append(rev)
201 todo.append(rev)
202 todo.extend(p.rev() for p in ctx.parents())
202 todo.extend(p.rev() for p in ctx.parents())
203 continue
203 continue
204 latesttags[rev] = pdate, pdist + 1, ptag
204 latesttags[rev] = pdate, pdist + 1, ptag
205 return latesttags[rev]
205 return latesttags[rev]
206
206
207 def getrenamedfn(repo, endrev=None):
207 def getrenamedfn(repo, endrev=None):
208 rcache = {}
208 rcache = {}
209 if endrev is None:
209 if endrev is None:
210 endrev = len(repo)
210 endrev = len(repo)
211
211
212 def getrenamed(fn, rev):
212 def getrenamed(fn, rev):
213 '''looks up all renames for a file (up to endrev) the first
213 '''looks up all renames for a file (up to endrev) the first
214 time the file is given. It indexes on the changerev and only
214 time the file is given. It indexes on the changerev and only
215 parses the manifest if linkrev != changerev.
215 parses the manifest if linkrev != changerev.
216 Returns rename info for fn at changerev rev.'''
216 Returns rename info for fn at changerev rev.'''
217 if fn not in rcache:
217 if fn not in rcache:
218 rcache[fn] = {}
218 rcache[fn] = {}
219 fl = repo.file(fn)
219 fl = repo.file(fn)
220 for i in fl:
220 for i in fl:
221 lr = fl.linkrev(i)
221 lr = fl.linkrev(i)
222 renamed = fl.renamed(fl.node(i))
222 renamed = fl.renamed(fl.node(i))
223 rcache[fn][lr] = renamed
223 rcache[fn][lr] = renamed
224 if lr >= endrev:
224 if lr >= endrev:
225 break
225 break
226 if rev in rcache[fn]:
226 if rev in rcache[fn]:
227 return rcache[fn][rev]
227 return rcache[fn][rev]
228
228
229 # If linkrev != rev (i.e. rev not found in rcache) fallback to
229 # If linkrev != rev (i.e. rev not found in rcache) fallback to
230 # filectx logic.
230 # filectx logic.
231 try:
231 try:
232 return repo[rev][fn].renamed()
232 return repo[rev][fn].renamed()
233 except error.LookupError:
233 except error.LookupError:
234 return None
234 return None
235
235
236 return getrenamed
236 return getrenamed
237
237
238 # default templates internally used for rendering of lists
238 # default templates internally used for rendering of lists
239 defaulttempl = {
239 defaulttempl = {
240 'parent': '{rev}:{node|formatnode} ',
240 'parent': '{rev}:{node|formatnode} ',
241 'manifest': '{rev}:{node|formatnode}',
241 'manifest': '{rev}:{node|formatnode}',
242 'file_copy': '{name} ({source})',
242 'file_copy': '{name} ({source})',
243 'envvar': '{key}={value}',
243 'envvar': '{key}={value}',
244 'extra': '{key}={value|stringescape}'
244 'extra': '{key}={value|stringescape}'
245 }
245 }
246 # filecopy is preserved for compatibility reasons
246 # filecopy is preserved for compatibility reasons
247 defaulttempl['filecopy'] = defaulttempl['file_copy']
247 defaulttempl['filecopy'] = defaulttempl['file_copy']
248
248
249 # keywords are callables like:
249 # keywords are callables like:
250 # fn(repo, ctx, templ, cache, revcache, **args)
250 # fn(repo, ctx, templ, cache, revcache, **args)
251 # with:
251 # with:
252 # repo - current repository instance
252 # repo - current repository instance
253 # ctx - the changectx being displayed
253 # ctx - the changectx being displayed
254 # templ - the templater instance
254 # templ - the templater instance
255 # cache - a cache dictionary for the whole templater run
255 # cache - a cache dictionary for the whole templater run
256 # revcache - a cache dictionary for the current revision
256 # revcache - a cache dictionary for the current revision
257 keywords = {}
257 keywords = {}
258
258
259 templatekeyword = registrar.templatekeyword(keywords)
259 templatekeyword = registrar.templatekeyword(keywords)
260
260
261 @templatekeyword('author')
261 @templatekeyword('author')
262 def showauthor(repo, ctx, templ, **args):
262 def showauthor(repo, ctx, templ, **args):
263 """String. The unmodified author of the changeset."""
263 """String. The unmodified author of the changeset."""
264 return ctx.user()
264 return ctx.user()
265
265
266 @templatekeyword('bisect')
266 @templatekeyword('bisect')
267 def showbisect(repo, ctx, templ, **args):
267 def showbisect(repo, ctx, templ, **args):
268 """String. The changeset bisection status."""
268 """String. The changeset bisection status."""
269 return hbisect.label(repo, ctx.node())
269 return hbisect.label(repo, ctx.node())
270
270
271 @templatekeyword('branch')
271 @templatekeyword('branch')
272 def showbranch(**args):
272 def showbranch(**args):
273 """String. The name of the branch on which the changeset was
273 """String. The name of the branch on which the changeset was
274 committed.
274 committed.
275 """
275 """
276 return args['ctx'].branch()
276 return args['ctx'].branch()
277
277
278 @templatekeyword('branches')
278 @templatekeyword('branches')
279 def showbranches(**args):
279 def showbranches(**args):
280 """List of strings. The name of the branch on which the
280 """List of strings. The name of the branch on which the
281 changeset was committed. Will be empty if the branch name was
281 changeset was committed. Will be empty if the branch name was
282 default. (DEPRECATED)
282 default. (DEPRECATED)
283 """
283 """
284 branch = args['ctx'].branch()
284 branch = args['ctx'].branch()
285 if branch != 'default':
285 if branch != 'default':
286 return showlist('branch', [branch], plural='branches', **args)
286 return showlist('branch', [branch], args, plural='branches')
287 return showlist('branch', [], plural='branches', **args)
287 return showlist('branch', [], args, plural='branches')
288
288
289 @templatekeyword('bookmarks')
289 @templatekeyword('bookmarks')
290 def showbookmarks(**args):
290 def showbookmarks(**args):
291 """List of strings. Any bookmarks associated with the
291 """List of strings. Any bookmarks associated with the
292 changeset. Also sets 'active', the name of the active bookmark.
292 changeset. Also sets 'active', the name of the active bookmark.
293 """
293 """
294 repo = args['ctx']._repo
294 repo = args['ctx']._repo
295 bookmarks = args['ctx'].bookmarks()
295 bookmarks = args['ctx'].bookmarks()
296 active = repo._activebookmark
296 active = repo._activebookmark
297 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
297 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
298 f = _showlist('bookmark', bookmarks, args)
298 f = _showlist('bookmark', bookmarks, args)
299 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
299 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
300
300
301 @templatekeyword('children')
301 @templatekeyword('children')
302 def showchildren(**args):
302 def showchildren(**args):
303 """List of strings. The children of the changeset."""
303 """List of strings. The children of the changeset."""
304 ctx = args['ctx']
304 ctx = args['ctx']
305 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
305 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
306 return showlist('children', childrevs, element='child', **args)
306 return showlist('children', childrevs, args, element='child')
307
307
308 # Deprecated, but kept alive for help generation a purpose.
308 # Deprecated, but kept alive for help generation a purpose.
309 @templatekeyword('currentbookmark')
309 @templatekeyword('currentbookmark')
310 def showcurrentbookmark(**args):
310 def showcurrentbookmark(**args):
311 """String. The active bookmark, if it is
311 """String. The active bookmark, if it is
312 associated with the changeset (DEPRECATED)"""
312 associated with the changeset (DEPRECATED)"""
313 return showactivebookmark(**args)
313 return showactivebookmark(**args)
314
314
315 @templatekeyword('activebookmark')
315 @templatekeyword('activebookmark')
316 def showactivebookmark(**args):
316 def showactivebookmark(**args):
317 """String. The active bookmark, if it is
317 """String. The active bookmark, if it is
318 associated with the changeset"""
318 associated with the changeset"""
319 active = args['repo']._activebookmark
319 active = args['repo']._activebookmark
320 if active and active in args['ctx'].bookmarks():
320 if active and active in args['ctx'].bookmarks():
321 return active
321 return active
322 return ''
322 return ''
323
323
324 @templatekeyword('date')
324 @templatekeyword('date')
325 def showdate(repo, ctx, templ, **args):
325 def showdate(repo, ctx, templ, **args):
326 """Date information. The date when the changeset was committed."""
326 """Date information. The date when the changeset was committed."""
327 return ctx.date()
327 return ctx.date()
328
328
329 @templatekeyword('desc')
329 @templatekeyword('desc')
330 def showdescription(repo, ctx, templ, **args):
330 def showdescription(repo, ctx, templ, **args):
331 """String. The text of the changeset description."""
331 """String. The text of the changeset description."""
332 s = ctx.description()
332 s = ctx.description()
333 if isinstance(s, encoding.localstr):
333 if isinstance(s, encoding.localstr):
334 # try hard to preserve utf-8 bytes
334 # try hard to preserve utf-8 bytes
335 return encoding.tolocal(encoding.fromlocal(s).strip())
335 return encoding.tolocal(encoding.fromlocal(s).strip())
336 else:
336 else:
337 return s.strip()
337 return s.strip()
338
338
339 @templatekeyword('diffstat')
339 @templatekeyword('diffstat')
340 def showdiffstat(repo, ctx, templ, **args):
340 def showdiffstat(repo, ctx, templ, **args):
341 """String. Statistics of changes with the following format:
341 """String. Statistics of changes with the following format:
342 "modified files: +added/-removed lines"
342 "modified files: +added/-removed lines"
343 """
343 """
344 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
344 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
345 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
345 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
346 return '%s: +%s/-%s' % (len(stats), adds, removes)
346 return '%s: +%s/-%s' % (len(stats), adds, removes)
347
347
348 @templatekeyword('envvars')
348 @templatekeyword('envvars')
349 def showenvvars(repo, **args):
349 def showenvvars(repo, **args):
350 """A dictionary of environment variables. (EXPERIMENTAL)"""
350 """A dictionary of environment variables. (EXPERIMENTAL)"""
351
351
352 env = repo.ui.exportableenviron()
352 env = repo.ui.exportableenviron()
353 env = util.sortdict((k, env[k]) for k in sorted(env))
353 env = util.sortdict((k, env[k]) for k in sorted(env))
354 makemap = lambda k: {'key': k, 'value': env[k]}
354 makemap = lambda k: {'key': k, 'value': env[k]}
355 c = [makemap(k) for k in env]
355 c = [makemap(k) for k in env]
356 f = _showlist('envvar', c, args, plural='envvars')
356 f = _showlist('envvar', c, args, plural='envvars')
357 return _hybrid(f, env, makemap,
357 return _hybrid(f, env, makemap,
358 lambda x: '%s=%s' % (x['key'], x['value']))
358 lambda x: '%s=%s' % (x['key'], x['value']))
359
359
360 @templatekeyword('extras')
360 @templatekeyword('extras')
361 def showextras(**args):
361 def showextras(**args):
362 """List of dicts with key, value entries of the 'extras'
362 """List of dicts with key, value entries of the 'extras'
363 field of this changeset."""
363 field of this changeset."""
364 extras = args['ctx'].extra()
364 extras = args['ctx'].extra()
365 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
365 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
366 makemap = lambda k: {'key': k, 'value': extras[k]}
366 makemap = lambda k: {'key': k, 'value': extras[k]}
367 c = [makemap(k) for k in extras]
367 c = [makemap(k) for k in extras]
368 f = _showlist('extra', c, args, plural='extras')
368 f = _showlist('extra', c, args, plural='extras')
369 return _hybrid(f, extras, makemap,
369 return _hybrid(f, extras, makemap,
370 lambda x: '%s=%s' % (x['key'], util.escapestr(x['value'])))
370 lambda x: '%s=%s' % (x['key'], util.escapestr(x['value'])))
371
371
372 @templatekeyword('file_adds')
372 @templatekeyword('file_adds')
373 def showfileadds(**args):
373 def showfileadds(**args):
374 """List of strings. Files added by this changeset."""
374 """List of strings. Files added by this changeset."""
375 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
375 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
376 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
376 return showlist('file_add', getfiles(repo, ctx, revcache)[1], args,
377 element='file', **args)
377 element='file')
378
378
379 @templatekeyword('file_copies')
379 @templatekeyword('file_copies')
380 def showfilecopies(**args):
380 def showfilecopies(**args):
381 """List of strings. Files copied in this changeset with
381 """List of strings. Files copied in this changeset with
382 their sources.
382 their sources.
383 """
383 """
384 cache, ctx = args['cache'], args['ctx']
384 cache, ctx = args['cache'], args['ctx']
385 copies = args['revcache'].get('copies')
385 copies = args['revcache'].get('copies')
386 if copies is None:
386 if copies is None:
387 if 'getrenamed' not in cache:
387 if 'getrenamed' not in cache:
388 cache['getrenamed'] = getrenamedfn(args['repo'])
388 cache['getrenamed'] = getrenamedfn(args['repo'])
389 copies = []
389 copies = []
390 getrenamed = cache['getrenamed']
390 getrenamed = cache['getrenamed']
391 for fn in ctx.files():
391 for fn in ctx.files():
392 rename = getrenamed(fn, ctx.rev())
392 rename = getrenamed(fn, ctx.rev())
393 if rename:
393 if rename:
394 copies.append((fn, rename[0]))
394 copies.append((fn, rename[0]))
395
395
396 copies = util.sortdict(copies)
396 copies = util.sortdict(copies)
397 makemap = lambda k: {'name': k, 'source': copies[k]}
397 makemap = lambda k: {'name': k, 'source': copies[k]}
398 c = [makemap(k) for k in copies]
398 c = [makemap(k) for k in copies]
399 f = _showlist('file_copy', c, args, plural='file_copies')
399 f = _showlist('file_copy', c, args, plural='file_copies')
400 return _hybrid(f, copies, makemap,
400 return _hybrid(f, copies, makemap,
401 lambda x: '%s (%s)' % (x['name'], x['source']))
401 lambda x: '%s (%s)' % (x['name'], x['source']))
402
402
403 # showfilecopiesswitch() displays file copies only if copy records are
403 # showfilecopiesswitch() displays file copies only if copy records are
404 # provided before calling the templater, usually with a --copies
404 # provided before calling the templater, usually with a --copies
405 # command line switch.
405 # command line switch.
406 @templatekeyword('file_copies_switch')
406 @templatekeyword('file_copies_switch')
407 def showfilecopiesswitch(**args):
407 def showfilecopiesswitch(**args):
408 """List of strings. Like "file_copies" but displayed
408 """List of strings. Like "file_copies" but displayed
409 only if the --copied switch is set.
409 only if the --copied switch is set.
410 """
410 """
411 copies = args['revcache'].get('copies') or []
411 copies = args['revcache'].get('copies') or []
412 copies = util.sortdict(copies)
412 copies = util.sortdict(copies)
413 makemap = lambda k: {'name': k, 'source': copies[k]}
413 makemap = lambda k: {'name': k, 'source': copies[k]}
414 c = [makemap(k) for k in copies]
414 c = [makemap(k) for k in copies]
415 f = _showlist('file_copy', c, args, plural='file_copies')
415 f = _showlist('file_copy', c, args, plural='file_copies')
416 return _hybrid(f, copies, makemap,
416 return _hybrid(f, copies, makemap,
417 lambda x: '%s (%s)' % (x['name'], x['source']))
417 lambda x: '%s (%s)' % (x['name'], x['source']))
418
418
419 @templatekeyword('file_dels')
419 @templatekeyword('file_dels')
420 def showfiledels(**args):
420 def showfiledels(**args):
421 """List of strings. Files removed by this changeset."""
421 """List of strings. Files removed by this changeset."""
422 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
422 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
423 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
423 return showlist('file_del', getfiles(repo, ctx, revcache)[2], args,
424 element='file', **args)
424 element='file')
425
425
426 @templatekeyword('file_mods')
426 @templatekeyword('file_mods')
427 def showfilemods(**args):
427 def showfilemods(**args):
428 """List of strings. Files modified by this changeset."""
428 """List of strings. Files modified by this changeset."""
429 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
429 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
430 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
430 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], args,
431 element='file', **args)
431 element='file')
432
432
433 @templatekeyword('files')
433 @templatekeyword('files')
434 def showfiles(**args):
434 def showfiles(**args):
435 """List of strings. All files modified, added, or removed by this
435 """List of strings. All files modified, added, or removed by this
436 changeset.
436 changeset.
437 """
437 """
438 return showlist('file', args['ctx'].files(), **args)
438 return showlist('file', args['ctx'].files(), args)
439
439
440 @templatekeyword('graphnode')
440 @templatekeyword('graphnode')
441 def showgraphnode(repo, ctx, **args):
441 def showgraphnode(repo, ctx, **args):
442 """String. The character representing the changeset node in
442 """String. The character representing the changeset node in
443 an ASCII revision graph"""
443 an ASCII revision graph"""
444 wpnodes = repo.dirstate.parents()
444 wpnodes = repo.dirstate.parents()
445 if wpnodes[1] == nullid:
445 if wpnodes[1] == nullid:
446 wpnodes = wpnodes[:1]
446 wpnodes = wpnodes[:1]
447 if ctx.node() in wpnodes:
447 if ctx.node() in wpnodes:
448 return '@'
448 return '@'
449 elif ctx.obsolete():
449 elif ctx.obsolete():
450 return 'x'
450 return 'x'
451 elif ctx.closesbranch():
451 elif ctx.closesbranch():
452 return '_'
452 return '_'
453 else:
453 else:
454 return 'o'
454 return 'o'
455
455
456 @templatekeyword('index')
456 @templatekeyword('index')
457 def showindex(**args):
457 def showindex(**args):
458 """Integer. The current iteration of the loop. (0 indexed)"""
458 """Integer. The current iteration of the loop. (0 indexed)"""
459 # just hosts documentation; should be overridden by template mapping
459 # just hosts documentation; should be overridden by template mapping
460 raise error.Abort(_("can't use index in this context"))
460 raise error.Abort(_("can't use index in this context"))
461
461
462 @templatekeyword('latesttag')
462 @templatekeyword('latesttag')
463 def showlatesttag(**args):
463 def showlatesttag(**args):
464 """List of strings. The global tags on the most recent globally
464 """List of strings. The global tags on the most recent globally
465 tagged ancestor of this changeset. If no such tags exist, the list
465 tagged ancestor of this changeset. If no such tags exist, the list
466 consists of the single string "null".
466 consists of the single string "null".
467 """
467 """
468 return showlatesttags(None, **args)
468 return showlatesttags(None, **args)
469
469
470 def showlatesttags(pattern, **args):
470 def showlatesttags(pattern, **args):
471 """helper method for the latesttag keyword and function"""
471 """helper method for the latesttag keyword and function"""
472 repo, ctx = args['repo'], args['ctx']
472 repo, ctx = args['repo'], args['ctx']
473 cache = args['cache']
473 cache = args['cache']
474 latesttags = getlatesttags(repo, ctx, cache, pattern)
474 latesttags = getlatesttags(repo, ctx, cache, pattern)
475
475
476 # latesttag[0] is an implementation detail for sorting csets on different
476 # latesttag[0] is an implementation detail for sorting csets on different
477 # branches in a stable manner- it is the date the tagged cset was created,
477 # branches in a stable manner- it is the date the tagged cset was created,
478 # not the date the tag was created. Therefore it isn't made visible here.
478 # not the date the tag was created. Therefore it isn't made visible here.
479 makemap = lambda v: {
479 makemap = lambda v: {
480 'changes': _showchangessincetag,
480 'changes': _showchangessincetag,
481 'distance': latesttags[1],
481 'distance': latesttags[1],
482 'latesttag': v, # BC with {latesttag % '{latesttag}'}
482 'latesttag': v, # BC with {latesttag % '{latesttag}'}
483 'tag': v
483 'tag': v
484 }
484 }
485
485
486 tags = latesttags[2]
486 tags = latesttags[2]
487 f = _showlist('latesttag', tags, args, separator=':')
487 f = _showlist('latesttag', tags, args, separator=':')
488 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
488 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
489
489
490 @templatekeyword('latesttagdistance')
490 @templatekeyword('latesttagdistance')
491 def showlatesttagdistance(repo, ctx, templ, cache, **args):
491 def showlatesttagdistance(repo, ctx, templ, cache, **args):
492 """Integer. Longest path to the latest tag."""
492 """Integer. Longest path to the latest tag."""
493 return getlatesttags(repo, ctx, cache)[1]
493 return getlatesttags(repo, ctx, cache)[1]
494
494
495 @templatekeyword('changessincelatesttag')
495 @templatekeyword('changessincelatesttag')
496 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
496 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
497 """Integer. All ancestors not in the latest tag."""
497 """Integer. All ancestors not in the latest tag."""
498 latesttag = getlatesttags(repo, ctx, cache)[2][0]
498 latesttag = getlatesttags(repo, ctx, cache)[2][0]
499
499
500 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
500 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
501
501
502 def _showchangessincetag(repo, ctx, **args):
502 def _showchangessincetag(repo, ctx, **args):
503 offset = 0
503 offset = 0
504 revs = [ctx.rev()]
504 revs = [ctx.rev()]
505 tag = args['tag']
505 tag = args['tag']
506
506
507 # The only() revset doesn't currently support wdir()
507 # The only() revset doesn't currently support wdir()
508 if ctx.rev() is None:
508 if ctx.rev() is None:
509 offset = 1
509 offset = 1
510 revs = [p.rev() for p in ctx.parents()]
510 revs = [p.rev() for p in ctx.parents()]
511
511
512 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
512 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
513
513
514 @templatekeyword('manifest')
514 @templatekeyword('manifest')
515 def showmanifest(**args):
515 def showmanifest(**args):
516 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
516 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
517 mnode = ctx.manifestnode()
517 mnode = ctx.manifestnode()
518 if mnode is None:
518 if mnode is None:
519 # just avoid crash, we might want to use the 'ff...' hash in future
519 # just avoid crash, we might want to use the 'ff...' hash in future
520 return
520 return
521 args = args.copy()
521 args = args.copy()
522 args.update({'rev': repo.manifestlog._revlog.rev(mnode),
522 args.update({'rev': repo.manifestlog._revlog.rev(mnode),
523 'node': hex(mnode)})
523 'node': hex(mnode)})
524 return templ('manifest', **args)
524 return templ('manifest', **args)
525
525
526 def shownames(namespace, **args):
526 def shownames(namespace, **args):
527 """helper method to generate a template keyword for a namespace"""
527 """helper method to generate a template keyword for a namespace"""
528 ctx = args['ctx']
528 ctx = args['ctx']
529 repo = ctx.repo()
529 repo = ctx.repo()
530 ns = repo.names[namespace]
530 ns = repo.names[namespace]
531 names = ns.names(repo, ctx.node())
531 names = ns.names(repo, ctx.node())
532 return showlist(ns.templatename, names, plural=namespace, **args)
532 return showlist(ns.templatename, names, args, plural=namespace)
533
533
534 @templatekeyword('namespaces')
534 @templatekeyword('namespaces')
535 def shownamespaces(**args):
535 def shownamespaces(**args):
536 """Dict of lists. Names attached to this changeset per
536 """Dict of lists. Names attached to this changeset per
537 namespace."""
537 namespace."""
538 ctx = args['ctx']
538 ctx = args['ctx']
539 repo = ctx.repo()
539 repo = ctx.repo()
540 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
540 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
541 **args))
541 args))
542 for k, ns in repo.names.iteritems())
542 for k, ns in repo.names.iteritems())
543 f = _showlist('namespace', list(namespaces), args)
543 f = _showlist('namespace', list(namespaces), args)
544 return _hybrid(f, namespaces,
544 return _hybrid(f, namespaces,
545 lambda k: {'namespace': k, 'names': namespaces[k]},
545 lambda k: {'namespace': k, 'names': namespaces[k]},
546 lambda x: x['namespace'])
546 lambda x: x['namespace'])
547
547
548 @templatekeyword('node')
548 @templatekeyword('node')
549 def shownode(repo, ctx, templ, **args):
549 def shownode(repo, ctx, templ, **args):
550 """String. The changeset identification hash, as a 40 hexadecimal
550 """String. The changeset identification hash, as a 40 hexadecimal
551 digit string.
551 digit string.
552 """
552 """
553 return ctx.hex()
553 return ctx.hex()
554
554
555 @templatekeyword('obsolete')
555 @templatekeyword('obsolete')
556 def showobsolete(repo, ctx, templ, **args):
556 def showobsolete(repo, ctx, templ, **args):
557 """String. Whether the changeset is obsolete.
557 """String. Whether the changeset is obsolete.
558 """
558 """
559 if ctx.obsolete():
559 if ctx.obsolete():
560 return 'obsolete'
560 return 'obsolete'
561 return ''
561 return ''
562
562
563 @templatekeyword('p1rev')
563 @templatekeyword('p1rev')
564 def showp1rev(repo, ctx, templ, **args):
564 def showp1rev(repo, ctx, templ, **args):
565 """Integer. The repository-local revision number of the changeset's
565 """Integer. The repository-local revision number of the changeset's
566 first parent, or -1 if the changeset has no parents."""
566 first parent, or -1 if the changeset has no parents."""
567 return ctx.p1().rev()
567 return ctx.p1().rev()
568
568
569 @templatekeyword('p2rev')
569 @templatekeyword('p2rev')
570 def showp2rev(repo, ctx, templ, **args):
570 def showp2rev(repo, ctx, templ, **args):
571 """Integer. The repository-local revision number of the changeset's
571 """Integer. The repository-local revision number of the changeset's
572 second parent, or -1 if the changeset has no second parent."""
572 second parent, or -1 if the changeset has no second parent."""
573 return ctx.p2().rev()
573 return ctx.p2().rev()
574
574
575 @templatekeyword('p1node')
575 @templatekeyword('p1node')
576 def showp1node(repo, ctx, templ, **args):
576 def showp1node(repo, ctx, templ, **args):
577 """String. The identification hash of the changeset's first parent,
577 """String. The identification hash of the changeset's first parent,
578 as a 40 digit hexadecimal string. If the changeset has no parents, all
578 as a 40 digit hexadecimal string. If the changeset has no parents, all
579 digits are 0."""
579 digits are 0."""
580 return ctx.p1().hex()
580 return ctx.p1().hex()
581
581
582 @templatekeyword('p2node')
582 @templatekeyword('p2node')
583 def showp2node(repo, ctx, templ, **args):
583 def showp2node(repo, ctx, templ, **args):
584 """String. The identification hash of the changeset's second
584 """String. The identification hash of the changeset's second
585 parent, as a 40 digit hexadecimal string. If the changeset has no second
585 parent, as a 40 digit hexadecimal string. If the changeset has no second
586 parent, all digits are 0."""
586 parent, all digits are 0."""
587 return ctx.p2().hex()
587 return ctx.p2().hex()
588
588
589 @templatekeyword('parents')
589 @templatekeyword('parents')
590 def showparents(**args):
590 def showparents(**args):
591 """List of strings. The parents of the changeset in "rev:node"
591 """List of strings. The parents of the changeset in "rev:node"
592 format. If the changeset has only one "natural" parent (the predecessor
592 format. If the changeset has only one "natural" parent (the predecessor
593 revision) nothing is shown."""
593 revision) nothing is shown."""
594 repo = args['repo']
594 repo = args['repo']
595 ctx = args['ctx']
595 ctx = args['ctx']
596 pctxs = scmutil.meaningfulparents(repo, ctx)
596 pctxs = scmutil.meaningfulparents(repo, ctx)
597 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
597 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
598 parents = [[('rev', p.rev()),
598 parents = [[('rev', p.rev()),
599 ('node', p.hex()),
599 ('node', p.hex()),
600 ('phase', p.phasestr())]
600 ('phase', p.phasestr())]
601 for p in pctxs]
601 for p in pctxs]
602 f = _showlist('parent', parents, args)
602 f = _showlist('parent', parents, args)
603 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
603 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
604 lambda d: _formatrevnode(d['ctx']))
604 lambda d: _formatrevnode(d['ctx']))
605
605
606 @templatekeyword('phase')
606 @templatekeyword('phase')
607 def showphase(repo, ctx, templ, **args):
607 def showphase(repo, ctx, templ, **args):
608 """String. The changeset phase name."""
608 """String. The changeset phase name."""
609 return ctx.phasestr()
609 return ctx.phasestr()
610
610
611 @templatekeyword('phaseidx')
611 @templatekeyword('phaseidx')
612 def showphaseidx(repo, ctx, templ, **args):
612 def showphaseidx(repo, ctx, templ, **args):
613 """Integer. The changeset phase index."""
613 """Integer. The changeset phase index."""
614 return ctx.phase()
614 return ctx.phase()
615
615
616 @templatekeyword('rev')
616 @templatekeyword('rev')
617 def showrev(repo, ctx, templ, **args):
617 def showrev(repo, ctx, templ, **args):
618 """Integer. The repository-local changeset revision number."""
618 """Integer. The repository-local changeset revision number."""
619 return scmutil.intrev(ctx.rev())
619 return scmutil.intrev(ctx.rev())
620
620
621 def showrevslist(name, revs, **args):
621 def showrevslist(name, revs, **args):
622 """helper to generate a list of revisions in which a mapped template will
622 """helper to generate a list of revisions in which a mapped template will
623 be evaluated"""
623 be evaluated"""
624 repo = args['ctx'].repo()
624 repo = args['ctx'].repo()
625 revs = [str(r) for r in revs] # ifcontains() needs a list of str
625 revs = [str(r) for r in revs] # ifcontains() needs a list of str
626 f = _showlist(name, revs, args)
626 f = _showlist(name, revs, args)
627 return _hybrid(f, revs,
627 return _hybrid(f, revs,
628 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
628 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
629 lambda d: d[name])
629 lambda d: d[name])
630
630
631 @templatekeyword('subrepos')
631 @templatekeyword('subrepos')
632 def showsubrepos(**args):
632 def showsubrepos(**args):
633 """List of strings. Updated subrepositories in the changeset."""
633 """List of strings. Updated subrepositories in the changeset."""
634 ctx = args['ctx']
634 ctx = args['ctx']
635 substate = ctx.substate
635 substate = ctx.substate
636 if not substate:
636 if not substate:
637 return showlist('subrepo', [], **args)
637 return showlist('subrepo', [], args)
638 psubstate = ctx.parents()[0].substate or {}
638 psubstate = ctx.parents()[0].substate or {}
639 subrepos = []
639 subrepos = []
640 for sub in substate:
640 for sub in substate:
641 if sub not in psubstate or substate[sub] != psubstate[sub]:
641 if sub not in psubstate or substate[sub] != psubstate[sub]:
642 subrepos.append(sub) # modified or newly added in ctx
642 subrepos.append(sub) # modified or newly added in ctx
643 for sub in psubstate:
643 for sub in psubstate:
644 if sub not in substate:
644 if sub not in substate:
645 subrepos.append(sub) # removed in ctx
645 subrepos.append(sub) # removed in ctx
646 return showlist('subrepo', sorted(subrepos), **args)
646 return showlist('subrepo', sorted(subrepos), args)
647
647
648 # don't remove "showtags" definition, even though namespaces will put
648 # don't remove "showtags" definition, even though namespaces will put
649 # a helper function for "tags" keyword into "keywords" map automatically,
649 # a helper function for "tags" keyword into "keywords" map automatically,
650 # because online help text is built without namespaces initialization
650 # because online help text is built without namespaces initialization
651 @templatekeyword('tags')
651 @templatekeyword('tags')
652 def showtags(**args):
652 def showtags(**args):
653 """List of strings. Any tags associated with the changeset."""
653 """List of strings. Any tags associated with the changeset."""
654 return shownames('tags', **args)
654 return shownames('tags', **args)
655
655
656 def loadkeyword(ui, extname, registrarobj):
656 def loadkeyword(ui, extname, registrarobj):
657 """Load template keyword from specified registrarobj
657 """Load template keyword from specified registrarobj
658 """
658 """
659 for name, func in registrarobj._table.iteritems():
659 for name, func in registrarobj._table.iteritems():
660 keywords[name] = func
660 keywords[name] = func
661
661
662 @templatekeyword('termwidth')
662 @templatekeyword('termwidth')
663 def termwidth(repo, ctx, templ, **args):
663 def termwidth(repo, ctx, templ, **args):
664 """Integer. The width of the current terminal."""
664 """Integer. The width of the current terminal."""
665 return repo.ui.termwidth()
665 return repo.ui.termwidth()
666
666
667 @templatekeyword('troubles')
667 @templatekeyword('troubles')
668 def showtroubles(**args):
668 def showtroubles(**args):
669 """List of strings. Evolution troubles affecting the changeset.
669 """List of strings. Evolution troubles affecting the changeset.
670
670
671 (EXPERIMENTAL)
671 (EXPERIMENTAL)
672 """
672 """
673 return showlist('trouble', args['ctx'].troubles(), **args)
673 return showlist('trouble', args['ctx'].troubles(), args)
674
674
675 # tell hggettext to extract docstrings from these functions:
675 # tell hggettext to extract docstrings from these functions:
676 i18nfunctions = keywords.values()
676 i18nfunctions = keywords.values()
@@ -1,1371 +1,1371
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 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 parser,
21 parser,
22 pycompat,
22 pycompat,
23 registrar,
23 registrar,
24 revset as revsetmod,
24 revset as revsetmod,
25 revsetlang,
25 revsetlang,
26 templatefilters,
26 templatefilters,
27 templatekw,
27 templatekw,
28 util,
28 util,
29 )
29 )
30
30
31 # template parsing
31 # template parsing
32
32
33 elements = {
33 elements = {
34 # token-type: binding-strength, primary, prefix, infix, suffix
34 # token-type: binding-strength, primary, prefix, infix, suffix
35 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
35 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
36 "%": (16, None, None, ("%", 16), None),
36 "%": (16, None, None, ("%", 16), None),
37 "|": (15, None, None, ("|", 15), None),
37 "|": (15, None, None, ("|", 15), None),
38 "*": (5, None, None, ("*", 5), None),
38 "*": (5, None, None, ("*", 5), None),
39 "/": (5, None, None, ("/", 5), None),
39 "/": (5, None, None, ("/", 5), None),
40 "+": (4, None, None, ("+", 4), None),
40 "+": (4, None, None, ("+", 4), None),
41 "-": (4, None, ("negate", 19), ("-", 4), None),
41 "-": (4, None, ("negate", 19), ("-", 4), None),
42 "=": (3, None, None, ("keyvalue", 3), None),
42 "=": (3, None, None, ("keyvalue", 3), None),
43 ",": (2, None, None, ("list", 2), None),
43 ",": (2, None, None, ("list", 2), None),
44 ")": (0, None, None, None, None),
44 ")": (0, None, None, None, None),
45 "integer": (0, "integer", None, None, None),
45 "integer": (0, "integer", None, None, None),
46 "symbol": (0, "symbol", None, None, None),
46 "symbol": (0, "symbol", None, None, None),
47 "string": (0, "string", None, None, None),
47 "string": (0, "string", None, None, None),
48 "template": (0, "template", None, None, None),
48 "template": (0, "template", None, None, None),
49 "end": (0, None, None, None, None),
49 "end": (0, None, None, None, None),
50 }
50 }
51
51
52 def tokenize(program, start, end, term=None):
52 def tokenize(program, start, end, term=None):
53 """Parse a template expression into a stream of tokens, which must end
53 """Parse a template expression into a stream of tokens, which must end
54 with term if specified"""
54 with term if specified"""
55 pos = start
55 pos = start
56 while pos < end:
56 while pos < end:
57 c = program[pos]
57 c = program[pos]
58 if c.isspace(): # skip inter-token whitespace
58 if c.isspace(): # skip inter-token whitespace
59 pass
59 pass
60 elif c in "(=,)%|+-*/": # handle simple operators
60 elif c in "(=,)%|+-*/": # handle simple operators
61 yield (c, None, pos)
61 yield (c, None, pos)
62 elif c in '"\'': # handle quoted templates
62 elif c in '"\'': # handle quoted templates
63 s = pos + 1
63 s = pos + 1
64 data, pos = _parsetemplate(program, s, end, c)
64 data, pos = _parsetemplate(program, s, end, c)
65 yield ('template', data, s)
65 yield ('template', data, s)
66 pos -= 1
66 pos -= 1
67 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
67 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
68 # handle quoted strings
68 # handle quoted strings
69 c = program[pos + 1]
69 c = program[pos + 1]
70 s = pos = pos + 2
70 s = pos = pos + 2
71 while pos < end: # find closing quote
71 while pos < end: # find closing quote
72 d = program[pos]
72 d = program[pos]
73 if d == '\\': # skip over escaped characters
73 if d == '\\': # skip over escaped characters
74 pos += 2
74 pos += 2
75 continue
75 continue
76 if d == c:
76 if d == c:
77 yield ('string', program[s:pos], s)
77 yield ('string', program[s:pos], s)
78 break
78 break
79 pos += 1
79 pos += 1
80 else:
80 else:
81 raise error.ParseError(_("unterminated string"), s)
81 raise error.ParseError(_("unterminated string"), s)
82 elif c.isdigit():
82 elif c.isdigit():
83 s = pos
83 s = pos
84 while pos < end:
84 while pos < end:
85 d = program[pos]
85 d = program[pos]
86 if not d.isdigit():
86 if not d.isdigit():
87 break
87 break
88 pos += 1
88 pos += 1
89 yield ('integer', program[s:pos], s)
89 yield ('integer', program[s:pos], s)
90 pos -= 1
90 pos -= 1
91 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
91 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
92 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
92 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
93 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
93 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
94 # where some of nested templates were preprocessed as strings and
94 # where some of nested templates were preprocessed as strings and
95 # then compiled. therefore, \"...\" was allowed. (issue4733)
95 # then compiled. therefore, \"...\" was allowed. (issue4733)
96 #
96 #
97 # processing flow of _evalifliteral() at 5ab28a2e9962:
97 # processing flow of _evalifliteral() at 5ab28a2e9962:
98 # outer template string -> stringify() -> compiletemplate()
98 # outer template string -> stringify() -> compiletemplate()
99 # ------------------------ ------------ ------------------
99 # ------------------------ ------------ ------------------
100 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
100 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
101 # ~~~~~~~~
101 # ~~~~~~~~
102 # escaped quoted string
102 # escaped quoted string
103 if c == 'r':
103 if c == 'r':
104 pos += 1
104 pos += 1
105 token = 'string'
105 token = 'string'
106 else:
106 else:
107 token = 'template'
107 token = 'template'
108 quote = program[pos:pos + 2]
108 quote = program[pos:pos + 2]
109 s = pos = pos + 2
109 s = pos = pos + 2
110 while pos < end: # find closing escaped quote
110 while pos < end: # find closing escaped quote
111 if program.startswith('\\\\\\', pos, end):
111 if program.startswith('\\\\\\', pos, end):
112 pos += 4 # skip over double escaped characters
112 pos += 4 # skip over double escaped characters
113 continue
113 continue
114 if program.startswith(quote, pos, end):
114 if program.startswith(quote, pos, end):
115 # interpret as if it were a part of an outer string
115 # interpret as if it were a part of an outer string
116 data = parser.unescapestr(program[s:pos])
116 data = parser.unescapestr(program[s:pos])
117 if token == 'template':
117 if token == 'template':
118 data = _parsetemplate(data, 0, len(data))[0]
118 data = _parsetemplate(data, 0, len(data))[0]
119 yield (token, data, s)
119 yield (token, data, s)
120 pos += 1
120 pos += 1
121 break
121 break
122 pos += 1
122 pos += 1
123 else:
123 else:
124 raise error.ParseError(_("unterminated string"), s)
124 raise error.ParseError(_("unterminated string"), s)
125 elif c.isalnum() or c in '_':
125 elif c.isalnum() or c in '_':
126 s = pos
126 s = pos
127 pos += 1
127 pos += 1
128 while pos < end: # find end of symbol
128 while pos < end: # find end of symbol
129 d = program[pos]
129 d = program[pos]
130 if not (d.isalnum() or d == "_"):
130 if not (d.isalnum() or d == "_"):
131 break
131 break
132 pos += 1
132 pos += 1
133 sym = program[s:pos]
133 sym = program[s:pos]
134 yield ('symbol', sym, s)
134 yield ('symbol', sym, s)
135 pos -= 1
135 pos -= 1
136 elif c == term:
136 elif c == term:
137 yield ('end', None, pos + 1)
137 yield ('end', None, pos + 1)
138 return
138 return
139 else:
139 else:
140 raise error.ParseError(_("syntax error"), pos)
140 raise error.ParseError(_("syntax error"), pos)
141 pos += 1
141 pos += 1
142 if term:
142 if term:
143 raise error.ParseError(_("unterminated template expansion"), start)
143 raise error.ParseError(_("unterminated template expansion"), start)
144 yield ('end', None, pos)
144 yield ('end', None, pos)
145
145
146 def _parsetemplate(tmpl, start, stop, quote=''):
146 def _parsetemplate(tmpl, start, stop, quote=''):
147 r"""
147 r"""
148 >>> _parsetemplate('foo{bar}"baz', 0, 12)
148 >>> _parsetemplate('foo{bar}"baz', 0, 12)
149 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
149 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
150 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
150 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
151 ([('string', 'foo'), ('symbol', 'bar')], 9)
151 ([('string', 'foo'), ('symbol', 'bar')], 9)
152 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
152 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
153 ([('string', 'foo')], 4)
153 ([('string', 'foo')], 4)
154 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
154 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
155 ([('string', 'foo"'), ('string', 'bar')], 9)
155 ([('string', 'foo"'), ('string', 'bar')], 9)
156 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
156 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
157 ([('string', 'foo\\')], 6)
157 ([('string', 'foo\\')], 6)
158 """
158 """
159 parsed = []
159 parsed = []
160 sepchars = '{' + quote
160 sepchars = '{' + quote
161 pos = start
161 pos = start
162 p = parser.parser(elements)
162 p = parser.parser(elements)
163 while pos < stop:
163 while pos < stop:
164 n = min((tmpl.find(c, pos, stop) for c in sepchars),
164 n = min((tmpl.find(c, pos, stop) for c in sepchars),
165 key=lambda n: (n < 0, n))
165 key=lambda n: (n < 0, n))
166 if n < 0:
166 if n < 0:
167 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
167 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
168 pos = stop
168 pos = stop
169 break
169 break
170 c = tmpl[n]
170 c = tmpl[n]
171 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
171 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
172 if bs % 2 == 1:
172 if bs % 2 == 1:
173 # escaped (e.g. '\{', '\\\{', but not '\\{')
173 # escaped (e.g. '\{', '\\\{', but not '\\{')
174 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
174 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
175 pos = n + 1
175 pos = n + 1
176 continue
176 continue
177 if n > pos:
177 if n > pos:
178 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
178 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
179 if c == quote:
179 if c == quote:
180 return parsed, n + 1
180 return parsed, n + 1
181
181
182 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
182 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
183 parsed.append(parseres)
183 parsed.append(parseres)
184
184
185 if quote:
185 if quote:
186 raise error.ParseError(_("unterminated string"), start)
186 raise error.ParseError(_("unterminated string"), start)
187 return parsed, pos
187 return parsed, pos
188
188
189 def _unnesttemplatelist(tree):
189 def _unnesttemplatelist(tree):
190 """Expand list of templates to node tuple
190 """Expand list of templates to node tuple
191
191
192 >>> def f(tree):
192 >>> def f(tree):
193 ... print prettyformat(_unnesttemplatelist(tree))
193 ... print prettyformat(_unnesttemplatelist(tree))
194 >>> f(('template', []))
194 >>> f(('template', []))
195 ('string', '')
195 ('string', '')
196 >>> f(('template', [('string', 'foo')]))
196 >>> f(('template', [('string', 'foo')]))
197 ('string', 'foo')
197 ('string', 'foo')
198 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
198 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
199 (template
199 (template
200 ('string', 'foo')
200 ('string', 'foo')
201 ('symbol', 'rev'))
201 ('symbol', 'rev'))
202 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
202 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
203 (template
203 (template
204 ('symbol', 'rev'))
204 ('symbol', 'rev'))
205 >>> f(('template', [('template', [('string', 'foo')])]))
205 >>> f(('template', [('template', [('string', 'foo')])]))
206 ('string', 'foo')
206 ('string', 'foo')
207 """
207 """
208 if not isinstance(tree, tuple):
208 if not isinstance(tree, tuple):
209 return tree
209 return tree
210 op = tree[0]
210 op = tree[0]
211 if op != 'template':
211 if op != 'template':
212 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
212 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
213
213
214 assert len(tree) == 2
214 assert len(tree) == 2
215 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
215 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
216 if not xs:
216 if not xs:
217 return ('string', '') # empty template ""
217 return ('string', '') # empty template ""
218 elif len(xs) == 1 and xs[0][0] == 'string':
218 elif len(xs) == 1 and xs[0][0] == 'string':
219 return xs[0] # fast path for string with no template fragment "x"
219 return xs[0] # fast path for string with no template fragment "x"
220 else:
220 else:
221 return (op,) + xs
221 return (op,) + xs
222
222
223 def parse(tmpl):
223 def parse(tmpl):
224 """Parse template string into tree"""
224 """Parse template string into tree"""
225 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
225 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
226 assert pos == len(tmpl), 'unquoted template should be consumed'
226 assert pos == len(tmpl), 'unquoted template should be consumed'
227 return _unnesttemplatelist(('template', parsed))
227 return _unnesttemplatelist(('template', parsed))
228
228
229 def _parseexpr(expr):
229 def _parseexpr(expr):
230 """Parse a template expression into tree
230 """Parse a template expression into tree
231
231
232 >>> _parseexpr('"foo"')
232 >>> _parseexpr('"foo"')
233 ('string', 'foo')
233 ('string', 'foo')
234 >>> _parseexpr('foo(bar)')
234 >>> _parseexpr('foo(bar)')
235 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
235 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
236 >>> _parseexpr('foo(')
236 >>> _parseexpr('foo(')
237 Traceback (most recent call last):
237 Traceback (most recent call last):
238 ...
238 ...
239 ParseError: ('not a prefix: end', 4)
239 ParseError: ('not a prefix: end', 4)
240 >>> _parseexpr('"foo" "bar"')
240 >>> _parseexpr('"foo" "bar"')
241 Traceback (most recent call last):
241 Traceback (most recent call last):
242 ...
242 ...
243 ParseError: ('invalid token', 7)
243 ParseError: ('invalid token', 7)
244 """
244 """
245 p = parser.parser(elements)
245 p = parser.parser(elements)
246 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
246 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
247 if pos != len(expr):
247 if pos != len(expr):
248 raise error.ParseError(_('invalid token'), pos)
248 raise error.ParseError(_('invalid token'), pos)
249 return _unnesttemplatelist(tree)
249 return _unnesttemplatelist(tree)
250
250
251 def prettyformat(tree):
251 def prettyformat(tree):
252 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
252 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
253
253
254 def compileexp(exp, context, curmethods):
254 def compileexp(exp, context, curmethods):
255 """Compile parsed template tree to (func, data) pair"""
255 """Compile parsed template tree to (func, data) pair"""
256 t = exp[0]
256 t = exp[0]
257 if t in curmethods:
257 if t in curmethods:
258 return curmethods[t](exp, context)
258 return curmethods[t](exp, context)
259 raise error.ParseError(_("unknown method '%s'") % t)
259 raise error.ParseError(_("unknown method '%s'") % t)
260
260
261 # template evaluation
261 # template evaluation
262
262
263 def getsymbol(exp):
263 def getsymbol(exp):
264 if exp[0] == 'symbol':
264 if exp[0] == 'symbol':
265 return exp[1]
265 return exp[1]
266 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
266 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
267
267
268 def getlist(x):
268 def getlist(x):
269 if not x:
269 if not x:
270 return []
270 return []
271 if x[0] == 'list':
271 if x[0] == 'list':
272 return getlist(x[1]) + [x[2]]
272 return getlist(x[1]) + [x[2]]
273 return [x]
273 return [x]
274
274
275 def gettemplate(exp, context):
275 def gettemplate(exp, context):
276 """Compile given template tree or load named template from map file;
276 """Compile given template tree or load named template from map file;
277 returns (func, data) pair"""
277 returns (func, data) pair"""
278 if exp[0] in ('template', 'string'):
278 if exp[0] in ('template', 'string'):
279 return compileexp(exp, context, methods)
279 return compileexp(exp, context, methods)
280 if exp[0] == 'symbol':
280 if exp[0] == 'symbol':
281 # unlike runsymbol(), here 'symbol' is always taken as template name
281 # unlike runsymbol(), here 'symbol' is always taken as template name
282 # even if it exists in mapping. this allows us to override mapping
282 # even if it exists in mapping. this allows us to override mapping
283 # by web templates, e.g. 'changelogtag' is redefined in map file.
283 # by web templates, e.g. 'changelogtag' is redefined in map file.
284 return context._load(exp[1])
284 return context._load(exp[1])
285 raise error.ParseError(_("expected template specifier"))
285 raise error.ParseError(_("expected template specifier"))
286
286
287 def findsymbolicname(arg):
287 def findsymbolicname(arg):
288 """Find symbolic name for the given compiled expression; returns None
288 """Find symbolic name for the given compiled expression; returns None
289 if nothing found reliably"""
289 if nothing found reliably"""
290 while True:
290 while True:
291 func, data = arg
291 func, data = arg
292 if func is runsymbol:
292 if func is runsymbol:
293 return data
293 return data
294 elif func is runfilter:
294 elif func is runfilter:
295 arg = data[0]
295 arg = data[0]
296 else:
296 else:
297 return None
297 return None
298
298
299 def evalfuncarg(context, mapping, arg):
299 def evalfuncarg(context, mapping, arg):
300 func, data = arg
300 func, data = arg
301 # func() may return string, generator of strings or arbitrary object such
301 # func() may return string, generator of strings or arbitrary object such
302 # as date tuple, but filter does not want generator.
302 # as date tuple, but filter does not want generator.
303 thing = func(context, mapping, data)
303 thing = func(context, mapping, data)
304 if isinstance(thing, types.GeneratorType):
304 if isinstance(thing, types.GeneratorType):
305 thing = stringify(thing)
305 thing = stringify(thing)
306 return thing
306 return thing
307
307
308 def evalboolean(context, mapping, arg):
308 def evalboolean(context, mapping, arg):
309 """Evaluate given argument as boolean, but also takes boolean literals"""
309 """Evaluate given argument as boolean, but also takes boolean literals"""
310 func, data = arg
310 func, data = arg
311 if func is runsymbol:
311 if func is runsymbol:
312 thing = func(context, mapping, data, default=None)
312 thing = func(context, mapping, data, default=None)
313 if thing is None:
313 if thing is None:
314 # not a template keyword, takes as a boolean literal
314 # not a template keyword, takes as a boolean literal
315 thing = util.parsebool(data)
315 thing = util.parsebool(data)
316 else:
316 else:
317 thing = func(context, mapping, data)
317 thing = func(context, mapping, data)
318 if isinstance(thing, bool):
318 if isinstance(thing, bool):
319 return thing
319 return thing
320 # other objects are evaluated as strings, which means 0 is True, but
320 # other objects are evaluated as strings, which means 0 is True, but
321 # empty dict/list should be False as they are expected to be ''
321 # empty dict/list should be False as they are expected to be ''
322 return bool(stringify(thing))
322 return bool(stringify(thing))
323
323
324 def evalinteger(context, mapping, arg, err):
324 def evalinteger(context, mapping, arg, err):
325 v = evalfuncarg(context, mapping, arg)
325 v = evalfuncarg(context, mapping, arg)
326 try:
326 try:
327 return int(v)
327 return int(v)
328 except (TypeError, ValueError):
328 except (TypeError, ValueError):
329 raise error.ParseError(err)
329 raise error.ParseError(err)
330
330
331 def evalstring(context, mapping, arg):
331 def evalstring(context, mapping, arg):
332 func, data = arg
332 func, data = arg
333 return stringify(func(context, mapping, data))
333 return stringify(func(context, mapping, data))
334
334
335 def evalstringliteral(context, mapping, arg):
335 def evalstringliteral(context, mapping, arg):
336 """Evaluate given argument as string template, but returns symbol name
336 """Evaluate given argument as string template, but returns symbol name
337 if it is unknown"""
337 if it is unknown"""
338 func, data = arg
338 func, data = arg
339 if func is runsymbol:
339 if func is runsymbol:
340 thing = func(context, mapping, data, default=data)
340 thing = func(context, mapping, data, default=data)
341 else:
341 else:
342 thing = func(context, mapping, data)
342 thing = func(context, mapping, data)
343 return stringify(thing)
343 return stringify(thing)
344
344
345 def runinteger(context, mapping, data):
345 def runinteger(context, mapping, data):
346 return int(data)
346 return int(data)
347
347
348 def runstring(context, mapping, data):
348 def runstring(context, mapping, data):
349 return data
349 return data
350
350
351 def _recursivesymbolblocker(key):
351 def _recursivesymbolblocker(key):
352 def showrecursion(**args):
352 def showrecursion(**args):
353 raise error.Abort(_("recursive reference '%s' in template") % key)
353 raise error.Abort(_("recursive reference '%s' in template") % key)
354 return showrecursion
354 return showrecursion
355
355
356 def _runrecursivesymbol(context, mapping, key):
356 def _runrecursivesymbol(context, mapping, key):
357 raise error.Abort(_("recursive reference '%s' in template") % key)
357 raise error.Abort(_("recursive reference '%s' in template") % key)
358
358
359 def runsymbol(context, mapping, key, default=''):
359 def runsymbol(context, mapping, key, default=''):
360 v = mapping.get(key)
360 v = mapping.get(key)
361 if v is None:
361 if v is None:
362 v = context._defaults.get(key)
362 v = context._defaults.get(key)
363 if v is None:
363 if v is None:
364 # put poison to cut recursion. we can't move this to parsing phase
364 # put poison to cut recursion. we can't move this to parsing phase
365 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
365 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
366 safemapping = mapping.copy()
366 safemapping = mapping.copy()
367 safemapping[key] = _recursivesymbolblocker(key)
367 safemapping[key] = _recursivesymbolblocker(key)
368 try:
368 try:
369 v = context.process(key, safemapping)
369 v = context.process(key, safemapping)
370 except TemplateNotFound:
370 except TemplateNotFound:
371 v = default
371 v = default
372 if callable(v):
372 if callable(v):
373 return v(**mapping)
373 return v(**mapping)
374 return v
374 return v
375
375
376 def buildtemplate(exp, context):
376 def buildtemplate(exp, context):
377 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
377 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
378 return (runtemplate, ctmpl)
378 return (runtemplate, ctmpl)
379
379
380 def runtemplate(context, mapping, template):
380 def runtemplate(context, mapping, template):
381 for func, data in template:
381 for func, data in template:
382 yield func(context, mapping, data)
382 yield func(context, mapping, data)
383
383
384 def buildfilter(exp, context):
384 def buildfilter(exp, context):
385 n = getsymbol(exp[2])
385 n = getsymbol(exp[2])
386 if n in context._filters:
386 if n in context._filters:
387 filt = context._filters[n]
387 filt = context._filters[n]
388 arg = compileexp(exp[1], context, methods)
388 arg = compileexp(exp[1], context, methods)
389 return (runfilter, (arg, filt))
389 return (runfilter, (arg, filt))
390 if n in funcs:
390 if n in funcs:
391 f = funcs[n]
391 f = funcs[n]
392 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
392 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
393 return (f, args)
393 return (f, args)
394 raise error.ParseError(_("unknown function '%s'") % n)
394 raise error.ParseError(_("unknown function '%s'") % n)
395
395
396 def runfilter(context, mapping, data):
396 def runfilter(context, mapping, data):
397 arg, filt = data
397 arg, filt = data
398 thing = evalfuncarg(context, mapping, arg)
398 thing = evalfuncarg(context, mapping, arg)
399 try:
399 try:
400 return filt(thing)
400 return filt(thing)
401 except (ValueError, AttributeError, TypeError):
401 except (ValueError, AttributeError, TypeError):
402 sym = findsymbolicname(arg)
402 sym = findsymbolicname(arg)
403 if sym:
403 if sym:
404 msg = (_("template filter '%s' is not compatible with keyword '%s'")
404 msg = (_("template filter '%s' is not compatible with keyword '%s'")
405 % (filt.func_name, sym))
405 % (filt.func_name, sym))
406 else:
406 else:
407 msg = _("incompatible use of template filter '%s'") % filt.func_name
407 msg = _("incompatible use of template filter '%s'") % filt.func_name
408 raise error.Abort(msg)
408 raise error.Abort(msg)
409
409
410 def buildmap(exp, context):
410 def buildmap(exp, context):
411 func, data = compileexp(exp[1], context, methods)
411 func, data = compileexp(exp[1], context, methods)
412 tfunc, tdata = gettemplate(exp[2], context)
412 tfunc, tdata = gettemplate(exp[2], context)
413 return (runmap, (func, data, tfunc, tdata))
413 return (runmap, (func, data, tfunc, tdata))
414
414
415 def runmap(context, mapping, data):
415 def runmap(context, mapping, data):
416 func, data, tfunc, tdata = data
416 func, data, tfunc, tdata = data
417 d = func(context, mapping, data)
417 d = func(context, mapping, data)
418 if util.safehasattr(d, 'itermaps'):
418 if util.safehasattr(d, 'itermaps'):
419 diter = d.itermaps()
419 diter = d.itermaps()
420 else:
420 else:
421 try:
421 try:
422 diter = iter(d)
422 diter = iter(d)
423 except TypeError:
423 except TypeError:
424 if func is runsymbol:
424 if func is runsymbol:
425 raise error.ParseError(_("keyword '%s' is not iterable") % data)
425 raise error.ParseError(_("keyword '%s' is not iterable") % data)
426 else:
426 else:
427 raise error.ParseError(_("%r is not iterable") % d)
427 raise error.ParseError(_("%r is not iterable") % d)
428
428
429 for i, v in enumerate(diter):
429 for i, v in enumerate(diter):
430 lm = mapping.copy()
430 lm = mapping.copy()
431 lm['index'] = i
431 lm['index'] = i
432 if isinstance(v, dict):
432 if isinstance(v, dict):
433 lm.update(v)
433 lm.update(v)
434 lm['originalnode'] = mapping.get('node')
434 lm['originalnode'] = mapping.get('node')
435 yield tfunc(context, lm, tdata)
435 yield tfunc(context, lm, tdata)
436 else:
436 else:
437 # v is not an iterable of dicts, this happen when 'key'
437 # v is not an iterable of dicts, this happen when 'key'
438 # has been fully expanded already and format is useless.
438 # has been fully expanded already and format is useless.
439 # If so, return the expanded value.
439 # If so, return the expanded value.
440 yield v
440 yield v
441
441
442 def buildnegate(exp, context):
442 def buildnegate(exp, context):
443 arg = compileexp(exp[1], context, exprmethods)
443 arg = compileexp(exp[1], context, exprmethods)
444 return (runnegate, arg)
444 return (runnegate, arg)
445
445
446 def runnegate(context, mapping, data):
446 def runnegate(context, mapping, data):
447 data = evalinteger(context, mapping, data,
447 data = evalinteger(context, mapping, data,
448 _('negation needs an integer argument'))
448 _('negation needs an integer argument'))
449 return -data
449 return -data
450
450
451 def buildarithmetic(exp, context, func):
451 def buildarithmetic(exp, context, func):
452 left = compileexp(exp[1], context, exprmethods)
452 left = compileexp(exp[1], context, exprmethods)
453 right = compileexp(exp[2], context, exprmethods)
453 right = compileexp(exp[2], context, exprmethods)
454 return (runarithmetic, (func, left, right))
454 return (runarithmetic, (func, left, right))
455
455
456 def runarithmetic(context, mapping, data):
456 def runarithmetic(context, mapping, data):
457 func, left, right = data
457 func, left, right = data
458 left = evalinteger(context, mapping, left,
458 left = evalinteger(context, mapping, left,
459 _('arithmetic only defined on integers'))
459 _('arithmetic only defined on integers'))
460 right = evalinteger(context, mapping, right,
460 right = evalinteger(context, mapping, right,
461 _('arithmetic only defined on integers'))
461 _('arithmetic only defined on integers'))
462 try:
462 try:
463 return func(left, right)
463 return func(left, right)
464 except ZeroDivisionError:
464 except ZeroDivisionError:
465 raise error.Abort(_('division by zero is not defined'))
465 raise error.Abort(_('division by zero is not defined'))
466
466
467 def buildfunc(exp, context):
467 def buildfunc(exp, context):
468 n = getsymbol(exp[1])
468 n = getsymbol(exp[1])
469 if n in funcs:
469 if n in funcs:
470 f = funcs[n]
470 f = funcs[n]
471 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
471 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
472 return (f, args)
472 return (f, args)
473 if n in context._filters:
473 if n in context._filters:
474 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
474 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
475 if len(args) != 1:
475 if len(args) != 1:
476 raise error.ParseError(_("filter %s expects one argument") % n)
476 raise error.ParseError(_("filter %s expects one argument") % n)
477 f = context._filters[n]
477 f = context._filters[n]
478 return (runfilter, (args[0], f))
478 return (runfilter, (args[0], f))
479 raise error.ParseError(_("unknown function '%s'") % n)
479 raise error.ParseError(_("unknown function '%s'") % n)
480
480
481 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
481 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
482 """Compile parsed tree of function arguments into list or dict of
482 """Compile parsed tree of function arguments into list or dict of
483 (func, data) pairs
483 (func, data) pairs
484
484
485 >>> context = engine(lambda t: (runsymbol, t))
485 >>> context = engine(lambda t: (runsymbol, t))
486 >>> def fargs(expr, argspec):
486 >>> def fargs(expr, argspec):
487 ... x = _parseexpr(expr)
487 ... x = _parseexpr(expr)
488 ... n = getsymbol(x[1])
488 ... n = getsymbol(x[1])
489 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
489 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
490 >>> fargs('a(l=1, k=2)', 'k l m').keys()
490 >>> fargs('a(l=1, k=2)', 'k l m').keys()
491 ['l', 'k']
491 ['l', 'k']
492 >>> args = fargs('a(opts=1, k=2)', '**opts')
492 >>> args = fargs('a(opts=1, k=2)', '**opts')
493 >>> args.keys(), args['opts'].keys()
493 >>> args.keys(), args['opts'].keys()
494 (['opts'], ['opts', 'k'])
494 (['opts'], ['opts', 'k'])
495 """
495 """
496 def compiledict(xs):
496 def compiledict(xs):
497 return util.sortdict((k, compileexp(x, context, curmethods))
497 return util.sortdict((k, compileexp(x, context, curmethods))
498 for k, x in xs.iteritems())
498 for k, x in xs.iteritems())
499 def compilelist(xs):
499 def compilelist(xs):
500 return [compileexp(x, context, curmethods) for x in xs]
500 return [compileexp(x, context, curmethods) for x in xs]
501
501
502 if not argspec:
502 if not argspec:
503 # filter or function with no argspec: return list of positional args
503 # filter or function with no argspec: return list of positional args
504 return compilelist(getlist(exp))
504 return compilelist(getlist(exp))
505
505
506 # function with argspec: return dict of named args
506 # function with argspec: return dict of named args
507 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
507 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
508 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
508 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
509 keyvaluenode='keyvalue', keynode='symbol')
509 keyvaluenode='keyvalue', keynode='symbol')
510 compargs = util.sortdict()
510 compargs = util.sortdict()
511 if varkey:
511 if varkey:
512 compargs[varkey] = compilelist(treeargs.pop(varkey))
512 compargs[varkey] = compilelist(treeargs.pop(varkey))
513 if optkey:
513 if optkey:
514 compargs[optkey] = compiledict(treeargs.pop(optkey))
514 compargs[optkey] = compiledict(treeargs.pop(optkey))
515 compargs.update(compiledict(treeargs))
515 compargs.update(compiledict(treeargs))
516 return compargs
516 return compargs
517
517
518 def buildkeyvaluepair(exp, content):
518 def buildkeyvaluepair(exp, content):
519 raise error.ParseError(_("can't use a key-value pair in this context"))
519 raise error.ParseError(_("can't use a key-value pair in this context"))
520
520
521 # dict of template built-in functions
521 # dict of template built-in functions
522 funcs = {}
522 funcs = {}
523
523
524 templatefunc = registrar.templatefunc(funcs)
524 templatefunc = registrar.templatefunc(funcs)
525
525
526 @templatefunc('date(date[, fmt])')
526 @templatefunc('date(date[, fmt])')
527 def date(context, mapping, args):
527 def date(context, mapping, args):
528 """Format a date. See :hg:`help dates` for formatting
528 """Format a date. See :hg:`help dates` for formatting
529 strings. The default is a Unix date format, including the timezone:
529 strings. The default is a Unix date format, including the timezone:
530 "Mon Sep 04 15:13:13 2006 0700"."""
530 "Mon Sep 04 15:13:13 2006 0700"."""
531 if not (1 <= len(args) <= 2):
531 if not (1 <= len(args) <= 2):
532 # i18n: "date" is a keyword
532 # i18n: "date" is a keyword
533 raise error.ParseError(_("date expects one or two arguments"))
533 raise error.ParseError(_("date expects one or two arguments"))
534
534
535 date = evalfuncarg(context, mapping, args[0])
535 date = evalfuncarg(context, mapping, args[0])
536 fmt = None
536 fmt = None
537 if len(args) == 2:
537 if len(args) == 2:
538 fmt = evalstring(context, mapping, args[1])
538 fmt = evalstring(context, mapping, args[1])
539 try:
539 try:
540 if fmt is None:
540 if fmt is None:
541 return util.datestr(date)
541 return util.datestr(date)
542 else:
542 else:
543 return util.datestr(date, fmt)
543 return util.datestr(date, fmt)
544 except (TypeError, ValueError):
544 except (TypeError, ValueError):
545 # i18n: "date" is a keyword
545 # i18n: "date" is a keyword
546 raise error.ParseError(_("date expects a date information"))
546 raise error.ParseError(_("date expects a date information"))
547
547
548 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
548 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
549 def dict_(context, mapping, args):
549 def dict_(context, mapping, args):
550 """Construct a dict from key-value pairs. A key may be omitted if
550 """Construct a dict from key-value pairs. A key may be omitted if
551 a value expression can provide an unambiguous name."""
551 a value expression can provide an unambiguous name."""
552 data = util.sortdict()
552 data = util.sortdict()
553
553
554 for v in args['args']:
554 for v in args['args']:
555 k = findsymbolicname(v)
555 k = findsymbolicname(v)
556 if not k:
556 if not k:
557 raise error.ParseError(_('dict key cannot be inferred'))
557 raise error.ParseError(_('dict key cannot be inferred'))
558 if k in data or k in args['kwargs']:
558 if k in data or k in args['kwargs']:
559 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
559 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
560 data[k] = evalfuncarg(context, mapping, v)
560 data[k] = evalfuncarg(context, mapping, v)
561
561
562 data.update((k, evalfuncarg(context, mapping, v))
562 data.update((k, evalfuncarg(context, mapping, v))
563 for k, v in args['kwargs'].iteritems())
563 for k, v in args['kwargs'].iteritems())
564 return templatekw.hybriddict(data)
564 return templatekw.hybriddict(data)
565
565
566 @templatefunc('diff([includepattern [, excludepattern]])')
566 @templatefunc('diff([includepattern [, excludepattern]])')
567 def diff(context, mapping, args):
567 def diff(context, mapping, args):
568 """Show a diff, optionally
568 """Show a diff, optionally
569 specifying files to include or exclude."""
569 specifying files to include or exclude."""
570 if len(args) > 2:
570 if len(args) > 2:
571 # i18n: "diff" is a keyword
571 # i18n: "diff" is a keyword
572 raise error.ParseError(_("diff expects zero, one, or two arguments"))
572 raise error.ParseError(_("diff expects zero, one, or two arguments"))
573
573
574 def getpatterns(i):
574 def getpatterns(i):
575 if i < len(args):
575 if i < len(args):
576 s = evalstring(context, mapping, args[i]).strip()
576 s = evalstring(context, mapping, args[i]).strip()
577 if s:
577 if s:
578 return [s]
578 return [s]
579 return []
579 return []
580
580
581 ctx = mapping['ctx']
581 ctx = mapping['ctx']
582 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
582 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
583
583
584 return ''.join(chunks)
584 return ''.join(chunks)
585
585
586 @templatefunc('files(pattern)')
586 @templatefunc('files(pattern)')
587 def files(context, mapping, args):
587 def files(context, mapping, args):
588 """All files of the current changeset matching the pattern. See
588 """All files of the current changeset matching the pattern. See
589 :hg:`help patterns`."""
589 :hg:`help patterns`."""
590 if not len(args) == 1:
590 if not len(args) == 1:
591 # i18n: "files" is a keyword
591 # i18n: "files" is a keyword
592 raise error.ParseError(_("files expects one argument"))
592 raise error.ParseError(_("files expects one argument"))
593
593
594 raw = evalstring(context, mapping, args[0])
594 raw = evalstring(context, mapping, args[0])
595 ctx = mapping['ctx']
595 ctx = mapping['ctx']
596 m = ctx.match([raw])
596 m = ctx.match([raw])
597 files = list(ctx.matches(m))
597 files = list(ctx.matches(m))
598 return templatekw.showlist("file", files, **mapping)
598 return templatekw.showlist("file", files, mapping)
599
599
600 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
600 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
601 def fill(context, mapping, args):
601 def fill(context, mapping, args):
602 """Fill many
602 """Fill many
603 paragraphs with optional indentation. See the "fill" filter."""
603 paragraphs with optional indentation. See the "fill" filter."""
604 if not (1 <= len(args) <= 4):
604 if not (1 <= len(args) <= 4):
605 # i18n: "fill" is a keyword
605 # i18n: "fill" is a keyword
606 raise error.ParseError(_("fill expects one to four arguments"))
606 raise error.ParseError(_("fill expects one to four arguments"))
607
607
608 text = evalstring(context, mapping, args[0])
608 text = evalstring(context, mapping, args[0])
609 width = 76
609 width = 76
610 initindent = ''
610 initindent = ''
611 hangindent = ''
611 hangindent = ''
612 if 2 <= len(args) <= 4:
612 if 2 <= len(args) <= 4:
613 width = evalinteger(context, mapping, args[1],
613 width = evalinteger(context, mapping, args[1],
614 # i18n: "fill" is a keyword
614 # i18n: "fill" is a keyword
615 _("fill expects an integer width"))
615 _("fill expects an integer width"))
616 try:
616 try:
617 initindent = evalstring(context, mapping, args[2])
617 initindent = evalstring(context, mapping, args[2])
618 hangindent = evalstring(context, mapping, args[3])
618 hangindent = evalstring(context, mapping, args[3])
619 except IndexError:
619 except IndexError:
620 pass
620 pass
621
621
622 return templatefilters.fill(text, width, initindent, hangindent)
622 return templatefilters.fill(text, width, initindent, hangindent)
623
623
624 @templatefunc('formatnode(node)')
624 @templatefunc('formatnode(node)')
625 def formatnode(context, mapping, args):
625 def formatnode(context, mapping, args):
626 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
626 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
627 if len(args) != 1:
627 if len(args) != 1:
628 # i18n: "formatnode" is a keyword
628 # i18n: "formatnode" is a keyword
629 raise error.ParseError(_("formatnode expects one argument"))
629 raise error.ParseError(_("formatnode expects one argument"))
630
630
631 ui = mapping['ui']
631 ui = mapping['ui']
632 node = evalstring(context, mapping, args[0])
632 node = evalstring(context, mapping, args[0])
633 if ui.debugflag:
633 if ui.debugflag:
634 return node
634 return node
635 return templatefilters.short(node)
635 return templatefilters.short(node)
636
636
637 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
637 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
638 argspec='text width fillchar left')
638 argspec='text width fillchar left')
639 def pad(context, mapping, args):
639 def pad(context, mapping, args):
640 """Pad text with a
640 """Pad text with a
641 fill character."""
641 fill character."""
642 if 'text' not in args or 'width' not in args:
642 if 'text' not in args or 'width' not in args:
643 # i18n: "pad" is a keyword
643 # i18n: "pad" is a keyword
644 raise error.ParseError(_("pad() expects two to four arguments"))
644 raise error.ParseError(_("pad() expects two to four arguments"))
645
645
646 width = evalinteger(context, mapping, args['width'],
646 width = evalinteger(context, mapping, args['width'],
647 # i18n: "pad" is a keyword
647 # i18n: "pad" is a keyword
648 _("pad() expects an integer width"))
648 _("pad() expects an integer width"))
649
649
650 text = evalstring(context, mapping, args['text'])
650 text = evalstring(context, mapping, args['text'])
651
651
652 left = False
652 left = False
653 fillchar = ' '
653 fillchar = ' '
654 if 'fillchar' in args:
654 if 'fillchar' in args:
655 fillchar = evalstring(context, mapping, args['fillchar'])
655 fillchar = evalstring(context, mapping, args['fillchar'])
656 if len(color.stripeffects(fillchar)) != 1:
656 if len(color.stripeffects(fillchar)) != 1:
657 # i18n: "pad" is a keyword
657 # i18n: "pad" is a keyword
658 raise error.ParseError(_("pad() expects a single fill character"))
658 raise error.ParseError(_("pad() expects a single fill character"))
659 if 'left' in args:
659 if 'left' in args:
660 left = evalboolean(context, mapping, args['left'])
660 left = evalboolean(context, mapping, args['left'])
661
661
662 fillwidth = width - encoding.colwidth(color.stripeffects(text))
662 fillwidth = width - encoding.colwidth(color.stripeffects(text))
663 if fillwidth <= 0:
663 if fillwidth <= 0:
664 return text
664 return text
665 if left:
665 if left:
666 return fillchar * fillwidth + text
666 return fillchar * fillwidth + text
667 else:
667 else:
668 return text + fillchar * fillwidth
668 return text + fillchar * fillwidth
669
669
670 @templatefunc('indent(text, indentchars[, firstline])')
670 @templatefunc('indent(text, indentchars[, firstline])')
671 def indent(context, mapping, args):
671 def indent(context, mapping, args):
672 """Indents all non-empty lines
672 """Indents all non-empty lines
673 with the characters given in the indentchars string. An optional
673 with the characters given in the indentchars string. An optional
674 third parameter will override the indent for the first line only
674 third parameter will override the indent for the first line only
675 if present."""
675 if present."""
676 if not (2 <= len(args) <= 3):
676 if not (2 <= len(args) <= 3):
677 # i18n: "indent" is a keyword
677 # i18n: "indent" is a keyword
678 raise error.ParseError(_("indent() expects two or three arguments"))
678 raise error.ParseError(_("indent() expects two or three arguments"))
679
679
680 text = evalstring(context, mapping, args[0])
680 text = evalstring(context, mapping, args[0])
681 indent = evalstring(context, mapping, args[1])
681 indent = evalstring(context, mapping, args[1])
682
682
683 if len(args) == 3:
683 if len(args) == 3:
684 firstline = evalstring(context, mapping, args[2])
684 firstline = evalstring(context, mapping, args[2])
685 else:
685 else:
686 firstline = indent
686 firstline = indent
687
687
688 # the indent function doesn't indent the first line, so we do it here
688 # the indent function doesn't indent the first line, so we do it here
689 return templatefilters.indent(firstline + text, indent)
689 return templatefilters.indent(firstline + text, indent)
690
690
691 @templatefunc('get(dict, key)')
691 @templatefunc('get(dict, key)')
692 def get(context, mapping, args):
692 def get(context, mapping, args):
693 """Get an attribute/key from an object. Some keywords
693 """Get an attribute/key from an object. Some keywords
694 are complex types. This function allows you to obtain the value of an
694 are complex types. This function allows you to obtain the value of an
695 attribute on these types."""
695 attribute on these types."""
696 if len(args) != 2:
696 if len(args) != 2:
697 # i18n: "get" is a keyword
697 # i18n: "get" is a keyword
698 raise error.ParseError(_("get() expects two arguments"))
698 raise error.ParseError(_("get() expects two arguments"))
699
699
700 dictarg = evalfuncarg(context, mapping, args[0])
700 dictarg = evalfuncarg(context, mapping, args[0])
701 if not util.safehasattr(dictarg, 'get'):
701 if not util.safehasattr(dictarg, 'get'):
702 # i18n: "get" is a keyword
702 # i18n: "get" is a keyword
703 raise error.ParseError(_("get() expects a dict as first argument"))
703 raise error.ParseError(_("get() expects a dict as first argument"))
704
704
705 key = evalfuncarg(context, mapping, args[1])
705 key = evalfuncarg(context, mapping, args[1])
706 return dictarg.get(key)
706 return dictarg.get(key)
707
707
708 @templatefunc('if(expr, then[, else])')
708 @templatefunc('if(expr, then[, else])')
709 def if_(context, mapping, args):
709 def if_(context, mapping, args):
710 """Conditionally execute based on the result of
710 """Conditionally execute based on the result of
711 an expression."""
711 an expression."""
712 if not (2 <= len(args) <= 3):
712 if not (2 <= len(args) <= 3):
713 # i18n: "if" is a keyword
713 # i18n: "if" is a keyword
714 raise error.ParseError(_("if expects two or three arguments"))
714 raise error.ParseError(_("if expects two or three arguments"))
715
715
716 test = evalboolean(context, mapping, args[0])
716 test = evalboolean(context, mapping, args[0])
717 if test:
717 if test:
718 yield args[1][0](context, mapping, args[1][1])
718 yield args[1][0](context, mapping, args[1][1])
719 elif len(args) == 3:
719 elif len(args) == 3:
720 yield args[2][0](context, mapping, args[2][1])
720 yield args[2][0](context, mapping, args[2][1])
721
721
722 @templatefunc('ifcontains(needle, haystack, then[, else])')
722 @templatefunc('ifcontains(needle, haystack, then[, else])')
723 def ifcontains(context, mapping, args):
723 def ifcontains(context, mapping, args):
724 """Conditionally execute based
724 """Conditionally execute based
725 on whether the item "needle" is in "haystack"."""
725 on whether the item "needle" is in "haystack"."""
726 if not (3 <= len(args) <= 4):
726 if not (3 <= len(args) <= 4):
727 # i18n: "ifcontains" is a keyword
727 # i18n: "ifcontains" is a keyword
728 raise error.ParseError(_("ifcontains expects three or four arguments"))
728 raise error.ParseError(_("ifcontains expects three or four arguments"))
729
729
730 needle = evalstring(context, mapping, args[0])
730 needle = evalstring(context, mapping, args[0])
731 haystack = evalfuncarg(context, mapping, args[1])
731 haystack = evalfuncarg(context, mapping, args[1])
732
732
733 if needle in haystack:
733 if needle in haystack:
734 yield args[2][0](context, mapping, args[2][1])
734 yield args[2][0](context, mapping, args[2][1])
735 elif len(args) == 4:
735 elif len(args) == 4:
736 yield args[3][0](context, mapping, args[3][1])
736 yield args[3][0](context, mapping, args[3][1])
737
737
738 @templatefunc('ifeq(expr1, expr2, then[, else])')
738 @templatefunc('ifeq(expr1, expr2, then[, else])')
739 def ifeq(context, mapping, args):
739 def ifeq(context, mapping, args):
740 """Conditionally execute based on
740 """Conditionally execute based on
741 whether 2 items are equivalent."""
741 whether 2 items are equivalent."""
742 if not (3 <= len(args) <= 4):
742 if not (3 <= len(args) <= 4):
743 # i18n: "ifeq" is a keyword
743 # i18n: "ifeq" is a keyword
744 raise error.ParseError(_("ifeq expects three or four arguments"))
744 raise error.ParseError(_("ifeq expects three or four arguments"))
745
745
746 test = evalstring(context, mapping, args[0])
746 test = evalstring(context, mapping, args[0])
747 match = evalstring(context, mapping, args[1])
747 match = evalstring(context, mapping, args[1])
748 if test == match:
748 if test == match:
749 yield args[2][0](context, mapping, args[2][1])
749 yield args[2][0](context, mapping, args[2][1])
750 elif len(args) == 4:
750 elif len(args) == 4:
751 yield args[3][0](context, mapping, args[3][1])
751 yield args[3][0](context, mapping, args[3][1])
752
752
753 @templatefunc('join(list, sep)')
753 @templatefunc('join(list, sep)')
754 def join(context, mapping, args):
754 def join(context, mapping, args):
755 """Join items in a list with a delimiter."""
755 """Join items in a list with a delimiter."""
756 if not (1 <= len(args) <= 2):
756 if not (1 <= len(args) <= 2):
757 # i18n: "join" is a keyword
757 # i18n: "join" is a keyword
758 raise error.ParseError(_("join expects one or two arguments"))
758 raise error.ParseError(_("join expects one or two arguments"))
759
759
760 joinset = args[0][0](context, mapping, args[0][1])
760 joinset = args[0][0](context, mapping, args[0][1])
761 if util.safehasattr(joinset, 'itermaps'):
761 if util.safehasattr(joinset, 'itermaps'):
762 jf = joinset.joinfmt
762 jf = joinset.joinfmt
763 joinset = [jf(x) for x in joinset.itermaps()]
763 joinset = [jf(x) for x in joinset.itermaps()]
764
764
765 joiner = " "
765 joiner = " "
766 if len(args) > 1:
766 if len(args) > 1:
767 joiner = evalstring(context, mapping, args[1])
767 joiner = evalstring(context, mapping, args[1])
768
768
769 first = True
769 first = True
770 for x in joinset:
770 for x in joinset:
771 if first:
771 if first:
772 first = False
772 first = False
773 else:
773 else:
774 yield joiner
774 yield joiner
775 yield x
775 yield x
776
776
777 @templatefunc('label(label, expr)')
777 @templatefunc('label(label, expr)')
778 def label(context, mapping, args):
778 def label(context, mapping, args):
779 """Apply a label to generated content. Content with
779 """Apply a label to generated content. Content with
780 a label applied can result in additional post-processing, such as
780 a label applied can result in additional post-processing, such as
781 automatic colorization."""
781 automatic colorization."""
782 if len(args) != 2:
782 if len(args) != 2:
783 # i18n: "label" is a keyword
783 # i18n: "label" is a keyword
784 raise error.ParseError(_("label expects two arguments"))
784 raise error.ParseError(_("label expects two arguments"))
785
785
786 ui = mapping['ui']
786 ui = mapping['ui']
787 thing = evalstring(context, mapping, args[1])
787 thing = evalstring(context, mapping, args[1])
788 # preserve unknown symbol as literal so effects like 'red', 'bold',
788 # preserve unknown symbol as literal so effects like 'red', 'bold',
789 # etc. don't need to be quoted
789 # etc. don't need to be quoted
790 label = evalstringliteral(context, mapping, args[0])
790 label = evalstringliteral(context, mapping, args[0])
791
791
792 return ui.label(thing, label)
792 return ui.label(thing, label)
793
793
794 @templatefunc('latesttag([pattern])')
794 @templatefunc('latesttag([pattern])')
795 def latesttag(context, mapping, args):
795 def latesttag(context, mapping, args):
796 """The global tags matching the given pattern on the
796 """The global tags matching the given pattern on the
797 most recent globally tagged ancestor of this changeset.
797 most recent globally tagged ancestor of this changeset.
798 If no such tags exist, the "{tag}" template resolves to
798 If no such tags exist, the "{tag}" template resolves to
799 the string "null"."""
799 the string "null"."""
800 if len(args) > 1:
800 if len(args) > 1:
801 # i18n: "latesttag" is a keyword
801 # i18n: "latesttag" is a keyword
802 raise error.ParseError(_("latesttag expects at most one argument"))
802 raise error.ParseError(_("latesttag expects at most one argument"))
803
803
804 pattern = None
804 pattern = None
805 if len(args) == 1:
805 if len(args) == 1:
806 pattern = evalstring(context, mapping, args[0])
806 pattern = evalstring(context, mapping, args[0])
807
807
808 return templatekw.showlatesttags(pattern, **mapping)
808 return templatekw.showlatesttags(pattern, **mapping)
809
809
810 @templatefunc('localdate(date[, tz])')
810 @templatefunc('localdate(date[, tz])')
811 def localdate(context, mapping, args):
811 def localdate(context, mapping, args):
812 """Converts a date to the specified timezone.
812 """Converts a date to the specified timezone.
813 The default is local date."""
813 The default is local date."""
814 if not (1 <= len(args) <= 2):
814 if not (1 <= len(args) <= 2):
815 # i18n: "localdate" is a keyword
815 # i18n: "localdate" is a keyword
816 raise error.ParseError(_("localdate expects one or two arguments"))
816 raise error.ParseError(_("localdate expects one or two arguments"))
817
817
818 date = evalfuncarg(context, mapping, args[0])
818 date = evalfuncarg(context, mapping, args[0])
819 try:
819 try:
820 date = util.parsedate(date)
820 date = util.parsedate(date)
821 except AttributeError: # not str nor date tuple
821 except AttributeError: # not str nor date tuple
822 # i18n: "localdate" is a keyword
822 # i18n: "localdate" is a keyword
823 raise error.ParseError(_("localdate expects a date information"))
823 raise error.ParseError(_("localdate expects a date information"))
824 if len(args) >= 2:
824 if len(args) >= 2:
825 tzoffset = None
825 tzoffset = None
826 tz = evalfuncarg(context, mapping, args[1])
826 tz = evalfuncarg(context, mapping, args[1])
827 if isinstance(tz, str):
827 if isinstance(tz, str):
828 tzoffset, remainder = util.parsetimezone(tz)
828 tzoffset, remainder = util.parsetimezone(tz)
829 if remainder:
829 if remainder:
830 tzoffset = None
830 tzoffset = None
831 if tzoffset is None:
831 if tzoffset is None:
832 try:
832 try:
833 tzoffset = int(tz)
833 tzoffset = int(tz)
834 except (TypeError, ValueError):
834 except (TypeError, ValueError):
835 # i18n: "localdate" is a keyword
835 # i18n: "localdate" is a keyword
836 raise error.ParseError(_("localdate expects a timezone"))
836 raise error.ParseError(_("localdate expects a timezone"))
837 else:
837 else:
838 tzoffset = util.makedate()[1]
838 tzoffset = util.makedate()[1]
839 return (date[0], tzoffset)
839 return (date[0], tzoffset)
840
840
841 @templatefunc('mod(a, b)')
841 @templatefunc('mod(a, b)')
842 def mod(context, mapping, args):
842 def mod(context, mapping, args):
843 """Calculate a mod b such that a / b + a mod b == a"""
843 """Calculate a mod b such that a / b + a mod b == a"""
844 if not len(args) == 2:
844 if not len(args) == 2:
845 # i18n: "mod" is a keyword
845 # i18n: "mod" is a keyword
846 raise error.ParseError(_("mod expects two arguments"))
846 raise error.ParseError(_("mod expects two arguments"))
847
847
848 func = lambda a, b: a % b
848 func = lambda a, b: a % b
849 return runarithmetic(context, mapping, (func, args[0], args[1]))
849 return runarithmetic(context, mapping, (func, args[0], args[1]))
850
850
851 @templatefunc('relpath(path)')
851 @templatefunc('relpath(path)')
852 def relpath(context, mapping, args):
852 def relpath(context, mapping, args):
853 """Convert a repository-absolute path into a filesystem path relative to
853 """Convert a repository-absolute path into a filesystem path relative to
854 the current working directory."""
854 the current working directory."""
855 if len(args) != 1:
855 if len(args) != 1:
856 # i18n: "relpath" is a keyword
856 # i18n: "relpath" is a keyword
857 raise error.ParseError(_("relpath expects one argument"))
857 raise error.ParseError(_("relpath expects one argument"))
858
858
859 repo = mapping['ctx'].repo()
859 repo = mapping['ctx'].repo()
860 path = evalstring(context, mapping, args[0])
860 path = evalstring(context, mapping, args[0])
861 return repo.pathto(path)
861 return repo.pathto(path)
862
862
863 @templatefunc('revset(query[, formatargs...])')
863 @templatefunc('revset(query[, formatargs...])')
864 def revset(context, mapping, args):
864 def revset(context, mapping, args):
865 """Execute a revision set query. See
865 """Execute a revision set query. See
866 :hg:`help revset`."""
866 :hg:`help revset`."""
867 if not len(args) > 0:
867 if not len(args) > 0:
868 # i18n: "revset" is a keyword
868 # i18n: "revset" is a keyword
869 raise error.ParseError(_("revset expects one or more arguments"))
869 raise error.ParseError(_("revset expects one or more arguments"))
870
870
871 raw = evalstring(context, mapping, args[0])
871 raw = evalstring(context, mapping, args[0])
872 ctx = mapping['ctx']
872 ctx = mapping['ctx']
873 repo = ctx.repo()
873 repo = ctx.repo()
874
874
875 def query(expr):
875 def query(expr):
876 m = revsetmod.match(repo.ui, expr)
876 m = revsetmod.match(repo.ui, expr)
877 return m(repo)
877 return m(repo)
878
878
879 if len(args) > 1:
879 if len(args) > 1:
880 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
880 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
881 revs = query(revsetlang.formatspec(raw, *formatargs))
881 revs = query(revsetlang.formatspec(raw, *formatargs))
882 revs = list(revs)
882 revs = list(revs)
883 else:
883 else:
884 revsetcache = mapping['cache'].setdefault("revsetcache", {})
884 revsetcache = mapping['cache'].setdefault("revsetcache", {})
885 if raw in revsetcache:
885 if raw in revsetcache:
886 revs = revsetcache[raw]
886 revs = revsetcache[raw]
887 else:
887 else:
888 revs = query(raw)
888 revs = query(raw)
889 revs = list(revs)
889 revs = list(revs)
890 revsetcache[raw] = revs
890 revsetcache[raw] = revs
891
891
892 return templatekw.showrevslist("revision", revs, **mapping)
892 return templatekw.showrevslist("revision", revs, **mapping)
893
893
894 @templatefunc('rstdoc(text, style)')
894 @templatefunc('rstdoc(text, style)')
895 def rstdoc(context, mapping, args):
895 def rstdoc(context, mapping, args):
896 """Format reStructuredText."""
896 """Format reStructuredText."""
897 if len(args) != 2:
897 if len(args) != 2:
898 # i18n: "rstdoc" is a keyword
898 # i18n: "rstdoc" is a keyword
899 raise error.ParseError(_("rstdoc expects two arguments"))
899 raise error.ParseError(_("rstdoc expects two arguments"))
900
900
901 text = evalstring(context, mapping, args[0])
901 text = evalstring(context, mapping, args[0])
902 style = evalstring(context, mapping, args[1])
902 style = evalstring(context, mapping, args[1])
903
903
904 return minirst.format(text, style=style, keep=['verbose'])
904 return minirst.format(text, style=style, keep=['verbose'])
905
905
906 @templatefunc('separate(sep, args)', argspec='sep *args')
906 @templatefunc('separate(sep, args)', argspec='sep *args')
907 def separate(context, mapping, args):
907 def separate(context, mapping, args):
908 """Add a separator between non-empty arguments."""
908 """Add a separator between non-empty arguments."""
909 if 'sep' not in args:
909 if 'sep' not in args:
910 # i18n: "separate" is a keyword
910 # i18n: "separate" is a keyword
911 raise error.ParseError(_("separate expects at least one argument"))
911 raise error.ParseError(_("separate expects at least one argument"))
912
912
913 sep = evalstring(context, mapping, args['sep'])
913 sep = evalstring(context, mapping, args['sep'])
914 first = True
914 first = True
915 for arg in args['args']:
915 for arg in args['args']:
916 argstr = evalstring(context, mapping, arg)
916 argstr = evalstring(context, mapping, arg)
917 if not argstr:
917 if not argstr:
918 continue
918 continue
919 if first:
919 if first:
920 first = False
920 first = False
921 else:
921 else:
922 yield sep
922 yield sep
923 yield argstr
923 yield argstr
924
924
925 @templatefunc('shortest(node, minlength=4)')
925 @templatefunc('shortest(node, minlength=4)')
926 def shortest(context, mapping, args):
926 def shortest(context, mapping, args):
927 """Obtain the shortest representation of
927 """Obtain the shortest representation of
928 a node."""
928 a node."""
929 if not (1 <= len(args) <= 2):
929 if not (1 <= len(args) <= 2):
930 # i18n: "shortest" is a keyword
930 # i18n: "shortest" is a keyword
931 raise error.ParseError(_("shortest() expects one or two arguments"))
931 raise error.ParseError(_("shortest() expects one or two arguments"))
932
932
933 node = evalstring(context, mapping, args[0])
933 node = evalstring(context, mapping, args[0])
934
934
935 minlength = 4
935 minlength = 4
936 if len(args) > 1:
936 if len(args) > 1:
937 minlength = evalinteger(context, mapping, args[1],
937 minlength = evalinteger(context, mapping, args[1],
938 # i18n: "shortest" is a keyword
938 # i18n: "shortest" is a keyword
939 _("shortest() expects an integer minlength"))
939 _("shortest() expects an integer minlength"))
940
940
941 # _partialmatch() of filtered changelog could take O(len(repo)) time,
941 # _partialmatch() of filtered changelog could take O(len(repo)) time,
942 # which would be unacceptably slow. so we look for hash collision in
942 # which would be unacceptably slow. so we look for hash collision in
943 # unfiltered space, which means some hashes may be slightly longer.
943 # unfiltered space, which means some hashes may be slightly longer.
944 cl = mapping['ctx']._repo.unfiltered().changelog
944 cl = mapping['ctx']._repo.unfiltered().changelog
945 def isvalid(test):
945 def isvalid(test):
946 try:
946 try:
947 if cl._partialmatch(test) is None:
947 if cl._partialmatch(test) is None:
948 return False
948 return False
949
949
950 try:
950 try:
951 i = int(test)
951 i = int(test)
952 # if we are a pure int, then starting with zero will not be
952 # if we are a pure int, then starting with zero will not be
953 # confused as a rev; or, obviously, if the int is larger than
953 # confused as a rev; or, obviously, if the int is larger than
954 # the value of the tip rev
954 # the value of the tip rev
955 if test[0] == '0' or i > len(cl):
955 if test[0] == '0' or i > len(cl):
956 return True
956 return True
957 return False
957 return False
958 except ValueError:
958 except ValueError:
959 return True
959 return True
960 except error.RevlogError:
960 except error.RevlogError:
961 return False
961 return False
962
962
963 shortest = node
963 shortest = node
964 startlength = max(6, minlength)
964 startlength = max(6, minlength)
965 length = startlength
965 length = startlength
966 while True:
966 while True:
967 test = node[:length]
967 test = node[:length]
968 if isvalid(test):
968 if isvalid(test):
969 shortest = test
969 shortest = test
970 if length == minlength or length > startlength:
970 if length == minlength or length > startlength:
971 return shortest
971 return shortest
972 length -= 1
972 length -= 1
973 else:
973 else:
974 length += 1
974 length += 1
975 if len(shortest) <= length:
975 if len(shortest) <= length:
976 return shortest
976 return shortest
977
977
978 @templatefunc('strip(text[, chars])')
978 @templatefunc('strip(text[, chars])')
979 def strip(context, mapping, args):
979 def strip(context, mapping, args):
980 """Strip characters from a string. By default,
980 """Strip characters from a string. By default,
981 strips all leading and trailing whitespace."""
981 strips all leading and trailing whitespace."""
982 if not (1 <= len(args) <= 2):
982 if not (1 <= len(args) <= 2):
983 # i18n: "strip" is a keyword
983 # i18n: "strip" is a keyword
984 raise error.ParseError(_("strip expects one or two arguments"))
984 raise error.ParseError(_("strip expects one or two arguments"))
985
985
986 text = evalstring(context, mapping, args[0])
986 text = evalstring(context, mapping, args[0])
987 if len(args) == 2:
987 if len(args) == 2:
988 chars = evalstring(context, mapping, args[1])
988 chars = evalstring(context, mapping, args[1])
989 return text.strip(chars)
989 return text.strip(chars)
990 return text.strip()
990 return text.strip()
991
991
992 @templatefunc('sub(pattern, replacement, expression)')
992 @templatefunc('sub(pattern, replacement, expression)')
993 def sub(context, mapping, args):
993 def sub(context, mapping, args):
994 """Perform text substitution
994 """Perform text substitution
995 using regular expressions."""
995 using regular expressions."""
996 if len(args) != 3:
996 if len(args) != 3:
997 # i18n: "sub" is a keyword
997 # i18n: "sub" is a keyword
998 raise error.ParseError(_("sub expects three arguments"))
998 raise error.ParseError(_("sub expects three arguments"))
999
999
1000 pat = evalstring(context, mapping, args[0])
1000 pat = evalstring(context, mapping, args[0])
1001 rpl = evalstring(context, mapping, args[1])
1001 rpl = evalstring(context, mapping, args[1])
1002 src = evalstring(context, mapping, args[2])
1002 src = evalstring(context, mapping, args[2])
1003 try:
1003 try:
1004 patre = re.compile(pat)
1004 patre = re.compile(pat)
1005 except re.error:
1005 except re.error:
1006 # i18n: "sub" is a keyword
1006 # i18n: "sub" is a keyword
1007 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1007 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1008 try:
1008 try:
1009 yield patre.sub(rpl, src)
1009 yield patre.sub(rpl, src)
1010 except re.error:
1010 except re.error:
1011 # i18n: "sub" is a keyword
1011 # i18n: "sub" is a keyword
1012 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1012 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1013
1013
1014 @templatefunc('startswith(pattern, text)')
1014 @templatefunc('startswith(pattern, text)')
1015 def startswith(context, mapping, args):
1015 def startswith(context, mapping, args):
1016 """Returns the value from the "text" argument
1016 """Returns the value from the "text" argument
1017 if it begins with the content from the "pattern" argument."""
1017 if it begins with the content from the "pattern" argument."""
1018 if len(args) != 2:
1018 if len(args) != 2:
1019 # i18n: "startswith" is a keyword
1019 # i18n: "startswith" is a keyword
1020 raise error.ParseError(_("startswith expects two arguments"))
1020 raise error.ParseError(_("startswith expects two arguments"))
1021
1021
1022 patn = evalstring(context, mapping, args[0])
1022 patn = evalstring(context, mapping, args[0])
1023 text = evalstring(context, mapping, args[1])
1023 text = evalstring(context, mapping, args[1])
1024 if text.startswith(patn):
1024 if text.startswith(patn):
1025 return text
1025 return text
1026 return ''
1026 return ''
1027
1027
1028 @templatefunc('word(number, text[, separator])')
1028 @templatefunc('word(number, text[, separator])')
1029 def word(context, mapping, args):
1029 def word(context, mapping, args):
1030 """Return the nth word from a string."""
1030 """Return the nth word from a string."""
1031 if not (2 <= len(args) <= 3):
1031 if not (2 <= len(args) <= 3):
1032 # i18n: "word" is a keyword
1032 # i18n: "word" is a keyword
1033 raise error.ParseError(_("word expects two or three arguments, got %d")
1033 raise error.ParseError(_("word expects two or three arguments, got %d")
1034 % len(args))
1034 % len(args))
1035
1035
1036 num = evalinteger(context, mapping, args[0],
1036 num = evalinteger(context, mapping, args[0],
1037 # i18n: "word" is a keyword
1037 # i18n: "word" is a keyword
1038 _("word expects an integer index"))
1038 _("word expects an integer index"))
1039 text = evalstring(context, mapping, args[1])
1039 text = evalstring(context, mapping, args[1])
1040 if len(args) == 3:
1040 if len(args) == 3:
1041 splitter = evalstring(context, mapping, args[2])
1041 splitter = evalstring(context, mapping, args[2])
1042 else:
1042 else:
1043 splitter = None
1043 splitter = None
1044
1044
1045 tokens = text.split(splitter)
1045 tokens = text.split(splitter)
1046 if num >= len(tokens) or num < -len(tokens):
1046 if num >= len(tokens) or num < -len(tokens):
1047 return ''
1047 return ''
1048 else:
1048 else:
1049 return tokens[num]
1049 return tokens[num]
1050
1050
1051 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1051 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1052 exprmethods = {
1052 exprmethods = {
1053 "integer": lambda e, c: (runinteger, e[1]),
1053 "integer": lambda e, c: (runinteger, e[1]),
1054 "string": lambda e, c: (runstring, e[1]),
1054 "string": lambda e, c: (runstring, e[1]),
1055 "symbol": lambda e, c: (runsymbol, e[1]),
1055 "symbol": lambda e, c: (runsymbol, e[1]),
1056 "template": buildtemplate,
1056 "template": buildtemplate,
1057 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1057 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1058 # ".": buildmember,
1058 # ".": buildmember,
1059 "|": buildfilter,
1059 "|": buildfilter,
1060 "%": buildmap,
1060 "%": buildmap,
1061 "func": buildfunc,
1061 "func": buildfunc,
1062 "keyvalue": buildkeyvaluepair,
1062 "keyvalue": buildkeyvaluepair,
1063 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1063 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1064 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1064 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1065 "negate": buildnegate,
1065 "negate": buildnegate,
1066 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1066 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1067 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1067 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1068 }
1068 }
1069
1069
1070 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1070 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1071 methods = exprmethods.copy()
1071 methods = exprmethods.copy()
1072 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1072 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1073
1073
1074 class _aliasrules(parser.basealiasrules):
1074 class _aliasrules(parser.basealiasrules):
1075 """Parsing and expansion rule set of template aliases"""
1075 """Parsing and expansion rule set of template aliases"""
1076 _section = _('template alias')
1076 _section = _('template alias')
1077 _parse = staticmethod(_parseexpr)
1077 _parse = staticmethod(_parseexpr)
1078
1078
1079 @staticmethod
1079 @staticmethod
1080 def _trygetfunc(tree):
1080 def _trygetfunc(tree):
1081 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1081 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1082 None"""
1082 None"""
1083 if tree[0] == 'func' and tree[1][0] == 'symbol':
1083 if tree[0] == 'func' and tree[1][0] == 'symbol':
1084 return tree[1][1], getlist(tree[2])
1084 return tree[1][1], getlist(tree[2])
1085 if tree[0] == '|' and tree[2][0] == 'symbol':
1085 if tree[0] == '|' and tree[2][0] == 'symbol':
1086 return tree[2][1], [tree[1]]
1086 return tree[2][1], [tree[1]]
1087
1087
1088 def expandaliases(tree, aliases):
1088 def expandaliases(tree, aliases):
1089 """Return new tree of aliases are expanded"""
1089 """Return new tree of aliases are expanded"""
1090 aliasmap = _aliasrules.buildmap(aliases)
1090 aliasmap = _aliasrules.buildmap(aliases)
1091 return _aliasrules.expand(aliasmap, tree)
1091 return _aliasrules.expand(aliasmap, tree)
1092
1092
1093 # template engine
1093 # template engine
1094
1094
1095 stringify = templatefilters.stringify
1095 stringify = templatefilters.stringify
1096
1096
1097 def _flatten(thing):
1097 def _flatten(thing):
1098 '''yield a single stream from a possibly nested set of iterators'''
1098 '''yield a single stream from a possibly nested set of iterators'''
1099 thing = templatekw.unwraphybrid(thing)
1099 thing = templatekw.unwraphybrid(thing)
1100 if isinstance(thing, str):
1100 if isinstance(thing, str):
1101 yield thing
1101 yield thing
1102 elif thing is None:
1102 elif thing is None:
1103 pass
1103 pass
1104 elif not util.safehasattr(thing, '__iter__'):
1104 elif not util.safehasattr(thing, '__iter__'):
1105 yield str(thing)
1105 yield str(thing)
1106 else:
1106 else:
1107 for i in thing:
1107 for i in thing:
1108 i = templatekw.unwraphybrid(i)
1108 i = templatekw.unwraphybrid(i)
1109 if isinstance(i, str):
1109 if isinstance(i, str):
1110 yield i
1110 yield i
1111 elif i is None:
1111 elif i is None:
1112 pass
1112 pass
1113 elif not util.safehasattr(i, '__iter__'):
1113 elif not util.safehasattr(i, '__iter__'):
1114 yield str(i)
1114 yield str(i)
1115 else:
1115 else:
1116 for j in _flatten(i):
1116 for j in _flatten(i):
1117 yield j
1117 yield j
1118
1118
1119 def unquotestring(s):
1119 def unquotestring(s):
1120 '''unwrap quotes if any; otherwise returns unmodified string'''
1120 '''unwrap quotes if any; otherwise returns unmodified string'''
1121 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1121 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1122 return s
1122 return s
1123 return s[1:-1]
1123 return s[1:-1]
1124
1124
1125 class engine(object):
1125 class engine(object):
1126 '''template expansion engine.
1126 '''template expansion engine.
1127
1127
1128 template expansion works like this. a map file contains key=value
1128 template expansion works like this. a map file contains key=value
1129 pairs. if value is quoted, it is treated as string. otherwise, it
1129 pairs. if value is quoted, it is treated as string. otherwise, it
1130 is treated as name of template file.
1130 is treated as name of template file.
1131
1131
1132 templater is asked to expand a key in map. it looks up key, and
1132 templater is asked to expand a key in map. it looks up key, and
1133 looks for strings like this: {foo}. it expands {foo} by looking up
1133 looks for strings like this: {foo}. it expands {foo} by looking up
1134 foo in map, and substituting it. expansion is recursive: it stops
1134 foo in map, and substituting it. expansion is recursive: it stops
1135 when there is no more {foo} to replace.
1135 when there is no more {foo} to replace.
1136
1136
1137 expansion also allows formatting and filtering.
1137 expansion also allows formatting and filtering.
1138
1138
1139 format uses key to expand each item in list. syntax is
1139 format uses key to expand each item in list. syntax is
1140 {key%format}.
1140 {key%format}.
1141
1141
1142 filter uses function to transform value. syntax is
1142 filter uses function to transform value. syntax is
1143 {key|filter1|filter2|...}.'''
1143 {key|filter1|filter2|...}.'''
1144
1144
1145 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1145 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1146 self._loader = loader
1146 self._loader = loader
1147 if filters is None:
1147 if filters is None:
1148 filters = {}
1148 filters = {}
1149 self._filters = filters
1149 self._filters = filters
1150 if defaults is None:
1150 if defaults is None:
1151 defaults = {}
1151 defaults = {}
1152 self._defaults = defaults
1152 self._defaults = defaults
1153 self._aliasmap = _aliasrules.buildmap(aliases)
1153 self._aliasmap = _aliasrules.buildmap(aliases)
1154 self._cache = {} # key: (func, data)
1154 self._cache = {} # key: (func, data)
1155
1155
1156 def _load(self, t):
1156 def _load(self, t):
1157 '''load, parse, and cache a template'''
1157 '''load, parse, and cache a template'''
1158 if t not in self._cache:
1158 if t not in self._cache:
1159 # put poison to cut recursion while compiling 't'
1159 # put poison to cut recursion while compiling 't'
1160 self._cache[t] = (_runrecursivesymbol, t)
1160 self._cache[t] = (_runrecursivesymbol, t)
1161 try:
1161 try:
1162 x = parse(self._loader(t))
1162 x = parse(self._loader(t))
1163 if self._aliasmap:
1163 if self._aliasmap:
1164 x = _aliasrules.expand(self._aliasmap, x)
1164 x = _aliasrules.expand(self._aliasmap, x)
1165 self._cache[t] = compileexp(x, self, methods)
1165 self._cache[t] = compileexp(x, self, methods)
1166 except: # re-raises
1166 except: # re-raises
1167 del self._cache[t]
1167 del self._cache[t]
1168 raise
1168 raise
1169 return self._cache[t]
1169 return self._cache[t]
1170
1170
1171 def process(self, t, mapping):
1171 def process(self, t, mapping):
1172 '''Perform expansion. t is name of map element to expand.
1172 '''Perform expansion. t is name of map element to expand.
1173 mapping contains added elements for use during expansion. Is a
1173 mapping contains added elements for use during expansion. Is a
1174 generator.'''
1174 generator.'''
1175 func, data = self._load(t)
1175 func, data = self._load(t)
1176 return _flatten(func(self, mapping, data))
1176 return _flatten(func(self, mapping, data))
1177
1177
1178 engines = {'default': engine}
1178 engines = {'default': engine}
1179
1179
1180 def stylelist():
1180 def stylelist():
1181 paths = templatepaths()
1181 paths = templatepaths()
1182 if not paths:
1182 if not paths:
1183 return _('no templates found, try `hg debuginstall` for more info')
1183 return _('no templates found, try `hg debuginstall` for more info')
1184 dirlist = os.listdir(paths[0])
1184 dirlist = os.listdir(paths[0])
1185 stylelist = []
1185 stylelist = []
1186 for file in dirlist:
1186 for file in dirlist:
1187 split = file.split(".")
1187 split = file.split(".")
1188 if split[-1] in ('orig', 'rej'):
1188 if split[-1] in ('orig', 'rej'):
1189 continue
1189 continue
1190 if split[0] == "map-cmdline":
1190 if split[0] == "map-cmdline":
1191 stylelist.append(split[1])
1191 stylelist.append(split[1])
1192 return ", ".join(sorted(stylelist))
1192 return ", ".join(sorted(stylelist))
1193
1193
1194 def _readmapfile(mapfile):
1194 def _readmapfile(mapfile):
1195 """Load template elements from the given map file"""
1195 """Load template elements from the given map file"""
1196 if not os.path.exists(mapfile):
1196 if not os.path.exists(mapfile):
1197 raise error.Abort(_("style '%s' not found") % mapfile,
1197 raise error.Abort(_("style '%s' not found") % mapfile,
1198 hint=_("available styles: %s") % stylelist())
1198 hint=_("available styles: %s") % stylelist())
1199
1199
1200 base = os.path.dirname(mapfile)
1200 base = os.path.dirname(mapfile)
1201 conf = config.config(includepaths=templatepaths())
1201 conf = config.config(includepaths=templatepaths())
1202 conf.read(mapfile)
1202 conf.read(mapfile)
1203
1203
1204 cache = {}
1204 cache = {}
1205 tmap = {}
1205 tmap = {}
1206 for key, val in conf[''].items():
1206 for key, val in conf[''].items():
1207 if not val:
1207 if not val:
1208 raise error.ParseError(_('missing value'), conf.source('', key))
1208 raise error.ParseError(_('missing value'), conf.source('', key))
1209 if val[0] in "'\"":
1209 if val[0] in "'\"":
1210 if val[0] != val[-1]:
1210 if val[0] != val[-1]:
1211 raise error.ParseError(_('unmatched quotes'),
1211 raise error.ParseError(_('unmatched quotes'),
1212 conf.source('', key))
1212 conf.source('', key))
1213 cache[key] = unquotestring(val)
1213 cache[key] = unquotestring(val)
1214 elif key == "__base__":
1214 elif key == "__base__":
1215 # treat as a pointer to a base class for this style
1215 # treat as a pointer to a base class for this style
1216 path = util.normpath(os.path.join(base, val))
1216 path = util.normpath(os.path.join(base, val))
1217
1217
1218 # fallback check in template paths
1218 # fallback check in template paths
1219 if not os.path.exists(path):
1219 if not os.path.exists(path):
1220 for p in templatepaths():
1220 for p in templatepaths():
1221 p2 = util.normpath(os.path.join(p, val))
1221 p2 = util.normpath(os.path.join(p, val))
1222 if os.path.isfile(p2):
1222 if os.path.isfile(p2):
1223 path = p2
1223 path = p2
1224 break
1224 break
1225 p3 = util.normpath(os.path.join(p2, "map"))
1225 p3 = util.normpath(os.path.join(p2, "map"))
1226 if os.path.isfile(p3):
1226 if os.path.isfile(p3):
1227 path = p3
1227 path = p3
1228 break
1228 break
1229
1229
1230 bcache, btmap = _readmapfile(path)
1230 bcache, btmap = _readmapfile(path)
1231 for k in bcache:
1231 for k in bcache:
1232 if k not in cache:
1232 if k not in cache:
1233 cache[k] = bcache[k]
1233 cache[k] = bcache[k]
1234 for k in btmap:
1234 for k in btmap:
1235 if k not in tmap:
1235 if k not in tmap:
1236 tmap[k] = btmap[k]
1236 tmap[k] = btmap[k]
1237 else:
1237 else:
1238 val = 'default', val
1238 val = 'default', val
1239 if ':' in val[1]:
1239 if ':' in val[1]:
1240 val = val[1].split(':', 1)
1240 val = val[1].split(':', 1)
1241 tmap[key] = val[0], os.path.join(base, val[1])
1241 tmap[key] = val[0], os.path.join(base, val[1])
1242 return cache, tmap
1242 return cache, tmap
1243
1243
1244 class TemplateNotFound(error.Abort):
1244 class TemplateNotFound(error.Abort):
1245 pass
1245 pass
1246
1246
1247 class templater(object):
1247 class templater(object):
1248
1248
1249 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1249 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1250 minchunk=1024, maxchunk=65536):
1250 minchunk=1024, maxchunk=65536):
1251 '''set up template engine.
1251 '''set up template engine.
1252 filters is dict of functions. each transforms a value into another.
1252 filters is dict of functions. each transforms a value into another.
1253 defaults is dict of default map definitions.
1253 defaults is dict of default map definitions.
1254 aliases is list of alias (name, replacement) pairs.
1254 aliases is list of alias (name, replacement) pairs.
1255 '''
1255 '''
1256 if filters is None:
1256 if filters is None:
1257 filters = {}
1257 filters = {}
1258 if defaults is None:
1258 if defaults is None:
1259 defaults = {}
1259 defaults = {}
1260 if cache is None:
1260 if cache is None:
1261 cache = {}
1261 cache = {}
1262 self.cache = cache.copy()
1262 self.cache = cache.copy()
1263 self.map = {}
1263 self.map = {}
1264 self.filters = templatefilters.filters.copy()
1264 self.filters = templatefilters.filters.copy()
1265 self.filters.update(filters)
1265 self.filters.update(filters)
1266 self.defaults = defaults
1266 self.defaults = defaults
1267 self._aliases = aliases
1267 self._aliases = aliases
1268 self.minchunk, self.maxchunk = minchunk, maxchunk
1268 self.minchunk, self.maxchunk = minchunk, maxchunk
1269 self.ecache = {}
1269 self.ecache = {}
1270
1270
1271 @classmethod
1271 @classmethod
1272 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1272 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1273 minchunk=1024, maxchunk=65536):
1273 minchunk=1024, maxchunk=65536):
1274 """Create templater from the specified map file"""
1274 """Create templater from the specified map file"""
1275 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1275 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1276 cache, tmap = _readmapfile(mapfile)
1276 cache, tmap = _readmapfile(mapfile)
1277 t.cache.update(cache)
1277 t.cache.update(cache)
1278 t.map = tmap
1278 t.map = tmap
1279 return t
1279 return t
1280
1280
1281 def __contains__(self, key):
1281 def __contains__(self, key):
1282 return key in self.cache or key in self.map
1282 return key in self.cache or key in self.map
1283
1283
1284 def load(self, t):
1284 def load(self, t):
1285 '''Get the template for the given template name. Use a local cache.'''
1285 '''Get the template for the given template name. Use a local cache.'''
1286 if t not in self.cache:
1286 if t not in self.cache:
1287 try:
1287 try:
1288 self.cache[t] = util.readfile(self.map[t][1])
1288 self.cache[t] = util.readfile(self.map[t][1])
1289 except KeyError as inst:
1289 except KeyError as inst:
1290 raise TemplateNotFound(_('"%s" not in template map') %
1290 raise TemplateNotFound(_('"%s" not in template map') %
1291 inst.args[0])
1291 inst.args[0])
1292 except IOError as inst:
1292 except IOError as inst:
1293 raise IOError(inst.args[0], _('template file %s: %s') %
1293 raise IOError(inst.args[0], _('template file %s: %s') %
1294 (self.map[t][1], inst.args[1]))
1294 (self.map[t][1], inst.args[1]))
1295 return self.cache[t]
1295 return self.cache[t]
1296
1296
1297 def __call__(self, t, **mapping):
1297 def __call__(self, t, **mapping):
1298 ttype = t in self.map and self.map[t][0] or 'default'
1298 ttype = t in self.map and self.map[t][0] or 'default'
1299 if ttype not in self.ecache:
1299 if ttype not in self.ecache:
1300 try:
1300 try:
1301 ecls = engines[ttype]
1301 ecls = engines[ttype]
1302 except KeyError:
1302 except KeyError:
1303 raise error.Abort(_('invalid template engine: %s') % ttype)
1303 raise error.Abort(_('invalid template engine: %s') % ttype)
1304 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1304 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1305 self._aliases)
1305 self._aliases)
1306 proc = self.ecache[ttype]
1306 proc = self.ecache[ttype]
1307
1307
1308 stream = proc.process(t, mapping)
1308 stream = proc.process(t, mapping)
1309 if self.minchunk:
1309 if self.minchunk:
1310 stream = util.increasingchunks(stream, min=self.minchunk,
1310 stream = util.increasingchunks(stream, min=self.minchunk,
1311 max=self.maxchunk)
1311 max=self.maxchunk)
1312 return stream
1312 return stream
1313
1313
1314 def templatepaths():
1314 def templatepaths():
1315 '''return locations used for template files.'''
1315 '''return locations used for template files.'''
1316 pathsrel = ['templates']
1316 pathsrel = ['templates']
1317 paths = [os.path.normpath(os.path.join(util.datapath, f))
1317 paths = [os.path.normpath(os.path.join(util.datapath, f))
1318 for f in pathsrel]
1318 for f in pathsrel]
1319 return [p for p in paths if os.path.isdir(p)]
1319 return [p for p in paths if os.path.isdir(p)]
1320
1320
1321 def templatepath(name):
1321 def templatepath(name):
1322 '''return location of template file. returns None if not found.'''
1322 '''return location of template file. returns None if not found.'''
1323 for p in templatepaths():
1323 for p in templatepaths():
1324 f = os.path.join(p, name)
1324 f = os.path.join(p, name)
1325 if os.path.exists(f):
1325 if os.path.exists(f):
1326 return f
1326 return f
1327 return None
1327 return None
1328
1328
1329 def stylemap(styles, paths=None):
1329 def stylemap(styles, paths=None):
1330 """Return path to mapfile for a given style.
1330 """Return path to mapfile for a given style.
1331
1331
1332 Searches mapfile in the following locations:
1332 Searches mapfile in the following locations:
1333 1. templatepath/style/map
1333 1. templatepath/style/map
1334 2. templatepath/map-style
1334 2. templatepath/map-style
1335 3. templatepath/map
1335 3. templatepath/map
1336 """
1336 """
1337
1337
1338 if paths is None:
1338 if paths is None:
1339 paths = templatepaths()
1339 paths = templatepaths()
1340 elif isinstance(paths, str):
1340 elif isinstance(paths, str):
1341 paths = [paths]
1341 paths = [paths]
1342
1342
1343 if isinstance(styles, str):
1343 if isinstance(styles, str):
1344 styles = [styles]
1344 styles = [styles]
1345
1345
1346 for style in styles:
1346 for style in styles:
1347 # only plain name is allowed to honor template paths
1347 # only plain name is allowed to honor template paths
1348 if (not style
1348 if (not style
1349 or style in (os.curdir, os.pardir)
1349 or style in (os.curdir, os.pardir)
1350 or pycompat.ossep in style
1350 or pycompat.ossep in style
1351 or pycompat.osaltsep and pycompat.osaltsep in style):
1351 or pycompat.osaltsep and pycompat.osaltsep in style):
1352 continue
1352 continue
1353 locations = [os.path.join(style, 'map'), 'map-' + style]
1353 locations = [os.path.join(style, 'map'), 'map-' + style]
1354 locations.append('map')
1354 locations.append('map')
1355
1355
1356 for path in paths:
1356 for path in paths:
1357 for location in locations:
1357 for location in locations:
1358 mapfile = os.path.join(path, location)
1358 mapfile = os.path.join(path, location)
1359 if os.path.isfile(mapfile):
1359 if os.path.isfile(mapfile):
1360 return style, mapfile
1360 return style, mapfile
1361
1361
1362 raise RuntimeError("No hgweb templates found in %r" % paths)
1362 raise RuntimeError("No hgweb templates found in %r" % paths)
1363
1363
1364 def loadfunction(ui, extname, registrarobj):
1364 def loadfunction(ui, extname, registrarobj):
1365 """Load template function from specified registrarobj
1365 """Load template function from specified registrarobj
1366 """
1366 """
1367 for name, func in registrarobj._table.iteritems():
1367 for name, func in registrarobj._table.iteritems():
1368 funcs[name] = func
1368 funcs[name] = func
1369
1369
1370 # tell hggettext to extract docstrings from these functions:
1370 # tell hggettext to extract docstrings from these functions:
1371 i18nfunctions = funcs.values()
1371 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now