You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1855 lines
53 KiB
Python
1855 lines
53 KiB
Python
![]()
2 months ago
|
"""Built-in template filters used with the ``|`` operator."""
|
||
|
import math
|
||
|
import random
|
||
|
import re
|
||
|
import typing
|
||
|
import typing as t
|
||
|
from collections import abc
|
||
|
from itertools import chain
|
||
|
from itertools import groupby
|
||
|
|
||
|
from markupsafe import escape
|
||
|
from markupsafe import Markup
|
||
|
from markupsafe import soft_str
|
||
|
|
||
|
from .async_utils import async_variant
|
||
|
from .async_utils import auto_aiter
|
||
|
from .async_utils import auto_await
|
||
|
from .async_utils import auto_to_list
|
||
|
from .exceptions import FilterArgumentError
|
||
|
from .runtime import Undefined
|
||
|
from .utils import htmlsafe_json_dumps
|
||
|
from .utils import pass_context
|
||
|
from .utils import pass_environment
|
||
|
from .utils import pass_eval_context
|
||
|
from .utils import pformat
|
||
|
from .utils import url_quote
|
||
|
from .utils import urlize
|
||
|
|
||
|
if t.TYPE_CHECKING:
|
||
|
import typing_extensions as te
|
||
|
from .environment import Environment
|
||
|
from .nodes import EvalContext
|
||
|
from .runtime import Context
|
||
|
from .sandbox import SandboxedEnvironment # noqa: F401
|
||
|
|
||
|
class HasHTML(te.Protocol):
|
||
|
def __html__(self) -> str:
|
||
|
pass
|
||
|
|
||
|
|
||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||
|
K = t.TypeVar("K")
|
||
|
V = t.TypeVar("V")
|
||
|
|
||
|
|
||
|
def ignore_case(value: V) -> V:
|
||
|
"""For use as a postprocessor for :func:`make_attrgetter`. Converts strings
|
||
|
to lowercase and returns other types as-is."""
|
||
|
if isinstance(value, str):
|
||
|
return t.cast(V, value.lower())
|
||
|
|
||
|
return value
|
||
|
|
||
|
|
||
|
def make_attrgetter(
|
||
|
environment: "Environment",
|
||
|
attribute: t.Optional[t.Union[str, int]],
|
||
|
postprocess: t.Optional[t.Callable[[t.Any], t.Any]] = None,
|
||
|
default: t.Optional[t.Any] = None,
|
||
|
) -> t.Callable[[t.Any], t.Any]:
|
||
|
"""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. Integer parts in paths are
|
||
|
looked up as integers.
|
||
|
"""
|
||
|
parts = _prepare_attribute_parts(attribute)
|
||
|
|
||
|
def attrgetter(item: t.Any) -> t.Any:
|
||
|
for part in parts:
|
||
|
item = environment.getitem(item, part)
|
||
|
|
||
|
if default is not None and isinstance(item, Undefined):
|
||
|
item = default
|
||
|
|
||
|
if postprocess is not None:
|
||
|
item = postprocess(item)
|
||
|
|
||
|
return item
|
||
|
|
||
|
return attrgetter
|
||
|
|
||
|
|
||
|
def make_multi_attrgetter(
|
||
|
environment: "Environment",
|
||
|
attribute: t.Optional[t.Union[str, int]],
|
||
|
postprocess: t.Optional[t.Callable[[t.Any], t.Any]] = None,
|
||
|
) -> t.Callable[[t.Any], t.List[t.Any]]:
|
||
|
"""Returns a callable that looks up the given comma separated
|
||
|
attributes from a passed object with the rules of the environment.
|
||
|
Dots are allowed to access attributes of each attribute. Integer
|
||
|
parts in paths are looked up as integers.
|
||
|
|
||
|
The value returned by the returned callable is a list of extracted
|
||
|
attribute values.
|
||
|
|
||
|
Examples of attribute: "attr1,attr2", "attr1.inner1.0,attr2.inner2.0", etc.
|
||
|
"""
|
||
|
if isinstance(attribute, str):
|
||
|
split: t.Sequence[t.Union[str, int, None]] = attribute.split(",")
|
||
|
else:
|
||
|
split = [attribute]
|
||
|
|
||
|
parts = [_prepare_attribute_parts(item) for item in split]
|
||
|
|
||
|
def attrgetter(item: t.Any) -> t.List[t.Any]:
|
||
|
items = [None] * len(parts)
|
||
|
|
||
|
for i, attribute_part in enumerate(parts):
|
||
|
item_i = item
|
||
|
|
||
|
for part in attribute_part:
|
||
|
item_i = environment.getitem(item_i, part)
|
||
|
|
||
|
if postprocess is not None:
|
||
|
item_i = postprocess(item_i)
|
||
|
|
||
|
items[i] = item_i
|
||
|
|
||
|
return items
|
||
|
|
||
|
return attrgetter
|
||
|
|
||
|
|
||
|
def _prepare_attribute_parts(
|
||
|
attr: t.Optional[t.Union[str, int]]
|
||
|
) -> t.List[t.Union[str, int]]:
|
||
|
if attr is None:
|
||
|
return []
|
||
|
|
||
|
if isinstance(attr, str):
|
||
|
return [int(x) if x.isdigit() else x for x in attr.split(".")]
|
||
|
|
||
|
return [attr]
|
||
|
|
||
|
|
||
|
def do_forceescape(value: "t.Union[str, HasHTML]") -> Markup:
|
||
|
"""Enforce HTML escaping. This will probably double escape variables."""
|
||
|
if hasattr(value, "__html__"):
|
||
|
value = t.cast("HasHTML", value).__html__()
|
||
|
|
||
|
return escape(str(value))
|
||
|
|
||
|
|
||
|
def do_urlencode(
|
||
|
value: t.Union[str, t.Mapping[str, t.Any], t.Iterable[t.Tuple[str, t.Any]]]
|
||
|
) -> str:
|
||
|
"""Quote data for use in a URL path or query using UTF-8.
|
||
|
|
||
|
Basic wrapper around :func:`urllib.parse.quote` when given a
|
||
|
string, or :func:`urllib.parse.urlencode` for a dict or iterable.
|
||
|
|
||
|
:param value: Data to quote. A string will be quoted directly. A
|
||
|
dict or iterable of ``(key, value)`` pairs will be joined as a
|
||
|
query string.
|
||
|
|
||
|
When given a string, "/" is not quoted. HTTP servers treat "/" and
|
||
|
"%2F" equivalently in paths. If you need quoted slashes, use the
|
||
|
``|replace("/", "%2F")`` filter.
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
if isinstance(value, str) or not isinstance(value, abc.Iterable):
|
||
|
return url_quote(value)
|
||
|
|
||
|
if isinstance(value, dict):
|
||
|
items: t.Iterable[t.Tuple[str, t.Any]] = value.items()
|
||
|
else:
|
||
|
items = value # type: ignore
|
||
|
|
||
|
return "&".join(
|
||
|
f"{url_quote(k, for_qs=True)}={url_quote(v, for_qs=True)}" for k, v in items
|
||
|
)
|
||
|
|
||
|
|
||
|
@pass_eval_context
|
||
|
def do_replace(
|
||
|
eval_ctx: "EvalContext", s: str, old: str, new: str, count: t.Optional[int] = None
|
||
|
) -> str:
|
||
|
"""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 str(s).replace(str(old), str(new), count)
|
||
|
|
||
|
if (
|
||
|
hasattr(old, "__html__")
|
||
|
or hasattr(new, "__html__")
|
||
|
and not hasattr(s, "__html__")
|
||
|
):
|
||
|
s = escape(s)
|
||
|
else:
|
||
|
s = soft_str(s)
|
||
|
|
||
|
return s.replace(soft_str(old), soft_str(new), count)
|
||
|
|
||
|
|
||
|
def do_upper(s: str) -> str:
|
||
|
"""Convert a value to uppercase."""
|
||
|
return soft_str(s).upper()
|
||
|
|
||
|
|
||
|
def do_lower(s: str) -> str:
|
||
|
"""Convert a value to lowercase."""
|
||
|
return soft_str(s).lower()
|
||
|
|
||
|
|
||
|
def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K, V]]:
|
||
|
"""Return an iterator over the ``(key, value)`` items of a mapping.
|
||
|
|
||
|
``x|items`` is the same as ``x.items()``, except if ``x`` is
|
||
|
undefined an empty iterator is returned.
|
||
|
|
||
|
This filter is useful if you expect the template to be rendered with
|
||
|
an implementation of Jinja in another programming language that does
|
||
|
not have a ``.items()`` method on its mapping type.
|
||
|
|
||
|
.. code-block:: html+jinja
|
||
|
|
||
|
<dl>
|
||
|
{% for key, value in my_dict|items %}
|
||
|
<dt>{{ key }}
|
||
|
<dd>{{ value }}
|
||
|
{% endfor %}
|
||
|
</dl>
|
||
|
|
||
|
.. versionadded:: 3.1
|
||
|
"""
|
||
|
if isinstance(value, Undefined):
|
||
|
return
|
||
|
|
||
|
if not isinstance(value, abc.Mapping):
|
||
|
raise TypeError("Can only get item pairs from a mapping.")
|
||
|
|
||
|
yield from value.items()
|
||
|
|
||
|
|
||
|
_space_re = re.compile(r"\s", flags=re.ASCII)
|
||
|
|
||
|
|
||
|
@pass_eval_context
|
||
|
def do_xmlattr(
|
||
|
eval_ctx: "EvalContext", d: t.Mapping[str, t.Any], autospace: bool = True
|
||
|
) -> str:
|
||
|
"""Create an SGML/XML attribute string based on the items in a dict.
|
||
|
|
||
|
If any key contains a space, this fails with a ``ValueError``. 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.
|
||
|
|
||
|
.. versionchanged:: 3.1.3
|
||
|
Keys with spaces are not allowed.
|
||
|
"""
|
||
|
items = []
|
||
|
|
||
|
for key, value in d.items():
|
||
|
if value is None or isinstance(value, Undefined):
|
||
|
continue
|
||
|
|
||
|
if _space_re.search(key) is not None:
|
||
|
raise ValueError(f"Spaces are not allowed in attributes: '{key}'")
|
||
|
|
||
|
items.append(f'{escape(key)}="{escape(value)}"')
|
||
|
|
||
|
rv = " ".join(items)
|
||
|
|
||
|
if autospace and rv:
|
||
|
rv = " " + rv
|
||
|
|
||
|
if eval_ctx.autoescape:
|
||
|
rv = Markup(rv)
|
||
|
|
||
|
return rv
|
||
|
|
||
|
|
||
|
def do_capitalize(s: str) -> str:
|
||
|
"""Capitalize a value. The first character will be uppercase, all others
|
||
|
lowercase.
|
||
|
"""
|
||
|
return soft_str(s).capitalize()
|
||
|
|
||
|
|
||
|
_word_beginning_split_re = re.compile(r"([-\s({\[<]+)")
|
||
|
|
||
|
|
||
|
def do_title(s: str) -> str:
|
||
|
"""Return a titlecased version of the value. I.e. words will start with
|
||
|
uppercase letters, all remaining characters are lowercase.
|
||
|
"""
|
||
|
return "".join(
|
||
|
[
|
||
|
item[0].upper() + item[1:].lower()
|
||
|
for item in _word_beginning_split_re.split(soft_str(s))
|
||
|
if item
|
||
|
]
|
||
|
)
|
||
|
|
||
|
|
||
|
def do_dictsort(
|
||
|
value: t.Mapping[K, V],
|
||
|
case_sensitive: bool = False,
|
||
|
by: 'te.Literal["key", "value"]' = "key",
|
||
|
reverse: bool = False,
|
||
|
) -> t.List[t.Tuple[K, V]]:
|
||
|
"""Sort a dict and yield (key, value) pairs. Python dicts may not
|
||
|
be in the order you want to display them in, so sort them first.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for key, value in mydict|dictsort %}
|
||
|
sort the dict by key, case insensitive
|
||
|
|
||
|
{% for key, value in mydict|dictsort(reverse=true) %}
|
||
|
sort the dict by key, case insensitive, reverse order
|
||
|
|
||
|
{% for key, value in mydict|dictsort(true) %}
|
||
|
sort the dict by key, case sensitive
|
||
|
|
||
|
{% for key, value in mydict|dictsort(false, 'value') %}
|
||
|
sort the dict by value, case insensitive
|
||
|
"""
|
||
|
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: t.Tuple[t.Any, t.Any]) -> t.Any:
|
||
|
value = item[pos]
|
||
|
|
||
|
if not case_sensitive:
|
||
|
value = ignore_case(value)
|
||
|
|
||
|
return value
|
||
|
|
||
|
return sorted(value.items(), key=sort_func, reverse=reverse)
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_sort(
|
||
|
environment: "Environment",
|
||
|
value: "t.Iterable[V]",
|
||
|
reverse: bool = False,
|
||
|
case_sensitive: bool = False,
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
) -> "t.List[V]":
|
||
|
"""Sort an iterable using Python's :func:`sorted`.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for city in cities|sort %}
|
||
|
...
|
||
|
{% endfor %}
|
||
|
|
||
|
:param reverse: Sort descending instead of ascending.
|
||
|
:param case_sensitive: When sorting strings, sort upper and lower
|
||
|
case separately.
|
||
|
:param attribute: When sorting objects or dicts, an attribute or
|
||
|
key to sort by. Can use dot notation like ``"address.city"``.
|
||
|
Can be a list of attributes like ``"age,name"``.
|
||
|
|
||
|
The sort is stable, it does not change the relative order of
|
||
|
elements that compare equal. This makes it is possible to chain
|
||
|
sorts on different attributes and ordering.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for user in users|sort(attribute="name")
|
||
|
|sort(reverse=true, attribute="age") %}
|
||
|
...
|
||
|
{% endfor %}
|
||
|
|
||
|
As a shortcut to chaining when the direction is the same for all
|
||
|
attributes, pass a comma separate list of attributes.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{% for user in users|sort(attribute="age,name") %}
|
||
|
...
|
||
|
{% endfor %}
|
||
|
|
||
|
.. versionchanged:: 2.11.0
|
||
|
The ``attribute`` parameter can be a comma separated list of
|
||
|
attributes, e.g. ``"age,name"``.
|
||
|
|
||
|
.. versionchanged:: 2.6
|
||
|
The ``attribute`` parameter was added.
|
||
|
"""
|
||
|
key_func = make_multi_attrgetter(
|
||
|
environment, attribute, postprocess=ignore_case if not case_sensitive else None
|
||
|
)
|
||
|
return sorted(value, key=key_func, reverse=reverse)
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_unique(
|
||
|
environment: "Environment",
|
||
|
value: "t.Iterable[V]",
|
||
|
case_sensitive: bool = False,
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
) -> "t.Iterator[V]":
|
||
|
"""Returns a list of unique items from the given iterable.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ ['foo', 'bar', 'foobar', 'FooBar']|unique|list }}
|
||
|
-> ['foo', 'bar', 'foobar']
|
||
|
|
||
|
The unique items are yielded in the same order as their first occurrence in
|
||
|
the iterable passed to the filter.
|
||
|
|
||
|
:param case_sensitive: Treat upper and lower case strings as distinct.
|
||
|
:param attribute: Filter objects with unique values for this attribute.
|
||
|
"""
|
||
|
getter = make_attrgetter(
|
||
|
environment, attribute, postprocess=ignore_case if not case_sensitive else None
|
||
|
)
|
||
|
seen = set()
|
||
|
|
||
|
for item in value:
|
||
|
key = getter(item)
|
||
|
|
||
|
if key not in seen:
|
||
|
seen.add(key)
|
||
|
yield item
|
||
|
|
||
|
|
||
|
def _min_or_max(
|
||
|
environment: "Environment",
|
||
|
value: "t.Iterable[V]",
|
||
|
func: "t.Callable[..., V]",
|
||
|
case_sensitive: bool,
|
||
|
attribute: t.Optional[t.Union[str, int]],
|
||
|
) -> "t.Union[V, Undefined]":
|
||
|
it = iter(value)
|
||
|
|
||
|
try:
|
||
|
first = next(it)
|
||
|
except StopIteration:
|
||
|
return environment.undefined("No aggregated item, sequence was empty.")
|
||
|
|
||
|
key_func = make_attrgetter(
|
||
|
environment, attribute, postprocess=ignore_case if not case_sensitive else None
|
||
|
)
|
||
|
return func(chain([first], it), key=key_func)
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_min(
|
||
|
environment: "Environment",
|
||
|
value: "t.Iterable[V]",
|
||
|
case_sensitive: bool = False,
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
) -> "t.Union[V, Undefined]":
|
||
|
"""Return the smallest item from the sequence.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ [1, 2, 3]|min }}
|
||
|
-> 1
|
||
|
|
||
|
:param case_sensitive: Treat upper and lower case strings as distinct.
|
||
|
:param attribute: Get the object with the min value of this attribute.
|
||
|
"""
|
||
|
return _min_or_max(environment, value, min, case_sensitive, attribute)
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_max(
|
||
|
environment: "Environment",
|
||
|
value: "t.Iterable[V]",
|
||
|
case_sensitive: bool = False,
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
) -> "t.Union[V, Undefined]":
|
||
|
"""Return the largest item from the sequence.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ [1, 2, 3]|max }}
|
||
|
-> 3
|
||
|
|
||
|
:param case_sensitive: Treat upper and lower case strings as distinct.
|
||
|
:param attribute: Get the object with the max value of this attribute.
|
||
|
"""
|
||
|
return _min_or_max(environment, value, max, case_sensitive, attribute)
|
||
|
|
||
|
|
||
|
def do_default(
|
||
|
value: V,
|
||
|
default_value: V = "", # type: ignore
|
||
|
boolean: bool = False,
|
||
|
) -> V:
|
||
|
"""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) }}
|
||
|
|
||
|
.. versionchanged:: 2.11
|
||
|
It's now possible to configure the :class:`~jinja2.Environment` with
|
||
|
:class:`~jinja2.ChainableUndefined` to make the `default` filter work
|
||
|
on nested elements and attributes that may contain undefined values
|
||
|
in the chain without getting an :exc:`~jinja2.UndefinedError`.
|
||
|
"""
|
||
|
if isinstance(value, Undefined) or (boolean and not value):
|
||
|
return default_value
|
||
|
|
||
|
return value
|
||
|
|
||
|
|
||
|
@pass_eval_context
|
||
|
def sync_do_join(
|
||
|
eval_ctx: "EvalContext",
|
||
|
value: t.Iterable,
|
||
|
d: str = "",
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
) -> str:
|
||
|
"""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
|
||
|
|
||
|
It is also possible to join certain attributes of an object:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|join(', ', attribute='username') }}
|
||
|
|
||
|
.. versionadded:: 2.6
|
||
|
The `attribute` parameter was added.
|
||
|
"""
|
||
|
if attribute is not None:
|
||
|
value = map(make_attrgetter(eval_ctx.environment, attribute), value)
|
||
|
|
||
|
# no automatic escaping? joining is a lot easier then
|
||
|
if not eval_ctx.autoescape:
|
||
|
return str(d).join(map(str, 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] = str(item)
|
||
|
|
||
|
if do_escape:
|
||
|
d = escape(d)
|
||
|
else:
|
||
|
d = str(d)
|
||
|
|
||
|
return d.join(value)
|
||
|
|
||
|
# no html involved, to normal joining
|
||
|
return soft_str(d).join(map(soft_str, value))
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_join) # type: ignore
|
||
|
async def do_join(
|
||
|
eval_ctx: "EvalContext",
|
||
|
value: t.Union[t.AsyncIterable, t.Iterable],
|
||
|
d: str = "",
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
) -> str:
|
||
|
return sync_do_join(eval_ctx, await auto_to_list(value), d, attribute)
|
||
|
|
||
|
|
||
|
def do_center(value: str, width: int = 80) -> str:
|
||
|
"""Centers the value in a field of a given width."""
|
||
|
return soft_str(value).center(width)
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def sync_do_first(
|
||
|
environment: "Environment", seq: "t.Iterable[V]"
|
||
|
) -> "t.Union[V, Undefined]":
|
||
|
"""Return the first item of a sequence."""
|
||
|
try:
|
||
|
return next(iter(seq))
|
||
|
except StopIteration:
|
||
|
return environment.undefined("No first item, sequence was empty.")
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_first) # type: ignore
|
||
|
async def do_first(
|
||
|
environment: "Environment", seq: "t.Union[t.AsyncIterable[V], t.Iterable[V]]"
|
||
|
) -> "t.Union[V, Undefined]":
|
||
|
try:
|
||
|
return await auto_aiter(seq).__anext__()
|
||
|
except StopAsyncIteration:
|
||
|
return environment.undefined("No first item, sequence was empty.")
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_last(
|
||
|
environment: "Environment", seq: "t.Reversible[V]"
|
||
|
) -> "t.Union[V, Undefined]":
|
||
|
"""Return the last item of a sequence.
|
||
|
|
||
|
Note: Does not work with generators. You may want to explicitly
|
||
|
convert it to a list:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ data | selectattr('name', '==', 'Jinja') | list | last }}
|
||
|
"""
|
||
|
try:
|
||
|
return next(iter(reversed(seq)))
|
||
|
except StopIteration:
|
||
|
return environment.undefined("No last item, sequence was empty.")
|
||
|
|
||
|
|
||
|
# No async do_last, it may not be safe in async mode.
|
||
|
|
||
|
|
||
|
@pass_context
|
||
|
def do_random(context: "Context", seq: "t.Sequence[V]") -> "t.Union[V, Undefined]":
|
||
|
"""Return a random item from the sequence."""
|
||
|
try:
|
||
|
return random.choice(seq)
|
||
|
except IndexError:
|
||
|
return context.environment.undefined("No random item, sequence was empty.")
|
||
|
|
||
|
|
||
|
def do_filesizeformat(value: t.Union[str, float, int], binary: bool = False) -> str:
|
||
|
"""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 = 1024 if binary else 1000
|
||
|
prefixes = [
|
||
|
("KiB" if binary else "kB"),
|
||
|
("MiB" if binary else "MB"),
|
||
|
("GiB" if binary else "GB"),
|
||
|
("TiB" if binary else "TB"),
|
||
|
("PiB" if binary else "PB"),
|
||
|
("EiB" if binary else "EB"),
|
||
|
("ZiB" if binary else "ZB"),
|
||
|
("YiB" if binary else "YB"),
|
||
|
]
|
||
|
|
||
|
if bytes == 1:
|
||
|
return "1 Byte"
|
||
|
elif bytes < base:
|
||
|
return f"{int(bytes)} Bytes"
|
||
|
else:
|
||
|
for i, prefix in enumerate(prefixes):
|
||
|
unit = base ** (i + 2)
|
||
|
|
||
|
if bytes < unit:
|
||
|
return f"{base * bytes / unit:.1f} {prefix}"
|
||
|
|
||
|
return f"{base * bytes / unit:.1f} {prefix}"
|
||
|
|
||
|
|
||
|
def do_pprint(value: t.Any) -> str:
|
||
|
"""Pretty print a variable. Useful for debugging."""
|
||
|
return pformat(value)
|
||
|
|
||
|
|
||
|
_uri_scheme_re = re.compile(r"^([\w.+-]{2,}:(/){0,2})$")
|
||
|
|
||
|
|
||
|
@pass_eval_context
|
||
|
def do_urlize(
|
||
|
eval_ctx: "EvalContext",
|
||
|
value: str,
|
||
|
trim_url_limit: t.Optional[int] = None,
|
||
|
nofollow: bool = False,
|
||
|
target: t.Optional[str] = None,
|
||
|
rel: t.Optional[str] = None,
|
||
|
extra_schemes: t.Optional[t.Iterable[str]] = None,
|
||
|
) -> str:
|
||
|
"""Convert URLs in text into clickable links.
|
||
|
|
||
|
This may not recognize links in some situations. Usually, a more
|
||
|
comprehensive formatter, such as a Markdown library, is a better
|
||
|
choice.
|
||
|
|
||
|
Works on ``http://``, ``https://``, ``www.``, ``mailto:``, and email
|
||
|
addresses. Links with trailing punctuation (periods, commas, closing
|
||
|
parentheses) and leading punctuation (opening parentheses) are
|
||
|
recognized excluding the punctuation. Email addresses that include
|
||
|
header fields are not recognized (for example,
|
||
|
``mailto:address@example.com?cc=copy@example.com``).
|
||
|
|
||
|
:param value: Original text containing URLs to link.
|
||
|
:param trim_url_limit: Shorten displayed URL values to this length.
|
||
|
:param nofollow: Add the ``rel=nofollow`` attribute to links.
|
||
|
:param target: Add the ``target`` attribute to links.
|
||
|
:param rel: Add the ``rel`` attribute to links.
|
||
|
:param extra_schemes: Recognize URLs that start with these schemes
|
||
|
in addition to the default behavior. Defaults to
|
||
|
``env.policies["urlize.extra_schemes"]``, which defaults to no
|
||
|
extra schemes.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
The ``extra_schemes`` parameter was added.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
Generate ``https://`` links for URLs without a scheme.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
The parsing rules were updated. Recognize email addresses with
|
||
|
or without the ``mailto:`` scheme. Validate IP addresses. Ignore
|
||
|
parentheses and brackets in more cases.
|
||
|
|
||
|
.. versionchanged:: 2.8
|
||
|
The ``target`` parameter was added.
|
||
|
"""
|
||
|
policies = eval_ctx.environment.policies
|
||
|
rel_parts = set((rel or "").split())
|
||
|
|
||
|
if nofollow:
|
||
|
rel_parts.add("nofollow")
|
||
|
|
||
|
rel_parts.update((policies["urlize.rel"] or "").split())
|
||
|
rel = " ".join(sorted(rel_parts)) or None
|
||
|
|
||
|
if target is None:
|
||
|
target = policies["urlize.target"]
|
||
|
|
||
|
if extra_schemes is None:
|
||
|
extra_schemes = policies["urlize.extra_schemes"] or ()
|
||
|
|
||
|
for scheme in extra_schemes:
|
||
|
if _uri_scheme_re.fullmatch(scheme) is None:
|
||
|
raise FilterArgumentError(f"{scheme!r} is not a valid URI scheme prefix.")
|
||
|
|
||
|
rv = urlize(
|
||
|
value,
|
||
|
trim_url_limit=trim_url_limit,
|
||
|
rel=rel,
|
||
|
target=target,
|
||
|
extra_schemes=extra_schemes,
|
||
|
)
|
||
|
|
||
|
if eval_ctx.autoescape:
|
||
|
rv = Markup(rv)
|
||
|
|
||
|
return rv
|
||
|
|
||
|
|
||
|
def do_indent(
|
||
|
s: str, width: t.Union[int, str] = 4, first: bool = False, blank: bool = False
|
||
|
) -> str:
|
||
|
"""Return a copy of the string with each line indented by 4 spaces. The
|
||
|
first line and blank lines are not indented by default.
|
||
|
|
||
|
:param width: Number of spaces, or a string, to indent by.
|
||
|
:param first: Don't skip indenting the first line.
|
||
|
:param blank: Don't skip indenting empty lines.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
``width`` can be a string.
|
||
|
|
||
|
.. versionchanged:: 2.10
|
||
|
Blank lines are not indented by default.
|
||
|
|
||
|
Rename the ``indentfirst`` argument to ``first``.
|
||
|
"""
|
||
|
if isinstance(width, str):
|
||
|
indention = width
|
||
|
else:
|
||
|
indention = " " * width
|
||
|
|
||
|
newline = "\n"
|
||
|
|
||
|
if isinstance(s, Markup):
|
||
|
indention = Markup(indention)
|
||
|
newline = Markup(newline)
|
||
|
|
||
|
s += newline # this quirk is necessary for splitlines method
|
||
|
|
||
|
if blank:
|
||
|
rv = (newline + indention).join(s.splitlines())
|
||
|
else:
|
||
|
lines = s.splitlines()
|
||
|
rv = lines.pop(0)
|
||
|
|
||
|
if lines:
|
||
|
rv += newline + newline.join(
|
||
|
indention + line if line else line for line in lines
|
||
|
)
|
||
|
|
||
|
if first:
|
||
|
rv = indention + rv
|
||
|
|
||
|
return rv
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_truncate(
|
||
|
env: "Environment",
|
||
|
s: str,
|
||
|
length: int = 255,
|
||
|
killwords: bool = False,
|
||
|
end: str = "...",
|
||
|
leeway: t.Optional[int] = None,
|
||
|
) -> str:
|
||
|
"""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 discard 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. Strings that only exceed the length by the tolerance
|
||
|
margin given in the fourth parameter will not be truncated.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ "foo bar baz qux"|truncate(9) }}
|
||
|
-> "foo..."
|
||
|
{{ "foo bar baz qux"|truncate(9, True) }}
|
||
|
-> "foo ba..."
|
||
|
{{ "foo bar baz qux"|truncate(11) }}
|
||
|
-> "foo bar baz qux"
|
||
|
{{ "foo bar baz qux"|truncate(11, False, '...', 0) }}
|
||
|
-> "foo bar..."
|
||
|
|
||
|
The default leeway on newer Jinja versions is 5 and was 0 before but
|
||
|
can be reconfigured globally.
|
||
|
"""
|
||
|
if leeway is None:
|
||
|
leeway = env.policies["truncate.leeway"]
|
||
|
|
||
|
assert length >= len(end), f"expected length >= {len(end)}, got {length}"
|
||
|
assert leeway >= 0, f"expected leeway >= 0, got {leeway}"
|
||
|
|
||
|
if len(s) <= length + leeway:
|
||
|
return s
|
||
|
|
||
|
if killwords:
|
||
|
return s[: length - len(end)] + end
|
||
|
|
||
|
result = s[: length - len(end)].rsplit(" ", 1)[0]
|
||
|
return result + end
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_wordwrap(
|
||
|
environment: "Environment",
|
||
|
s: str,
|
||
|
width: int = 79,
|
||
|
break_long_words: bool = True,
|
||
|
wrapstring: t.Optional[str] = None,
|
||
|
break_on_hyphens: bool = True,
|
||
|
) -> str:
|
||
|
"""Wrap a string to the given width. Existing newlines are treated
|
||
|
as paragraphs to be wrapped separately.
|
||
|
|
||
|
:param s: Original text to wrap.
|
||
|
:param width: Maximum length of wrapped lines.
|
||
|
:param break_long_words: If a word is longer than ``width``, break
|
||
|
it across lines.
|
||
|
:param break_on_hyphens: If a word contains hyphens, it may be split
|
||
|
across lines.
|
||
|
:param wrapstring: String to join each wrapped line. Defaults to
|
||
|
:attr:`Environment.newline_sequence`.
|
||
|
|
||
|
.. versionchanged:: 2.11
|
||
|
Existing newlines are treated as paragraphs wrapped separately.
|
||
|
|
||
|
.. versionchanged:: 2.11
|
||
|
Added the ``break_on_hyphens`` parameter.
|
||
|
|
||
|
.. versionchanged:: 2.7
|
||
|
Added the ``wrapstring`` parameter.
|
||
|
"""
|
||
|
import textwrap
|
||
|
|
||
|
if wrapstring is None:
|
||
|
wrapstring = environment.newline_sequence
|
||
|
|
||
|
# textwrap.wrap doesn't consider existing newlines when wrapping.
|
||
|
# If the string has a newline before width, wrap will still insert
|
||
|
# a newline at width, resulting in a short line. Instead, split and
|
||
|
# wrap each paragraph individually.
|
||
|
return wrapstring.join(
|
||
|
[
|
||
|
wrapstring.join(
|
||
|
textwrap.wrap(
|
||
|
line,
|
||
|
width=width,
|
||
|
expand_tabs=False,
|
||
|
replace_whitespace=False,
|
||
|
break_long_words=break_long_words,
|
||
|
break_on_hyphens=break_on_hyphens,
|
||
|
)
|
||
|
)
|
||
|
for line in s.splitlines()
|
||
|
]
|
||
|
)
|
||
|
|
||
|
|
||
|
_word_re = re.compile(r"\w+")
|
||
|
|
||
|
|
||
|
def do_wordcount(s: str) -> int:
|
||
|
"""Count the words in that string."""
|
||
|
return len(_word_re.findall(soft_str(s)))
|
||
|
|
||
|
|
||
|
def do_int(value: t.Any, default: int = 0, base: int = 10) -> int:
|
||
|
"""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. You
|
||
|
can also override the default base (10) in the second
|
||
|
parameter, which handles input with prefixes such as
|
||
|
0b, 0o and 0x for bases 2, 8 and 16 respectively.
|
||
|
The base is ignored for decimal numbers and non-string values.
|
||
|
"""
|
||
|
try:
|
||
|
if isinstance(value, str):
|
||
|
return int(value, base)
|
||
|
|
||
|
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: t.Any, default: float = 0.0) -> float:
|
||
|
"""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: str, *args: t.Any, **kwargs: t.Any) -> str:
|
||
|
"""Apply the given values to a `printf-style`_ format string, like
|
||
|
``string % values``.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ "%s, %s!"|format(greeting, name) }}
|
||
|
Hello, World!
|
||
|
|
||
|
In most cases it should be more convenient and efficient to use the
|
||
|
``%`` operator or :meth:`str.format`.
|
||
|
|
||
|
.. code-block:: text
|
||
|
|
||
|
{{ "%s, %s!" % (greeting, name) }}
|
||
|
{{ "{}, {}!".format(greeting, name) }}
|
||
|
|
||
|
.. _printf-style: https://docs.python.org/library/stdtypes.html
|
||
|
#printf-style-string-formatting
|
||
|
"""
|
||
|
if args and kwargs:
|
||
|
raise FilterArgumentError(
|
||
|
"can't handle positional and keyword arguments at the same time"
|
||
|
)
|
||
|
|
||
|
return soft_str(value) % (kwargs or args)
|
||
|
|
||
|
|
||
|
def do_trim(value: str, chars: t.Optional[str] = None) -> str:
|
||
|
"""Strip leading and trailing characters, by default whitespace."""
|
||
|
return soft_str(value).strip(chars)
|
||
|
|
||
|
|
||
|
def do_striptags(value: "t.Union[str, HasHTML]") -> str:
|
||
|
"""Strip SGML/XML tags and replace adjacent whitespace by one space."""
|
||
|
if hasattr(value, "__html__"):
|
||
|
value = t.cast("HasHTML", value).__html__()
|
||
|
|
||
|
return Markup(str(value)).striptags()
|
||
|
|
||
|
|
||
|
def sync_do_slice(
|
||
|
value: "t.Collection[V]", slices: int, fill_with: "t.Optional[V]" = None
|
||
|
) -> "t.Iterator[t.List[V]]":
|
||
|
"""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="columnwrapper">
|
||
|
{%- 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 range(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
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_slice) # type: ignore
|
||
|
async def do_slice(
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
slices: int,
|
||
|
fill_with: t.Optional[t.Any] = None,
|
||
|
) -> "t.Iterator[t.List[V]]":
|
||
|
return sync_do_slice(await auto_to_list(value), slices, fill_with)
|
||
|
|
||
|
|
||
|
def do_batch(
|
||
|
value: "t.Iterable[V]", linecount: int, fill_with: "t.Optional[V]" = None
|
||
|
) -> "t.Iterator[t.List[V]]":
|
||
|
"""
|
||
|
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 up 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>
|
||
|
"""
|
||
|
tmp: "t.List[V]" = []
|
||
|
|
||
|
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: float,
|
||
|
precision: int = 0,
|
||
|
method: 'te.Literal["common", "ceil", "floor"]' = "common",
|
||
|
) -> float:
|
||
|
"""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 method not 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 t.cast(float, func(value * (10**precision)) / (10**precision))
|
||
|
|
||
|
|
||
|
class _GroupTuple(t.NamedTuple):
|
||
|
grouper: t.Any
|
||
|
list: t.List
|
||
|
|
||
|
# Use the regular tuple repr to hide this subclass if users print
|
||
|
# out the value during debugging.
|
||
|
def __repr__(self) -> str:
|
||
|
return tuple.__repr__(self)
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return tuple.__str__(self)
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def sync_do_groupby(
|
||
|
environment: "Environment",
|
||
|
value: "t.Iterable[V]",
|
||
|
attribute: t.Union[str, int],
|
||
|
default: t.Optional[t.Any] = None,
|
||
|
case_sensitive: bool = False,
|
||
|
) -> "t.List[_GroupTuple]":
|
||
|
"""Group a sequence of objects by an attribute using Python's
|
||
|
:func:`itertools.groupby`. The attribute can use dot notation for
|
||
|
nested access, like ``"address.city"``. Unlike Python's ``groupby``,
|
||
|
the values are sorted first so only one group is returned for each
|
||
|
unique value.
|
||
|
|
||
|
For example, a list of ``User`` objects with a ``city`` attribute
|
||
|
can be rendered in groups. In this example, ``grouper`` refers to
|
||
|
the ``city`` value of the group.
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<ul>{% for city, items in users|groupby("city") %}
|
||
|
<li>{{ city }}
|
||
|
<ul>{% for user in items %}
|
||
|
<li>{{ user.name }}
|
||
|
{% endfor %}</ul>
|
||
|
</li>
|
||
|
{% endfor %}</ul>
|
||
|
|
||
|
``groupby`` yields namedtuples of ``(grouper, list)``, which
|
||
|
can be used instead of the tuple unpacking above. ``grouper`` is the
|
||
|
value of the attribute, and ``list`` is the items with that value.
|
||
|
|
||
|
.. sourcecode:: html+jinja
|
||
|
|
||
|
<ul>{% for group in users|groupby("city") %}
|
||
|
<li>{{ group.grouper }}: {{ group.list|join(", ") }}
|
||
|
{% endfor %}</ul>
|
||
|
|
||
|
You can specify a ``default`` value to use if an object in the list
|
||
|
does not have the given attribute.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
<ul>{% for city, items in users|groupby("city", default="NY") %}
|
||
|
<li>{{ city }}: {{ items|map(attribute="name")|join(", ") }}</li>
|
||
|
{% endfor %}</ul>
|
||
|
|
||
|
Like the :func:`~jinja-filters.sort` filter, sorting and grouping is
|
||
|
case-insensitive by default. The ``key`` for each group will have
|
||
|
the case of the first item in that group of values. For example, if
|
||
|
a list of users has cities ``["CA", "NY", "ca"]``, the "CA" group
|
||
|
will have two values. This can be disabled by passing
|
||
|
``case_sensitive=True``.
|
||
|
|
||
|
.. versionchanged:: 3.1
|
||
|
Added the ``case_sensitive`` parameter. Sorting and grouping is
|
||
|
case-insensitive by default, matching other filters that do
|
||
|
comparisons.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
Added the ``default`` parameter.
|
||
|
|
||
|
.. versionchanged:: 2.6
|
||
|
The attribute supports dot notation for nested access.
|
||
|
"""
|
||
|
expr = make_attrgetter(
|
||
|
environment,
|
||
|
attribute,
|
||
|
postprocess=ignore_case if not case_sensitive else None,
|
||
|
default=default,
|
||
|
)
|
||
|
out = [
|
||
|
_GroupTuple(key, list(values))
|
||
|
for key, values in groupby(sorted(value, key=expr), expr)
|
||
|
]
|
||
|
|
||
|
if not case_sensitive:
|
||
|
# Return the real key from the first value instead of the lowercase key.
|
||
|
output_expr = make_attrgetter(environment, attribute, default=default)
|
||
|
out = [_GroupTuple(output_expr(values[0]), values) for _, values in out]
|
||
|
|
||
|
return out
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_groupby) # type: ignore
|
||
|
async def do_groupby(
|
||
|
environment: "Environment",
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
attribute: t.Union[str, int],
|
||
|
default: t.Optional[t.Any] = None,
|
||
|
case_sensitive: bool = False,
|
||
|
) -> "t.List[_GroupTuple]":
|
||
|
expr = make_attrgetter(
|
||
|
environment,
|
||
|
attribute,
|
||
|
postprocess=ignore_case if not case_sensitive else None,
|
||
|
default=default,
|
||
|
)
|
||
|
out = [
|
||
|
_GroupTuple(key, await auto_to_list(values))
|
||
|
for key, values in groupby(sorted(await auto_to_list(value), key=expr), expr)
|
||
|
]
|
||
|
|
||
|
if not case_sensitive:
|
||
|
# Return the real key from the first value instead of the lowercase key.
|
||
|
output_expr = make_attrgetter(environment, attribute, default=default)
|
||
|
out = [_GroupTuple(output_expr(values[0]), values) for _, values in out]
|
||
|
|
||
|
return out
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def sync_do_sum(
|
||
|
environment: "Environment",
|
||
|
iterable: "t.Iterable[V]",
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
start: V = 0, # type: ignore
|
||
|
) -> V:
|
||
|
"""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
|
||
|
|
||
|
Total: {{ items|sum(attribute='price') }}
|
||
|
|
||
|
.. versionchanged:: 2.6
|
||
|
The ``attribute`` parameter was added to allow summing up over
|
||
|
attributes. Also the ``start`` parameter was moved on to the right.
|
||
|
"""
|
||
|
if attribute is not None:
|
||
|
iterable = map(make_attrgetter(environment, attribute), iterable)
|
||
|
|
||
|
return sum(iterable, start) # type: ignore[no-any-return, call-overload]
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_sum) # type: ignore
|
||
|
async def do_sum(
|
||
|
environment: "Environment",
|
||
|
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
attribute: t.Optional[t.Union[str, int]] = None,
|
||
|
start: V = 0, # type: ignore
|
||
|
) -> V:
|
||
|
rv = start
|
||
|
|
||
|
if attribute is not None:
|
||
|
func = make_attrgetter(environment, attribute)
|
||
|
else:
|
||
|
|
||
|
def func(x: V) -> V:
|
||
|
return x
|
||
|
|
||
|
async for item in auto_aiter(iterable):
|
||
|
rv += func(item)
|
||
|
|
||
|
return rv
|
||
|
|
||
|
|
||
|
def sync_do_list(value: "t.Iterable[V]") -> "t.List[V]":
|
||
|
"""Convert the value into a list. If it was a string the returned list
|
||
|
will be a list of characters.
|
||
|
"""
|
||
|
return list(value)
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_list) # type: ignore
|
||
|
async def do_list(value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]") -> "t.List[V]":
|
||
|
return await auto_to_list(value)
|
||
|
|
||
|
|
||
|
def do_mark_safe(value: str) -> Markup:
|
||
|
"""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: str) -> str:
|
||
|
"""Mark a value as unsafe. This is the reverse operation for :func:`safe`."""
|
||
|
return str(value)
|
||
|
|
||
|
|
||
|
@typing.overload
|
||
|
def do_reverse(value: str) -> str:
|
||
|
...
|
||
|
|
||
|
|
||
|
@typing.overload
|
||
|
def do_reverse(value: "t.Iterable[V]") -> "t.Iterable[V]":
|
||
|
...
|
||
|
|
||
|
|
||
|
def do_reverse(value: t.Union[str, t.Iterable[V]]) -> t.Union[str, t.Iterable[V]]:
|
||
|
"""Reverse the object or return an iterator that iterates over it the other
|
||
|
way round.
|
||
|
"""
|
||
|
if isinstance(value, str):
|
||
|
return value[::-1]
|
||
|
|
||
|
try:
|
||
|
return reversed(value) # type: ignore
|
||
|
except TypeError:
|
||
|
try:
|
||
|
rv = list(value)
|
||
|
rv.reverse()
|
||
|
return rv
|
||
|
except TypeError as e:
|
||
|
raise FilterArgumentError("argument must be iterable") from e
|
||
|
|
||
|
|
||
|
@pass_environment
|
||
|
def do_attr(
|
||
|
environment: "Environment", obj: t.Any, name: str
|
||
|
) -> t.Union[Undefined, t.Any]:
|
||
|
"""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:
|
||
|
environment = t.cast("SandboxedEnvironment", environment)
|
||
|
|
||
|
if not environment.is_safe_attribute(obj, name, value):
|
||
|
return environment.unsafe_undefined(obj, name)
|
||
|
|
||
|
return value
|
||
|
|
||
|
return environment.undefined(obj=obj, name=name)
|
||
|
|
||
|
|
||
|
@typing.overload
|
||
|
def sync_do_map(
|
||
|
context: "Context", value: t.Iterable, name: str, *args: t.Any, **kwargs: t.Any
|
||
|
) -> t.Iterable:
|
||
|
...
|
||
|
|
||
|
|
||
|
@typing.overload
|
||
|
def sync_do_map(
|
||
|
context: "Context",
|
||
|
value: t.Iterable,
|
||
|
*,
|
||
|
attribute: str = ...,
|
||
|
default: t.Optional[t.Any] = None,
|
||
|
) -> t.Iterable:
|
||
|
...
|
||
|
|
||
|
|
||
|
@pass_context
|
||
|
def sync_do_map(
|
||
|
context: "Context", value: t.Iterable, *args: t.Any, **kwargs: t.Any
|
||
|
) -> t.Iterable:
|
||
|
"""Applies a filter on a sequence of objects or looks up an attribute.
|
||
|
This is useful when dealing with lists of objects but you are really
|
||
|
only interested in a certain value of it.
|
||
|
|
||
|
The basic usage is mapping on an attribute. Imagine you have a list
|
||
|
of users but you are only interested in a list of usernames:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
Users on this page: {{ users|map(attribute='username')|join(', ') }}
|
||
|
|
||
|
You can specify a ``default`` value to use if an object in the list
|
||
|
does not have the given attribute.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|map(attribute="username", default="Anonymous")|join(", ") }}
|
||
|
|
||
|
Alternatively you can let it invoke a filter by passing the name of the
|
||
|
filter and the arguments afterwards. A good example would be applying a
|
||
|
text conversion filter on a sequence:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
Users on this page: {{ titles|map('lower')|join(', ') }}
|
||
|
|
||
|
Similar to a generator comprehension such as:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
(u.username for u in users)
|
||
|
(getattr(u, "username", "Anonymous") for u in users)
|
||
|
(do_lower(x) for x in titles)
|
||
|
|
||
|
.. versionchanged:: 2.11.0
|
||
|
Added the ``default`` parameter.
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
if value:
|
||
|
func = prepare_map(context, args, kwargs)
|
||
|
|
||
|
for item in value:
|
||
|
yield func(item)
|
||
|
|
||
|
|
||
|
@typing.overload
|
||
|
def do_map(
|
||
|
context: "Context",
|
||
|
value: t.Union[t.AsyncIterable, t.Iterable],
|
||
|
name: str,
|
||
|
*args: t.Any,
|
||
|
**kwargs: t.Any,
|
||
|
) -> t.Iterable:
|
||
|
...
|
||
|
|
||
|
|
||
|
@typing.overload
|
||
|
def do_map(
|
||
|
context: "Context",
|
||
|
value: t.Union[t.AsyncIterable, t.Iterable],
|
||
|
*,
|
||
|
attribute: str = ...,
|
||
|
default: t.Optional[t.Any] = None,
|
||
|
) -> t.Iterable:
|
||
|
...
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_map) # type: ignore
|
||
|
async def do_map(
|
||
|
context: "Context",
|
||
|
value: t.Union[t.AsyncIterable, t.Iterable],
|
||
|
*args: t.Any,
|
||
|
**kwargs: t.Any,
|
||
|
) -> t.AsyncIterable:
|
||
|
if value:
|
||
|
func = prepare_map(context, args, kwargs)
|
||
|
|
||
|
async for item in auto_aiter(value):
|
||
|
yield await auto_await(func(item))
|
||
|
|
||
|
|
||
|
@pass_context
|
||
|
def sync_do_select(
|
||
|
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
|
||
|
) -> "t.Iterator[V]":
|
||
|
"""Filters a sequence of objects by applying a test to each object,
|
||
|
and only selecting the objects with the test succeeding.
|
||
|
|
||
|
If no test is specified, each object will be evaluated as a boolean.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ numbers|select("odd") }}
|
||
|
{{ numbers|select("odd") }}
|
||
|
{{ numbers|select("divisibleby", 3) }}
|
||
|
{{ numbers|select("lessthan", 42) }}
|
||
|
{{ strings|select("equalto", "mystring") }}
|
||
|
|
||
|
Similar to a generator comprehension such as:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
(n for n in numbers if test_odd(n))
|
||
|
(n for n in numbers if test_divisibleby(n, 3))
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return select_or_reject(context, value, args, kwargs, lambda x: x, False)
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_select) # type: ignore
|
||
|
async def do_select(
|
||
|
context: "Context",
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
*args: t.Any,
|
||
|
**kwargs: t.Any,
|
||
|
) -> "t.AsyncIterator[V]":
|
||
|
return async_select_or_reject(context, value, args, kwargs, lambda x: x, False)
|
||
|
|
||
|
|
||
|
@pass_context
|
||
|
def sync_do_reject(
|
||
|
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
|
||
|
) -> "t.Iterator[V]":
|
||
|
"""Filters a sequence of objects by applying a test to each object,
|
||
|
and rejecting the objects with the test succeeding.
|
||
|
|
||
|
If no test is specified, each object will be evaluated as a boolean.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ numbers|reject("odd") }}
|
||
|
|
||
|
Similar to a generator comprehension such as:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
(n for n in numbers if not test_odd(n))
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return select_or_reject(context, value, args, kwargs, lambda x: not x, False)
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_reject) # type: ignore
|
||
|
async def do_reject(
|
||
|
context: "Context",
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
*args: t.Any,
|
||
|
**kwargs: t.Any,
|
||
|
) -> "t.AsyncIterator[V]":
|
||
|
return async_select_or_reject(context, value, args, kwargs, lambda x: not x, False)
|
||
|
|
||
|
|
||
|
@pass_context
|
||
|
def sync_do_selectattr(
|
||
|
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
|
||
|
) -> "t.Iterator[V]":
|
||
|
"""Filters a sequence of objects by applying a test to the specified
|
||
|
attribute of each object, and only selecting the objects with the
|
||
|
test succeeding.
|
||
|
|
||
|
If no test is specified, the attribute's value will be evaluated as
|
||
|
a boolean.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|selectattr("is_active") }}
|
||
|
{{ users|selectattr("email", "none") }}
|
||
|
|
||
|
Similar to a generator comprehension such as:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
(u for user in users if user.is_active)
|
||
|
(u for user in users if test_none(user.email))
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return select_or_reject(context, value, args, kwargs, lambda x: x, True)
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_selectattr) # type: ignore
|
||
|
async def do_selectattr(
|
||
|
context: "Context",
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
*args: t.Any,
|
||
|
**kwargs: t.Any,
|
||
|
) -> "t.AsyncIterator[V]":
|
||
|
return async_select_or_reject(context, value, args, kwargs, lambda x: x, True)
|
||
|
|
||
|
|
||
|
@pass_context
|
||
|
def sync_do_rejectattr(
|
||
|
context: "Context", value: "t.Iterable[V]", *args: t.Any, **kwargs: t.Any
|
||
|
) -> "t.Iterator[V]":
|
||
|
"""Filters a sequence of objects by applying a test to the specified
|
||
|
attribute of each object, and rejecting the objects with the test
|
||
|
succeeding.
|
||
|
|
||
|
If no test is specified, the attribute's value will be evaluated as
|
||
|
a boolean.
|
||
|
|
||
|
.. sourcecode:: jinja
|
||
|
|
||
|
{{ users|rejectattr("is_active") }}
|
||
|
{{ users|rejectattr("email", "none") }}
|
||
|
|
||
|
Similar to a generator comprehension such as:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
(u for user in users if not user.is_active)
|
||
|
(u for user in users if not test_none(user.email))
|
||
|
|
||
|
.. versionadded:: 2.7
|
||
|
"""
|
||
|
return select_or_reject(context, value, args, kwargs, lambda x: not x, True)
|
||
|
|
||
|
|
||
|
@async_variant(sync_do_rejectattr) # type: ignore
|
||
|
async def do_rejectattr(
|
||
|
context: "Context",
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
*args: t.Any,
|
||
|
**kwargs: t.Any,
|
||
|
) -> "t.AsyncIterator[V]":
|
||
|
return async_select_or_reject(context, value, args, kwargs, lambda x: not x, True)
|
||
|
|
||
|
|
||
|
@pass_eval_context
|
||
|
def do_tojson(
|
||
|
eval_ctx: "EvalContext", value: t.Any, indent: t.Optional[int] = None
|
||
|
) -> Markup:
|
||
|
"""Serialize an object to a string of JSON, and mark it safe to
|
||
|
render in HTML. This filter is only for use in HTML documents.
|
||
|
|
||
|
The returned string is safe to render in HTML documents and
|
||
|
``<script>`` tags. The exception is in HTML attributes that are
|
||
|
double quoted; either use single quotes or the ``|forceescape``
|
||
|
filter.
|
||
|
|
||
|
:param value: The object to serialize to JSON.
|
||
|
:param indent: The ``indent`` parameter passed to ``dumps``, for
|
||
|
pretty-printing the value.
|
||
|
|
||
|
.. versionadded:: 2.9
|
||
|
"""
|
||
|
policies = eval_ctx.environment.policies
|
||
|
dumps = policies["json.dumps_function"]
|
||
|
kwargs = policies["json.dumps_kwargs"]
|
||
|
|
||
|
if indent is not None:
|
||
|
kwargs = kwargs.copy()
|
||
|
kwargs["indent"] = indent
|
||
|
|
||
|
return htmlsafe_json_dumps(value, dumps=dumps, **kwargs)
|
||
|
|
||
|
|
||
|
def prepare_map(
|
||
|
context: "Context", args: t.Tuple, kwargs: t.Dict[str, t.Any]
|
||
|
) -> t.Callable[[t.Any], t.Any]:
|
||
|
if not args and "attribute" in kwargs:
|
||
|
attribute = kwargs.pop("attribute")
|
||
|
default = kwargs.pop("default", None)
|
||
|
|
||
|
if kwargs:
|
||
|
raise FilterArgumentError(
|
||
|
f"Unexpected keyword argument {next(iter(kwargs))!r}"
|
||
|
)
|
||
|
|
||
|
func = make_attrgetter(context.environment, attribute, default=default)
|
||
|
else:
|
||
|
try:
|
||
|
name = args[0]
|
||
|
args = args[1:]
|
||
|
except LookupError:
|
||
|
raise FilterArgumentError("map requires a filter argument") from None
|
||
|
|
||
|
def func(item: t.Any) -> t.Any:
|
||
|
return context.environment.call_filter(
|
||
|
name, item, args, kwargs, context=context
|
||
|
)
|
||
|
|
||
|
return func
|
||
|
|
||
|
|
||
|
def prepare_select_or_reject(
|
||
|
context: "Context",
|
||
|
args: t.Tuple,
|
||
|
kwargs: t.Dict[str, t.Any],
|
||
|
modfunc: t.Callable[[t.Any], t.Any],
|
||
|
lookup_attr: bool,
|
||
|
) -> t.Callable[[t.Any], t.Any]:
|
||
|
if lookup_attr:
|
||
|
try:
|
||
|
attr = args[0]
|
||
|
except LookupError:
|
||
|
raise FilterArgumentError("Missing parameter for attribute name") from None
|
||
|
|
||
|
transfunc = make_attrgetter(context.environment, attr)
|
||
|
off = 1
|
||
|
else:
|
||
|
off = 0
|
||
|
|
||
|
def transfunc(x: V) -> V:
|
||
|
return x
|
||
|
|
||
|
try:
|
||
|
name = args[off]
|
||
|
args = args[1 + off :]
|
||
|
|
||
|
def func(item: t.Any) -> t.Any:
|
||
|
return context.environment.call_test(name, item, args, kwargs)
|
||
|
|
||
|
except LookupError:
|
||
|
func = bool # type: ignore
|
||
|
|
||
|
return lambda item: modfunc(func(transfunc(item)))
|
||
|
|
||
|
|
||
|
def select_or_reject(
|
||
|
context: "Context",
|
||
|
value: "t.Iterable[V]",
|
||
|
args: t.Tuple,
|
||
|
kwargs: t.Dict[str, t.Any],
|
||
|
modfunc: t.Callable[[t.Any], t.Any],
|
||
|
lookup_attr: bool,
|
||
|
) -> "t.Iterator[V]":
|
||
|
if value:
|
||
|
func = prepare_select_or_reject(context, args, kwargs, modfunc, lookup_attr)
|
||
|
|
||
|
for item in value:
|
||
|
if func(item):
|
||
|
yield item
|
||
|
|
||
|
|
||
|
async def async_select_or_reject(
|
||
|
context: "Context",
|
||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||
|
args: t.Tuple,
|
||
|
kwargs: t.Dict[str, t.Any],
|
||
|
modfunc: t.Callable[[t.Any], t.Any],
|
||
|
lookup_attr: bool,
|
||
|
) -> "t.AsyncIterator[V]":
|
||
|
if value:
|
||
|
func = prepare_select_or_reject(context, args, kwargs, modfunc, lookup_attr)
|
||
|
|
||
|
async for item in auto_aiter(value):
|
||
|
if func(item):
|
||
|
yield item
|
||
|
|
||
|
|
||
|
FILTERS = {
|
||
|
"abs": abs,
|
||
|
"attr": do_attr,
|
||
|
"batch": do_batch,
|
||
|
"capitalize": do_capitalize,
|
||
|
"center": do_center,
|
||
|
"count": len,
|
||
|
"d": do_default,
|
||
|
"default": do_default,
|
||
|
"dictsort": do_dictsort,
|
||
|
"e": escape,
|
||
|
"escape": escape,
|
||
|
"filesizeformat": do_filesizeformat,
|
||
|
"first": do_first,
|
||
|
"float": do_float,
|
||
|
"forceescape": do_forceescape,
|
||
|
"format": do_format,
|
||
|
"groupby": do_groupby,
|
||
|
"indent": do_indent,
|
||
|
"int": do_int,
|
||
|
"join": do_join,
|
||
|
"last": do_last,
|
||
|
"length": len,
|
||
|
"list": do_list,
|
||
|
"lower": do_lower,
|
||
|
"items": do_items,
|
||
|
"map": do_map,
|
||
|
"min": do_min,
|
||
|
"max": do_max,
|
||
|
"pprint": do_pprint,
|
||
|
"random": do_random,
|
||
|
"reject": do_reject,
|
||
|
"rejectattr": do_rejectattr,
|
||
|
"replace": do_replace,
|
||
|
"reverse": do_reverse,
|
||
|
"round": do_round,
|
||
|
"safe": do_mark_safe,
|
||
|
"select": do_select,
|
||
|
"selectattr": do_selectattr,
|
||
|
"slice": do_slice,
|
||
|
"sort": do_sort,
|
||
|
"string": soft_str,
|
||
|
"striptags": do_striptags,
|
||
|
"sum": do_sum,
|
||
|
"title": do_title,
|
||
|
"trim": do_trim,
|
||
|
"truncate": do_truncate,
|
||
|
"unique": do_unique,
|
||
|
"upper": do_upper,
|
||
|
"urlencode": do_urlencode,
|
||
|
"urlize": do_urlize,
|
||
|
"wordcount": do_wordcount,
|
||
|
"wordwrap": do_wordwrap,
|
||
|
"xmlattr": do_xmlattr,
|
||
|
"tojson": do_tojson,
|
||
|
}
|