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