# -*- coding: utf-8 -*-
"""
- jinja.filters
- ~~~~~~~~~~~~~
+ jinja2.filters
+ ~~~~~~~~~~~~~~
Bundled jinja filters.
"""
import re
import math
+import textwrap
from random import choice
-try:
- from operator import itemgetter
-except ImportError:
- itemgetter = lambda a: lambda b: b[a]
-from urllib import urlencode, quote
-from itertools import imap
+from operator import itemgetter
+from itertools import imap, groupby
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode
from jinja2.runtime import Undefined
+from jinja2.exceptions import FilterArgumentError
-
-_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
+_word_re = re.compile(r'\w+')
def contextfilter(f):
return f
-def do_replace(s, old, new, count=None):
+def do_forceescape(value):
+ """Enforce HTML escaping. This will probably double escape variables."""
+ if hasattr(value, '__html__'):
+ value = value.__html__()
+ return escape(unicode(value))
+
+
+@environmentfilter
+def do_replace(environment, s, old, new, count=None):
"""Return a copy of the value with all occurrences of a substring
replaced with a new one. The first argument is the substring
that should be replaced, the second is the replacement string.
"""
if count is None:
count = -1
+ if not environment.autoescape:
+ return unicode(s).replace(unicode(old), unicode(new), count)
if hasattr(old, '__html__') or hasattr(new, '__html__') and \
not hasattr(s, '__html__'):
s = escape(s)
return soft_unicode(s).lower()
-def do_escape(s):
- """XML escape ``&``, ``<``, ``>``, and ``"`` in a string of data.
-
- This method will have no effect it the value is already escaped.
- """
- return escape(s)
-
-
-def do_xmlattr(d, autospace=False):
+@environmentfilter
+def do_xmlattr(_environment, d, autospace=True):
"""Create an SGML/XML attribute string based on the items in a dict.
All values that are neither `none` nor `undefined` are automatically
escaped:
.. sourcecode:: html+jinja
- <ul{{ {'class': 'my_list', 'missing': None,
+ <ul{{ {'class': 'my_list', 'missing': none,
'id': 'list-%d'|format(variable)}|xmlattr }}>
...
</ul>
</ul>
As you can see it automatically prepends a space in front of the item
- if the filter returned something. You can disable this by passing
- `false` as only argument to the filter.
+ if the filter returned something unless the second parameter is false.
"""
- if not hasattr(d, 'iteritems'):
- raise TypeError('a dict is required')
- result = []
- for key, value in d.iteritems():
- if value is not None and not isinstance(value, Undefined):
- result.append(u'%s="%s"' % (
- escape(env.to_unicode(key)),
- escape(env.to_unicode(value), True)
- ))
rv = u' '.join(
u'%s="%s"' % (escape(key), escape(value))
for key, value in d.iteritems()
if value is not None and not isinstance(value, Undefined)
)
- if autospace:
- rv = ' ' + rv
- return Markup(rv)
+ if autospace and rv:
+ rv = u' ' + rv
+ if _environment.autoescape:
+ rv = Markup(rv)
+ return rv
def do_capitalize(s):
return value
-def do_join(value, d=u''):
+@environmentfilter
+def do_join(environment, value, d=u''):
"""Return a string which is the concatenation of the strings in the
sequence. The separator between elements is an empty string per
- default, you can define ith with the optional parameter:
+ default, you can define it with the optional parameter:
.. sourcecode:: jinja
{{ [1, 2, 3]|join }}
-> 123
"""
+ # no automatic escaping? joining is a lot eaiser then
+ if not environment.autoescape:
+ return unicode(d).join(imap(unicode, value))
+
# if the delimiter doesn't have an html representation we check
# if any of the items has. If yes we do a coercion to Markup
- if not hasttr(d, '__html__'):
+ if not hasattr(d, '__html__'):
value = list(value)
do_escape = False
for idx, item in enumerate(value):
@environmentfilter
def do_first(environment, seq):
- """Return the frist item of a sequence."""
+ """Return the first item of a sequence."""
try:
return iter(seq).next()
except StopIteration:
- return environment.undefined('seq|first',
- extra='the sequence was empty')
+ return environment.undefined('No first item, sequence was empty.')
@environmentfilter
try:
return iter(reversed(seq)).next()
except StopIteration:
- return environment.undefined('seq|last',
- extra='the sequence was empty')
+ return environment.undefined('No last item, sequence was empty.')
@environmentfilter
try:
return choice(seq)
except IndexError:
- return environment.undefined('seq|random',
- extra='the sequence was empty')
+ return environment.undefined('No random item, sequence was empty.')
def do_filesizeformat(value):
return pformat(value, verbose=verbose)
-def do_urlize(value, trim_url_limit=None, nofollow=False):
+@environmentfilter
+def do_urlize(environment, value, trim_url_limit=None, nofollow=False):
"""Converts URLs in plain text into clickable links.
If you pass the filter an additional integer it will shorten the urls
.. sourcecode:: jinja
- {{ mytext|urlize(40, True) }}
+ {{ mytext|urlize(40, true) }}
links are shortened to 40 chars and defined with rel="nofollow"
"""
- return urlize(soft_unicode(value), trim_url_limit, nofollow)
+ rv = urlize(soft_unicode(value), trim_url_limit, nofollow)
+ if environment.autoescape:
+ rv = Markup(rv)
+ return rv
def do_indent(s, width=4, indentfirst=False):
- """
- {{ s|indent[ width[ indentfirst[ usetab]]] }}
-
- Return a copy of the passed string, each line indented by
+ """Return a copy of the passed string, each line indented by
4 spaces. The first line is not indented. If you want to
change the number of spaces or indent the first line too
you can pass additional parameters to the filter:
.. sourcecode:: jinja
- {{ mytext|indent(2, True) }}
+ {{ mytext|indent(2, true) }}
indent by two spaces and indent the first line too.
"""
indention = ' ' * width
def do_truncate(s, length=255, killwords=False, end='...'):
- """
- Return a truncated copy of the string. The length is specified
+ """Return a truncated copy of the string. The length is specified
with the first parameter which defaults to ``255``. If the second
parameter is ``true`` the filter will cut the text at length. Otherwise
it will try to save the last word. If the text was in fact
return u' '.join(result)
-def do_wordwrap(s, pos=79, hard=False):
+def do_wordwrap(s, width=79, break_long_words=True):
"""
Return a copy of the string passed to the filter wrapped after
- ``79`` characters. You can override this default using the first
- parameter. If you set the second parameter to `true` Jinja will
- also split words apart (usually a bad idea because it makes
- reading hard).
+ ``79`` characters. You can override this default using the first
+ parameter. If you set the second parameter to `false` Jinja will not
+ split words apart if they are longer than `width`.
"""
- if len(s) < pos:
- return s
- if hard:
- return u'\n'.join(s[idx:idx + pos] for idx in
- xrange(0, len(s), pos))
-
- # TODO: switch to wordwrap.wrap
- # code from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
- return reduce(lambda line, word, pos=pos: u'%s%s%s' %
- (line, u' \n'[(len(line)-line.rfind('\n') - 1 +
- len(word.split('\n', 1)[0]) >= pos)],
- word), s.split(' '))
+ return textwrap.wrap(s, width=width, expand_tabs=False,
+ replace_whitespace=False,
+ break_long_words=break_long_words)
def do_wordcount(s):
"""Count the words in that string."""
- return len(x for x in s.split() if x)
+ return len(_word_re.findall(s))
def do_int(value, default=0):
try:
return int(value)
except (TypeError, ValueError):
+ # this quirk is necessary so that "42.23"|int gives 42.
try:
return int(float(value))
except (TypeError, ValueError):
return default
-def do_string(value):
- """Convert the value into an string."""
- return soft_unicode(value)
-
-
def do_format(value, *args, **kwargs):
"""
Apply python string formatting on an object:
{{ "%s - %s"|format("Hello?", "Foo!") }}
-> Hello? - Foo!
"""
- if kwargs:
- kwargs.update(idx, arg in enumerate(args))
- args = kwargs
- return soft_unicode(value) % args
+ if args and kwargs:
+ raise FilterArgumentError('can\'t handle positional and keyword '
+ 'arguments at the same time')
+ return soft_unicode(value) % (kwargs or args)
def do_trim(value):
def do_striptags(value):
"""Strip SGML/XML tags and replace adjacent whitespace by one space.
"""
- return ' '.join(_striptags_re.sub('', value).split())
+ if hasattr(value, '__html__'):
+ value = value.__html__()
+ return Markup(unicode(value)).striptags()
def do_slice(value, slices, fill_with=None):
If you pass it a second argument it's used to fill missing
values on the last iteration.
"""
- result = []
seq = list(value)
length = len(seq)
items_per_slice = length // slices
tmp = seq[start:end]
if fill_with is not None and slice_number >= slices_with_extra:
tmp.append(fill_with)
- result.append(tmp)
- return result
+ yield tmp
def do_batch(value, linecount, fill_with=None):
tmp = []
for item in value:
if len(tmp) == linecount:
- result.append(tmp)
+ yield tmp
tmp = []
tmp.append(item)
if tmp:
if fill_with is not None and len(tmp) < linecount:
tmp += [fill_with] * (linecount - len(tmp))
- result.append(tmp)
- return result
+ yield tmp
def do_round(value, precision=0, method='common'):
def do_sort(value, reverse=False):
"""Sort a sequence. Per default it sorts ascending, if you pass it
- `True` as first argument it will reverse the sorting.
+ true as first argument it will reverse the sorting.
"""
return sorted(value, reverse=reverse)
{% endfor %}
</ul>
+ Additionally it's possible to use tuple unpacking for the grouper and
+ list:
+
+ .. sourcecode:: html+jinja
+
+ <ul>
+ {% for grouper, list in persons|groupby('gender') %}
+ ...
+ {% endfor %}
+ </ul>
+
As you can see the item we're grouping by is stored in the `grouper`
attribute and the `list` contains all the objects that have this grouper
in common.
"""
expr = lambda x: environment.subscribe(x, attribute)
- return sorted([{
- 'grouper': a,
- 'list': b
- } for a, b in groupby(sorted(value, key=expr), expr)],
- key=itemgetter('grouper'))
+ return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
+
+
+class _GroupTuple(tuple):
+ __slots__ = ()
+ grouper = property(itemgetter(0))
+ list = property(itemgetter(1))
+
+ def __new__(cls, (key, value)):
+ return tuple.__new__(cls, (key, list(value)))
+
+
+def do_list(value):
+ """Convert the value into a list. If it was a string the returned list
+ will be a list of characters.
+ """
+ return list(value)
+
+
+def do_mark_safe(value):
+ """Mark the value as safe which means that in an environment with automatic
+ escaping enabled this variable will not be escaped.
+ """
+ return Markup(value)
+
+
+def do_reverse(value):
+ """Reverse the object or return an iterator the iterates over it the other
+ way round.
+ """
+ if isinstance(value, basestring):
+ return value[::-1]
+ try:
+ return reversed(value)
+ except TypeError:
+ try:
+ rv = list(value)
+ rv.reverse()
+ return rv
+ except TypeError:
+ raise FilterArgumentError('argument must be iterable')
FILTERS = {
'replace': do_replace,
'upper': do_upper,
'lower': do_lower,
- 'escape': do_escape,
- 'e': do_escape,
- 'xmlattr': do_xmlattr,
+ 'escape': escape,
+ 'e': escape,
+ 'forceescape': do_forceescape,
'capitalize': do_capitalize,
'title': do_title,
'default': do_default,
+ 'd': do_default,
'join': do_join,
'count': len,
'dictsort': do_dictsort,
'length': len,
- 'reverse': reversed,
+ 'reverse': do_reverse,
'center': do_center,
+ 'indent': do_indent,
'title': do_title,
'capitalize': do_capitalize,
'first': do_first,
'random': do_random,
'filesizeformat': do_filesizeformat,
'pprint': do_pprint,
- 'indent': do_indent,
'truncate': do_truncate,
'wordwrap': do_wordwrap,
'wordcount': do_wordcount,
'int': do_int,
'float': do_float,
- 'string': do_string,
+ 'string': soft_unicode,
+ 'list': do_list,
'urlize': do_urlize,
'format': do_format,
'trim': do_trim,
'round': do_round,
'sort': do_sort,
'groupby': do_groupby,
- 'safe': Markup
+ 'safe': do_mark_safe,
+ 'xmlattr': do_xmlattr
}