live up to @mitsuhiko's ridiculous expectations
[jinja2.git] / jinja2 / filters.py
index 8b66ce807958f2ce5ff9c5ccc3d8029a005c75bb..69e67e2913974f6fe7a3f5d088d2c266bcc8de21 100644 (file)
@@ -1,38 +1,96 @@
 # -*- coding: utf-8 -*-
 """
-    jinja.filters
-    ~~~~~~~~~~~~~
+    jinja2.filters
+    ~~~~~~~~~~~~~~
 
     Bundled jinja filters.
 
-    :copyright: 2008 by Armin Ronacher, Christoph Hack.
+    :copyright: (c) 2010 by the Jinja Team.
     :license: BSD, see LICENSE for more details.
 """
 import re
+import math
+import urllib
 from random import choice
-try:
-    from operator import itemgetter
-except ImportError:
-    itemgetter = lambda a: lambda b: b[a]
-from urllib import urlencode, quote
-from jinja.utils import escape
+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+(?u)')
 
 
 def contextfilter(f):
-    """
-    Decorator for marking context dependent filters. The current context
-    argument will be passed as first argument.
+    """Decorator for marking context dependent filters. The current
+    :class:`Context` will be passed as first argument.
     """
     f.contextfilter = True
     return f
 
 
-def do_replace(s, old, new, count=None):
+def evalcontextfilter(f):
+    """Decorator for marking eval-context dependent filters.  An eval
+    context object is passed as first argument.  For more information
+    about the eval context, see :ref:`eval-context`.
+
+    .. versionadded:: 2.4
+    """
+    f.evalcontextfilter = True
+    return f
+
+
+def environmentfilter(f):
+    """Decorator for marking evironment dependent filters.  The current
+    :class:`Environment` is passed to the filter as first argument.
     """
-    Return a copy of the value with all occurrences of a substring
+    f.environmentfilter = True
+    return f
+
+
+def make_attrgetter(environment, attribute):
+    """Returns a callable that looks up the given attribute from a
+    passed object with the rules of the environment.  Dots are allowed
+    to access attributes of attributes.
+    """
+    if not isinstance(attribute, basestring) or '.' not in attribute:
+        return lambda x: environment.getitem(x, attribute)
+    attribute = attribute.split('.')
+    def attrgetter(item):
+        for part in attribute:
+            item = environment.getitem(item, part)
+        return item
+    return attrgetter
+
+
+def do_forceescape(value):
+    """Enforce HTML escaping.  This will probably double escape variables."""
+    if hasattr(value, '__html__'):
+        value = value.__html__()
+    return escape(unicode(value))
+
+def do_urlescape(value):
+    """Escape strings for use in URLs (uses UTF-8 encoding)."""
+    def utf8(o):
+        return unicode(o).encode('utf8')
+    
+    if isinstance(value, basestring):
+        return urllib.quote(utf8(value))
+    
+    if hasattr(value, 'items'):
+        # convert dictionaries to list of 2-tuples
+        value = value.items()
+    
+    if hasattr(value, 'next'):
+        # convert generators to list
+        value = list(value)
+    
+    return urllib.urlencode([(utf8(k), utf8(v)) for (k, v) in value])
+
+@evalcontextfilter
+def do_replace(eval_ctx, 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 the optional third argument ``count`` is given, only the first
@@ -46,59 +104,37 @@ def do_replace(s, old, new, count=None):
         {{ "aaaaargh"|replace("a", "d'oh, ", 2) }}
             -> d'oh, d'oh, aaargh
     """
-    if not isinstance(old, basestring) or \
-       not isinstance(new, basestring):
-        raise FilterArgumentError('the replace filter requires '
-                                  'string replacement arguments')
     if count is None:
-        return s.replace(old, new)
-    if not isinstance(count, (int, long)):
-        raise FilterArgumentError('the count parameter of the '
-                                   'replace filter requires '
-                                   'an integer')
-    return s.replace(old, new, count)
+        count = -1
+    if not eval_ctx.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)
+    else:
+        s = soft_unicode(s)
+    return s.replace(soft_unicode(old), soft_unicode(new), count)
 
 
 def do_upper(s):
-    """
-    Convert a value to uppercase.
-    """
-    return s.upper()
+    """Convert a value to uppercase."""
+    return soft_unicode(s).upper()
 
 
 def do_lower(s):
-    """
-    Convert a value to lowercase.
-    """
-    return s.lower()
+    """Convert a value to lowercase."""
+    return soft_unicode(s).lower()
 
 
-def do_escape(s, attribute=False):
-    """
-    XML escape ``&``, ``<``, and ``>`` in a string of data. If the
-    optional parameter is `true` this filter will also convert
-    ``"`` to ``&quot;``. This filter is just used if the environment
-    was configured with disabled `auto_escape`.
-
-    This method will have no effect it the value is already escaped.
-    """
-    # XXX: Does this still exists?
-    #if isinstance(s, TemplateData):
-    #    return s
-    if hasattr(s, '__html__'):
-        return s.__html__()
-    return escape(unicode(s), attribute)
-
-
-def do_xmlattr(d, autospace=False):
-    """
-    Create an SGML/XML attribute string based on the items in a dict.
+@evalcontextfilter
+def do_xmlattr(_eval_ctx, 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>
@@ -112,45 +148,36 @@ def do_xmlattr(d, autospace=False):
         </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.
-
-    *New in Jinja 1.1*
-    """
-    if not hasattr(d, 'iteritems'):
-        raise TypeError('a dict is required')
-    result = []
-    for key, value in d.iteritems():
-        if value not in (None, env.undefined_singleton):
-            result.append(u'%s="%s"' % (
-                escape(env.to_unicode(key)),
-                escape(env.to_unicode(value), True)
-            ))
-    rv = u' '.join(result)
-    if autospace:
-        rv = ' ' + rv
+    if the filter returned something unless the second parameter is false.
+    """
+    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 and rv:
+        rv = u' ' + rv
+    if _eval_ctx.autoescape:
+        rv = Markup(rv)
     return rv
 
 
 def do_capitalize(s):
-    """
-    Capitalize a value. The first character will be uppercase, all others
+    """Capitalize a value. The first character will be uppercase, all others
     lowercase.
     """
-    return unicode(s).capitalize()
+    return soft_unicode(s).capitalize()
 
 
 def do_title(s):
-    """
-    Return a titlecased version of the value. I.e. words will start with
+    """Return a titlecased version of the value. I.e. words will start with
     uppercase letters, all remaining characters are lowercase.
     """
-    return unicode(s).title()
+    return soft_unicode(s).title()
 
 
-def do_dictsort(case_sensitive=False, by='key'):
-    """
-    Sort a dict and yield (key, value) pairs. Because python dicts are
+def do_dictsort(value, case_sensitive=False, by='key'):
+    """Sort a dict and yield (key, value) pairs. Because python dicts are
     unsorted you may want to use this function to order them by either
     key or value:
 
@@ -173,24 +200,59 @@ def do_dictsort(case_sensitive=False, by='key'):
     else:
         raise FilterArgumentError('You can only sort by either '
                                   '"key" or "value"')
-    def sort_func(value, env):
-        if isinstance(value, basestring):
-            value = env.to_unicode(value)
-            if not case_sensitive:
-                value = value.lower()
+    def sort_func(item):
+        value = item[pos]
+        if isinstance(value, basestring) and not case_sensitive:
+            value = value.lower()
         return value
 
-    def wrapped(env, context, value):
-        items = value.items()
-        items.sort(lambda a, b: cmp(sort_func(a[pos], env),
-                                    sort_func(b[pos], env)))
-        return items
-    return wrapped
+    return sorted(value.items(), key=sort_func)
 
 
-def do_default(value, default_value=u'', boolean=False):
+@environmentfilter
+def do_sort(environment, value, reverse=False, case_sensitive=False,
+            attribute=None):
+    """Sort an iterable.  Per default it sorts ascending, if you pass it
+    true as first argument it will reverse the sorting.
+
+    If the iterable is made of strings the third parameter can be used to
+    control the case sensitiveness of the comparison which is disabled by
+    default.
+
+    .. sourcecode:: jinja
+
+        {% for item in iterable|sort %}
+            ...
+        {% endfor %}
+
+    It is also possible to sort by an attribute (for example to sort
+    by the date of an object) by specifying the `attribute` parameter:
+
+    .. sourcecode:: jinja
+
+        {% for item in iterable|sort(attribute='date') %}
+            ...
+        {% endfor %}
+
+    .. versionchanged:: 2.6
+       The `attribute` parameter was added.
     """
-    If the value is undefined it will return the passed default value,
+    if not case_sensitive:
+        def sort_func(item):
+            if isinstance(item, basestring):
+                item = item.lower()
+            return item
+    else:
+        sort_func = None
+    if attribute is not None:
+        getter = make_attrgetter(environment, attribute)
+        def sort_func(item, processor=sort_func or (lambda x: x)):
+            return processor(getter(item))
+    return sorted(value, key=sort_func, reverse=reverse)
+
+
+def do_default(value, default_value=u'', boolean=False):
+    """If the value is undefined it will return the passed default value,
     otherwise the value of the variable:
 
     .. sourcecode:: jinja
@@ -206,17 +268,16 @@ def do_default(value, default_value=u'', boolean=False):
 
         {{ ''|default('the string was empty', true) }}
     """
-    # XXX: undefined_sigleton
-    if (boolean and not value) or value in (env.undefined_singleton, None):
+    if (boolean and not value) or isinstance(value, Undefined):
         return default_value
     return value
 
 
-def do_join(value, d=u''):
-    """
-    Return a string which is the concatenation of the strings in the
+@evalcontextfilter
+def do_join(eval_ctx, value, d=u'', attribute=None):
+    """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
 
@@ -225,147 +286,107 @@ def do_join(value, d=u''):
 
         {{ [1, 2, 3]|join }}
             -> 123
-    """
-    return unicode(d).join([unicode(x) for x in value])
-
-
-def do_count():
-    """
-    Return the length of the value. In case if getting an integer or float
-    it will convert it into a string an return the length of the new
-    string. If the object has no length it will of corse return 0.
-    """
-    try:
-        if type(value) in (int, float, long):
-            return len(str(value))
-        return len(value)
-    except TypeError:
-        return 0
 
-
-def do_reverse(l):
-    """
-    Return a reversed list of the sequence filtered. You can use this
-    for example for reverse iteration:
+    It is also possible to join certain attributes of an object:
 
     .. sourcecode:: jinja
 
-        {% for item in seq|reverse %}
-            {{ item|e }}
-        {% endfor %}
-    """
-    try:
-        return value[::-1]
-    except:
-        l = list(value)
-        l.reverse()
-        return l
+        {{ users|join(', ', attribute='username') }}
+
+    .. versionadded:: 2.6
+       The `attribute` parameter was added.
+    """
+    if attribute is not None:
+        value = imap(make_attrgetter(eval_ctx.environment, attribute), value)
+
+    # no automatic escaping?  joining is a lot eaiser then
+    if not eval_ctx.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 hasattr(d, '__html__'):
+        value = list(value)
+        do_escape = False
+        for idx, item in enumerate(value):
+            if hasattr(item, '__html__'):
+                do_escape = True
+            else:
+                value[idx] = unicode(item)
+        if do_escape:
+            d = escape(d)
+        else:
+            d = unicode(d)
+        return d.join(value)
+
+    # no html involved, to normal joining
+    return soft_unicode(d).join(imap(soft_unicode, value))
 
 
 def do_center(value, width=80):
-    """
-    Centers the value in a field of a given width.
-    """
+    """Centers the value in a field of a given width."""
     return unicode(value).center(width)
 
 
-def do_first(seq):
-    """
-    Return the frist item of a sequence.
-    """
+@environmentfilter
+def do_first(environment, seq):
+    """Return the first item of a sequence."""
     try:
         return iter(seq).next()
     except StopIteration:
-        return env.undefined_singleton
+        return environment.undefined('No first item, sequence was empty.')
 
 
-def do_last(seq):
-    """
-    Return the last item of a sequence.
-    """
+@environmentfilter
+def do_last(environment, seq):
+    """Return the last item of a sequence."""
     try:
         return iter(reversed(seq)).next()
     except StopIteration:
-        return env.undefined_singleton
+        return environment.undefined('No last item, sequence was empty.')
 
 
-def do_random():
-    """
-    Return a random item from the sequence.
-    """
+@environmentfilter
+def do_random(environment, seq):
+    """Return a random item from the sequence."""
     try:
         return choice(seq)
     except IndexError:
-        return env.undefined_singleton
-
-
-def do_urlencode(value):
-    """
-    urlencode a string or directory.
-
-    .. sourcecode:: jinja
-
-        {{ {'foo': 'bar', 'blub': 'blah'}|urlencode }}
-            -> foo=bar&blub=blah
-
-        {{ 'Hello World' }}
-            -> Hello%20World
-    """
-    if isinstance(value, dict):
-        tmp = {}
-        for key, value in value.iteritems():
-            # XXX env.charset?
-            key = unicode(key).encode(env.charset)
-            value = unicode(value).encode(env.charset)
-            tmp[key] = value
-        return urlencode(tmp)
+        return environment.undefined('No random item, sequence was empty.')
+
+
+def do_filesizeformat(value, binary=False):
+    """Format the value like a 'human-readable' file size (i.e. 13 kB,
+    4.1 MB, 102 Bytes, etc).  Per default decimal prefixes are used (Mega,
+    Giga, etc.), if the second parameter is set to `True` the binary
+    prefixes are used (Mebi, Gibi).
+    """
+    bytes = float(value)
+    base = binary and 1024 or 1000
+    prefixes = [
+        (binary and 'KiB' or 'kB'),
+        (binary and 'MiB' or 'MB'),
+        (binary and 'GiB' or 'GB'),
+        (binary and 'TiB' or 'TB'),
+        (binary and 'PiB' or 'PB'),
+        (binary and 'EiB' or 'EB'),
+        (binary and 'ZiB' or 'ZB'),
+        (binary and 'YiB' or 'YB')
+    ]
+    if bytes == 1:
+        return '1 Byte'
+    elif bytes < base:
+        return '%d Bytes' % bytes
     else:
-        # XXX: env.charset?
-        return quote(unicode(value).encode(env.charset))
-
-
-def do_jsonencode(value):
-    """
-    JSON dump a variable. just works if simplejson is installed.
-
-    .. sourcecode:: jinja
-
-        {{ 'Hello World'|jsonencode }}
-            -> "Hello World"
-    """
-    global simplejson
-    try:
-        simplejson
-    except NameError:
-        import simplejson
-    return simplejson.dumps(value)
-
-
-def do_filesizeformat():
-    """
-    Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102
-    bytes, etc).
-    """
-    def wrapped(env, context, value):
-        # fail silently
-        try:
-            bytes = float(value)
-        except TypeError:
-            bytes = 0
-
-        if bytes < 1024:
-            return "%d Byte%s" % (bytes, bytes != 1 and 's' or '')
-        elif bytes < 1024 * 1024:
-            return "%.1f KB" % (bytes / 1024)
-        elif bytes < 1024 * 1024 * 1024:
-            return "%.1f MB" % (bytes / (1024 * 1024))
-        return "%.1f GB" % (bytes / (1024 * 1024 * 1024))
-    return wrapped
+        for i, prefix in enumerate(prefixes):
+            unit = base ** (i + 2)
+            if bytes < unit:
+                return '%.1f %s' % ((base * bytes / unit), prefix)
+        return '%.1f %s' % ((base * bytes / unit), prefix)
 
 
 def do_pprint(value, verbose=False):
-    """
-    Pretty print a variable. Useful for debugging.
+    """Pretty print a variable. Useful for debugging.
 
     With Jinja 1.2 onwards you can pass it a parameter.  If this parameter
     is truthy the output will be more verbose (this requires `pretty`)
@@ -373,9 +394,9 @@ def do_pprint(value, verbose=False):
     return pformat(value, verbose=verbose)
 
 
-def do_urlize(value, trim_url_limit=None, nofollow=False):
-    """
-    Converts URLs in plain text into clickable links.
+@evalcontextfilter
+def do_urlize(eval_ctx, 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
     to that number. Also a third argument exists that makes the urls
@@ -383,35 +404,35 @@ def do_urlize(value, trim_url_limit=None, nofollow=False):
 
     .. sourcecode:: jinja
 
-        {{ mytext|urlize(40, True) }}
+        {{ mytext|urlize(40, true) }}
             links are shortened to 40 chars and defined with rel="nofollow"
     """
-    return urlize(unicode(value), trim_url_limit, nofollow)
+    rv = urlize(value, trim_url_limit, nofollow)
+    if eval_ctx.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
+    indention = u' ' * width
+    rv = (u'\n' + indention).join(s.splitlines())
     if indentfirst:
-        return u'\n'.join([indention + line for line in s.splitlines()])
-    return s.replace('\n', '\n' + indention)
+        rv = indention + rv
+    return rv
 
 
 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
@@ -440,110 +461,52 @@ def do_truncate(s, length=255, killwords=False, end='...'):
     result.append(end)
     return u' '.join(result)
 
-
-def do_wordwrap(s, pos=79, hard=False):
+@environmentfilter
+def do_wordwrap(environment, 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)])
-    # 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(' '))
+    import textwrap
+    return environment.newline_sequence.join(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])
-
-
-def do_textile(s):
-    """
-    Prase the string using textile.
-
-    requires the `PyTextile`_ library.
+    """Count the words in that string."""
+    return len(_word_re.findall(s))
 
-    .. _PyTextile: http://dealmeida.net/projects/textile/
-    """
-    from textile import textile
-    return textile(s.encode('utf-8')).decode('utf-8')
-
-
-def do_markdown(s):
-    """
-    Parse the string using markdown.
-
-    requires the `Python-markdown`_ library.
-
-    .. _Python-markdown: http://www.freewisdom.org/projects/python-markdown/
-    """
-    from markdown import markdown
-    return markdown(s.encode('utf-8')).decode('utf-8')
 
-
-def do_rst(s):
-    """
-    Parse the string using the reStructuredText parser from the
-    docutils package.
-
-    requires `docutils`_.
-
-    .. _docutils: http://docutils.sourceforge.net/
-    """
-    from docutils.core import publish_parts
-    parts = publish_parts(source=s, writer_name='html4css1')
-    return parts['fragment']
-
-def do_int(default=0):
-    """
-    Convert the value into an integer. If the
+def do_int(value, default=0):
+    """Convert the value into an integer. If the
     conversion doesn't work it will return ``0``. You can
     override this default using the first parameter.
     """
-    def wrapped(env, context, value):
+    try:
+        return int(value)
+    except (TypeError, ValueError):
+        # this quirk is necessary so that "42.23"|int gives 42.
         try:
-            return int(value)
+            return int(float(value))
         except (TypeError, ValueError):
-            try:
-                return int(float(value))
-            except (TypeError, ValueError):
-                return default
-    return wrapped
+            return default
 
 
-def do_float(default=0.0):
-    """
-    Convert the value into a floating point number. If the
+def do_float(value, default=0.0):
+    """Convert the value into a floating point number. If the
     conversion doesn't work it will return ``0.0``. You can
     override this default using the first parameter.
     """
-    def wrapped(env, context, value):
-        try:
-            return float(value)
-        except (TypeError, ValueError):
-            return default
-    return wrapped
-
-
-def do_string():
-    """
-    Convert the value into an string.
-    """
-    return lambda e, c, v: e.to_unicode(v)
+    try:
+        return float(value)
+    except (TypeError, ValueError):
+        return default
 
 
-def do_format(*args):
+def do_format(value, *args, **kwargs):
     """
     Apply python string formatting on an object:
 
@@ -551,90 +514,30 @@ def do_format(*args):
 
         {{ "%s - %s"|format("Hello?", "Foo!") }}
             -> Hello? - Foo!
-
-    Note that you cannot use the mapping syntax (``%(name)s``)
-    like in python. Use `|dformat` for that.
-    """
-    def wrapped(env, context, value):
-        return env.to_unicode(value) % args
-    return wrapped
-
-
-def do_dformat(d):
-    """
-    Apply python mapping string formatting on an object:
-
-    .. sourcecode:: jinja
-
-        {{ "Hello %(username)s!"|dformat({'username': 'John Doe'}) }}
-            -> Hello John Doe!
-
-    This is useful when adding variables to translateable
-    string expressions.
-
-    *New in Jinja 1.1*
     """
-    if not isinstance(d, dict):
-        raise FilterArgumentError('dict required')
-    def wrapped(env, context, value):
-        return env.to_unicode(value) % d
-    return wrapped
+    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):
-    """
-    Strip leading and trailing whitespace.
-    """
-    return value.strip()
-
-
-def do_capture(name='captured', clean=False):
-    """
-    Store the value in a variable called ``captured`` or a variable
-    with the name provided. Useful for filter blocks:
-
-    .. sourcecode:: jinja
-
-        {% filter capture('foo') %}
-            ...
-        {% endfilter %}
-        {{ foo }}
-
-    This will output "..." two times. One time from the filter block
-    and one time from the variable. If you don't want the filter to
-    output something you can use it in `clean` mode:
-
-    .. sourcecode:: jinja
-
-        {% filter capture('foo', True) %}
-            ...
-        {% endfilter %}
-        {{ foo }}
-    """
-    if not isinstance(name, basestring):
-        raise FilterArgumentError('You can only capture into variables')
-    def wrapped(env, context, value):
-        context[name] = value
-        if clean:
-            return TemplateData()
-        return value
-    return wrapped
+    """Strip leading and trailing whitespace."""
+    return soft_unicode(value).strip()
 
 
 def do_striptags(value):
+    """Strip SGML/XML tags and replace adjacent whitespace by one space.
     """
-    Strip SGML/XML tags and replace adjacent whitespace by one space.
-
-    *new in Jinja 1.1*
-    """
-    return ' '.join(_striptags_re.sub('', value).split())
+    if hasattr(value, '__html__'):
+        value = value.__html__()
+    return Markup(unicode(value)).striptags()
 
 
-def do_slice(slices, fill_with=None):
-    """
-    Slice an iterator and return a list of lists containing
+def do_slice(value, slices, fill_with=None):
+    """Slice an iterator and return a list of lists containing
     those items. Useful if you want to create a div containing
-    three div tags that represent columns:
+    three ul tags that represent columns:
 
     .. sourcecode:: html+jinja
 
@@ -650,30 +553,24 @@ def do_slice(slices, fill_with=None):
 
     If you pass it a second argument it's used to fill missing
     values on the last iteration.
+    """
+    seq = list(value)
+    length = len(seq)
+    items_per_slice = length // slices
+    slices_with_extra = length % slices
+    offset = 0
+    for slice_number in xrange(slices):
+        start = offset + slice_number * items_per_slice
+        if slice_number < slices_with_extra:
+            offset += 1
+        end = offset + (slice_number + 1) * items_per_slice
+        tmp = seq[start:end]
+        if fill_with is not None and slice_number >= slices_with_extra:
+            tmp.append(fill_with)
+        yield tmp
+
 
-    *new in Jinja 1.1*
-    """
-    def wrapped(env, context, value):
-        result = []
-        seq = list(value)
-        length = len(seq)
-        items_per_slice = length // slices
-        slices_with_extra = length % slices
-        offset = 0
-        for slice_number in xrange(slices):
-            start = offset + slice_number * items_per_slice
-            if slice_number < slices_with_extra:
-                offset += 1
-            end = offset + (slice_number + 1) * items_per_slice
-            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
-    return wrapped
-
-
-def do_batch(linecount, fill_with=None):
+def do_batch(value, linecount, fill_with=None):
     """
     A filter that batches items. It works pretty much like `slice`
     just the other way round. It returns a list of lists with the
@@ -686,55 +583,27 @@ def do_batch(linecount, fill_with=None):
         {%- for row in items|batch(3, '&nbsp;') %}
           <tr>
           {%- for column in row %}
-            <tr>{{ column }}</td>
+            <td>{{ column }}</td>
           {%- endfor %}
           </tr>
         {%- endfor %}
         </table>
-
-    *new in Jinja 1.1*
-    """
-    def wrapped(env, context, value):
-        result = []
-        tmp = []
-        for item in value:
-            if len(tmp) == linecount:
-                result.append(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
-    return wrapped
-
-
-def do_sum():
-    """
-    Sum up the given sequence of numbers.
-
-    *new in Jinja 1.1*
-    """
-    def wrapped(env, context, value):
-        return sum(value)
-    return wrapped
-
-
-def do_abs():
-    """
-    Return the absolute value of a number.
-
-    *new in Jinja 1.1*
     """
-    def wrapped(env, context, value):
-        return abs(value)
-    return wrapped
-
-
-def do_round(precision=0, method='common'):
-    """
-    Round the number to a given precision. The first
+    result = []
+    tmp = []
+    for item in value:
+        if len(tmp) == linecount:
+            yield tmp
+            tmp = []
+        tmp.append(item)
+    if tmp:
+        if fill_with is not None and len(tmp) < linecount:
+            tmp += [fill_with] * (linecount - len(tmp))
+        yield tmp
+
+
+def do_round(value, precision=0, method='common'):
+    """Round the number to a given precision. The first
     parameter specifies the precision (default is ``0``), the
     second the rounding method:
 
@@ -747,44 +616,29 @@ def do_round(precision=0, method='common'):
     .. sourcecode:: jinja
 
         {{ 42.55|round }}
-            -> 43
+            -> 43.0
         {{ 42.55|round(1, 'floor') }}
             -> 42.5
 
-    *new in Jinja 1.1*
-    """
-    if not method in ('common', 'ceil', 'floor'):
-        raise FilterArgumentError('method must be common, ceil or floor')
-    if precision < 0:
-        raise FilterArgumentError('precision must be a postive integer '
-                                  'or zero.')
-    def wrapped(env, context, value):
-        if method == 'common':
-            return round(value, precision)
-        import math
-        func = getattr(math, method)
-        if precision:
-            return func(value * 10 * precision) / (10 * precision)
-        else:
-            return func(value)
-    return wrapped
-
+    Note that even if rounded to 0 precision, a float is returned.  If
+    you need a real integer, pipe it through `int`:
 
-def do_sort(reverse=False):
-    """
-    Sort a sequence. Per default it sorts ascending, if you pass it
-    `True` as first argument it will reverse the sorting.
+    .. sourcecode:: jinja
 
-    *new in Jinja 1.1*
+        {{ 42.55|round|int }}
+            -> 43
     """
-    def wrapped(env, context, value):
-        return sorted(value, reverse=reverse)
-    return wrapped
+    if not method in ('common', 'ceil', 'floor'):
+        raise FilterArgumentError('method must be common, ceil or floor')
+    if method == 'common':
+        return round(value, precision)
+    func = getattr(math, method)
+    return func(value * (10 ** precision)) / (10 ** precision)
 
 
-def do_groupby(attribute):
-    """
-    Group a sequence of objects by a common attribute.
+@environmentfilter
+def do_groupby(environment, value, attribute):
+    """Group a sequence of objects by a common attribute.
 
     If you for example have a list of dicts or objects that represent persons
     with `gender`, `first_name` and `last_name` attributes and you want to
@@ -802,112 +656,165 @@ def do_groupby(attribute):
         {% 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.
 
-    *New in Jinja 1.2*
+    .. versionchanged:: 2.6
+       It's now possible to use dotted notation to group by the child
+       attribute of another attribute.
     """
-    def wrapped(env, context, value):
-        expr = lambda x: env.get_attribute(x, attribute)
-        return sorted([{
-            'grouper':  a,
-            'list':     list(b)
-        } for a, b in groupby(sorted(value, key=expr), expr)],
-            key=itemgetter('grouper'))
-    return wrapped
+    expr = make_attrgetter(environment, attribute)
+    return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
 
 
-def do_getattribute(attribute):
-    """
-    Get one attribute from an object. Normally you don't have to use this
-    filter because the attribute and subscript expressions try to either
-    get an attribute of an object or an item. In some situations it could
-    be that there is an item *and* an attribute with the same name. In that
-    situation only the item is returned, never the attribute.
+class _GroupTuple(tuple):
+    __slots__ = ()
+    grouper = property(itemgetter(0))
+    list = property(itemgetter(1))
+
+    def __new__(cls, (key, value)):
+        return tuple.__new__(cls, (key, list(value)))
+
+
+@environmentfilter
+def do_sum(environment, iterable, attribute=None, start=0):
+    """Returns the sum of a sequence of numbers plus the value of parameter
+    'start' (which defaults to 0).  When the sequence is empty it returns
+    start.
+
+    It is also possible to sum up only certain attributes:
 
     .. sourcecode:: jinja
 
-        {{ foo.bar }} -> {{ foo|getattribute('bar') }}
+        Total: {{ items|sum(attribute='price') }}
 
-    *New in Jinja 1.2*
+    .. versionchanged:: 2.6
+       The `attribute` parameter was added to allow suming up over
+       attributes.  Also the `start` parameter was moved on to the right.
     """
-    def wrapped(env, context, value):
-        try:
-            return get_attribute(value, attribute)
-        except (SecurityException, AttributeError):
-            return env.undefined_singleton
-    return wrapped
+    if attribute is not None:
+        iterable = imap(make_attrgetter(environment, attribute), iterable)
+    return sum(iterable, start)
 
 
-def do_getitem(key):
+def do_list(value):
+    """Convert the value into a list.  If it was a string the returned list
+    will be a list of characters.
     """
-    This filter basically works like the normal subscript expression but
-    it doesn't fall back to attribute lookup. If an item does not exist for
-    an object undefined is returned.
+    return list(value)
 
-    .. sourcecode:: jinja
 
-        {{ foo.bar }} -> {{ foo|getitem('bar') }}
+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_mark_unsafe(value):
+    """Mark a value as unsafe.  This is the reverse operation for :func:`safe`."""
+    return unicode(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')
+
 
-    *New in Jinja 1.2*
+@environmentfilter
+def do_attr(environment, obj, name):
+    """Get an attribute of an object.  ``foo|attr("bar")`` works like
+    ``foo["bar"]`` just that always an attribute is returned and items are not
+    looked up.
+
+    See :ref:`Notes on subscriptions <notes-on-subscriptions>` for more details.
     """
-    def wrapped(env, context, value):
+    try:
+        name = str(name)
+    except UnicodeError:
+        pass
+    else:
         try:
-            return value[key]
-        except (TypeError, KeyError, IndexError, AttributeError):
-            return env.undefined_singleton
-    return wrapped
+            value = getattr(obj, name)
+        except AttributeError:
+            pass
+        else:
+            if environment.sandboxed and not \
+               environment.is_safe_attribute(obj, name, value):
+                return environment.unsafe_undefined(obj, name)
+            return value
+    return environment.undefined(obj=obj, name=name)
 
 
 FILTERS = {
+    'attr':                 do_attr,
     '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':                do_count,
+    'count':                len,
     'dictsort':             do_dictsort,
-    'length':               do_count,
+    'sort':                 do_sort,
+    'length':               len,
     'reverse':              do_reverse,
     'center':               do_center,
+    'indent':               do_indent,
     'title':                do_title,
     'capitalize':           do_capitalize,
     'first':                do_first,
     'last':                 do_last,
     'random':               do_random,
-    'urlencode':            do_urlencode,
-    'jsonencode':           do_jsonencode,
     'filesizeformat':       do_filesizeformat,
     'pprint':               do_pprint,
-    'indent':               do_indent,
     'truncate':             do_truncate,
     'wordwrap':             do_wordwrap,
     'wordcount':            do_wordcount,
-    'textile':              do_textile,
-    'markdown':             do_markdown,
-    'rst':                  do_rst,
     'int':                  do_int,
     'float':                do_float,
-    'string':               do_string,
+    'string':               soft_unicode,
+    'list':                 do_list,
     'urlize':               do_urlize,
     'format':               do_format,
-    'dformat':              do_dformat,
-    'capture':              do_capture,
     'trim':                 do_trim,
     'striptags':            do_striptags,
     'slice':                do_slice,
     'batch':                do_batch,
     'sum':                  do_sum,
-    'abs':                  do_abs,
+    'abs':                  abs,
     'round':                do_round,
-    'sort':                 do_sort,
     'groupby':              do_groupby,
-    'getattribute':         do_getattribute,
-    'getitem':              do_getitem
+    'safe':                 do_mark_safe,
+    'xmlattr':              do_xmlattr,
+    'urlescape':            do_urlescape
 }