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