diff options
Diffstat (limited to 'lib/jinja2/filters.py')
-rw-r--r-- | lib/jinja2/filters.py | 719 |
1 files changed, 719 insertions, 0 deletions
diff --git a/lib/jinja2/filters.py b/lib/jinja2/filters.py new file mode 100644 index 000000000..d1848e434 --- /dev/null +++ b/lib/jinja2/filters.py @@ -0,0 +1,719 @@ +# -*- coding: utf-8 -*- +""" + jinja2.filters + ~~~~~~~~~~~~~~ + + Bundled jinja filters. + + :copyright: (c) 2010 by the Jinja Team. + :license: BSD, see LICENSE for more details. +""" +import re +import math +from random import choice +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, SecurityError + + +_word_re = re.compile(r'\w+(?u)') + + +def contextfilter(f): + """Decorator for marking context dependent filters. The current + :class:`Context` will be passed as first argument. + """ + f.contextfilter = True + return f + + +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. + """ + f.environmentfilter = True + return f + + +def do_forceescape(value): + """Enforce HTML escaping. This will probably double escape variables.""" + if hasattr(value, '__html__'): + value = value.__html__() + return escape(unicode(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 + ``count`` occurrences are replaced: + + .. sourcecode:: jinja + + {{ "Hello World"|replace("Hello", "Goodbye") }} + -> Goodbye World + + {{ "aaaaargh"|replace("a", "d'oh, ", 2) }} + -> d'oh, d'oh, aaargh + """ + if count is None: + 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 soft_unicode(s).upper() + + +def do_lower(s): + """Convert a value to lowercase.""" + return soft_unicode(s).lower() + + +@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, + 'id': 'list-%d'|format(variable)}|xmlattr }}> + ... + </ul> + + Results in something like this: + + .. sourcecode:: html + + <ul class="my_list" id="list-42"> + ... + </ul> + + As you can see it automatically prepends a space in front of the item + 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 + lowercase. + """ + return soft_unicode(s).capitalize() + + +def do_title(s): + """Return a titlecased version of the value. I.e. words will start with + uppercase letters, all remaining characters are lowercase. + """ + return soft_unicode(s).title() + + +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: + + .. sourcecode:: jinja + + {% for item in mydict|dictsort %} + sort the dict by key, case insensitive + + {% for item in mydict|dicsort(true) %} + sort the dict by key, case sensitive + + {% for item in mydict|dictsort(false, 'value') %} + sort the dict by key, case insensitive, sorted + normally and ordered by value. + """ + if by == 'key': + pos = 0 + elif by == 'value': + pos = 1 + else: + raise FilterArgumentError('You can only sort by either ' + '"key" or "value"') + def sort_func(item): + value = item[pos] + if isinstance(value, basestring) and not case_sensitive: + value = value.lower() + return value + + return sorted(value.items(), key=sort_func) + + +def do_sort(value, reverse=False, case_sensitive=False): + """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 %} + """ + if not case_sensitive: + def sort_func(item): + if isinstance(item, basestring): + item = item.lower() + return item + else: + sort_func = None + 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 + + {{ my_variable|default('my_variable is not defined') }} + + This will output the value of ``my_variable`` if the variable was + defined, otherwise ``'my_variable is not defined'``. If you want + to use default with variables that evaluate to false you have to + set the second parameter to `true`: + + .. sourcecode:: jinja + + {{ ''|default('the string was empty', true) }} + """ + if (boolean and not value) or isinstance(value, Undefined): + return default_value + return value + + +@evalcontextfilter +def do_join(eval_ctx, 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 it with the optional parameter: + + .. sourcecode:: jinja + + {{ [1, 2, 3]|join('|') }} + -> 1|2|3 + + {{ [1, 2, 3]|join }} + -> 123 + """ + # 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.""" + return unicode(value).center(width) + + +@environmentfilter +def do_first(environment, seq): + """Return the first item of a sequence.""" + try: + return iter(seq).next() + except StopIteration: + return environment.undefined('No first item, sequence was empty.') + + +@environmentfilter +def do_last(environment, seq): + """Return the last item of a sequence.""" + try: + return iter(reversed(seq)).next() + except StopIteration: + return environment.undefined('No last item, sequence was empty.') + + +@environmentfilter +def do_random(environment, seq): + """Return a random item from the sequence.""" + try: + return choice(seq) + except IndexError: + 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 + middle = binary and 'i' or '' + if bytes < base: + return "%d Byte%s" % (bytes, bytes != 1 and 's' or '') + elif bytes < base * base: + return "%.1f K%sB" % (bytes / base, middle) + elif bytes < base * base * base: + return "%.1f M%sB" % (bytes / (base * base), middle) + return "%.1f G%sB" % (bytes / (base * base * base), middle) + + +def do_pprint(value, verbose=False): + """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`) + """ + return pformat(value, verbose=verbose) + + +@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 + "nofollow": + + .. sourcecode:: jinja + + {{ mytext|urlize(40, true) }} + links are shortened to 40 chars and defined with rel="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): + """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) }} + indent by two spaces and indent the first line too. + """ + indention = u' ' * width + rv = (u'\n' + indention).join(s.splitlines()) + if indentfirst: + 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 + 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 + truncated it will append an ellipsis sign (``"..."``). If you want a + different ellipsis sign than ``"..."`` you can specify it using the + third parameter. + + .. sourcecode jinja:: + + {{ mytext|truncate(300, false, '»') }} + truncate mytext to 300 chars, don't split up words, use a + right pointing double arrow as ellipsis sign. + """ + if len(s) <= length: + return s + elif killwords: + return s[:length] + end + words = s.split(' ') + result = [] + m = 0 + for word in words: + m += len(word) + 1 + if m > length: + break + result.append(word) + result.append(end) + return u' '.join(result) + + +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 `false` Jinja will not + split words apart if they are longer than `width`. + """ + import textwrap + return u'\n'.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(_word_re.findall(s)) + + +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. + """ + 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_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. + """ + try: + return float(value) + except (TypeError, ValueError): + return default + + +def do_format(value, *args, **kwargs): + """ + Apply python string formatting on an object: + + .. sourcecode:: jinja + + {{ "%s - %s"|format("Hello?", "Foo!") }} + -> Hello? - Foo! + """ + 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 soft_unicode(value).strip() + + +def do_striptags(value): + """Strip SGML/XML tags and replace adjacent whitespace by one space. + """ + if hasattr(value, '__html__'): + value = value.__html__() + return Markup(unicode(value)).striptags() + + +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 ul tags that represent columns: + + .. sourcecode:: html+jinja + + <div class="columwrapper"> + {%- for column in items|slice(3) %} + <ul class="column-{{ loop.index }}"> + {%- for item in column %} + <li>{{ item }}</li> + {%- endfor %} + </ul> + {%- endfor %} + </div> + + 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 + + +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 + given number of items. If you provide a second parameter this + is used to fill missing items. See this example: + + .. sourcecode:: html+jinja + + <table> + {%- for row in items|batch(3, ' ') %} + <tr> + {%- for column in row %} + <td>{{ column }}</td> + {%- endfor %} + </tr> + {%- endfor %} + </table> + """ + 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: + + - ``'common'`` rounds either up or down + - ``'ceil'`` always rounds up + - ``'floor'`` always rounds down + + If you don't specify a method ``'common'`` is used. + + .. sourcecode:: jinja + + {{ 42.55|round }} + -> 43.0 + {{ 42.55|round(1, 'floor') }} + -> 42.5 + + Note that even if rounded to 0 precision, a float is returned. If + you need a real integer, pipe it through `int`: + + .. sourcecode:: jinja + + {{ 42.55|round|int }} + -> 43 + """ + 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) + + +@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 + group all users by genders you can do something like the following + snippet: + + .. sourcecode:: html+jinja + + <ul> + {% for group in persons|groupby('gender') %} + <li>{{ group.grouper }}<ul> + {% for person in group.list %} + <li>{{ person.first_name }} {{ person.last_name }}</li> + {% endfor %}</ul></li> + {% 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.getitem(x, attribute) + 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_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') + + +@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. + """ + try: + name = str(name) + except UnicodeError: + pass + else: + try: + 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': 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, + '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, + 'filesizeformat': do_filesizeformat, + 'pprint': do_pprint, + 'truncate': do_truncate, + 'wordwrap': do_wordwrap, + 'wordcount': do_wordcount, + 'int': do_int, + 'float': do_float, + 'string': soft_unicode, + 'list': do_list, + 'urlize': do_urlize, + 'format': do_format, + 'trim': do_trim, + 'striptags': do_striptags, + 'slice': do_slice, + 'batch': do_batch, + 'sum': sum, + 'abs': abs, + 'round': do_round, + 'groupby': do_groupby, + 'safe': do_mark_safe, + 'xmlattr': do_xmlattr +} |