##// END OF EJS Templates
templater: add indentation arguments to the fill function
Sean Farley -
r19228:889807c7 default
parent child Browse files
Show More
@@ -1,404 +1,405
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 import cgi, re, os, time, urllib
8 import cgi, re, os, time, urllib
9 import encoding, node, util
9 import encoding, node, util
10 import hbisect
10 import hbisect
11
11
12 def addbreaks(text):
12 def addbreaks(text):
13 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
13 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
14 every line except the last.
14 every line except the last.
15 """
15 """
16 return text.replace('\n', '<br/>\n')
16 return text.replace('\n', '<br/>\n')
17
17
18 agescales = [("year", 3600 * 24 * 365),
18 agescales = [("year", 3600 * 24 * 365),
19 ("month", 3600 * 24 * 30),
19 ("month", 3600 * 24 * 30),
20 ("week", 3600 * 24 * 7),
20 ("week", 3600 * 24 * 7),
21 ("day", 3600 * 24),
21 ("day", 3600 * 24),
22 ("hour", 3600),
22 ("hour", 3600),
23 ("minute", 60),
23 ("minute", 60),
24 ("second", 1)]
24 ("second", 1)]
25
25
26 def age(date):
26 def age(date):
27 """:age: Date. Returns a human-readable date/time difference between the
27 """:age: Date. Returns a human-readable date/time difference between the
28 given date/time and the current date/time.
28 given date/time and the current date/time.
29 """
29 """
30
30
31 def plural(t, c):
31 def plural(t, c):
32 if c == 1:
32 if c == 1:
33 return t
33 return t
34 return t + "s"
34 return t + "s"
35 def fmt(t, c):
35 def fmt(t, c):
36 return "%d %s" % (c, plural(t, c))
36 return "%d %s" % (c, plural(t, c))
37
37
38 now = time.time()
38 now = time.time()
39 then = date[0]
39 then = date[0]
40 future = False
40 future = False
41 if then > now:
41 if then > now:
42 future = True
42 future = True
43 delta = max(1, int(then - now))
43 delta = max(1, int(then - now))
44 if delta > agescales[0][1] * 30:
44 if delta > agescales[0][1] * 30:
45 return 'in the distant future'
45 return 'in the distant future'
46 else:
46 else:
47 delta = max(1, int(now - then))
47 delta = max(1, int(now - then))
48 if delta > agescales[0][1] * 2:
48 if delta > agescales[0][1] * 2:
49 return util.shortdate(date)
49 return util.shortdate(date)
50
50
51 for t, s in agescales:
51 for t, s in agescales:
52 n = delta // s
52 n = delta // s
53 if n >= 2 or s == 1:
53 if n >= 2 or s == 1:
54 if future:
54 if future:
55 return '%s from now' % fmt(t, n)
55 return '%s from now' % fmt(t, n)
56 return '%s ago' % fmt(t, n)
56 return '%s ago' % fmt(t, n)
57
57
58 def basename(path):
58 def basename(path):
59 """:basename: Any text. Treats the text as a path, and returns the last
59 """:basename: Any text. Treats the text as a path, and returns the last
60 component of the path after splitting by the path separator
60 component of the path after splitting by the path separator
61 (ignoring trailing separators). For example, "foo/bar/baz" becomes
61 (ignoring trailing separators). For example, "foo/bar/baz" becomes
62 "baz" and "foo/bar//" becomes "bar".
62 "baz" and "foo/bar//" becomes "bar".
63 """
63 """
64 return os.path.basename(path)
64 return os.path.basename(path)
65
65
66 def datefilter(text):
66 def datefilter(text):
67 """:date: Date. Returns a date in a Unix date format, including the
67 """:date: Date. Returns a date in a Unix date format, including the
68 timezone: "Mon Sep 04 15:13:13 2006 0700".
68 timezone: "Mon Sep 04 15:13:13 2006 0700".
69 """
69 """
70 return util.datestr(text)
70 return util.datestr(text)
71
71
72 def domain(author):
72 def domain(author):
73 """:domain: Any text. Finds the first string that looks like an email
73 """:domain: Any text. Finds the first string that looks like an email
74 address, and extracts just the domain component. Example: ``User
74 address, and extracts just the domain component. Example: ``User
75 <user@example.com>`` becomes ``example.com``.
75 <user@example.com>`` becomes ``example.com``.
76 """
76 """
77 f = author.find('@')
77 f = author.find('@')
78 if f == -1:
78 if f == -1:
79 return ''
79 return ''
80 author = author[f + 1:]
80 author = author[f + 1:]
81 f = author.find('>')
81 f = author.find('>')
82 if f >= 0:
82 if f >= 0:
83 author = author[:f]
83 author = author[:f]
84 return author
84 return author
85
85
86 def email(text):
86 def email(text):
87 """:email: Any text. Extracts the first string that looks like an email
87 """:email: Any text. Extracts the first string that looks like an email
88 address. Example: ``User <user@example.com>`` becomes
88 address. Example: ``User <user@example.com>`` becomes
89 ``user@example.com``.
89 ``user@example.com``.
90 """
90 """
91 return util.email(text)
91 return util.email(text)
92
92
93 def escape(text):
93 def escape(text):
94 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
94 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
95 and ">" with XML entities, and filters out NUL characters.
95 and ">" with XML entities, and filters out NUL characters.
96 """
96 """
97 return cgi.escape(text.replace('\0', ''), True)
97 return cgi.escape(text.replace('\0', ''), True)
98
98
99 para_re = None
99 para_re = None
100 space_re = None
100 space_re = None
101
101
102 def fill(text, width):
102 def fill(text, width, initindent = '', hangindent = ''):
103 '''fill many paragraphs.'''
103 '''fill many paragraphs with optional indentation.'''
104 global para_re, space_re
104 global para_re, space_re
105 if para_re is None:
105 if para_re is None:
106 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
106 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
107 space_re = re.compile(r' +')
107 space_re = re.compile(r' +')
108
108
109 def findparas():
109 def findparas():
110 start = 0
110 start = 0
111 while True:
111 while True:
112 m = para_re.search(text, start)
112 m = para_re.search(text, start)
113 if not m:
113 if not m:
114 uctext = unicode(text[start:], encoding.encoding)
114 uctext = unicode(text[start:], encoding.encoding)
115 w = len(uctext)
115 w = len(uctext)
116 while 0 < w and uctext[w - 1].isspace():
116 while 0 < w and uctext[w - 1].isspace():
117 w -= 1
117 w -= 1
118 yield (uctext[:w].encode(encoding.encoding),
118 yield (uctext[:w].encode(encoding.encoding),
119 uctext[w:].encode(encoding.encoding))
119 uctext[w:].encode(encoding.encoding))
120 break
120 break
121 yield text[start:m.start(0)], m.group(1)
121 yield text[start:m.start(0)], m.group(1)
122 start = m.end(1)
122 start = m.end(1)
123
123
124 return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest
124 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
125 width, initindent, hangindent) + rest
125 for para, rest in findparas()])
126 for para, rest in findparas()])
126
127
127 def fill68(text):
128 def fill68(text):
128 """:fill68: Any text. Wraps the text to fit in 68 columns."""
129 """:fill68: Any text. Wraps the text to fit in 68 columns."""
129 return fill(text, 68)
130 return fill(text, 68)
130
131
131 def fill76(text):
132 def fill76(text):
132 """:fill76: Any text. Wraps the text to fit in 76 columns."""
133 """:fill76: Any text. Wraps the text to fit in 76 columns."""
133 return fill(text, 76)
134 return fill(text, 76)
134
135
135 def firstline(text):
136 def firstline(text):
136 """:firstline: Any text. Returns the first line of text."""
137 """:firstline: Any text. Returns the first line of text."""
137 try:
138 try:
138 return text.splitlines(True)[0].rstrip('\r\n')
139 return text.splitlines(True)[0].rstrip('\r\n')
139 except IndexError:
140 except IndexError:
140 return ''
141 return ''
141
142
142 def hexfilter(text):
143 def hexfilter(text):
143 """:hex: Any text. Convert a binary Mercurial node identifier into
144 """:hex: Any text. Convert a binary Mercurial node identifier into
144 its long hexadecimal representation.
145 its long hexadecimal representation.
145 """
146 """
146 return node.hex(text)
147 return node.hex(text)
147
148
148 def hgdate(text):
149 def hgdate(text):
149 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
150 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
150 25200" (Unix timestamp, timezone offset).
151 25200" (Unix timestamp, timezone offset).
151 """
152 """
152 return "%d %d" % text
153 return "%d %d" % text
153
154
154 def isodate(text):
155 def isodate(text):
155 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
156 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
156 +0200".
157 +0200".
157 """
158 """
158 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
159 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
159
160
160 def isodatesec(text):
161 def isodatesec(text):
161 """:isodatesec: Date. Returns the date in ISO 8601 format, including
162 """:isodatesec: Date. Returns the date in ISO 8601 format, including
162 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
163 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
163 filter.
164 filter.
164 """
165 """
165 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
166 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
166
167
167 def indent(text, prefix):
168 def indent(text, prefix):
168 '''indent each non-empty line of text after first with prefix.'''
169 '''indent each non-empty line of text after first with prefix.'''
169 lines = text.splitlines()
170 lines = text.splitlines()
170 num_lines = len(lines)
171 num_lines = len(lines)
171 endswithnewline = text[-1:] == '\n'
172 endswithnewline = text[-1:] == '\n'
172 def indenter():
173 def indenter():
173 for i in xrange(num_lines):
174 for i in xrange(num_lines):
174 l = lines[i]
175 l = lines[i]
175 if i and l.strip():
176 if i and l.strip():
176 yield prefix
177 yield prefix
177 yield l
178 yield l
178 if i < num_lines - 1 or endswithnewline:
179 if i < num_lines - 1 or endswithnewline:
179 yield '\n'
180 yield '\n'
180 return "".join(indenter())
181 return "".join(indenter())
181
182
182 def json(obj):
183 def json(obj):
183 if obj is None or obj is False or obj is True:
184 if obj is None or obj is False or obj is True:
184 return {None: 'null', False: 'false', True: 'true'}[obj]
185 return {None: 'null', False: 'false', True: 'true'}[obj]
185 elif isinstance(obj, int) or isinstance(obj, float):
186 elif isinstance(obj, int) or isinstance(obj, float):
186 return str(obj)
187 return str(obj)
187 elif isinstance(obj, str):
188 elif isinstance(obj, str):
188 u = unicode(obj, encoding.encoding, 'replace')
189 u = unicode(obj, encoding.encoding, 'replace')
189 return '"%s"' % jsonescape(u)
190 return '"%s"' % jsonescape(u)
190 elif isinstance(obj, unicode):
191 elif isinstance(obj, unicode):
191 return '"%s"' % jsonescape(obj)
192 return '"%s"' % jsonescape(obj)
192 elif util.safehasattr(obj, 'keys'):
193 elif util.safehasattr(obj, 'keys'):
193 out = []
194 out = []
194 for k, v in obj.iteritems():
195 for k, v in obj.iteritems():
195 s = '%s: %s' % (json(k), json(v))
196 s = '%s: %s' % (json(k), json(v))
196 out.append(s)
197 out.append(s)
197 return '{' + ', '.join(out) + '}'
198 return '{' + ', '.join(out) + '}'
198 elif util.safehasattr(obj, '__iter__'):
199 elif util.safehasattr(obj, '__iter__'):
199 out = []
200 out = []
200 for i in obj:
201 for i in obj:
201 out.append(json(i))
202 out.append(json(i))
202 return '[' + ', '.join(out) + ']'
203 return '[' + ', '.join(out) + ']'
203 else:
204 else:
204 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
205 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
205
206
206 def _uescape(c):
207 def _uescape(c):
207 if ord(c) < 0x80:
208 if ord(c) < 0x80:
208 return c
209 return c
209 else:
210 else:
210 return '\\u%04x' % ord(c)
211 return '\\u%04x' % ord(c)
211
212
212 _escapes = [
213 _escapes = [
213 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
214 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
214 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
215 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
215 ]
216 ]
216
217
217 def jsonescape(s):
218 def jsonescape(s):
218 for k, v in _escapes:
219 for k, v in _escapes:
219 s = s.replace(k, v)
220 s = s.replace(k, v)
220 return ''.join(_uescape(c) for c in s)
221 return ''.join(_uescape(c) for c in s)
221
222
222 def localdate(text):
223 def localdate(text):
223 """:localdate: Date. Converts a date to local date."""
224 """:localdate: Date. Converts a date to local date."""
224 return (util.parsedate(text)[0], util.makedate()[1])
225 return (util.parsedate(text)[0], util.makedate()[1])
225
226
226 def nonempty(str):
227 def nonempty(str):
227 """:nonempty: Any text. Returns '(none)' if the string is empty."""
228 """:nonempty: Any text. Returns '(none)' if the string is empty."""
228 return str or "(none)"
229 return str or "(none)"
229
230
230 def obfuscate(text):
231 def obfuscate(text):
231 """:obfuscate: Any text. Returns the input text rendered as a sequence of
232 """:obfuscate: Any text. Returns the input text rendered as a sequence of
232 XML entities.
233 XML entities.
233 """
234 """
234 text = unicode(text, encoding.encoding, 'replace')
235 text = unicode(text, encoding.encoding, 'replace')
235 return ''.join(['&#%d;' % ord(c) for c in text])
236 return ''.join(['&#%d;' % ord(c) for c in text])
236
237
237 def permissions(flags):
238 def permissions(flags):
238 if "l" in flags:
239 if "l" in flags:
239 return "lrwxrwxrwx"
240 return "lrwxrwxrwx"
240 if "x" in flags:
241 if "x" in flags:
241 return "-rwxr-xr-x"
242 return "-rwxr-xr-x"
242 return "-rw-r--r--"
243 return "-rw-r--r--"
243
244
244 def person(author):
245 def person(author):
245 """:person: Any text. Returns the name before an email address,
246 """:person: Any text. Returns the name before an email address,
246 interpreting it as per RFC 5322.
247 interpreting it as per RFC 5322.
247
248
248 >>> person('foo@bar')
249 >>> person('foo@bar')
249 'foo'
250 'foo'
250 >>> person('Foo Bar <foo@bar>')
251 >>> person('Foo Bar <foo@bar>')
251 'Foo Bar'
252 'Foo Bar'
252 >>> person('"Foo Bar" <foo@bar>')
253 >>> person('"Foo Bar" <foo@bar>')
253 'Foo Bar'
254 'Foo Bar'
254 >>> person('"Foo \"buz\" Bar" <foo@bar>')
255 >>> person('"Foo \"buz\" Bar" <foo@bar>')
255 'Foo "buz" Bar'
256 'Foo "buz" Bar'
256 >>> # The following are invalid, but do exist in real-life
257 >>> # The following are invalid, but do exist in real-life
257 ...
258 ...
258 >>> person('Foo "buz" Bar <foo@bar>')
259 >>> person('Foo "buz" Bar <foo@bar>')
259 'Foo "buz" Bar'
260 'Foo "buz" Bar'
260 >>> person('"Foo Bar <foo@bar>')
261 >>> person('"Foo Bar <foo@bar>')
261 'Foo Bar'
262 'Foo Bar'
262 """
263 """
263 if '@' not in author:
264 if '@' not in author:
264 return author
265 return author
265 f = author.find('<')
266 f = author.find('<')
266 if f != -1:
267 if f != -1:
267 return author[:f].strip(' "').replace('\\"', '"')
268 return author[:f].strip(' "').replace('\\"', '"')
268 f = author.find('@')
269 f = author.find('@')
269 return author[:f].replace('.', ' ')
270 return author[:f].replace('.', ' ')
270
271
271 def rfc3339date(text):
272 def rfc3339date(text):
272 """:rfc3339date: Date. Returns a date using the Internet date format
273 """:rfc3339date: Date. Returns a date using the Internet date format
273 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
274 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
274 """
275 """
275 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
276 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
276
277
277 def rfc822date(text):
278 def rfc822date(text):
278 """:rfc822date: Date. Returns a date using the same format used in email
279 """:rfc822date: Date. Returns a date using the same format used in email
279 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
280 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
280 """
281 """
281 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
282 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
282
283
283 def short(text):
284 def short(text):
284 """:short: Changeset hash. Returns the short form of a changeset hash,
285 """:short: Changeset hash. Returns the short form of a changeset hash,
285 i.e. a 12 hexadecimal digit string.
286 i.e. a 12 hexadecimal digit string.
286 """
287 """
287 return text[:12]
288 return text[:12]
288
289
289 def shortbisect(text):
290 def shortbisect(text):
290 """:shortbisect: Any text. Treats `text` as a bisection status, and
291 """:shortbisect: Any text. Treats `text` as a bisection status, and
291 returns a single-character representing the status (G: good, B: bad,
292 returns a single-character representing the status (G: good, B: bad,
292 S: skipped, U: untested, I: ignored). Returns single space if `text`
293 S: skipped, U: untested, I: ignored). Returns single space if `text`
293 is not a valid bisection status.
294 is not a valid bisection status.
294 """
295 """
295 return hbisect.shortlabel(text) or ' '
296 return hbisect.shortlabel(text) or ' '
296
297
297 def shortdate(text):
298 def shortdate(text):
298 """:shortdate: Date. Returns a date like "2006-09-18"."""
299 """:shortdate: Date. Returns a date like "2006-09-18"."""
299 return util.shortdate(text)
300 return util.shortdate(text)
300
301
301 def stringescape(text):
302 def stringescape(text):
302 return text.encode('string_escape')
303 return text.encode('string_escape')
303
304
304 def stringify(thing):
305 def stringify(thing):
305 """:stringify: Any type. Turns the value into text by converting values into
306 """:stringify: Any type. Turns the value into text by converting values into
306 text and concatenating them.
307 text and concatenating them.
307 """
308 """
308 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
309 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
309 return "".join([stringify(t) for t in thing if t is not None])
310 return "".join([stringify(t) for t in thing if t is not None])
310 return str(thing)
311 return str(thing)
311
312
312 def strip(text):
313 def strip(text):
313 """:strip: Any text. Strips all leading and trailing whitespace."""
314 """:strip: Any text. Strips all leading and trailing whitespace."""
314 return text.strip()
315 return text.strip()
315
316
316 def stripdir(text):
317 def stripdir(text):
317 """:stripdir: Treat the text as path and strip a directory level, if
318 """:stripdir: Treat the text as path and strip a directory level, if
318 possible. For example, "foo" and "foo/bar" becomes "foo".
319 possible. For example, "foo" and "foo/bar" becomes "foo".
319 """
320 """
320 dir = os.path.dirname(text)
321 dir = os.path.dirname(text)
321 if dir == "":
322 if dir == "":
322 return os.path.basename(text)
323 return os.path.basename(text)
323 else:
324 else:
324 return dir
325 return dir
325
326
326 def tabindent(text):
327 def tabindent(text):
327 """:tabindent: Any text. Returns the text, with every line except the
328 """:tabindent: Any text. Returns the text, with every line except the
328 first starting with a tab character.
329 first starting with a tab character.
329 """
330 """
330 return indent(text, '\t')
331 return indent(text, '\t')
331
332
332 def urlescape(text):
333 def urlescape(text):
333 """:urlescape: Any text. Escapes all "special" characters. For example,
334 """:urlescape: Any text. Escapes all "special" characters. For example,
334 "foo bar" becomes "foo%20bar".
335 "foo bar" becomes "foo%20bar".
335 """
336 """
336 return urllib.quote(text)
337 return urllib.quote(text)
337
338
338 def userfilter(text):
339 def userfilter(text):
339 """:user: Any text. Returns a short representation of a user name or email
340 """:user: Any text. Returns a short representation of a user name or email
340 address."""
341 address."""
341 return util.shortuser(text)
342 return util.shortuser(text)
342
343
343 def emailuser(text):
344 def emailuser(text):
344 """:emailuser: Any text. Returns the user portion of an email address."""
345 """:emailuser: Any text. Returns the user portion of an email address."""
345 return util.emailuser(text)
346 return util.emailuser(text)
346
347
347 def xmlescape(text):
348 def xmlescape(text):
348 text = (text
349 text = (text
349 .replace('&', '&amp;')
350 .replace('&', '&amp;')
350 .replace('<', '&lt;')
351 .replace('<', '&lt;')
351 .replace('>', '&gt;')
352 .replace('>', '&gt;')
352 .replace('"', '&quot;')
353 .replace('"', '&quot;')
353 .replace("'", '&#39;')) # &apos; invalid in HTML
354 .replace("'", '&#39;')) # &apos; invalid in HTML
354 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
355 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
355
356
356 filters = {
357 filters = {
357 "addbreaks": addbreaks,
358 "addbreaks": addbreaks,
358 "age": age,
359 "age": age,
359 "basename": basename,
360 "basename": basename,
360 "date": datefilter,
361 "date": datefilter,
361 "domain": domain,
362 "domain": domain,
362 "email": email,
363 "email": email,
363 "escape": escape,
364 "escape": escape,
364 "fill68": fill68,
365 "fill68": fill68,
365 "fill76": fill76,
366 "fill76": fill76,
366 "firstline": firstline,
367 "firstline": firstline,
367 "hex": hexfilter,
368 "hex": hexfilter,
368 "hgdate": hgdate,
369 "hgdate": hgdate,
369 "isodate": isodate,
370 "isodate": isodate,
370 "isodatesec": isodatesec,
371 "isodatesec": isodatesec,
371 "json": json,
372 "json": json,
372 "jsonescape": jsonescape,
373 "jsonescape": jsonescape,
373 "localdate": localdate,
374 "localdate": localdate,
374 "nonempty": nonempty,
375 "nonempty": nonempty,
375 "obfuscate": obfuscate,
376 "obfuscate": obfuscate,
376 "permissions": permissions,
377 "permissions": permissions,
377 "person": person,
378 "person": person,
378 "rfc3339date": rfc3339date,
379 "rfc3339date": rfc3339date,
379 "rfc822date": rfc822date,
380 "rfc822date": rfc822date,
380 "short": short,
381 "short": short,
381 "shortbisect": shortbisect,
382 "shortbisect": shortbisect,
382 "shortdate": shortdate,
383 "shortdate": shortdate,
383 "stringescape": stringescape,
384 "stringescape": stringescape,
384 "stringify": stringify,
385 "stringify": stringify,
385 "strip": strip,
386 "strip": strip,
386 "stripdir": stripdir,
387 "stripdir": stripdir,
387 "tabindent": tabindent,
388 "tabindent": tabindent,
388 "urlescape": urlescape,
389 "urlescape": urlescape,
389 "user": userfilter,
390 "user": userfilter,
390 "emailuser": emailuser,
391 "emailuser": emailuser,
391 "xmlescape": xmlescape,
392 "xmlescape": xmlescape,
392 }
393 }
393
394
394 def websub(text, websubtable):
395 def websub(text, websubtable):
395 """:websub: Any text. Only applies to hgweb. Applies the regular
396 """:websub: Any text. Only applies to hgweb. Applies the regular
396 expression replacements defined in the websub section.
397 expression replacements defined in the websub section.
397 """
398 """
398 if websubtable:
399 if websubtable:
399 for regexp, format in websubtable:
400 for regexp, format in websubtable:
400 text = regexp.sub(format, text)
401 text = regexp.sub(format, text)
401 return text
402 return text
402
403
403 # tell hggettext to extract docstrings from these functions:
404 # tell hggettext to extract docstrings from these functions:
404 i18nfunctions = filters.values()
405 i18nfunctions = filters.values()
@@ -1,554 +1,565
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 i18n import _
8 from i18n import _
9 import sys, os, re
9 import sys, os, re
10 import util, config, templatefilters, parser, error
10 import util, config, templatefilters, parser, error
11 import types
11 import types
12 import minirst
12 import minirst
13
13
14 # template parsing
14 # template parsing
15
15
16 elements = {
16 elements = {
17 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
18 ",": (2, None, ("list", 2)),
18 ",": (2, None, ("list", 2)),
19 "|": (5, None, ("|", 5)),
19 "|": (5, None, ("|", 5)),
20 "%": (6, None, ("%", 6)),
20 "%": (6, None, ("%", 6)),
21 ")": (0, None, None),
21 ")": (0, None, None),
22 "symbol": (0, ("symbol",), None),
22 "symbol": (0, ("symbol",), None),
23 "string": (0, ("string",), None),
23 "string": (0, ("string",), None),
24 "end": (0, None, None),
24 "end": (0, None, None),
25 }
25 }
26
26
27 def tokenizer(data):
27 def tokenizer(data):
28 program, start, end = data
28 program, start, end = data
29 pos = start
29 pos = start
30 while pos < end:
30 while pos < end:
31 c = program[pos]
31 c = program[pos]
32 if c.isspace(): # skip inter-token whitespace
32 if c.isspace(): # skip inter-token whitespace
33 pass
33 pass
34 elif c in "(,)%|": # handle simple operators
34 elif c in "(,)%|": # handle simple operators
35 yield (c, None, pos)
35 yield (c, None, pos)
36 elif (c in '"\'' or c == 'r' and
36 elif (c in '"\'' or c == 'r' and
37 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
37 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
38 if c == 'r':
38 if c == 'r':
39 pos += 1
39 pos += 1
40 c = program[pos]
40 c = program[pos]
41 decode = False
41 decode = False
42 else:
42 else:
43 decode = True
43 decode = True
44 pos += 1
44 pos += 1
45 s = pos
45 s = pos
46 while pos < end: # find closing quote
46 while pos < end: # find closing quote
47 d = program[pos]
47 d = program[pos]
48 if decode and d == '\\': # skip over escaped characters
48 if decode and d == '\\': # skip over escaped characters
49 pos += 2
49 pos += 2
50 continue
50 continue
51 if d == c:
51 if d == c:
52 if not decode:
52 if not decode:
53 yield ('string', program[s:pos].replace('\\', r'\\'), s)
53 yield ('string', program[s:pos].replace('\\', r'\\'), s)
54 break
54 break
55 yield ('string', program[s:pos].decode('string-escape'), s)
55 yield ('string', program[s:pos].decode('string-escape'), s)
56 break
56 break
57 pos += 1
57 pos += 1
58 else:
58 else:
59 raise error.ParseError(_("unterminated string"), s)
59 raise error.ParseError(_("unterminated string"), s)
60 elif c.isalnum() or c in '_':
60 elif c.isalnum() or c in '_':
61 s = pos
61 s = pos
62 pos += 1
62 pos += 1
63 while pos < end: # find end of symbol
63 while pos < end: # find end of symbol
64 d = program[pos]
64 d = program[pos]
65 if not (d.isalnum() or d == "_"):
65 if not (d.isalnum() or d == "_"):
66 break
66 break
67 pos += 1
67 pos += 1
68 sym = program[s:pos]
68 sym = program[s:pos]
69 yield ('symbol', sym, s)
69 yield ('symbol', sym, s)
70 pos -= 1
70 pos -= 1
71 elif c == '}':
71 elif c == '}':
72 pos += 1
72 pos += 1
73 break
73 break
74 else:
74 else:
75 raise error.ParseError(_("syntax error"), pos)
75 raise error.ParseError(_("syntax error"), pos)
76 pos += 1
76 pos += 1
77 yield ('end', None, pos)
77 yield ('end', None, pos)
78
78
79 def compiletemplate(tmpl, context):
79 def compiletemplate(tmpl, context):
80 parsed = []
80 parsed = []
81 pos, stop = 0, len(tmpl)
81 pos, stop = 0, len(tmpl)
82 p = parser.parser(tokenizer, elements)
82 p = parser.parser(tokenizer, elements)
83
83
84 while pos < stop:
84 while pos < stop:
85 n = tmpl.find('{', pos)
85 n = tmpl.find('{', pos)
86 if n < 0:
86 if n < 0:
87 parsed.append(("string", tmpl[pos:]))
87 parsed.append(("string", tmpl[pos:]))
88 break
88 break
89 if n > 0 and tmpl[n - 1] == '\\':
89 if n > 0 and tmpl[n - 1] == '\\':
90 # escaped
90 # escaped
91 parsed.append(("string", tmpl[pos:n - 1] + "{"))
91 parsed.append(("string", tmpl[pos:n - 1] + "{"))
92 pos = n + 1
92 pos = n + 1
93 continue
93 continue
94 if n > pos:
94 if n > pos:
95 parsed.append(("string", tmpl[pos:n]))
95 parsed.append(("string", tmpl[pos:n]))
96
96
97 pd = [tmpl, n + 1, stop]
97 pd = [tmpl, n + 1, stop]
98 parseres, pos = p.parse(pd)
98 parseres, pos = p.parse(pd)
99 parsed.append(parseres)
99 parsed.append(parseres)
100
100
101 return [compileexp(e, context) for e in parsed]
101 return [compileexp(e, context) for e in parsed]
102
102
103 def compileexp(exp, context):
103 def compileexp(exp, context):
104 t = exp[0]
104 t = exp[0]
105 if t in methods:
105 if t in methods:
106 return methods[t](exp, context)
106 return methods[t](exp, context)
107 raise error.ParseError(_("unknown method '%s'") % t)
107 raise error.ParseError(_("unknown method '%s'") % t)
108
108
109 # template evaluation
109 # template evaluation
110
110
111 def getsymbol(exp):
111 def getsymbol(exp):
112 if exp[0] == 'symbol':
112 if exp[0] == 'symbol':
113 return exp[1]
113 return exp[1]
114 raise error.ParseError(_("expected a symbol"))
114 raise error.ParseError(_("expected a symbol"))
115
115
116 def getlist(x):
116 def getlist(x):
117 if not x:
117 if not x:
118 return []
118 return []
119 if x[0] == 'list':
119 if x[0] == 'list':
120 return getlist(x[1]) + [x[2]]
120 return getlist(x[1]) + [x[2]]
121 return [x]
121 return [x]
122
122
123 def getfilter(exp, context):
123 def getfilter(exp, context):
124 f = getsymbol(exp)
124 f = getsymbol(exp)
125 if f not in context._filters:
125 if f not in context._filters:
126 raise error.ParseError(_("unknown function '%s'") % f)
126 raise error.ParseError(_("unknown function '%s'") % f)
127 return context._filters[f]
127 return context._filters[f]
128
128
129 def gettemplate(exp, context):
129 def gettemplate(exp, context):
130 if exp[0] == 'string':
130 if exp[0] == 'string':
131 return compiletemplate(exp[1], context)
131 return compiletemplate(exp[1], context)
132 if exp[0] == 'symbol':
132 if exp[0] == 'symbol':
133 return context._load(exp[1])
133 return context._load(exp[1])
134 raise error.ParseError(_("expected template specifier"))
134 raise error.ParseError(_("expected template specifier"))
135
135
136 def runstring(context, mapping, data):
136 def runstring(context, mapping, data):
137 return data
137 return data
138
138
139 def runsymbol(context, mapping, key):
139 def runsymbol(context, mapping, key):
140 v = mapping.get(key)
140 v = mapping.get(key)
141 if v is None:
141 if v is None:
142 v = context._defaults.get(key, '')
142 v = context._defaults.get(key, '')
143 if util.safehasattr(v, '__call__'):
143 if util.safehasattr(v, '__call__'):
144 return v(**mapping)
144 return v(**mapping)
145 if isinstance(v, types.GeneratorType):
145 if isinstance(v, types.GeneratorType):
146 v = list(v)
146 v = list(v)
147 mapping[key] = v
147 mapping[key] = v
148 return v
148 return v
149 return v
149 return v
150
150
151 def buildfilter(exp, context):
151 def buildfilter(exp, context):
152 func, data = compileexp(exp[1], context)
152 func, data = compileexp(exp[1], context)
153 filt = getfilter(exp[2], context)
153 filt = getfilter(exp[2], context)
154 return (runfilter, (func, data, filt))
154 return (runfilter, (func, data, filt))
155
155
156 def runfilter(context, mapping, data):
156 def runfilter(context, mapping, data):
157 func, data, filt = data
157 func, data, filt = data
158 try:
158 try:
159 return filt(func(context, mapping, data))
159 return filt(func(context, mapping, data))
160 except (ValueError, AttributeError, TypeError):
160 except (ValueError, AttributeError, TypeError):
161 if isinstance(data, tuple):
161 if isinstance(data, tuple):
162 dt = data[1]
162 dt = data[1]
163 else:
163 else:
164 dt = data
164 dt = data
165 raise util.Abort(_("template filter '%s' is not compatible with "
165 raise util.Abort(_("template filter '%s' is not compatible with "
166 "keyword '%s'") % (filt.func_name, dt))
166 "keyword '%s'") % (filt.func_name, dt))
167
167
168 def buildmap(exp, context):
168 def buildmap(exp, context):
169 func, data = compileexp(exp[1], context)
169 func, data = compileexp(exp[1], context)
170 ctmpl = gettemplate(exp[2], context)
170 ctmpl = gettemplate(exp[2], context)
171 return (runmap, (func, data, ctmpl))
171 return (runmap, (func, data, ctmpl))
172
172
173 def runtemplate(context, mapping, template):
173 def runtemplate(context, mapping, template):
174 for func, data in template:
174 for func, data in template:
175 yield func(context, mapping, data)
175 yield func(context, mapping, data)
176
176
177 def runmap(context, mapping, data):
177 def runmap(context, mapping, data):
178 func, data, ctmpl = data
178 func, data, ctmpl = data
179 d = func(context, mapping, data)
179 d = func(context, mapping, data)
180 if util.safehasattr(d, '__call__'):
180 if util.safehasattr(d, '__call__'):
181 d = d()
181 d = d()
182
182
183 lm = mapping.copy()
183 lm = mapping.copy()
184
184
185 for i in d:
185 for i in d:
186 if isinstance(i, dict):
186 if isinstance(i, dict):
187 lm.update(i)
187 lm.update(i)
188 lm['originalnode'] = mapping.get('node')
188 lm['originalnode'] = mapping.get('node')
189 yield runtemplate(context, lm, ctmpl)
189 yield runtemplate(context, lm, ctmpl)
190 else:
190 else:
191 # v is not an iterable of dicts, this happen when 'key'
191 # v is not an iterable of dicts, this happen when 'key'
192 # has been fully expanded already and format is useless.
192 # has been fully expanded already and format is useless.
193 # If so, return the expanded value.
193 # If so, return the expanded value.
194 yield i
194 yield i
195
195
196 def buildfunc(exp, context):
196 def buildfunc(exp, context):
197 n = getsymbol(exp[1])
197 n = getsymbol(exp[1])
198 args = [compileexp(x, context) for x in getlist(exp[2])]
198 args = [compileexp(x, context) for x in getlist(exp[2])]
199 if n in funcs:
199 if n in funcs:
200 f = funcs[n]
200 f = funcs[n]
201 return (f, args)
201 return (f, args)
202 if n in context._filters:
202 if n in context._filters:
203 if len(args) != 1:
203 if len(args) != 1:
204 raise error.ParseError(_("filter %s expects one argument") % n)
204 raise error.ParseError(_("filter %s expects one argument") % n)
205 f = context._filters[n]
205 f = context._filters[n]
206 return (runfilter, (args[0][0], args[0][1], f))
206 return (runfilter, (args[0][0], args[0][1], f))
207
207
208 def get(context, mapping, args):
208 def get(context, mapping, args):
209 if len(args) != 2:
209 if len(args) != 2:
210 # i18n: "get" is a keyword
210 # i18n: "get" is a keyword
211 raise error.ParseError(_("get() expects two arguments"))
211 raise error.ParseError(_("get() expects two arguments"))
212
212
213 dictarg = args[0][0](context, mapping, args[0][1])
213 dictarg = args[0][0](context, mapping, args[0][1])
214 if not util.safehasattr(dictarg, 'get'):
214 if not util.safehasattr(dictarg, 'get'):
215 # i18n: "get" is a keyword
215 # i18n: "get" is a keyword
216 raise error.ParseError(_("get() expects a dict as first argument"))
216 raise error.ParseError(_("get() expects a dict as first argument"))
217
217
218 key = args[1][0](context, mapping, args[1][1])
218 key = args[1][0](context, mapping, args[1][1])
219 yield dictarg.get(key)
219 yield dictarg.get(key)
220
220
221 def join(context, mapping, args):
221 def join(context, mapping, args):
222 if not (1 <= len(args) <= 2):
222 if not (1 <= len(args) <= 2):
223 # i18n: "join" is a keyword
223 # i18n: "join" is a keyword
224 raise error.ParseError(_("join expects one or two arguments"))
224 raise error.ParseError(_("join expects one or two arguments"))
225
225
226 joinset = args[0][0](context, mapping, args[0][1])
226 joinset = args[0][0](context, mapping, args[0][1])
227 if util.safehasattr(joinset, '__call__'):
227 if util.safehasattr(joinset, '__call__'):
228 jf = joinset.joinfmt
228 jf = joinset.joinfmt
229 joinset = [jf(x) for x in joinset()]
229 joinset = [jf(x) for x in joinset()]
230
230
231 joiner = " "
231 joiner = " "
232 if len(args) > 1:
232 if len(args) > 1:
233 joiner = args[1][0](context, mapping, args[1][1])
233 joiner = args[1][0](context, mapping, args[1][1])
234
234
235 first = True
235 first = True
236 for x in joinset:
236 for x in joinset:
237 if first:
237 if first:
238 first = False
238 first = False
239 else:
239 else:
240 yield joiner
240 yield joiner
241 yield x
241 yield x
242
242
243 def sub(context, mapping, args):
243 def sub(context, mapping, args):
244 if len(args) != 3:
244 if len(args) != 3:
245 # i18n: "sub" is a keyword
245 # i18n: "sub" is a keyword
246 raise error.ParseError(_("sub expects three arguments"))
246 raise error.ParseError(_("sub expects three arguments"))
247
247
248 pat = stringify(args[0][0](context, mapping, args[0][1]))
248 pat = stringify(args[0][0](context, mapping, args[0][1]))
249 rpl = stringify(args[1][0](context, mapping, args[1][1]))
249 rpl = stringify(args[1][0](context, mapping, args[1][1]))
250 src = stringify(args[2][0](context, mapping, args[2][1]))
250 src = stringify(args[2][0](context, mapping, args[2][1]))
251 src = stringify(runtemplate(context, mapping,
251 src = stringify(runtemplate(context, mapping,
252 compiletemplate(src, context)))
252 compiletemplate(src, context)))
253 yield re.sub(pat, rpl, src)
253 yield re.sub(pat, rpl, src)
254
254
255 def if_(context, mapping, args):
255 def if_(context, mapping, args):
256 if not (2 <= len(args) <= 3):
256 if not (2 <= len(args) <= 3):
257 # i18n: "if" is a keyword
257 # i18n: "if" is a keyword
258 raise error.ParseError(_("if expects two or three arguments"))
258 raise error.ParseError(_("if expects two or three arguments"))
259
259
260 test = stringify(args[0][0](context, mapping, args[0][1]))
260 test = stringify(args[0][0](context, mapping, args[0][1]))
261 if test:
261 if test:
262 t = stringify(args[1][0](context, mapping, args[1][1]))
262 t = stringify(args[1][0](context, mapping, args[1][1]))
263 yield runtemplate(context, mapping, compiletemplate(t, context))
263 yield runtemplate(context, mapping, compiletemplate(t, context))
264 elif len(args) == 3:
264 elif len(args) == 3:
265 t = stringify(args[2][0](context, mapping, args[2][1]))
265 t = stringify(args[2][0](context, mapping, args[2][1]))
266 yield runtemplate(context, mapping, compiletemplate(t, context))
266 yield runtemplate(context, mapping, compiletemplate(t, context))
267
267
268 def ifeq(context, mapping, args):
268 def ifeq(context, mapping, args):
269 if not (3 <= len(args) <= 4):
269 if not (3 <= len(args) <= 4):
270 # i18n: "ifeq" is a keyword
270 # i18n: "ifeq" is a keyword
271 raise error.ParseError(_("ifeq expects three or four arguments"))
271 raise error.ParseError(_("ifeq expects three or four arguments"))
272
272
273 test = stringify(args[0][0](context, mapping, args[0][1]))
273 test = stringify(args[0][0](context, mapping, args[0][1]))
274 match = stringify(args[1][0](context, mapping, args[1][1]))
274 match = stringify(args[1][0](context, mapping, args[1][1]))
275 if test == match:
275 if test == match:
276 t = stringify(args[2][0](context, mapping, args[2][1]))
276 t = stringify(args[2][0](context, mapping, args[2][1]))
277 yield runtemplate(context, mapping, compiletemplate(t, context))
277 yield runtemplate(context, mapping, compiletemplate(t, context))
278 elif len(args) == 4:
278 elif len(args) == 4:
279 t = stringify(args[3][0](context, mapping, args[3][1]))
279 t = stringify(args[3][0](context, mapping, args[3][1]))
280 yield runtemplate(context, mapping, compiletemplate(t, context))
280 yield runtemplate(context, mapping, compiletemplate(t, context))
281
281
282 def label(context, mapping, args):
282 def label(context, mapping, args):
283 if len(args) != 2:
283 if len(args) != 2:
284 # i18n: "label" is a keyword
284 # i18n: "label" is a keyword
285 raise error.ParseError(_("label expects two arguments"))
285 raise error.ParseError(_("label expects two arguments"))
286
286
287 # ignore args[0] (the label string) since this is supposed to be a a no-op
287 # ignore args[0] (the label string) since this is supposed to be a a no-op
288 t = stringify(args[1][0](context, mapping, args[1][1]))
288 t = stringify(args[1][0](context, mapping, args[1][1]))
289 yield runtemplate(context, mapping, compiletemplate(t, context))
289 yield runtemplate(context, mapping, compiletemplate(t, context))
290
290
291 def rstdoc(context, mapping, args):
291 def rstdoc(context, mapping, args):
292 if len(args) != 2:
292 if len(args) != 2:
293 # i18n: "rstdoc" is a keyword
293 # i18n: "rstdoc" is a keyword
294 raise error.ParseError(_("rstdoc expects two arguments"))
294 raise error.ParseError(_("rstdoc expects two arguments"))
295
295
296 text = stringify(args[0][0](context, mapping, args[0][1]))
296 text = stringify(args[0][0](context, mapping, args[0][1]))
297 style = stringify(args[1][0](context, mapping, args[1][1]))
297 style = stringify(args[1][0](context, mapping, args[1][1]))
298
298
299 return minirst.format(text, style=style, keep=['verbose'])
299 return minirst.format(text, style=style, keep=['verbose'])
300
300
301 def fill(context, mapping, args):
301 def fill(context, mapping, args):
302 if not (1 <= len(args) <= 2):
302 if not (1 <= len(args) <= 4):
303 raise error.ParseError(_("fill expects one or two arguments"))
303 raise error.ParseError(_("fill expects one to four arguments"))
304
304
305 text = stringify(args[0][0](context, mapping, args[0][1]))
305 text = stringify(args[0][0](context, mapping, args[0][1]))
306 width = 76
306 width = 76
307 if len(args) == 2:
307 initindent = ''
308 hangindent = ''
309 if 2 <= len(args) <= 4:
308 try:
310 try:
309 width = int(stringify(args[1][0](context, mapping, args[1][1])))
311 width = int(stringify(args[1][0](context, mapping, args[1][1])))
310 except ValueError:
312 except ValueError:
311 raise error.ParseError(_("fill expects an integer width"))
313 raise error.ParseError(_("fill expects an integer width"))
314 try:
315 initindent = stringify(args[2][0](context, mapping, args[2][1]))
316 initindent = stringify(runtemplate(context, mapping,
317 compiletemplate(initindent, context)))
318 hangindent = stringify(args[3][0](context, mapping, args[3][1]))
319 hangindent = stringify(runtemplate(context, mapping,
320 compiletemplate(hangindent, context)))
321 except IndexError:
322 pass
312
323
313 return templatefilters.fill(text, width)
324 return templatefilters.fill(text, width, initindent, hangindent)
314
325
315 def date(context, mapping, args):
326 def date(context, mapping, args):
316 if not (1 <= len(args) <= 2):
327 if not (1 <= len(args) <= 2):
317 raise error.ParseError(_("date expects one or two arguments"))
328 raise error.ParseError(_("date expects one or two arguments"))
318
329
319 date = args[0][0](context, mapping, args[0][1])
330 date = args[0][0](context, mapping, args[0][1])
320 if len(args) == 2:
331 if len(args) == 2:
321 fmt = stringify(args[1][0](context, mapping, args[1][1]))
332 fmt = stringify(args[1][0](context, mapping, args[1][1]))
322 return util.datestr(date, fmt)
333 return util.datestr(date, fmt)
323 return util.datestr(date)
334 return util.datestr(date)
324
335
325 methods = {
336 methods = {
326 "string": lambda e, c: (runstring, e[1]),
337 "string": lambda e, c: (runstring, e[1]),
327 "symbol": lambda e, c: (runsymbol, e[1]),
338 "symbol": lambda e, c: (runsymbol, e[1]),
328 "group": lambda e, c: compileexp(e[1], c),
339 "group": lambda e, c: compileexp(e[1], c),
329 # ".": buildmember,
340 # ".": buildmember,
330 "|": buildfilter,
341 "|": buildfilter,
331 "%": buildmap,
342 "%": buildmap,
332 "func": buildfunc,
343 "func": buildfunc,
333 }
344 }
334
345
335 funcs = {
346 funcs = {
336 "get": get,
347 "get": get,
337 "if": if_,
348 "if": if_,
338 "ifeq": ifeq,
349 "ifeq": ifeq,
339 "join": join,
350 "join": join,
340 "label": label,
351 "label": label,
341 "rstdoc": rstdoc,
352 "rstdoc": rstdoc,
342 "sub": sub,
353 "sub": sub,
343 "fill": fill,
354 "fill": fill,
344 "date": date,
355 "date": date,
345 }
356 }
346
357
347 # template engine
358 # template engine
348
359
349 path = ['templates', '../templates']
360 path = ['templates', '../templates']
350 stringify = templatefilters.stringify
361 stringify = templatefilters.stringify
351
362
352 def _flatten(thing):
363 def _flatten(thing):
353 '''yield a single stream from a possibly nested set of iterators'''
364 '''yield a single stream from a possibly nested set of iterators'''
354 if isinstance(thing, str):
365 if isinstance(thing, str):
355 yield thing
366 yield thing
356 elif not util.safehasattr(thing, '__iter__'):
367 elif not util.safehasattr(thing, '__iter__'):
357 if thing is not None:
368 if thing is not None:
358 yield str(thing)
369 yield str(thing)
359 else:
370 else:
360 for i in thing:
371 for i in thing:
361 if isinstance(i, str):
372 if isinstance(i, str):
362 yield i
373 yield i
363 elif not util.safehasattr(i, '__iter__'):
374 elif not util.safehasattr(i, '__iter__'):
364 if i is not None:
375 if i is not None:
365 yield str(i)
376 yield str(i)
366 elif i is not None:
377 elif i is not None:
367 for j in _flatten(i):
378 for j in _flatten(i):
368 yield j
379 yield j
369
380
370 def parsestring(s, quoted=True):
381 def parsestring(s, quoted=True):
371 '''parse a string using simple c-like syntax.
382 '''parse a string using simple c-like syntax.
372 string must be in quotes if quoted is True.'''
383 string must be in quotes if quoted is True.'''
373 if quoted:
384 if quoted:
374 if len(s) < 2 or s[0] != s[-1]:
385 if len(s) < 2 or s[0] != s[-1]:
375 raise SyntaxError(_('unmatched quotes'))
386 raise SyntaxError(_('unmatched quotes'))
376 return s[1:-1].decode('string_escape')
387 return s[1:-1].decode('string_escape')
377
388
378 return s.decode('string_escape')
389 return s.decode('string_escape')
379
390
380 class engine(object):
391 class engine(object):
381 '''template expansion engine.
392 '''template expansion engine.
382
393
383 template expansion works like this. a map file contains key=value
394 template expansion works like this. a map file contains key=value
384 pairs. if value is quoted, it is treated as string. otherwise, it
395 pairs. if value is quoted, it is treated as string. otherwise, it
385 is treated as name of template file.
396 is treated as name of template file.
386
397
387 templater is asked to expand a key in map. it looks up key, and
398 templater is asked to expand a key in map. it looks up key, and
388 looks for strings like this: {foo}. it expands {foo} by looking up
399 looks for strings like this: {foo}. it expands {foo} by looking up
389 foo in map, and substituting it. expansion is recursive: it stops
400 foo in map, and substituting it. expansion is recursive: it stops
390 when there is no more {foo} to replace.
401 when there is no more {foo} to replace.
391
402
392 expansion also allows formatting and filtering.
403 expansion also allows formatting and filtering.
393
404
394 format uses key to expand each item in list. syntax is
405 format uses key to expand each item in list. syntax is
395 {key%format}.
406 {key%format}.
396
407
397 filter uses function to transform value. syntax is
408 filter uses function to transform value. syntax is
398 {key|filter1|filter2|...}.'''
409 {key|filter1|filter2|...}.'''
399
410
400 def __init__(self, loader, filters={}, defaults={}):
411 def __init__(self, loader, filters={}, defaults={}):
401 self._loader = loader
412 self._loader = loader
402 self._filters = filters
413 self._filters = filters
403 self._defaults = defaults
414 self._defaults = defaults
404 self._cache = {}
415 self._cache = {}
405
416
406 def _load(self, t):
417 def _load(self, t):
407 '''load, parse, and cache a template'''
418 '''load, parse, and cache a template'''
408 if t not in self._cache:
419 if t not in self._cache:
409 self._cache[t] = compiletemplate(self._loader(t), self)
420 self._cache[t] = compiletemplate(self._loader(t), self)
410 return self._cache[t]
421 return self._cache[t]
411
422
412 def process(self, t, mapping):
423 def process(self, t, mapping):
413 '''Perform expansion. t is name of map element to expand.
424 '''Perform expansion. t is name of map element to expand.
414 mapping contains added elements for use during expansion. Is a
425 mapping contains added elements for use during expansion. Is a
415 generator.'''
426 generator.'''
416 return _flatten(runtemplate(self, mapping, self._load(t)))
427 return _flatten(runtemplate(self, mapping, self._load(t)))
417
428
418 engines = {'default': engine}
429 engines = {'default': engine}
419
430
420 def stylelist():
431 def stylelist():
421 path = templatepath()[0]
432 path = templatepath()[0]
422 dirlist = os.listdir(path)
433 dirlist = os.listdir(path)
423 stylelist = []
434 stylelist = []
424 for file in dirlist:
435 for file in dirlist:
425 split = file.split(".")
436 split = file.split(".")
426 if split[0] == "map-cmdline":
437 if split[0] == "map-cmdline":
427 stylelist.append(split[1])
438 stylelist.append(split[1])
428 return ", ".join(sorted(stylelist))
439 return ", ".join(sorted(stylelist))
429
440
430 class templater(object):
441 class templater(object):
431
442
432 def __init__(self, mapfile, filters={}, defaults={}, cache={},
443 def __init__(self, mapfile, filters={}, defaults={}, cache={},
433 minchunk=1024, maxchunk=65536):
444 minchunk=1024, maxchunk=65536):
434 '''set up template engine.
445 '''set up template engine.
435 mapfile is name of file to read map definitions from.
446 mapfile is name of file to read map definitions from.
436 filters is dict of functions. each transforms a value into another.
447 filters is dict of functions. each transforms a value into another.
437 defaults is dict of default map definitions.'''
448 defaults is dict of default map definitions.'''
438 self.mapfile = mapfile or 'template'
449 self.mapfile = mapfile or 'template'
439 self.cache = cache.copy()
450 self.cache = cache.copy()
440 self.map = {}
451 self.map = {}
441 self.base = (mapfile and os.path.dirname(mapfile)) or ''
452 self.base = (mapfile and os.path.dirname(mapfile)) or ''
442 self.filters = templatefilters.filters.copy()
453 self.filters = templatefilters.filters.copy()
443 self.filters.update(filters)
454 self.filters.update(filters)
444 self.defaults = defaults
455 self.defaults = defaults
445 self.minchunk, self.maxchunk = minchunk, maxchunk
456 self.minchunk, self.maxchunk = minchunk, maxchunk
446 self.ecache = {}
457 self.ecache = {}
447
458
448 if not mapfile:
459 if not mapfile:
449 return
460 return
450 if not os.path.exists(mapfile):
461 if not os.path.exists(mapfile):
451 raise util.Abort(_("style '%s' not found") % mapfile,
462 raise util.Abort(_("style '%s' not found") % mapfile,
452 hint=_("available styles: %s") % stylelist())
463 hint=_("available styles: %s") % stylelist())
453
464
454 conf = config.config()
465 conf = config.config()
455 conf.read(mapfile)
466 conf.read(mapfile)
456
467
457 for key, val in conf[''].items():
468 for key, val in conf[''].items():
458 if not val:
469 if not val:
459 raise SyntaxError(_('%s: missing value') % conf.source('', key))
470 raise SyntaxError(_('%s: missing value') % conf.source('', key))
460 if val[0] in "'\"":
471 if val[0] in "'\"":
461 try:
472 try:
462 self.cache[key] = parsestring(val)
473 self.cache[key] = parsestring(val)
463 except SyntaxError, inst:
474 except SyntaxError, inst:
464 raise SyntaxError('%s: %s' %
475 raise SyntaxError('%s: %s' %
465 (conf.source('', key), inst.args[0]))
476 (conf.source('', key), inst.args[0]))
466 else:
477 else:
467 val = 'default', val
478 val = 'default', val
468 if ':' in val[1]:
479 if ':' in val[1]:
469 val = val[1].split(':', 1)
480 val = val[1].split(':', 1)
470 self.map[key] = val[0], os.path.join(self.base, val[1])
481 self.map[key] = val[0], os.path.join(self.base, val[1])
471
482
472 def __contains__(self, key):
483 def __contains__(self, key):
473 return key in self.cache or key in self.map
484 return key in self.cache or key in self.map
474
485
475 def load(self, t):
486 def load(self, t):
476 '''Get the template for the given template name. Use a local cache.'''
487 '''Get the template for the given template name. Use a local cache.'''
477 if t not in self.cache:
488 if t not in self.cache:
478 try:
489 try:
479 self.cache[t] = util.readfile(self.map[t][1])
490 self.cache[t] = util.readfile(self.map[t][1])
480 except KeyError, inst:
491 except KeyError, inst:
481 raise util.Abort(_('"%s" not in template map') % inst.args[0])
492 raise util.Abort(_('"%s" not in template map') % inst.args[0])
482 except IOError, inst:
493 except IOError, inst:
483 raise IOError(inst.args[0], _('template file %s: %s') %
494 raise IOError(inst.args[0], _('template file %s: %s') %
484 (self.map[t][1], inst.args[1]))
495 (self.map[t][1], inst.args[1]))
485 return self.cache[t]
496 return self.cache[t]
486
497
487 def __call__(self, t, **mapping):
498 def __call__(self, t, **mapping):
488 ttype = t in self.map and self.map[t][0] or 'default'
499 ttype = t in self.map and self.map[t][0] or 'default'
489 if ttype not in self.ecache:
500 if ttype not in self.ecache:
490 self.ecache[ttype] = engines[ttype](self.load,
501 self.ecache[ttype] = engines[ttype](self.load,
491 self.filters, self.defaults)
502 self.filters, self.defaults)
492 proc = self.ecache[ttype]
503 proc = self.ecache[ttype]
493
504
494 stream = proc.process(t, mapping)
505 stream = proc.process(t, mapping)
495 if self.minchunk:
506 if self.minchunk:
496 stream = util.increasingchunks(stream, min=self.minchunk,
507 stream = util.increasingchunks(stream, min=self.minchunk,
497 max=self.maxchunk)
508 max=self.maxchunk)
498 return stream
509 return stream
499
510
500 def templatepath(name=None):
511 def templatepath(name=None):
501 '''return location of template file or directory (if no name).
512 '''return location of template file or directory (if no name).
502 returns None if not found.'''
513 returns None if not found.'''
503 normpaths = []
514 normpaths = []
504
515
505 # executable version (py2exe) doesn't support __file__
516 # executable version (py2exe) doesn't support __file__
506 if util.mainfrozen():
517 if util.mainfrozen():
507 module = sys.executable
518 module = sys.executable
508 else:
519 else:
509 module = __file__
520 module = __file__
510 for f in path:
521 for f in path:
511 if f.startswith('/'):
522 if f.startswith('/'):
512 p = f
523 p = f
513 else:
524 else:
514 fl = f.split('/')
525 fl = f.split('/')
515 p = os.path.join(os.path.dirname(module), *fl)
526 p = os.path.join(os.path.dirname(module), *fl)
516 if name:
527 if name:
517 p = os.path.join(p, name)
528 p = os.path.join(p, name)
518 if name and os.path.exists(p):
529 if name and os.path.exists(p):
519 return os.path.normpath(p)
530 return os.path.normpath(p)
520 elif os.path.isdir(p):
531 elif os.path.isdir(p):
521 normpaths.append(os.path.normpath(p))
532 normpaths.append(os.path.normpath(p))
522
533
523 return normpaths
534 return normpaths
524
535
525 def stylemap(styles, paths=None):
536 def stylemap(styles, paths=None):
526 """Return path to mapfile for a given style.
537 """Return path to mapfile for a given style.
527
538
528 Searches mapfile in the following locations:
539 Searches mapfile in the following locations:
529 1. templatepath/style/map
540 1. templatepath/style/map
530 2. templatepath/map-style
541 2. templatepath/map-style
531 3. templatepath/map
542 3. templatepath/map
532 """
543 """
533
544
534 if paths is None:
545 if paths is None:
535 paths = templatepath()
546 paths = templatepath()
536 elif isinstance(paths, str):
547 elif isinstance(paths, str):
537 paths = [paths]
548 paths = [paths]
538
549
539 if isinstance(styles, str):
550 if isinstance(styles, str):
540 styles = [styles]
551 styles = [styles]
541
552
542 for style in styles:
553 for style in styles:
543 if not style:
554 if not style:
544 continue
555 continue
545 locations = [os.path.join(style, 'map'), 'map-' + style]
556 locations = [os.path.join(style, 'map'), 'map-' + style]
546 locations.append('map')
557 locations.append('map')
547
558
548 for path in paths:
559 for path in paths:
549 for location in locations:
560 for location in locations:
550 mapfile = os.path.join(path, location)
561 mapfile = os.path.join(path, location)
551 if os.path.isfile(mapfile):
562 if os.path.isfile(mapfile):
552 return style, mapfile
563 return style, mapfile
553
564
554 raise RuntimeError("No hgweb templates found in %r" % paths)
565 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now