##// END OF EJS Templates
uescape: also encode non-printable char under 128...
Pierre-Yves David -
r26843:f580c78e default
parent child Browse files
Show More
@@ -1,435 +1,435 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 import urllib
14 import urllib
15
15
16 from . import (
16 from . import (
17 encoding,
17 encoding,
18 hbisect,
18 hbisect,
19 node,
19 node,
20 templatekw,
20 templatekw,
21 util,
21 util,
22 )
22 )
23
23
24 def addbreaks(text):
24 def addbreaks(text):
25 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
25 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
26 every line except the last.
26 every line except the last.
27 """
27 """
28 return text.replace('\n', '<br/>\n')
28 return text.replace('\n', '<br/>\n')
29
29
30 agescales = [("year", 3600 * 24 * 365, 'Y'),
30 agescales = [("year", 3600 * 24 * 365, 'Y'),
31 ("month", 3600 * 24 * 30, 'M'),
31 ("month", 3600 * 24 * 30, 'M'),
32 ("week", 3600 * 24 * 7, 'W'),
32 ("week", 3600 * 24 * 7, 'W'),
33 ("day", 3600 * 24, 'd'),
33 ("day", 3600 * 24, 'd'),
34 ("hour", 3600, 'h'),
34 ("hour", 3600, 'h'),
35 ("minute", 60, 'm'),
35 ("minute", 60, 'm'),
36 ("second", 1, 's')]
36 ("second", 1, 's')]
37
37
38 def age(date, abbrev=False):
38 def age(date, abbrev=False):
39 """:age: Date. Returns a human-readable date/time difference between the
39 """:age: Date. Returns a human-readable date/time difference between the
40 given date/time and the current date/time.
40 given date/time and the current date/time.
41 """
41 """
42
42
43 def plural(t, c):
43 def plural(t, c):
44 if c == 1:
44 if c == 1:
45 return t
45 return t
46 return t + "s"
46 return t + "s"
47 def fmt(t, c, a):
47 def fmt(t, c, a):
48 if abbrev:
48 if abbrev:
49 return "%d%s" % (c, a)
49 return "%d%s" % (c, a)
50 return "%d %s" % (c, plural(t, c))
50 return "%d %s" % (c, plural(t, c))
51
51
52 now = time.time()
52 now = time.time()
53 then = date[0]
53 then = date[0]
54 future = False
54 future = False
55 if then > now:
55 if then > now:
56 future = True
56 future = True
57 delta = max(1, int(then - now))
57 delta = max(1, int(then - now))
58 if delta > agescales[0][1] * 30:
58 if delta > agescales[0][1] * 30:
59 return 'in the distant future'
59 return 'in the distant future'
60 else:
60 else:
61 delta = max(1, int(now - then))
61 delta = max(1, int(now - then))
62 if delta > agescales[0][1] * 2:
62 if delta > agescales[0][1] * 2:
63 return util.shortdate(date)
63 return util.shortdate(date)
64
64
65 for t, s, a in agescales:
65 for t, s, a in agescales:
66 n = delta // s
66 n = delta // s
67 if n >= 2 or s == 1:
67 if n >= 2 or s == 1:
68 if future:
68 if future:
69 return '%s from now' % fmt(t, n, a)
69 return '%s from now' % fmt(t, n, a)
70 return '%s ago' % fmt(t, n, a)
70 return '%s ago' % fmt(t, n, a)
71
71
72 def basename(path):
72 def basename(path):
73 """:basename: Any text. Treats the text as a path, and returns the last
73 """:basename: Any text. Treats the text as a path, and returns the last
74 component of the path after splitting by the path separator
74 component of the path after splitting by the path separator
75 (ignoring trailing separators). For example, "foo/bar/baz" becomes
75 (ignoring trailing separators). For example, "foo/bar/baz" becomes
76 "baz" and "foo/bar//" becomes "bar".
76 "baz" and "foo/bar//" becomes "bar".
77 """
77 """
78 return os.path.basename(path)
78 return os.path.basename(path)
79
79
80 def count(i):
80 def count(i):
81 """:count: List or text. Returns the length as an integer."""
81 """:count: List or text. Returns the length as an integer."""
82 return len(i)
82 return len(i)
83
83
84 def domain(author):
84 def domain(author):
85 """:domain: Any text. Finds the first string that looks like an email
85 """:domain: Any text. Finds the first string that looks like an email
86 address, and extracts just the domain component. Example: ``User
86 address, and extracts just the domain component. Example: ``User
87 <user@example.com>`` becomes ``example.com``.
87 <user@example.com>`` becomes ``example.com``.
88 """
88 """
89 f = author.find('@')
89 f = author.find('@')
90 if f == -1:
90 if f == -1:
91 return ''
91 return ''
92 author = author[f + 1:]
92 author = author[f + 1:]
93 f = author.find('>')
93 f = author.find('>')
94 if f >= 0:
94 if f >= 0:
95 author = author[:f]
95 author = author[:f]
96 return author
96 return author
97
97
98 def email(text):
98 def email(text):
99 """:email: Any text. Extracts the first string that looks like an email
99 """:email: Any text. Extracts the first string that looks like an email
100 address. Example: ``User <user@example.com>`` becomes
100 address. Example: ``User <user@example.com>`` becomes
101 ``user@example.com``.
101 ``user@example.com``.
102 """
102 """
103 return util.email(text)
103 return util.email(text)
104
104
105 def escape(text):
105 def escape(text):
106 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
106 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
107 and ">" with XML entities, and filters out NUL characters.
107 and ">" with XML entities, and filters out NUL characters.
108 """
108 """
109 return cgi.escape(text.replace('\0', ''), True)
109 return cgi.escape(text.replace('\0', ''), True)
110
110
111 para_re = None
111 para_re = None
112 space_re = None
112 space_re = None
113
113
114 def fill(text, width, initindent='', hangindent=''):
114 def fill(text, width, initindent='', hangindent=''):
115 '''fill many paragraphs with optional indentation.'''
115 '''fill many paragraphs with optional indentation.'''
116 global para_re, space_re
116 global para_re, space_re
117 if para_re is None:
117 if para_re is None:
118 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
118 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
119 space_re = re.compile(r' +')
119 space_re = re.compile(r' +')
120
120
121 def findparas():
121 def findparas():
122 start = 0
122 start = 0
123 while True:
123 while True:
124 m = para_re.search(text, start)
124 m = para_re.search(text, start)
125 if not m:
125 if not m:
126 uctext = unicode(text[start:], encoding.encoding)
126 uctext = unicode(text[start:], encoding.encoding)
127 w = len(uctext)
127 w = len(uctext)
128 while 0 < w and uctext[w - 1].isspace():
128 while 0 < w and uctext[w - 1].isspace():
129 w -= 1
129 w -= 1
130 yield (uctext[:w].encode(encoding.encoding),
130 yield (uctext[:w].encode(encoding.encoding),
131 uctext[w:].encode(encoding.encoding))
131 uctext[w:].encode(encoding.encoding))
132 break
132 break
133 yield text[start:m.start(0)], m.group(1)
133 yield text[start:m.start(0)], m.group(1)
134 start = m.end(1)
134 start = m.end(1)
135
135
136 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
136 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
137 width, initindent, hangindent) + rest
137 width, initindent, hangindent) + rest
138 for para, rest in findparas()])
138 for para, rest in findparas()])
139
139
140 def fill68(text):
140 def fill68(text):
141 """:fill68: Any text. Wraps the text to fit in 68 columns."""
141 """:fill68: Any text. Wraps the text to fit in 68 columns."""
142 return fill(text, 68)
142 return fill(text, 68)
143
143
144 def fill76(text):
144 def fill76(text):
145 """:fill76: Any text. Wraps the text to fit in 76 columns."""
145 """:fill76: Any text. Wraps the text to fit in 76 columns."""
146 return fill(text, 76)
146 return fill(text, 76)
147
147
148 def firstline(text):
148 def firstline(text):
149 """:firstline: Any text. Returns the first line of text."""
149 """:firstline: Any text. Returns the first line of text."""
150 try:
150 try:
151 return text.splitlines(True)[0].rstrip('\r\n')
151 return text.splitlines(True)[0].rstrip('\r\n')
152 except IndexError:
152 except IndexError:
153 return ''
153 return ''
154
154
155 def hexfilter(text):
155 def hexfilter(text):
156 """:hex: Any text. Convert a binary Mercurial node identifier into
156 """:hex: Any text. Convert a binary Mercurial node identifier into
157 its long hexadecimal representation.
157 its long hexadecimal representation.
158 """
158 """
159 return node.hex(text)
159 return node.hex(text)
160
160
161 def hgdate(text):
161 def hgdate(text):
162 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
162 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
163 25200" (Unix timestamp, timezone offset).
163 25200" (Unix timestamp, timezone offset).
164 """
164 """
165 return "%d %d" % text
165 return "%d %d" % text
166
166
167 def isodate(text):
167 def isodate(text):
168 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
168 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
169 +0200".
169 +0200".
170 """
170 """
171 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
171 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
172
172
173 def isodatesec(text):
173 def isodatesec(text):
174 """:isodatesec: Date. Returns the date in ISO 8601 format, including
174 """:isodatesec: Date. Returns the date in ISO 8601 format, including
175 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
175 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
176 filter.
176 filter.
177 """
177 """
178 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
178 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
179
179
180 def indent(text, prefix):
180 def indent(text, prefix):
181 '''indent each non-empty line of text after first with prefix.'''
181 '''indent each non-empty line of text after first with prefix.'''
182 lines = text.splitlines()
182 lines = text.splitlines()
183 num_lines = len(lines)
183 num_lines = len(lines)
184 endswithnewline = text[-1:] == '\n'
184 endswithnewline = text[-1:] == '\n'
185 def indenter():
185 def indenter():
186 for i in xrange(num_lines):
186 for i in xrange(num_lines):
187 l = lines[i]
187 l = lines[i]
188 if i and l.strip():
188 if i and l.strip():
189 yield prefix
189 yield prefix
190 yield l
190 yield l
191 if i < num_lines - 1 or endswithnewline:
191 if i < num_lines - 1 or endswithnewline:
192 yield '\n'
192 yield '\n'
193 return "".join(indenter())
193 return "".join(indenter())
194
194
195 def json(obj):
195 def json(obj):
196 if obj is None or obj is False or obj is True:
196 if obj is None or obj is False or obj is True:
197 return {None: 'null', False: 'false', True: 'true'}[obj]
197 return {None: 'null', False: 'false', True: 'true'}[obj]
198 elif isinstance(obj, int) or isinstance(obj, float):
198 elif isinstance(obj, int) or isinstance(obj, float):
199 return str(obj)
199 return str(obj)
200 elif isinstance(obj, str):
200 elif isinstance(obj, str):
201 u = unicode(obj, encoding.encoding, 'replace')
201 u = unicode(obj, encoding.encoding, 'replace')
202 return '"%s"' % jsonescape(u)
202 return '"%s"' % jsonescape(u)
203 elif isinstance(obj, unicode):
203 elif isinstance(obj, unicode):
204 return '"%s"' % jsonescape(obj)
204 return '"%s"' % jsonescape(obj)
205 elif util.safehasattr(obj, 'keys'):
205 elif util.safehasattr(obj, 'keys'):
206 out = []
206 out = []
207 for k, v in sorted(obj.iteritems()):
207 for k, v in sorted(obj.iteritems()):
208 s = '%s: %s' % (json(k), json(v))
208 s = '%s: %s' % (json(k), json(v))
209 out.append(s)
209 out.append(s)
210 return '{' + ', '.join(out) + '}'
210 return '{' + ', '.join(out) + '}'
211 elif util.safehasattr(obj, '__iter__'):
211 elif util.safehasattr(obj, '__iter__'):
212 out = []
212 out = []
213 for i in obj:
213 for i in obj:
214 out.append(json(i))
214 out.append(json(i))
215 return '[' + ', '.join(out) + ']'
215 return '[' + ', '.join(out) + ']'
216 elif util.safehasattr(obj, '__call__'):
216 elif util.safehasattr(obj, '__call__'):
217 return json(obj())
217 return json(obj())
218 else:
218 else:
219 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
219 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
220
220
221 def _uescape(c):
221 def _uescape(c):
222 if ord(c) < 0x80:
222 if 0x20 <= ord(c) < 0x80:
223 return c
223 return c
224 else:
224 else:
225 return '\\u%04x' % ord(c)
225 return '\\u%04x' % ord(c)
226
226
227 _escapes = [
227 _escapes = [
228 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
228 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
229 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
229 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
230 ('<', '\\u003c'), ('>', '\\u003e'), ('\0', '\\u0000')
230 ('<', '\\u003c'), ('>', '\\u003e'), ('\0', '\\u0000')
231 ]
231 ]
232
232
233 def jsonescape(s):
233 def jsonescape(s):
234 for k, v in _escapes:
234 for k, v in _escapes:
235 s = s.replace(k, v)
235 s = s.replace(k, v)
236 return ''.join(_uescape(c) for c in s)
236 return ''.join(_uescape(c) for c in s)
237
237
238 def lower(text):
238 def lower(text):
239 """:lower: Any text. Converts the text to lowercase."""
239 """:lower: Any text. Converts the text to lowercase."""
240 return encoding.lower(text)
240 return encoding.lower(text)
241
241
242 def nonempty(str):
242 def nonempty(str):
243 """:nonempty: Any text. Returns '(none)' if the string is empty."""
243 """:nonempty: Any text. Returns '(none)' if the string is empty."""
244 return str or "(none)"
244 return str or "(none)"
245
245
246 def obfuscate(text):
246 def obfuscate(text):
247 """:obfuscate: Any text. Returns the input text rendered as a sequence of
247 """:obfuscate: Any text. Returns the input text rendered as a sequence of
248 XML entities.
248 XML entities.
249 """
249 """
250 text = unicode(text, encoding.encoding, 'replace')
250 text = unicode(text, encoding.encoding, 'replace')
251 return ''.join(['&#%d;' % ord(c) for c in text])
251 return ''.join(['&#%d;' % ord(c) for c in text])
252
252
253 def permissions(flags):
253 def permissions(flags):
254 if "l" in flags:
254 if "l" in flags:
255 return "lrwxrwxrwx"
255 return "lrwxrwxrwx"
256 if "x" in flags:
256 if "x" in flags:
257 return "-rwxr-xr-x"
257 return "-rwxr-xr-x"
258 return "-rw-r--r--"
258 return "-rw-r--r--"
259
259
260 def person(author):
260 def person(author):
261 """:person: Any text. Returns the name before an email address,
261 """:person: Any text. Returns the name before an email address,
262 interpreting it as per RFC 5322.
262 interpreting it as per RFC 5322.
263
263
264 >>> person('foo@bar')
264 >>> person('foo@bar')
265 'foo'
265 'foo'
266 >>> person('Foo Bar <foo@bar>')
266 >>> person('Foo Bar <foo@bar>')
267 'Foo Bar'
267 'Foo Bar'
268 >>> person('"Foo Bar" <foo@bar>')
268 >>> person('"Foo Bar" <foo@bar>')
269 'Foo Bar'
269 'Foo Bar'
270 >>> person('"Foo \"buz\" Bar" <foo@bar>')
270 >>> person('"Foo \"buz\" Bar" <foo@bar>')
271 'Foo "buz" Bar'
271 'Foo "buz" Bar'
272 >>> # The following are invalid, but do exist in real-life
272 >>> # The following are invalid, but do exist in real-life
273 ...
273 ...
274 >>> person('Foo "buz" Bar <foo@bar>')
274 >>> person('Foo "buz" Bar <foo@bar>')
275 'Foo "buz" Bar'
275 'Foo "buz" Bar'
276 >>> person('"Foo Bar <foo@bar>')
276 >>> person('"Foo Bar <foo@bar>')
277 'Foo Bar'
277 'Foo Bar'
278 """
278 """
279 if '@' not in author:
279 if '@' not in author:
280 return author
280 return author
281 f = author.find('<')
281 f = author.find('<')
282 if f != -1:
282 if f != -1:
283 return author[:f].strip(' "').replace('\\"', '"')
283 return author[:f].strip(' "').replace('\\"', '"')
284 f = author.find('@')
284 f = author.find('@')
285 return author[:f].replace('.', ' ')
285 return author[:f].replace('.', ' ')
286
286
287 def revescape(text):
287 def revescape(text):
288 """:revescape: Any text. Escapes all "special" characters, except @.
288 """:revescape: Any text. Escapes all "special" characters, except @.
289 Forward slashes are escaped twice to prevent web servers from prematurely
289 Forward slashes are escaped twice to prevent web servers from prematurely
290 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
290 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
291 """
291 """
292 return urllib.quote(text, safe='/@').replace('/', '%252F')
292 return urllib.quote(text, safe='/@').replace('/', '%252F')
293
293
294 def rfc3339date(text):
294 def rfc3339date(text):
295 """:rfc3339date: Date. Returns a date using the Internet date format
295 """:rfc3339date: Date. Returns a date using the Internet date format
296 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
296 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
297 """
297 """
298 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
298 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
299
299
300 def rfc822date(text):
300 def rfc822date(text):
301 """:rfc822date: Date. Returns a date using the same format used in email
301 """:rfc822date: Date. Returns a date using the same format used in email
302 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
302 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
303 """
303 """
304 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
304 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
305
305
306 def short(text):
306 def short(text):
307 """:short: Changeset hash. Returns the short form of a changeset hash,
307 """:short: Changeset hash. Returns the short form of a changeset hash,
308 i.e. a 12 hexadecimal digit string.
308 i.e. a 12 hexadecimal digit string.
309 """
309 """
310 return text[:12]
310 return text[:12]
311
311
312 def shortbisect(text):
312 def shortbisect(text):
313 """:shortbisect: Any text. Treats `text` as a bisection status, and
313 """:shortbisect: Any text. Treats `text` as a bisection status, and
314 returns a single-character representing the status (G: good, B: bad,
314 returns a single-character representing the status (G: good, B: bad,
315 S: skipped, U: untested, I: ignored). Returns single space if `text`
315 S: skipped, U: untested, I: ignored). Returns single space if `text`
316 is not a valid bisection status.
316 is not a valid bisection status.
317 """
317 """
318 return hbisect.shortlabel(text) or ' '
318 return hbisect.shortlabel(text) or ' '
319
319
320 def shortdate(text):
320 def shortdate(text):
321 """:shortdate: Date. Returns a date like "2006-09-18"."""
321 """:shortdate: Date. Returns a date like "2006-09-18"."""
322 return util.shortdate(text)
322 return util.shortdate(text)
323
323
324 def splitlines(text):
324 def splitlines(text):
325 """:splitlines: Any text. Split text into a list of lines."""
325 """:splitlines: Any text. Split text into a list of lines."""
326 return templatekw.showlist('line', text.splitlines(), 'lines')
326 return templatekw.showlist('line', text.splitlines(), 'lines')
327
327
328 def stringescape(text):
328 def stringescape(text):
329 return text.encode('string_escape')
329 return text.encode('string_escape')
330
330
331 def stringify(thing):
331 def stringify(thing):
332 """:stringify: Any type. Turns the value into text by converting values into
332 """:stringify: Any type. Turns the value into text by converting values into
333 text and concatenating them.
333 text and concatenating them.
334 """
334 """
335 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
335 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
336 return "".join([stringify(t) for t in thing if t is not None])
336 return "".join([stringify(t) for t in thing if t is not None])
337 if thing is None:
337 if thing is None:
338 return ""
338 return ""
339 return str(thing)
339 return str(thing)
340
340
341 def stripdir(text):
341 def stripdir(text):
342 """:stripdir: Treat the text as path and strip a directory level, if
342 """:stripdir: Treat the text as path and strip a directory level, if
343 possible. For example, "foo" and "foo/bar" becomes "foo".
343 possible. For example, "foo" and "foo/bar" becomes "foo".
344 """
344 """
345 dir = os.path.dirname(text)
345 dir = os.path.dirname(text)
346 if dir == "":
346 if dir == "":
347 return os.path.basename(text)
347 return os.path.basename(text)
348 else:
348 else:
349 return dir
349 return dir
350
350
351 def tabindent(text):
351 def tabindent(text):
352 """:tabindent: Any text. Returns the text, with every non-empty line
352 """:tabindent: Any text. Returns the text, with every non-empty line
353 except the first starting with a tab character.
353 except the first starting with a tab character.
354 """
354 """
355 return indent(text, '\t')
355 return indent(text, '\t')
356
356
357 def upper(text):
357 def upper(text):
358 """:upper: Any text. Converts the text to uppercase."""
358 """:upper: Any text. Converts the text to uppercase."""
359 return encoding.upper(text)
359 return encoding.upper(text)
360
360
361 def urlescape(text):
361 def urlescape(text):
362 """:urlescape: Any text. Escapes all "special" characters. For example,
362 """:urlescape: Any text. Escapes all "special" characters. For example,
363 "foo bar" becomes "foo%20bar".
363 "foo bar" becomes "foo%20bar".
364 """
364 """
365 return urllib.quote(text)
365 return urllib.quote(text)
366
366
367 def userfilter(text):
367 def userfilter(text):
368 """:user: Any text. Returns a short representation of a user name or email
368 """:user: Any text. Returns a short representation of a user name or email
369 address."""
369 address."""
370 return util.shortuser(text)
370 return util.shortuser(text)
371
371
372 def emailuser(text):
372 def emailuser(text):
373 """:emailuser: Any text. Returns the user portion of an email address."""
373 """:emailuser: Any text. Returns the user portion of an email address."""
374 return util.emailuser(text)
374 return util.emailuser(text)
375
375
376 def xmlescape(text):
376 def xmlescape(text):
377 text = (text
377 text = (text
378 .replace('&', '&amp;')
378 .replace('&', '&amp;')
379 .replace('<', '&lt;')
379 .replace('<', '&lt;')
380 .replace('>', '&gt;')
380 .replace('>', '&gt;')
381 .replace('"', '&quot;')
381 .replace('"', '&quot;')
382 .replace("'", '&#39;')) # &apos; invalid in HTML
382 .replace("'", '&#39;')) # &apos; invalid in HTML
383 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
383 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
384
384
385 filters = {
385 filters = {
386 "addbreaks": addbreaks,
386 "addbreaks": addbreaks,
387 "age": age,
387 "age": age,
388 "basename": basename,
388 "basename": basename,
389 "count": count,
389 "count": count,
390 "domain": domain,
390 "domain": domain,
391 "email": email,
391 "email": email,
392 "escape": escape,
392 "escape": escape,
393 "fill68": fill68,
393 "fill68": fill68,
394 "fill76": fill76,
394 "fill76": fill76,
395 "firstline": firstline,
395 "firstline": firstline,
396 "hex": hexfilter,
396 "hex": hexfilter,
397 "hgdate": hgdate,
397 "hgdate": hgdate,
398 "isodate": isodate,
398 "isodate": isodate,
399 "isodatesec": isodatesec,
399 "isodatesec": isodatesec,
400 "json": json,
400 "json": json,
401 "jsonescape": jsonescape,
401 "jsonescape": jsonescape,
402 "lower": lower,
402 "lower": lower,
403 "nonempty": nonempty,
403 "nonempty": nonempty,
404 "obfuscate": obfuscate,
404 "obfuscate": obfuscate,
405 "permissions": permissions,
405 "permissions": permissions,
406 "person": person,
406 "person": person,
407 "revescape": revescape,
407 "revescape": revescape,
408 "rfc3339date": rfc3339date,
408 "rfc3339date": rfc3339date,
409 "rfc822date": rfc822date,
409 "rfc822date": rfc822date,
410 "short": short,
410 "short": short,
411 "shortbisect": shortbisect,
411 "shortbisect": shortbisect,
412 "shortdate": shortdate,
412 "shortdate": shortdate,
413 "splitlines": splitlines,
413 "splitlines": splitlines,
414 "stringescape": stringescape,
414 "stringescape": stringescape,
415 "stringify": stringify,
415 "stringify": stringify,
416 "stripdir": stripdir,
416 "stripdir": stripdir,
417 "tabindent": tabindent,
417 "tabindent": tabindent,
418 "upper": upper,
418 "upper": upper,
419 "urlescape": urlescape,
419 "urlescape": urlescape,
420 "user": userfilter,
420 "user": userfilter,
421 "emailuser": emailuser,
421 "emailuser": emailuser,
422 "xmlescape": xmlescape,
422 "xmlescape": xmlescape,
423 }
423 }
424
424
425 def websub(text, websubtable):
425 def websub(text, websubtable):
426 """:websub: Any text. Only applies to hgweb. Applies the regular
426 """:websub: Any text. Only applies to hgweb. Applies the regular
427 expression replacements defined in the websub section.
427 expression replacements defined in the websub section.
428 """
428 """
429 if websubtable:
429 if websubtable:
430 for regexp, format in websubtable:
430 for regexp, format in websubtable:
431 text = regexp.sub(format, text)
431 text = regexp.sub(format, text)
432 return text
432 return text
433
433
434 # tell hggettext to extract docstrings from these functions:
434 # tell hggettext to extract docstrings from these functions:
435 i18nfunctions = filters.values()
435 i18nfunctions = filters.values()
@@ -1,47 +1,60 b''
1
1
2 $ cat > engine.py << EOF
2 $ cat > engine.py << EOF
3 >
3 >
4 > from mercurial import templater
4 > from mercurial import templater
5 >
5 >
6 > class mytemplater(object):
6 > class mytemplater(object):
7 > def __init__(self, loader, filters, defaults):
7 > def __init__(self, loader, filters, defaults):
8 > self.loader = loader
8 > self.loader = loader
9 >
9 >
10 > def process(self, t, map):
10 > def process(self, t, map):
11 > tmpl = self.loader(t)
11 > tmpl = self.loader(t)
12 > for k, v in map.iteritems():
12 > for k, v in map.iteritems():
13 > if k in ('templ', 'ctx', 'repo', 'revcache', 'cache'):
13 > if k in ('templ', 'ctx', 'repo', 'revcache', 'cache'):
14 > continue
14 > continue
15 > if hasattr(v, '__call__'):
15 > if hasattr(v, '__call__'):
16 > v = v(**map)
16 > v = v(**map)
17 > v = templater.stringify(v)
17 > v = templater.stringify(v)
18 > tmpl = tmpl.replace('{{%s}}' % k, v)
18 > tmpl = tmpl.replace('{{%s}}' % k, v)
19 > yield tmpl
19 > yield tmpl
20 >
20 >
21 > templater.engines['my'] = mytemplater
21 > templater.engines['my'] = mytemplater
22 > EOF
22 > EOF
23 $ hg init test
23 $ hg init test
24 $ echo '[extensions]' > test/.hg/hgrc
24 $ echo '[extensions]' > test/.hg/hgrc
25 $ echo "engine = `pwd`/engine.py" >> test/.hg/hgrc
25 $ echo "engine = `pwd`/engine.py" >> test/.hg/hgrc
26 $ cd test
26 $ cd test
27 $ cat > mymap << EOF
27 $ cat > mymap << EOF
28 > changeset = my:changeset.txt
28 > changeset = my:changeset.txt
29 > EOF
29 > EOF
30 $ cat > changeset.txt << EOF
30 $ cat > changeset.txt << EOF
31 > {{rev}} {{node}} {{author}}
31 > {{rev}} {{node}} {{author}}
32 > EOF
32 > EOF
33 $ hg ci -Ama
33 $ hg ci -Ama
34 adding changeset.txt
34 adding changeset.txt
35 adding mymap
35 adding mymap
36 $ hg log --style=./mymap
36 $ hg log --style=./mymap
37 0 97e5f848f0936960273bbf75be6388cd0350a32b test
37 0 97e5f848f0936960273bbf75be6388cd0350a32b test
38
38
39 $ cat > changeset.txt << EOF
39 $ cat > changeset.txt << EOF
40 > {{p1rev}} {{p1node}} {{p2rev}} {{p2node}}
40 > {{p1rev}} {{p1node}} {{p2rev}} {{p2node}}
41 > EOF
41 > EOF
42 $ hg ci -Ama
42 $ hg ci -Ama
43 $ hg log --style=./mymap
43 $ hg log --style=./mymap
44 0 97e5f848f0936960273bbf75be6388cd0350a32b -1 0000000000000000000000000000000000000000
44 0 97e5f848f0936960273bbf75be6388cd0350a32b -1 0000000000000000000000000000000000000000
45 -1 0000000000000000000000000000000000000000 -1 0000000000000000000000000000000000000000
45 -1 0000000000000000000000000000000000000000 -1 0000000000000000000000000000000000000000
46
46
47 Fuzzing the unicode escaper to ensure it produces valid data
48
49 #if hypothesis
50
51 >>> from hypothesishelpers import *
52 >>> import mercurial.templatefilters as tf
53 >>> import json
54 >>> @check(st.text().map(lambda s: s.encode('utf-8')))
55 ... def testtfescapeproducesvalidjson(text):
56 ... json.loads('"' + tf.jsonescape(text) + '"')
57
58 #endif
59
47 $ cd ..
60 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now