comparison mercurial/templatefilters.py @ 28693:11f623b5668f

templatefilters: use templatefilter to mark a function as template filter Using decorator can localize changes for adding (or removing) a template filter function in source code. This patch also removes leading ":FILTER:" part in help document of each filters, because using templatefilter makes it useless. This patch uses not 'filter' but 'templatefilter' as a decorator name, because the former name hides Python built-in one, even though the latter is a little redundant in 'templatefilters.py'.
author FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
date Wed, 30 Mar 2016 02:10:44 +0900
parents 6b3b958daf03
children 032c4c2f802a
comparison
equal deleted inserted replaced
28692:6b3b958daf03 28693:11f623b5668f
15 15
16 from . import ( 16 from . import (
17 encoding, 17 encoding,
18 hbisect, 18 hbisect,
19 node, 19 node,
20 registrar,
20 templatekw, 21 templatekw,
21 util, 22 util,
22 ) 23 )
23 24
25 # filters are callables like:
26 # fn(obj)
27 # with:
28 # obj - object to be filtered (text, date, list and so on)
29 filters = {}
30
31 templatefilter = registrar.templatefilter(filters)
32
33 @templatefilter('addbreaks')
24 def addbreaks(text): 34 def addbreaks(text):
25 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of 35 """Any text. Add an XHTML "<br />" tag before the end of
26 every line except the last. 36 every line except the last.
27 """ 37 """
28 return text.replace('\n', '<br/>\n') 38 return text.replace('\n', '<br/>\n')
29 39
30 agescales = [("year", 3600 * 24 * 365, 'Y'), 40 agescales = [("year", 3600 * 24 * 365, 'Y'),
33 ("day", 3600 * 24, 'd'), 43 ("day", 3600 * 24, 'd'),
34 ("hour", 3600, 'h'), 44 ("hour", 3600, 'h'),
35 ("minute", 60, 'm'), 45 ("minute", 60, 'm'),
36 ("second", 1, 's')] 46 ("second", 1, 's')]
37 47
48 @templatefilter('age')
38 def age(date, abbrev=False): 49 def age(date, abbrev=False):
39 """:age: Date. Returns a human-readable date/time difference between the 50 """Date. Returns a human-readable date/time difference between the
40 given date/time and the current date/time. 51 given date/time and the current date/time.
41 """ 52 """
42 53
43 def plural(t, c): 54 def plural(t, c):
44 if c == 1: 55 if c == 1:
67 if n >= 2 or s == 1: 78 if n >= 2 or s == 1:
68 if future: 79 if future:
69 return '%s from now' % fmt(t, n, a) 80 return '%s from now' % fmt(t, n, a)
70 return '%s ago' % fmt(t, n, a) 81 return '%s ago' % fmt(t, n, a)
71 82
83 @templatefilter('basename')
72 def basename(path): 84 def basename(path):
73 """:basename: Any text. Treats the text as a path, and returns the last 85 """Any text. Treats the text as a path, and returns the last
74 component of the path after splitting by the path separator 86 component of the path after splitting by the path separator
75 (ignoring trailing separators). For example, "foo/bar/baz" becomes 87 (ignoring trailing separators). For example, "foo/bar/baz" becomes
76 "baz" and "foo/bar//" becomes "bar". 88 "baz" and "foo/bar//" becomes "bar".
77 """ 89 """
78 return os.path.basename(path) 90 return os.path.basename(path)
79 91
92 @templatefilter('count')
80 def count(i): 93 def count(i):
81 """:count: List or text. Returns the length as an integer.""" 94 """List or text. Returns the length as an integer."""
82 return len(i) 95 return len(i)
83 96
97 @templatefilter('domain')
84 def domain(author): 98 def domain(author):
85 """:domain: Any text. Finds the first string that looks like an email 99 """Any text. Finds the first string that looks like an email
86 address, and extracts just the domain component. Example: ``User 100 address, and extracts just the domain component. Example: ``User
87 <user@example.com>`` becomes ``example.com``. 101 <user@example.com>`` becomes ``example.com``.
88 """ 102 """
89 f = author.find('@') 103 f = author.find('@')
90 if f == -1: 104 if f == -1:
93 f = author.find('>') 107 f = author.find('>')
94 if f >= 0: 108 if f >= 0:
95 author = author[:f] 109 author = author[:f]
96 return author 110 return author
97 111
112 @templatefilter('email')
98 def email(text): 113 def email(text):
99 """:email: Any text. Extracts the first string that looks like an email 114 """Any text. Extracts the first string that looks like an email
100 address. Example: ``User <user@example.com>`` becomes 115 address. Example: ``User <user@example.com>`` becomes
101 ``user@example.com``. 116 ``user@example.com``.
102 """ 117 """
103 return util.email(text) 118 return util.email(text)
104 119
120 @templatefilter('escape')
105 def escape(text): 121 def escape(text):
106 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<" 122 """Any text. Replaces the special XML/XHTML characters "&", "<"
107 and ">" with XML entities, and filters out NUL characters. 123 and ">" with XML entities, and filters out NUL characters.
108 """ 124 """
109 return cgi.escape(text.replace('\0', ''), True) 125 return cgi.escape(text.replace('\0', ''), True)
110 126
111 para_re = None 127 para_re = None
135 151
136 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)), 152 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
137 width, initindent, hangindent) + rest 153 width, initindent, hangindent) + rest
138 for para, rest in findparas()]) 154 for para, rest in findparas()])
139 155
156 @templatefilter('fill68')
140 def fill68(text): 157 def fill68(text):
141 """:fill68: Any text. Wraps the text to fit in 68 columns.""" 158 """Any text. Wraps the text to fit in 68 columns."""
142 return fill(text, 68) 159 return fill(text, 68)
143 160
161 @templatefilter('fill76')
144 def fill76(text): 162 def fill76(text):
145 """:fill76: Any text. Wraps the text to fit in 76 columns.""" 163 """Any text. Wraps the text to fit in 76 columns."""
146 return fill(text, 76) 164 return fill(text, 76)
147 165
166 @templatefilter('firstline')
148 def firstline(text): 167 def firstline(text):
149 """:firstline: Any text. Returns the first line of text.""" 168 """Any text. Returns the first line of text."""
150 try: 169 try:
151 return text.splitlines(True)[0].rstrip('\r\n') 170 return text.splitlines(True)[0].rstrip('\r\n')
152 except IndexError: 171 except IndexError:
153 return '' 172 return ''
154 173
174 @templatefilter('hex')
155 def hexfilter(text): 175 def hexfilter(text):
156 """:hex: Any text. Convert a binary Mercurial node identifier into 176 """Any text. Convert a binary Mercurial node identifier into
157 its long hexadecimal representation. 177 its long hexadecimal representation.
158 """ 178 """
159 return node.hex(text) 179 return node.hex(text)
160 180
181 @templatefilter('hgdate')
161 def hgdate(text): 182 def hgdate(text):
162 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993 183 """Date. Returns the date as a pair of numbers: "1157407993
163 25200" (Unix timestamp, timezone offset). 184 25200" (Unix timestamp, timezone offset).
164 """ 185 """
165 return "%d %d" % text 186 return "%d %d" % text
166 187
188 @templatefilter('isodate')
167 def isodate(text): 189 def isodate(text):
168 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00 190 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
169 +0200". 191 +0200".
170 """ 192 """
171 return util.datestr(text, '%Y-%m-%d %H:%M %1%2') 193 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
172 194
195 @templatefilter('isodatesec')
173 def isodatesec(text): 196 def isodatesec(text):
174 """:isodatesec: Date. Returns the date in ISO 8601 format, including 197 """Date. Returns the date in ISO 8601 format, including
175 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date 198 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
176 filter. 199 filter.
177 """ 200 """
178 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2') 201 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
179 202
190 yield l 213 yield l
191 if i < num_lines - 1 or endswithnewline: 214 if i < num_lines - 1 or endswithnewline:
192 yield '\n' 215 yield '\n'
193 return "".join(indenter()) 216 return "".join(indenter())
194 217
218 @templatefilter('json')
195 def json(obj): 219 def json(obj):
196 if obj is None or obj is False or obj is True: 220 if obj is None or obj is False or obj is True:
197 return {None: 'null', False: 'false', True: 'true'}[obj] 221 return {None: 'null', False: 'false', True: 'true'}[obj]
198 elif isinstance(obj, int) or isinstance(obj, float): 222 elif isinstance(obj, int) or isinstance(obj, float):
199 return str(obj) 223 return str(obj)
213 elif util.safehasattr(obj, '__call__'): 237 elif util.safehasattr(obj, '__call__'):
214 return json(obj()) 238 return json(obj())
215 else: 239 else:
216 raise TypeError('cannot encode type %s' % obj.__class__.__name__) 240 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
217 241
242 @templatefilter('lower')
218 def lower(text): 243 def lower(text):
219 """:lower: Any text. Converts the text to lowercase.""" 244 """Any text. Converts the text to lowercase."""
220 return encoding.lower(text) 245 return encoding.lower(text)
221 246
247 @templatefilter('nonempty')
222 def nonempty(str): 248 def nonempty(str):
223 """:nonempty: Any text. Returns '(none)' if the string is empty.""" 249 """Any text. Returns '(none)' if the string is empty."""
224 return str or "(none)" 250 return str or "(none)"
225 251
252 @templatefilter('obfuscate')
226 def obfuscate(text): 253 def obfuscate(text):
227 """:obfuscate: Any text. Returns the input text rendered as a sequence of 254 """Any text. Returns the input text rendered as a sequence of
228 XML entities. 255 XML entities.
229 """ 256 """
230 text = unicode(text, encoding.encoding, 'replace') 257 text = unicode(text, encoding.encoding, 'replace')
231 return ''.join(['&#%d;' % ord(c) for c in text]) 258 return ''.join(['&#%d;' % ord(c) for c in text])
232 259
260 @templatefilter('permissions')
233 def permissions(flags): 261 def permissions(flags):
234 if "l" in flags: 262 if "l" in flags:
235 return "lrwxrwxrwx" 263 return "lrwxrwxrwx"
236 if "x" in flags: 264 if "x" in flags:
237 return "-rwxr-xr-x" 265 return "-rwxr-xr-x"
238 return "-rw-r--r--" 266 return "-rw-r--r--"
239 267
268 @templatefilter('person')
240 def person(author): 269 def person(author):
241 """:person: Any text. Returns the name before an email address, 270 """Any text. Returns the name before an email address,
242 interpreting it as per RFC 5322. 271 interpreting it as per RFC 5322.
243 272
244 >>> person('foo@bar') 273 >>> person('foo@bar')
245 'foo' 274 'foo'
246 >>> person('Foo Bar <foo@bar>') 275 >>> person('Foo Bar <foo@bar>')
262 if f != -1: 291 if f != -1:
263 return author[:f].strip(' "').replace('\\"', '"') 292 return author[:f].strip(' "').replace('\\"', '"')
264 f = author.find('@') 293 f = author.find('@')
265 return author[:f].replace('.', ' ') 294 return author[:f].replace('.', ' ')
266 295
296 @templatefilter('revescape')
267 def revescape(text): 297 def revescape(text):
268 """:revescape: Any text. Escapes all "special" characters, except @. 298 """Any text. Escapes all "special" characters, except @.
269 Forward slashes are escaped twice to prevent web servers from prematurely 299 Forward slashes are escaped twice to prevent web servers from prematurely
270 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz". 300 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
271 """ 301 """
272 return urllib.quote(text, safe='/@').replace('/', '%252F') 302 return urllib.quote(text, safe='/@').replace('/', '%252F')
273 303
304 @templatefilter('rfc3339date')
274 def rfc3339date(text): 305 def rfc3339date(text):
275 """:rfc3339date: Date. Returns a date using the Internet date format 306 """Date. Returns a date using the Internet date format
276 specified in RFC 3339: "2009-08-18T13:00:13+02:00". 307 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
277 """ 308 """
278 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2") 309 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
279 310
311 @templatefilter('rfc822date')
280 def rfc822date(text): 312 def rfc822date(text):
281 """:rfc822date: Date. Returns a date using the same format used in email 313 """Date. Returns a date using the same format used in email
282 headers: "Tue, 18 Aug 2009 13:00:13 +0200". 314 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
283 """ 315 """
284 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2") 316 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
285 317
318 @templatefilter('short')
286 def short(text): 319 def short(text):
287 """:short: Changeset hash. Returns the short form of a changeset hash, 320 """Changeset hash. Returns the short form of a changeset hash,
288 i.e. a 12 hexadecimal digit string. 321 i.e. a 12 hexadecimal digit string.
289 """ 322 """
290 return text[:12] 323 return text[:12]
291 324
325 @templatefilter('shortbisect')
292 def shortbisect(text): 326 def shortbisect(text):
293 """:shortbisect: Any text. Treats `text` as a bisection status, and 327 """Any text. Treats `text` as a bisection status, and
294 returns a single-character representing the status (G: good, B: bad, 328 returns a single-character representing the status (G: good, B: bad,
295 S: skipped, U: untested, I: ignored). Returns single space if `text` 329 S: skipped, U: untested, I: ignored). Returns single space if `text`
296 is not a valid bisection status. 330 is not a valid bisection status.
297 """ 331 """
298 return hbisect.shortlabel(text) or ' ' 332 return hbisect.shortlabel(text) or ' '
299 333
334 @templatefilter('shortdate')
300 def shortdate(text): 335 def shortdate(text):
301 """:shortdate: Date. Returns a date like "2006-09-18".""" 336 """Date. Returns a date like "2006-09-18"."""
302 return util.shortdate(text) 337 return util.shortdate(text)
303 338
339 @templatefilter('splitlines')
304 def splitlines(text): 340 def splitlines(text):
305 """:splitlines: Any text. Split text into a list of lines.""" 341 """Any text. Split text into a list of lines."""
306 return templatekw.showlist('line', text.splitlines(), 'lines') 342 return templatekw.showlist('line', text.splitlines(), 'lines')
307 343
344 @templatefilter('stringescape')
308 def stringescape(text): 345 def stringescape(text):
309 return text.encode('string_escape') 346 return text.encode('string_escape')
310 347
348 @templatefilter('stringify')
311 def stringify(thing): 349 def stringify(thing):
312 """:stringify: Any type. Turns the value into text by converting values into 350 """Any type. Turns the value into text by converting values into
313 text and concatenating them. 351 text and concatenating them.
314 """ 352 """
315 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str): 353 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
316 return "".join([stringify(t) for t in thing if t is not None]) 354 return "".join([stringify(t) for t in thing if t is not None])
317 if thing is None: 355 if thing is None:
318 return "" 356 return ""
319 return str(thing) 357 return str(thing)
320 358
359 @templatefilter('stripdir')
321 def stripdir(text): 360 def stripdir(text):
322 """:stripdir: Treat the text as path and strip a directory level, if 361 """Treat the text as path and strip a directory level, if
323 possible. For example, "foo" and "foo/bar" becomes "foo". 362 possible. For example, "foo" and "foo/bar" becomes "foo".
324 """ 363 """
325 dir = os.path.dirname(text) 364 dir = os.path.dirname(text)
326 if dir == "": 365 if dir == "":
327 return os.path.basename(text) 366 return os.path.basename(text)
328 else: 367 else:
329 return dir 368 return dir
330 369
370 @templatefilter('tabindent')
331 def tabindent(text): 371 def tabindent(text):
332 """:tabindent: Any text. Returns the text, with every non-empty line 372 """Any text. Returns the text, with every non-empty line
333 except the first starting with a tab character. 373 except the first starting with a tab character.
334 """ 374 """
335 return indent(text, '\t') 375 return indent(text, '\t')
336 376
377 @templatefilter('upper')
337 def upper(text): 378 def upper(text):
338 """:upper: Any text. Converts the text to uppercase.""" 379 """Any text. Converts the text to uppercase."""
339 return encoding.upper(text) 380 return encoding.upper(text)
340 381
382 @templatefilter('urlescape')
341 def urlescape(text): 383 def urlescape(text):
342 """:urlescape: Any text. Escapes all "special" characters. For example, 384 """Any text. Escapes all "special" characters. For example,
343 "foo bar" becomes "foo%20bar". 385 "foo bar" becomes "foo%20bar".
344 """ 386 """
345 return urllib.quote(text) 387 return urllib.quote(text)
346 388
389 @templatefilter('user')
347 def userfilter(text): 390 def userfilter(text):
348 """:user: Any text. Returns a short representation of a user name or email 391 """Any text. Returns a short representation of a user name or email
349 address.""" 392 address."""
350 return util.shortuser(text) 393 return util.shortuser(text)
351 394
395 @templatefilter('emailuser')
352 def emailuser(text): 396 def emailuser(text):
353 """:emailuser: Any text. Returns the user portion of an email address.""" 397 """Any text. Returns the user portion of an email address."""
354 return util.emailuser(text) 398 return util.emailuser(text)
355 399
400 @templatefilter('utf8')
356 def utf8(text): 401 def utf8(text):
357 """:utf8: Any text. Converts from the local character encoding to UTF-8.""" 402 """Any text. Converts from the local character encoding to UTF-8."""
358 return encoding.fromlocal(text) 403 return encoding.fromlocal(text)
359 404
405 @templatefilter('xmlescape')
360 def xmlescape(text): 406 def xmlescape(text):
361 text = (text 407 text = (text
362 .replace('&', '&amp;') 408 .replace('&', '&amp;')
363 .replace('<', '&lt;') 409 .replace('<', '&lt;')
364 .replace('>', '&gt;') 410 .replace('>', '&gt;')
365 .replace('"', '&quot;') 411 .replace('"', '&quot;')
366 .replace("'", '&#39;')) # &apos; invalid in HTML 412 .replace("'", '&#39;')) # &apos; invalid in HTML
367 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text) 413 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
368 414
369 filters = {
370 "addbreaks": addbreaks,
371 "age": age,
372 "basename": basename,
373 "count": count,
374 "domain": domain,
375 "email": email,
376 "escape": escape,
377 "fill68": fill68,
378 "fill76": fill76,
379 "firstline": firstline,
380 "hex": hexfilter,
381 "hgdate": hgdate,
382 "isodate": isodate,
383 "isodatesec": isodatesec,
384 "json": json,
385 "lower": lower,
386 "nonempty": nonempty,
387 "obfuscate": obfuscate,
388 "permissions": permissions,
389 "person": person,
390 "revescape": revescape,
391 "rfc3339date": rfc3339date,
392 "rfc822date": rfc822date,
393 "short": short,
394 "shortbisect": shortbisect,
395 "shortdate": shortdate,
396 "splitlines": splitlines,
397 "stringescape": stringescape,
398 "stringify": stringify,
399 "stripdir": stripdir,
400 "tabindent": tabindent,
401 "upper": upper,
402 "urlescape": urlescape,
403 "user": userfilter,
404 "emailuser": emailuser,
405 "utf8": utf8,
406 "xmlescape": xmlescape,
407 }
408
409 def websub(text, websubtable): 415 def websub(text, websubtable):
410 """:websub: Any text. Only applies to hgweb. Applies the regular 416 """:websub: Any text. Only applies to hgweb. Applies the regular
411 expression replacements defined in the websub section. 417 expression replacements defined in the websub section.
412 """ 418 """
413 if websubtable: 419 if websubtable: