##// END OF EJS Templates
templates: make `firstline` filter not keep '\v', '\f' and similar...
Martin von Zweigbergk -
r49884:1138674e default
parent child Browse files
Show More
@@ -1,554 +1,554 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 pycompat.xrange(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 try:
272 return text.splitlines(True)[0].rstrip(b'\r\n')
272 return text.splitlines()[0]
273 273 except IndexError:
274 274 return b''
275 275
276 276
277 277 @templatefilter(b'hex', intype=bytes)
278 278 def hexfilter(text):
279 279 """Any text. Convert a binary Mercurial node identifier into
280 280 its long hexadecimal representation.
281 281 """
282 282 return hex(text)
283 283
284 284
285 285 @templatefilter(b'hgdate', intype=templateutil.date)
286 286 def hgdate(text):
287 287 """Date. Returns the date as a pair of numbers: "1157407993
288 288 25200" (Unix timestamp, timezone offset).
289 289 """
290 290 return b"%d %d" % text
291 291
292 292
293 293 @templatefilter(b'isodate', intype=templateutil.date)
294 294 def isodate(text):
295 295 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
296 296 +0200".
297 297 """
298 298 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
299 299
300 300
301 301 @templatefilter(b'isodatesec', intype=templateutil.date)
302 302 def isodatesec(text):
303 303 """Date. Returns the date in ISO 8601 format, including
304 304 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
305 305 filter.
306 306 """
307 307 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
308 308
309 309
310 310 def indent(text, prefix, firstline=b''):
311 311 '''indent each non-empty line of text after first with prefix.'''
312 312 lines = text.splitlines()
313 313 num_lines = len(lines)
314 314 endswithnewline = text[-1:] == b'\n'
315 315
316 316 def indenter():
317 317 for i in pycompat.xrange(num_lines):
318 318 l = lines[i]
319 319 if l.strip():
320 320 yield prefix if i else firstline
321 321 yield l
322 322 if i < num_lines - 1 or endswithnewline:
323 323 yield b'\n'
324 324
325 325 return b"".join(indenter())
326 326
327 327
328 328 @templatefilter(b'json')
329 329 def json(obj, paranoid=True):
330 330 """Any object. Serializes the object to a JSON formatted text."""
331 331 if obj is None:
332 332 return b'null'
333 333 elif obj is False:
334 334 return b'false'
335 335 elif obj is True:
336 336 return b'true'
337 337 elif isinstance(obj, (int, int, float)):
338 338 return pycompat.bytestr(obj)
339 339 elif isinstance(obj, bytes):
340 340 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
341 341 elif isinstance(obj, type(u'')):
342 342 raise error.ProgrammingError(
343 343 b'Mercurial only does output with bytes: %r' % obj
344 344 )
345 345 elif util.safehasattr(obj, b'keys'):
346 346 out = [
347 347 b'"%s": %s'
348 348 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
349 349 for k, v in sorted(obj.items())
350 350 ]
351 351 return b'{' + b', '.join(out) + b'}'
352 352 elif util.safehasattr(obj, b'__iter__'):
353 353 out = [json(i, paranoid) for i in obj]
354 354 return b'[' + b', '.join(out) + b']'
355 355 raise error.ProgrammingError(b'cannot encode %r' % obj)
356 356
357 357
358 358 @templatefilter(b'lower', intype=bytes)
359 359 def lower(text):
360 360 """Any text. Converts the text to lowercase."""
361 361 return encoding.lower(text)
362 362
363 363
364 364 @templatefilter(b'nonempty', intype=bytes)
365 365 def nonempty(text):
366 366 """Any text. Returns '(none)' if the string is empty."""
367 367 return text or b"(none)"
368 368
369 369
370 370 @templatefilter(b'obfuscate', intype=bytes)
371 371 def obfuscate(text):
372 372 """Any text. Returns the input text rendered as a sequence of
373 373 XML entities.
374 374 """
375 375 text = str(text, pycompat.sysstr(encoding.encoding), r'replace')
376 376 return b''.join([b'&#%d;' % ord(c) for c in text])
377 377
378 378
379 379 @templatefilter(b'permissions', intype=bytes)
380 380 def permissions(flags):
381 381 if b"l" in flags:
382 382 return b"lrwxrwxrwx"
383 383 if b"x" in flags:
384 384 return b"-rwxr-xr-x"
385 385 return b"-rw-r--r--"
386 386
387 387
388 388 @templatefilter(b'person', intype=bytes)
389 389 def person(author):
390 390 """Any text. Returns the name before an email address,
391 391 interpreting it as per RFC 5322.
392 392 """
393 393 return stringutil.person(author)
394 394
395 395
396 396 @templatefilter(b'revescape', intype=bytes)
397 397 def revescape(text):
398 398 """Any text. Escapes all "special" characters, except @.
399 399 Forward slashes are escaped twice to prevent web servers from prematurely
400 400 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
401 401 """
402 402 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
403 403
404 404
405 405 @templatefilter(b'rfc3339date', intype=templateutil.date)
406 406 def rfc3339date(text):
407 407 """Date. Returns a date using the Internet date format
408 408 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
409 409 """
410 410 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
411 411
412 412
413 413 @templatefilter(b'rfc822date', intype=templateutil.date)
414 414 def rfc822date(text):
415 415 """Date. Returns a date using the same format used in email
416 416 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
417 417 """
418 418 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
419 419
420 420
421 421 @templatefilter(b'short', intype=bytes)
422 422 def short(text):
423 423 """Changeset hash. Returns the short form of a changeset hash,
424 424 i.e. a 12 hexadecimal digit string.
425 425 """
426 426 return text[:12]
427 427
428 428
429 429 @templatefilter(b'shortbisect', intype=bytes)
430 430 def shortbisect(label):
431 431 """Any text. Treats `label` as a bisection status, and
432 432 returns a single-character representing the status (G: good, B: bad,
433 433 S: skipped, U: untested, I: ignored). Returns single space if `text`
434 434 is not a valid bisection status.
435 435 """
436 436 if label:
437 437 return label[0:1].upper()
438 438 return b' '
439 439
440 440
441 441 @templatefilter(b'shortdate', intype=templateutil.date)
442 442 def shortdate(text):
443 443 """Date. Returns a date like "2006-09-18"."""
444 444 return dateutil.shortdate(text)
445 445
446 446
447 447 @templatefilter(b'slashpath', intype=bytes)
448 448 def slashpath(path):
449 449 """Any text. Replaces the native path separator with slash."""
450 450 return util.pconvert(path)
451 451
452 452
453 453 @templatefilter(b'splitlines', intype=bytes)
454 454 def splitlines(text):
455 455 """Any text. Split text into a list of lines."""
456 456 return templateutil.hybridlist(text.splitlines(), name=b'line')
457 457
458 458
459 459 @templatefilter(b'stringescape', intype=bytes)
460 460 def stringescape(text):
461 461 return stringutil.escapestr(text)
462 462
463 463
464 464 @templatefilter(b'stringify', intype=bytes)
465 465 def stringify(thing):
466 466 """Any type. Turns the value into text by converting values into
467 467 text and concatenating them.
468 468 """
469 469 return thing # coerced by the intype
470 470
471 471
472 472 @templatefilter(b'stripdir', intype=bytes)
473 473 def stripdir(text):
474 474 """Treat the text as path and strip a directory level, if
475 475 possible. For example, "foo" and "foo/bar" becomes "foo".
476 476 """
477 477 dir = os.path.dirname(text)
478 478 if dir == b"":
479 479 return os.path.basename(text)
480 480 else:
481 481 return dir
482 482
483 483
484 484 @templatefilter(b'tabindent', intype=bytes)
485 485 def tabindent(text):
486 486 """Any text. Returns the text, with every non-empty line
487 487 except the first starting with a tab character.
488 488 """
489 489 return indent(text, b'\t')
490 490
491 491
492 492 @templatefilter(b'upper', intype=bytes)
493 493 def upper(text):
494 494 """Any text. Converts the text to uppercase."""
495 495 return encoding.upper(text)
496 496
497 497
498 498 @templatefilter(b'urlescape', intype=bytes)
499 499 def urlescape(text):
500 500 """Any text. Escapes all "special" characters. For example,
501 501 "foo bar" becomes "foo%20bar".
502 502 """
503 503 return urlreq.quote(text)
504 504
505 505
506 506 @templatefilter(b'user', intype=bytes)
507 507 def userfilter(text):
508 508 """Any text. Returns a short representation of a user name or email
509 509 address."""
510 510 return stringutil.shortuser(text)
511 511
512 512
513 513 @templatefilter(b'emailuser', intype=bytes)
514 514 def emailuser(text):
515 515 """Any text. Returns the user portion of an email address."""
516 516 return stringutil.emailuser(text)
517 517
518 518
519 519 @templatefilter(b'utf8', intype=bytes)
520 520 def utf8(text):
521 521 """Any text. Converts from the local character encoding to UTF-8."""
522 522 return encoding.fromlocal(text)
523 523
524 524
525 525 @templatefilter(b'xmlescape', intype=bytes)
526 526 def xmlescape(text):
527 527 text = (
528 528 text.replace(b'&', b'&amp;')
529 529 .replace(b'<', b'&lt;')
530 530 .replace(b'>', b'&gt;')
531 531 .replace(b'"', b'&quot;')
532 532 .replace(b"'", b'&#39;')
533 533 ) # &apos; invalid in HTML
534 534 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
535 535
536 536
537 537 def websub(text, websubtable):
538 538 """:websub: Any text. Only applies to hgweb. Applies the regular
539 539 expression replacements defined in the websub section.
540 540 """
541 541 if websubtable:
542 542 for regexp, format in websubtable:
543 543 text = regexp.sub(format, text)
544 544 return text
545 545
546 546
547 547 def loadfilter(ui, extname, registrarobj):
548 548 """Load template filter from specified registrarobj"""
549 549 for name, func in registrarobj._table.items():
550 550 filters[name] = func
551 551
552 552
553 553 # tell hggettext to extract docstrings from these functions:
554 554 i18nfunctions = filters.values()
General Comments 0
You need to be logged in to leave comments. Login now