templatefilters.py
432 lines
| 12.4 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 | |||
Gregory Szorc
|
r25983 | from __future__ import absolute_import | ||
import cgi | ||||
import os | ||||
import re | ||||
import time | ||||
from . import ( | ||||
encoding, | ||||
hbisect, | ||||
node, | ||||
FUJIWARA Katsunori
|
r28693 | registrar, | ||
Gregory Szorc
|
r25983 | templatekw, | ||
util, | ||||
) | ||||
Matt Mackall
|
r5976 | |||
timeless
|
r28883 | urlerr = util.urlerr | ||
urlreq = util.urlreq | ||||
FUJIWARA Katsunori
|
r28693 | # filters are callables like: | ||
# fn(obj) | ||||
# with: | ||||
# obj - object to be filtered (text, date, list and so on) | ||||
filters = {} | ||||
templatefilter = registrar.templatefilter(filters) | ||||
@templatefilter('addbreaks') | ||||
Patrick Mezard
|
r13588 | def addbreaks(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Add an XHTML "<br />" tag before the end of | ||
Patrick Mezard
|
r13591 | 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 | |||
FUJIWARA Katsunori
|
r28693 | @templatefilter('age') | ||
David Soria Parra
|
r19736 | def age(date, abbrev=False): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns a human-readable date/time difference between the | ||
Patrick Mezard
|
r13591 | 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 | |||
FUJIWARA Katsunori
|
r28693 | @templatefilter('basename') | ||
Patrick Mezard
|
r13590 | def basename(path): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Treats the text as a path, and returns the last | ||
Patrick Mezard
|
r13591 | 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) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('count') | ||
Anton Shestakov
|
r22668 | def count(i): | ||
FUJIWARA Katsunori
|
r28693 | """List or text. Returns the length as an integer.""" | ||
Anton Shestakov
|
r22668 | return len(i) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('domain') | ||
Patrick Mezard
|
r13588 | def domain(author): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Finds the first string that looks like an email | ||
Patrick Mezard
|
r13591 | 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 | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('email') | ||
Patrick Mezard
|
r13590 | def email(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Extracts the first string that looks like an email | ||
Patrick Mezard
|
r13591 | address. Example: ``User <user@example.com>`` becomes | ||
``user@example.com``. | ||||
""" | ||||
Patrick Mezard
|
r13590 | return util.email(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('escape') | ||
Patrick Mezard
|
r13590 | def escape(text): | ||
FUJIWARA Katsunori
|
r28693 | """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()]) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('fill68') | ||
Patrick Mezard
|
r13590 | def fill68(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Wraps the text to fit in 68 columns.""" | ||
Patrick Mezard
|
r13590 | return fill(text, 68) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('fill76') | ||
Patrick Mezard
|
r13590 | def fill76(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Wraps the text to fit in 76 columns.""" | ||
Patrick Mezard
|
r13590 | return fill(text, 76) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('firstline') | ||
Matt Mackall
|
r5976 | def firstline(text): | ||
FUJIWARA Katsunori
|
r28693 | """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 '' | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('hex') | ||
Patrick Mezard
|
r13590 | def hexfilter(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Convert a binary Mercurial node identifier into | ||
Patrick Mezard
|
r13591 | its long hexadecimal representation. | ||
""" | ||||
Patrick Mezard
|
r13590 | return node.hex(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('hgdate') | ||
Patrick Mezard
|
r13590 | def hgdate(text): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns the date as a pair of numbers: "1157407993 | ||
Patrick Mezard
|
r13591 | 25200" (Unix timestamp, timezone offset). | ||
""" | ||||
Patrick Mezard
|
r13590 | return "%d %d" % text | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('isodate') | ||
Patrick Mezard
|
r13590 | def isodate(text): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00 | ||
Patrick Mezard
|
r13591 | +0200". | ||
""" | ||||
Patrick Mezard
|
r13590 | return util.datestr(text, '%Y-%m-%d %H:%M %1%2') | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('isodatesec') | ||
Patrick Mezard
|
r13590 | def isodatesec(text): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns the date in ISO 8601 format, including | ||
Patrick Mezard
|
r13591 | 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()) | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('json') | ||
Yuya Nishihara
|
r31782 | def json(obj, paranoid=True): | ||
Yuya Nishihara
|
r31780 | if obj is None: | ||
return 'null' | ||||
elif obj is False: | ||||
return 'false' | ||||
elif obj is True: | ||||
return 'true' | ||||
Matt Harbison
|
r31728 | elif isinstance(obj, (int, long, float)): | ||
Dirkjan Ochtman
|
r6691 | return str(obj) | ||
elif isinstance(obj, str): | ||||
Yuya Nishihara
|
r31782 | return '"%s"' % encoding.jsonescape(obj, paranoid=paranoid) | ||
Augie Fackler
|
r14967 | elif util.safehasattr(obj, 'keys'): | ||
Yuya Nishihara
|
r31781 | out = ['%s: %s' % (json(k), json(v)) | ||
for k, v in sorted(obj.iteritems())] | ||||
Dirkjan Ochtman
|
r6691 | return '{' + ', '.join(out) + '}' | ||
Augie Fackler
|
r14944 | elif util.safehasattr(obj, '__iter__'): | ||
Yuya Nishihara
|
r31781 | out = [json(i) for i in obj] | ||
Dirkjan Ochtman
|
r6691 | return '[' + ', '.join(out) + ']' | ||
else: | ||||
raise TypeError('cannot encode type %s' % obj.__class__.__name__) | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('lower') | ||
Yuya Nishihara
|
r24566 | def lower(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Converts the text to lowercase.""" | ||
Yuya Nishihara
|
r24566 | return encoding.lower(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('nonempty') | ||
Patrick Mezard
|
r13588 | def nonempty(str): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Returns '(none)' if the string is empty.""" | ||
Patrick Mezard
|
r13588 | return str or "(none)" | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('obfuscate') | ||
Patrick Mezard
|
r13588 | def obfuscate(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Returns the input text rendered as a sequence of | ||
Patrick Mezard
|
r13591 | XML entities. | ||
""" | ||||
Patrick Mezard
|
r13588 | text = unicode(text, encoding.encoding, 'replace') | ||
return ''.join(['&#%d;' % ord(c) for c in text]) | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('permissions') | ||
Patrick Mezard
|
r13588 | def permissions(flags): | ||
if "l" in flags: | ||||
return "lrwxrwxrwx" | ||||
if "x" in flags: | ||||
return "-rwxr-xr-x" | ||||
return "-rw-r--r--" | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('person') | ||
Patrick Mezard
|
r13588 | def person(author): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Returns the name before an email address, | ||
"Yann E. MORIN"
|
r16235 | 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 | |||
FUJIWARA Katsunori
|
r28693 | @templatefilter('revescape') | ||
r25778 | def revescape(text): | |||
FUJIWARA Katsunori
|
r28693 | """Any text. Escapes all "special" characters, except @. | ||
r25778 | Forward slashes are escaped twice to prevent web servers from prematurely | |||
unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz". | ||||
""" | ||||
timeless
|
r28883 | return urlreq.quote(text, safe='/@').replace('/', '%252F') | ||
r25778 | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('rfc3339date') | ||
Patrick Mezard
|
r13590 | def rfc3339date(text): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns a date using the Internet date format | ||
Patrick Mezard
|
r13591 | 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") | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('rfc822date') | ||
Patrick Mezard
|
r13590 | def rfc822date(text): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns a date using the same format used in email | ||
Patrick Mezard
|
r13591 | 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") | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('short') | ||
Patrick Mezard
|
r13590 | def short(text): | ||
FUJIWARA Katsunori
|
r28693 | """Changeset hash. Returns the short form of a changeset hash, | ||
Patrick Mezard
|
r13591 | i.e. a 12 hexadecimal digit string. | ||
""" | ||||
Patrick Mezard
|
r13590 | return text[:12] | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('shortbisect') | ||
"Yann E. MORIN"
|
r15155 | def shortbisect(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Treats `text` as a bisection status, and | ||
"Yann E. MORIN"
|
r15155 | 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 ' ' | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('shortdate') | ||
Patrick Mezard
|
r13590 | def shortdate(text): | ||
FUJIWARA Katsunori
|
r28693 | """Date. Returns a date like "2006-09-18".""" | ||
Patrick Mezard
|
r13590 | return util.shortdate(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('splitlines') | ||
Ryan McElroy
|
r21820 | def splitlines(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Split text into a list of lines.""" | ||
Yuya Nishihara
|
r32037 | return templatekw.showlist('line', text.splitlines(), {}, plural='lines') | ||
Ryan McElroy
|
r21820 | |||
FUJIWARA Katsunori
|
r28693 | @templatefilter('stringescape') | ||
Patrick Mezard
|
r13590 | def stringescape(text): | ||
Yuya Nishihara
|
r31451 | return util.escapestr(text) | ||
Patrick Mezard
|
r13590 | |||
FUJIWARA Katsunori
|
r28693 | @templatefilter('stringify') | ||
Patrick Mezard
|
r13588 | def stringify(thing): | ||
FUJIWARA Katsunori
|
r28693 | """Any type. Turns the value into text by converting values into | ||
Patrick Mezard
|
r13591 | text and concatenating them. | ||
""" | ||||
Yuya Nishihara
|
r31880 | thing = templatekw.unwraphybrid(thing) | ||
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) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('stripdir') | ||
Aleix Conchillo Flaque
|
r8158 | def stripdir(text): | ||
FUJIWARA Katsunori
|
r28693 | """Treat the text as path and strip a directory level, if | ||
Patrick Mezard
|
r13591 | 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 | ||||
FUJIWARA Katsunori
|
r28693 | @templatefilter('tabindent') | ||
Patrick Mezard
|
r13590 | def tabindent(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Returns the text, with every non-empty line | ||
Matt Mackall
|
r19467 | except the first starting with a tab character. | ||
Patrick Mezard
|
r13591 | """ | ||
Patrick Mezard
|
r13590 | return indent(text, '\t') | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('upper') | ||
Yuya Nishihara
|
r24566 | def upper(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Converts the text to uppercase.""" | ||
Yuya Nishihara
|
r24566 | return encoding.upper(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('urlescape') | ||
Patrick Mezard
|
r13590 | def urlescape(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Escapes all "special" characters. For example, | ||
Patrick Mezard
|
r13591 | "foo bar" becomes "foo%20bar". | ||
""" | ||||
timeless
|
r28883 | return urlreq.quote(text) | ||
Patrick Mezard
|
r13590 | |||
FUJIWARA Katsunori
|
r28693 | @templatefilter('user') | ||
Patrick Mezard
|
r13590 | def userfilter(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Returns a short representation of a user name or email | ||
Matteo Capobianco
|
r16360 | address.""" | ||
Patrick Mezard
|
r13590 | return util.shortuser(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('emailuser') | ||
Matteo Capobianco
|
r16360 | def emailuser(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Returns the user portion of an email address.""" | ||
Matteo Capobianco
|
r16360 | return util.emailuser(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('utf8') | ||
Yuya Nishihara
|
r28209 | def utf8(text): | ||
FUJIWARA Katsunori
|
r28693 | """Any text. Converts from the local character encoding to UTF-8.""" | ||
Yuya Nishihara
|
r28209 | return encoding.fromlocal(text) | ||
FUJIWARA Katsunori
|
r28693 | @templatefilter('xmlescape') | ||
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 | |||
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 | ||||
FUJIWARA Katsunori
|
r28692 | def loadfilter(ui, extname, registrarobj): | ||
"""Load template filter from specified registrarobj | ||||
""" | ||||
for name, func in registrarobj._table.iteritems(): | ||||
filters[name] = func | ||||
Patrick Mezard
|
r13591 | # tell hggettext to extract docstrings from these functions: | ||
i18nfunctions = filters.values() | ||||