##// END OF EJS Templates
templatefilters: drop old jsonescape() function...
Yuya Nishihara -
r28213:93b5c540 default
parent child Browse files
Show More
@@ -1,436 +1,419 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 return '"%s"' % encoding.jsonescape(obj, paranoid=True)
202 202 elif util.safehasattr(obj, 'keys'):
203 203 out = []
204 204 for k, v in sorted(obj.iteritems()):
205 205 s = '%s: %s' % (json(k), json(v))
206 206 out.append(s)
207 207 return '{' + ', '.join(out) + '}'
208 208 elif util.safehasattr(obj, '__iter__'):
209 209 out = []
210 210 for i in obj:
211 211 out.append(json(i))
212 212 return '[' + ', '.join(out) + ']'
213 213 elif util.safehasattr(obj, '__call__'):
214 214 return json(obj())
215 215 else:
216 216 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
217 217
218 def _uescape(c):
219 if 0x20 <= ord(c) < 0x80:
220 return c
221 else:
222 return '\\u%04x' % ord(c)
223
224 _escapes = [
225 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
226 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
227 ('<', '\\u003c'), ('>', '\\u003e'), ('\0', '\\u0000')
228 ]
229
230 def jsonescape(s):
231 for k, v in _escapes:
232 s = s.replace(k, v)
233 return ''.join(_uescape(c) for c in s)
234
235 218 def lower(text):
236 219 """:lower: Any text. Converts the text to lowercase."""
237 220 return encoding.lower(text)
238 221
239 222 def nonempty(str):
240 223 """:nonempty: Any text. Returns '(none)' if the string is empty."""
241 224 return str or "(none)"
242 225
243 226 def obfuscate(text):
244 227 """:obfuscate: Any text. Returns the input text rendered as a sequence of
245 228 XML entities.
246 229 """
247 230 text = unicode(text, encoding.encoding, 'replace')
248 231 return ''.join(['&#%d;' % ord(c) for c in text])
249 232
250 233 def permissions(flags):
251 234 if "l" in flags:
252 235 return "lrwxrwxrwx"
253 236 if "x" in flags:
254 237 return "-rwxr-xr-x"
255 238 return "-rw-r--r--"
256 239
257 240 def person(author):
258 241 """:person: Any text. Returns the name before an email address,
259 242 interpreting it as per RFC 5322.
260 243
261 244 >>> person('foo@bar')
262 245 'foo'
263 246 >>> person('Foo Bar <foo@bar>')
264 247 'Foo Bar'
265 248 >>> person('"Foo Bar" <foo@bar>')
266 249 'Foo Bar'
267 250 >>> person('"Foo \"buz\" Bar" <foo@bar>')
268 251 'Foo "buz" Bar'
269 252 >>> # The following are invalid, but do exist in real-life
270 253 ...
271 254 >>> person('Foo "buz" Bar <foo@bar>')
272 255 'Foo "buz" Bar'
273 256 >>> person('"Foo Bar <foo@bar>')
274 257 'Foo Bar'
275 258 """
276 259 if '@' not in author:
277 260 return author
278 261 f = author.find('<')
279 262 if f != -1:
280 263 return author[:f].strip(' "').replace('\\"', '"')
281 264 f = author.find('@')
282 265 return author[:f].replace('.', ' ')
283 266
284 267 def revescape(text):
285 268 """:revescape: Any text. Escapes all "special" characters, except @.
286 269 Forward slashes are escaped twice to prevent web servers from prematurely
287 270 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
288 271 """
289 272 return urllib.quote(text, safe='/@').replace('/', '%252F')
290 273
291 274 def rfc3339date(text):
292 275 """:rfc3339date: Date. Returns a date using the Internet date format
293 276 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
294 277 """
295 278 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
296 279
297 280 def rfc822date(text):
298 281 """:rfc822date: Date. Returns a date using the same format used in email
299 282 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
300 283 """
301 284 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
302 285
303 286 def short(text):
304 287 """:short: Changeset hash. Returns the short form of a changeset hash,
305 288 i.e. a 12 hexadecimal digit string.
306 289 """
307 290 return text[:12]
308 291
309 292 def shortbisect(text):
310 293 """:shortbisect: Any text. Treats `text` as a bisection status, and
311 294 returns a single-character representing the status (G: good, B: bad,
312 295 S: skipped, U: untested, I: ignored). Returns single space if `text`
313 296 is not a valid bisection status.
314 297 """
315 298 return hbisect.shortlabel(text) or ' '
316 299
317 300 def shortdate(text):
318 301 """:shortdate: Date. Returns a date like "2006-09-18"."""
319 302 return util.shortdate(text)
320 303
321 304 def splitlines(text):
322 305 """:splitlines: Any text. Split text into a list of lines."""
323 306 return templatekw.showlist('line', text.splitlines(), 'lines')
324 307
325 308 def stringescape(text):
326 309 return text.encode('string_escape')
327 310
328 311 def stringify(thing):
329 312 """:stringify: Any type. Turns the value into text by converting values into
330 313 text and concatenating them.
331 314 """
332 315 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
333 316 return "".join([stringify(t) for t in thing if t is not None])
334 317 if thing is None:
335 318 return ""
336 319 return str(thing)
337 320
338 321 def stripdir(text):
339 322 """:stripdir: Treat the text as path and strip a directory level, if
340 323 possible. For example, "foo" and "foo/bar" becomes "foo".
341 324 """
342 325 dir = os.path.dirname(text)
343 326 if dir == "":
344 327 return os.path.basename(text)
345 328 else:
346 329 return dir
347 330
348 331 def tabindent(text):
349 332 """:tabindent: Any text. Returns the text, with every non-empty line
350 333 except the first starting with a tab character.
351 334 """
352 335 return indent(text, '\t')
353 336
354 337 def upper(text):
355 338 """:upper: Any text. Converts the text to uppercase."""
356 339 return encoding.upper(text)
357 340
358 341 def urlescape(text):
359 342 """:urlescape: Any text. Escapes all "special" characters. For example,
360 343 "foo bar" becomes "foo%20bar".
361 344 """
362 345 return urllib.quote(text)
363 346
364 347 def userfilter(text):
365 348 """:user: Any text. Returns a short representation of a user name or email
366 349 address."""
367 350 return util.shortuser(text)
368 351
369 352 def emailuser(text):
370 353 """:emailuser: Any text. Returns the user portion of an email address."""
371 354 return util.emailuser(text)
372 355
373 356 def utf8(text):
374 357 """:utf8: Any text. Converts from the local character encoding to UTF-8."""
375 358 return encoding.fromlocal(text)
376 359
377 360 def xmlescape(text):
378 361 text = (text
379 362 .replace('&', '&amp;')
380 363 .replace('<', '&lt;')
381 364 .replace('>', '&gt;')
382 365 .replace('"', '&quot;')
383 366 .replace("'", '&#39;')) # &apos; invalid in HTML
384 367 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
385 368
386 369 filters = {
387 370 "addbreaks": addbreaks,
388 371 "age": age,
389 372 "basename": basename,
390 373 "count": count,
391 374 "domain": domain,
392 375 "email": email,
393 376 "escape": escape,
394 377 "fill68": fill68,
395 378 "fill76": fill76,
396 379 "firstline": firstline,
397 380 "hex": hexfilter,
398 381 "hgdate": hgdate,
399 382 "isodate": isodate,
400 383 "isodatesec": isodatesec,
401 384 "json": json,
402 385 "lower": lower,
403 386 "nonempty": nonempty,
404 387 "obfuscate": obfuscate,
405 388 "permissions": permissions,
406 389 "person": person,
407 390 "revescape": revescape,
408 391 "rfc3339date": rfc3339date,
409 392 "rfc822date": rfc822date,
410 393 "short": short,
411 394 "shortbisect": shortbisect,
412 395 "shortdate": shortdate,
413 396 "splitlines": splitlines,
414 397 "stringescape": stringescape,
415 398 "stringify": stringify,
416 399 "stripdir": stripdir,
417 400 "tabindent": tabindent,
418 401 "upper": upper,
419 402 "urlescape": urlescape,
420 403 "user": userfilter,
421 404 "emailuser": emailuser,
422 405 "utf8": utf8,
423 406 "xmlescape": xmlescape,
424 407 }
425 408
426 409 def websub(text, websubtable):
427 410 """:websub: Any text. Only applies to hgweb. Applies the regular
428 411 expression replacements defined in the websub section.
429 412 """
430 413 if websubtable:
431 414 for regexp, format in websubtable:
432 415 text = regexp.sub(format, text)
433 416 return text
434 417
435 418 # tell hggettext to extract docstrings from these functions:
436 419 i18nfunctions = filters.values()
@@ -1,60 +1,47 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
60 47 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now