templatefilters.py
441 lines
| 13.1 KiB
| text/x-python
|
PythonLexer
/ mercurial / templatefilters.py
Matt Mackall
|
r5976 | # template-filters.py - common template expansion filters | ||
# | ||||
# Copyright 2005-2008 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Matt Mackall
|
r5976 | |||
FUJIWARA Katsunori
|
r11297 | import cgi, re, os, time, urllib | ||
Sean Farley
|
r19227 | import encoding, node, util | ||
"Yann E. MORIN"
|
r15155 | import hbisect | ||
Ryan McElroy
|
r21820 | import templatekw | ||
Matt Mackall
|
r5976 | |||
Patrick Mezard
|
r13588 | def addbreaks(text): | ||
Patrick Mezard
|
r13591 | """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of | ||
every line except the last. | ||||
""" | ||||
Patrick Mezard
|
r13588 | return text.replace('\n', '<br/>\n') | ||
Dirkjan Ochtman
|
r8360 | |||
David Soria Parra
|
r19736 | agescales = [("year", 3600 * 24 * 365, 'Y'), | ||
("month", 3600 * 24 * 30, 'M'), | ||||
("week", 3600 * 24 * 7, 'W'), | ||||
("day", 3600 * 24, 'd'), | ||||
("hour", 3600, 'h'), | ||||
("minute", 60, 'm'), | ||||
("second", 1, 's')] | ||||
Matt Mackall
|
r5976 | |||
David Soria Parra
|
r19736 | def age(date, abbrev=False): | ||
Patrick Mezard
|
r13591 | """:age: Date. Returns a human-readable date/time difference between the | ||
given date/time and the current date/time. | ||||
""" | ||||
Matt Mackall
|
r5976 | |||
def plural(t, c): | ||||
if c == 1: | ||||
return t | ||||
return t + "s" | ||||
David Soria Parra
|
r19736 | def fmt(t, c, a): | ||
if abbrev: | ||||
return "%d%s" % (c, a) | ||||
Matt Mackall
|
r5976 | return "%d %s" % (c, plural(t, c)) | ||
now = time.time() | ||||
then = date[0] | ||||
timeless
|
r13666 | future = False | ||
Dirkjan Ochtman
|
r7682 | if then > now: | ||
timeless
|
r13666 | future = True | ||
delta = max(1, int(then - now)) | ||||
if delta > agescales[0][1] * 30: | ||||
return 'in the distant future' | ||||
else: | ||||
delta = max(1, int(now - then)) | ||||
if delta > agescales[0][1] * 2: | ||||
return util.shortdate(date) | ||||
Dirkjan Ochtman
|
r9722 | |||
David Soria Parra
|
r19736 | for t, s, a in agescales: | ||
Alejandro Santos
|
r9029 | n = delta // s | ||
Matt Mackall
|
r5976 | if n >= 2 or s == 1: | ||
timeless
|
r13666 | if future: | ||
David Soria Parra
|
r19736 | return '%s from now' % fmt(t, n, a) | ||
return '%s ago' % fmt(t, n, a) | ||||
Matt Mackall
|
r5976 | |||
Patrick Mezard
|
r13590 | def basename(path): | ||
Patrick Mezard
|
r13591 | """:basename: Any text. Treats the text as a path, and returns the last | ||
component of the path after splitting by the path separator | ||||
(ignoring trailing separators). For example, "foo/bar/baz" becomes | ||||
"baz" and "foo/bar//" becomes "bar". | ||||
""" | ||||
Patrick Mezard
|
r13590 | return os.path.basename(path) | ||
Anton Shestakov
|
r22668 | def count(i): | ||
""":count: List or text. Returns the length as an integer.""" | ||||
return len(i) | ||||
Patrick Mezard
|
r13590 | def datefilter(text): | ||
Patrick Mezard
|
r13591 | """:date: Date. Returns a date in a Unix date format, including the | ||
timezone: "Mon Sep 04 15:13:13 2006 0700". | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.datestr(text) | ||
Patrick Mezard
|
r13588 | def domain(author): | ||
Patrick Mezard
|
r13591 | """:domain: Any text. Finds the first string that looks like an email | ||
address, and extracts just the domain component. Example: ``User | ||||
<user@example.com>`` becomes ``example.com``. | ||||
""" | ||||
Patrick Mezard
|
r13588 | f = author.find('@') | ||
if f == -1: | ||||
return '' | ||||
author = author[f + 1:] | ||||
f = author.find('>') | ||||
if f >= 0: | ||||
author = author[:f] | ||||
return author | ||||
Patrick Mezard
|
r13590 | def email(text): | ||
Patrick Mezard
|
r13591 | """:email: Any text. Extracts the first string that looks like an email | ||
address. Example: ``User <user@example.com>`` becomes | ||||
``user@example.com``. | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.email(text) | ||
def escape(text): | ||||
Patrick Mezard
|
r13591 | """:escape: Any text. Replaces the special XML/XHTML characters "&", "<" | ||
Siddharth Agarwal
|
r17772 | and ">" with XML entities, and filters out NUL characters. | ||
Patrick Mezard
|
r13591 | """ | ||
Siddharth Agarwal
|
r17772 | return cgi.escape(text.replace('\0', ''), True) | ||
Patrick Mezard
|
r13590 | |||
Matt Mackall
|
r5976 | para_re = None | ||
space_re = None | ||||
Mads Kiilerich
|
r19872 | def fill(text, width, initindent='', hangindent=''): | ||
Sean Farley
|
r19228 | '''fill many paragraphs with optional indentation.''' | ||
Matt Mackall
|
r5976 | global para_re, space_re | ||
if para_re is None: | ||||
para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M) | ||||
space_re = re.compile(r' +') | ||||
def findparas(): | ||||
start = 0 | ||||
while True: | ||||
m = para_re.search(text, start) | ||||
if not m: | ||||
FUJIWARA Katsunori
|
r11297 | uctext = unicode(text[start:], encoding.encoding) | ||
w = len(uctext) | ||||
while 0 < w and uctext[w - 1].isspace(): | ||||
Matt Mackall
|
r10282 | w -= 1 | ||
FUJIWARA Katsunori
|
r11297 | yield (uctext[:w].encode(encoding.encoding), | ||
uctext[w:].encode(encoding.encoding)) | ||||
Matt Mackall
|
r5976 | break | ||
yield text[start:m.start(0)], m.group(1) | ||||
start = m.end(1) | ||||
Sean Farley
|
r19228 | return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)), | ||
width, initindent, hangindent) + rest | ||||
Matt Mackall
|
r5976 | for para, rest in findparas()]) | ||
Patrick Mezard
|
r13590 | def fill68(text): | ||
Patrick Mezard
|
r13591 | """:fill68: Any text. Wraps the text to fit in 68 columns.""" | ||
Patrick Mezard
|
r13590 | return fill(text, 68) | ||
def fill76(text): | ||||
Patrick Mezard
|
r13591 | """:fill76: Any text. Wraps the text to fit in 76 columns.""" | ||
Patrick Mezard
|
r13590 | return fill(text, 76) | ||
Matt Mackall
|
r5976 | def firstline(text): | ||
Patrick Mezard
|
r13591 | """:firstline: Any text. Returns the first line of text.""" | ||
Matt Mackall
|
r5976 | try: | ||
Nicolas Dumazet
|
r9136 | return text.splitlines(True)[0].rstrip('\r\n') | ||
Matt Mackall
|
r5976 | except IndexError: | ||
return '' | ||||
Patrick Mezard
|
r13590 | def hexfilter(text): | ||
Patrick Mezard
|
r13591 | """:hex: Any text. Convert a binary Mercurial node identifier into | ||
its long hexadecimal representation. | ||||
""" | ||||
Patrick Mezard
|
r13590 | return node.hex(text) | ||
def hgdate(text): | ||||
Patrick Mezard
|
r13591 | """:hgdate: Date. Returns the date as a pair of numbers: "1157407993 | ||
25200" (Unix timestamp, timezone offset). | ||||
""" | ||||
Patrick Mezard
|
r13590 | return "%d %d" % text | ||
def isodate(text): | ||||
Patrick Mezard
|
r13591 | """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00 | ||
+0200". | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.datestr(text, '%Y-%m-%d %H:%M %1%2') | ||
def isodatesec(text): | ||||
Patrick Mezard
|
r13591 | """:isodatesec: Date. Returns the date in ISO 8601 format, including | ||
seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date | ||||
filter. | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2') | ||
Matt Mackall
|
r5976 | def indent(text, prefix): | ||
'''indent each non-empty line of text after first with prefix.''' | ||||
lines = text.splitlines() | ||||
num_lines = len(lines) | ||||
Nicolas Dumazet
|
r9387 | endswithnewline = text[-1:] == '\n' | ||
Matt Mackall
|
r5976 | def indenter(): | ||
for i in xrange(num_lines): | ||||
l = lines[i] | ||||
if i and l.strip(): | ||||
yield prefix | ||||
yield l | ||||
Nicolas Dumazet
|
r9387 | if i < num_lines - 1 or endswithnewline: | ||
Matt Mackall
|
r5976 | yield '\n' | ||
return "".join(indenter()) | ||||
Dirkjan Ochtman
|
r6691 | def json(obj): | ||
if obj is None or obj is False or obj is True: | ||||
return {None: 'null', False: 'false', True: 'true'}[obj] | ||||
elif isinstance(obj, int) or isinstance(obj, float): | ||||
return str(obj) | ||||
elif isinstance(obj, str): | ||||
Yuya Nishihara
|
r11765 | u = unicode(obj, encoding.encoding, 'replace') | ||
Yuya Nishihara
|
r11890 | return '"%s"' % jsonescape(u) | ||
Dirkjan Ochtman
|
r6691 | elif isinstance(obj, unicode): | ||
Yuya Nishihara
|
r11890 | return '"%s"' % jsonescape(obj) | ||
Augie Fackler
|
r14967 | elif util.safehasattr(obj, 'keys'): | ||
Dirkjan Ochtman
|
r6691 | out = [] | ||
Gregory Szorc
|
r23708 | for k, v in sorted(obj.iteritems()): | ||
Dirkjan Ochtman
|
r6691 | s = '%s: %s' % (json(k), json(v)) | ||
out.append(s) | ||||
return '{' + ', '.join(out) + '}' | ||||
Augie Fackler
|
r14944 | elif util.safehasattr(obj, '__iter__'): | ||
Dirkjan Ochtman
|
r6691 | out = [] | ||
for i in obj: | ||||
out.append(json(i)) | ||||
return '[' + ', '.join(out) + ']' | ||||
Gregory Szorc
|
r23707 | elif util.safehasattr(obj, '__call__'): | ||
return json(obj()) | ||||
Dirkjan Ochtman
|
r6691 | else: | ||
raise TypeError('cannot encode type %s' % obj.__class__.__name__) | ||||
Patrick Mezard
|
r13589 | def _uescape(c): | ||
Patrick Mezard
|
r13588 | if ord(c) < 0x80: | ||
return c | ||||
else: | ||||
return '\\u%04x' % ord(c) | ||||
_escapes = [ | ||||
('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'), | ||||
('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'), | ||||
Matt Mackall
|
r21867 | ('<', '\\u003c'), ('>', '\\u003e'), ('\0', '\\u0000') | ||
Patrick Mezard
|
r13588 | ] | ||
def jsonescape(s): | ||||
for k, v in _escapes: | ||||
s = s.replace(k, v) | ||||
Patrick Mezard
|
r13589 | return ''.join(_uescape(c) for c in s) | ||
Patrick Mezard
|
r13588 | |||
Patrick Mezard
|
r13590 | def localdate(text): | ||
Patrick Mezard
|
r13591 | """:localdate: Date. Converts a date to local date.""" | ||
Christian Ebert
|
r17755 | return (util.parsedate(text)[0], util.makedate()[1]) | ||
Patrick Mezard
|
r13590 | |||
Yuya Nishihara
|
r24566 | def lower(text): | ||
""":lower: Any text. Converts the text to lowercase.""" | ||||
return encoding.lower(text) | ||||
Patrick Mezard
|
r13588 | def nonempty(str): | ||
Patrick Mezard
|
r13591 | """:nonempty: Any text. Returns '(none)' if the string is empty.""" | ||
Patrick Mezard
|
r13588 | return str or "(none)" | ||
def obfuscate(text): | ||||
Patrick Mezard
|
r13591 | """:obfuscate: Any text. Returns the input text rendered as a sequence of | ||
XML entities. | ||||
""" | ||||
Patrick Mezard
|
r13588 | text = unicode(text, encoding.encoding, 'replace') | ||
return ''.join(['&#%d;' % ord(c) for c in text]) | ||||
def permissions(flags): | ||||
if "l" in flags: | ||||
return "lrwxrwxrwx" | ||||
if "x" in flags: | ||||
return "-rwxr-xr-x" | ||||
return "-rw-r--r--" | ||||
def person(author): | ||||
"Yann E. MORIN"
|
r16235 | """:person: Any text. Returns the name before an email address, | ||
interpreting it as per RFC 5322. | ||||
"Yann E. MORIN"
|
r16251 | |||
>>> person('foo@bar') | ||||
'foo' | ||||
>>> person('Foo Bar <foo@bar>') | ||||
'Foo Bar' | ||||
>>> person('"Foo Bar" <foo@bar>') | ||||
'Foo Bar' | ||||
>>> person('"Foo \"buz\" Bar" <foo@bar>') | ||||
'Foo "buz" Bar' | ||||
>>> # The following are invalid, but do exist in real-life | ||||
... | ||||
>>> person('Foo "buz" Bar <foo@bar>') | ||||
'Foo "buz" Bar' | ||||
>>> person('"Foo Bar <foo@bar>') | ||||
'Foo Bar' | ||||
"Yann E. MORIN"
|
r16235 | """ | ||
Brodie Rao
|
r16686 | if '@' not in author: | ||
Patrick Mezard
|
r13588 | return author | ||
f = author.find('<') | ||||
Adrian Buehlmann
|
r13951 | if f != -1: | ||
"Yann E. MORIN"
|
r16235 | return author[:f].strip(' "').replace('\\"', '"') | ||
Adrian Buehlmann
|
r13951 | f = author.find('@') | ||
return author[:f].replace('.', ' ') | ||||
Patrick Mezard
|
r13588 | |||
r25778 | def revescape(text): | |||
""":revescape: Any text. Escapes all "special" characters, except @. | ||||
Forward slashes are escaped twice to prevent web servers from prematurely | ||||
unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz". | ||||
""" | ||||
return urllib.quote(text, safe='/@').replace('/', '%252F') | ||||
Patrick Mezard
|
r13590 | def rfc3339date(text): | ||
Patrick Mezard
|
r13591 | """:rfc3339date: Date. Returns a date using the Internet date format | ||
specified in RFC 3339: "2009-08-18T13:00:13+02:00". | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2") | ||
def rfc822date(text): | ||||
Patrick Mezard
|
r13591 | """:rfc822date: Date. Returns a date using the same format used in email | ||
headers: "Tue, 18 Aug 2009 13:00:13 +0200". | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2") | ||
def short(text): | ||||
Patrick Mezard
|
r13591 | """:short: Changeset hash. Returns the short form of a changeset hash, | ||
i.e. a 12 hexadecimal digit string. | ||||
""" | ||||
Patrick Mezard
|
r13590 | return text[:12] | ||
"Yann E. MORIN"
|
r15155 | def shortbisect(text): | ||
""":shortbisect: Any text. Treats `text` as a bisection status, and | ||||
returns a single-character representing the status (G: good, B: bad, | ||||
S: skipped, U: untested, I: ignored). Returns single space if `text` | ||||
is not a valid bisection status. | ||||
""" | ||||
return hbisect.shortlabel(text) or ' ' | ||||
Patrick Mezard
|
r13590 | def shortdate(text): | ||
Patrick Mezard
|
r13591 | """:shortdate: Date. Returns a date like "2006-09-18".""" | ||
Patrick Mezard
|
r13590 | return util.shortdate(text) | ||
Ryan McElroy
|
r21820 | def splitlines(text): | ||
""":splitlines: Any text. Split text into a list of lines.""" | ||||
return templatekw.showlist('line', text.splitlines(), 'lines') | ||||
Patrick Mezard
|
r13590 | def stringescape(text): | ||
return text.encode('string_escape') | ||||
Patrick Mezard
|
r13588 | def stringify(thing): | ||
Patrick Mezard
|
r13591 | """:stringify: Any type. Turns the value into text by converting values into | ||
text and concatenating them. | ||||
""" | ||||
Augie Fackler
|
r14944 | if util.safehasattr(thing, '__iter__') and not isinstance(thing, str): | ||
Patrick Mezard
|
r13588 | return "".join([stringify(t) for t in thing if t is not None]) | ||
Jordi Gutiérrez Hermoso
|
r25000 | if thing is None: | ||
return "" | ||||
Patrick Mezard
|
r13588 | return str(thing) | ||
Patrick Mezard
|
r13590 | def strip(text): | ||
Patrick Mezard
|
r13591 | """:strip: Any text. Strips all leading and trailing whitespace.""" | ||
Patrick Mezard
|
r13590 | return text.strip() | ||
Aleix Conchillo Flaque
|
r8158 | def stripdir(text): | ||
Patrick Mezard
|
r13591 | """:stripdir: Treat the text as path and strip a directory level, if | ||
possible. For example, "foo" and "foo/bar" becomes "foo". | ||||
""" | ||||
Aleix Conchillo Flaque
|
r8158 | dir = os.path.dirname(text) | ||
if dir == "": | ||||
return os.path.basename(text) | ||||
else: | ||||
return dir | ||||
Patrick Mezard
|
r13590 | def tabindent(text): | ||
Matt Mackall
|
r19467 | """:tabindent: Any text. Returns the text, with every non-empty line | ||
except the first starting with a tab character. | ||||
Patrick Mezard
|
r13591 | """ | ||
Patrick Mezard
|
r13590 | return indent(text, '\t') | ||
Yuya Nishihara
|
r24566 | def upper(text): | ||
""":upper: Any text. Converts the text to uppercase.""" | ||||
return encoding.upper(text) | ||||
Patrick Mezard
|
r13590 | def urlescape(text): | ||
Patrick Mezard
|
r13591 | """:urlescape: Any text. Escapes all "special" characters. For example, | ||
"foo bar" becomes "foo%20bar". | ||||
""" | ||||
Patrick Mezard
|
r13590 | return urllib.quote(text) | ||
def userfilter(text): | ||||
Matteo Capobianco
|
r16360 | """:user: Any text. Returns a short representation of a user name or email | ||
address.""" | ||||
Patrick Mezard
|
r13590 | return util.shortuser(text) | ||
Matteo Capobianco
|
r16360 | def emailuser(text): | ||
""":emailuser: Any text. Returns the user portion of an email address.""" | ||||
return util.emailuser(text) | ||||
Patrick Mezard
|
r13588 | def xmlescape(text): | ||
text = (text | ||||
.replace('&', '&') | ||||
.replace('<', '<') | ||||
.replace('>', '>') | ||||
.replace('"', '"') | ||||
.replace("'", ''')) # ' invalid in HTML | ||||
return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text) | ||||
Rocco Rutte
|
r8234 | |||
Matt Mackall
|
r5976 | filters = { | ||
Patrick Mezard
|
r13587 | "addbreaks": addbreaks, | ||
Patrick Mezard
|
r13586 | "age": age, | ||
Patrick Mezard
|
r13590 | "basename": basename, | ||
Anton Shestakov
|
r22668 | "count": count, | ||
Patrick Mezard
|
r13590 | "date": datefilter, | ||
Matt Mackall
|
r5976 | "domain": domain, | ||
Patrick Mezard
|
r13590 | "email": email, | ||
"escape": escape, | ||||
"fill68": fill68, | ||||
"fill76": fill76, | ||||
Matt Mackall
|
r5976 | "firstline": firstline, | ||
Patrick Mezard
|
r13590 | "hex": hexfilter, | ||
"hgdate": hgdate, | ||||
"isodate": isodate, | ||||
"isodatesec": isodatesec, | ||||
Dirkjan Ochtman
|
r8014 | "json": json, | ||
"jsonescape": jsonescape, | ||||
Patrick Mezard
|
r13590 | "localdate": localdate, | ||
Yuya Nishihara
|
r24566 | "lower": lower, | ||
Rocco Rutte
|
r8234 | "nonempty": nonempty, | ||
Matt Mackall
|
r5976 | "obfuscate": obfuscate, | ||
"permissions": permissions, | ||||
"person": person, | ||||
r25778 | "revescape": revescape, | |||
Patrick Mezard
|
r13590 | "rfc3339date": rfc3339date, | ||
"rfc822date": rfc822date, | ||||
"short": short, | ||||
"Yann E. MORIN"
|
r15155 | "shortbisect": shortbisect, | ||
Patrick Mezard
|
r13590 | "shortdate": shortdate, | ||
Ryan McElroy
|
r21820 | "splitlines": splitlines, | ||
Patrick Mezard
|
r13590 | "stringescape": stringescape, | ||
Dirkjan Ochtman
|
r8360 | "stringify": stringify, | ||
Patrick Mezard
|
r13590 | "strip": strip, | ||
Patrick Mezard
|
r13586 | "stripdir": stripdir, | ||
Patrick Mezard
|
r13590 | "tabindent": tabindent, | ||
Yuya Nishihara
|
r24566 | "upper": upper, | ||
Patrick Mezard
|
r13590 | "urlescape": urlescape, | ||
"user": userfilter, | ||||
Matteo Capobianco
|
r16360 | "emailuser": emailuser, | ||
Jesse Glick
|
r6174 | "xmlescape": xmlescape, | ||
Dirkjan Ochtman
|
r6691 | } | ||
Patrick Mezard
|
r13591 | |||
Angel Ezquerra
|
r18627 | def websub(text, websubtable): | ||
""":websub: Any text. Only applies to hgweb. Applies the regular | ||||
expression replacements defined in the websub section. | ||||
""" | ||||
if websubtable: | ||||
for regexp, format in websubtable: | ||||
text = regexp.sub(format, text) | ||||
return text | ||||
Patrick Mezard
|
r13591 | # tell hggettext to extract docstrings from these functions: | ||
i18nfunctions = filters.values() | ||||