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