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