diff options
Diffstat (limited to 'deps/v8/third_party/jinja2/filters.py')
-rw-r--r-- | deps/v8/third_party/jinja2/filters.py | 1080 |
1 files changed, 769 insertions, 311 deletions
diff --git a/deps/v8/third_party/jinja2/filters.py b/deps/v8/third_party/jinja2/filters.py index 74b108dcec..ed07c4c0e2 100644 --- a/deps/v8/third_party/jinja2/filters.py +++ b/deps/v8/third_party/jinja2/filters.py @@ -1,79 +1,75 @@ -# -*- coding: utf-8 -*- """Built-in template filters used with the ``|`` operator.""" import math import random import re -import warnings -from collections import namedtuple +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_unicode +from markupsafe import soft_str -from ._compat import abc -from ._compat import imap -from ._compat import iteritems -from ._compat import string_types -from ._compat import text_type +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 unicode_urlencode +from .utils import url_quote from .utils import urlize -_word_re = re.compile(r"\w+", re.UNICODE) -_word_beginning_split_re = re.compile(r"([-\s\(\{\[\<]+)", re.UNICODE) +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 - -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 + class HasHTML(te.Protocol): + def __html__(self) -> str: + pass -def environmentfilter(f): - """Decorator for marking environment dependent filters. The current - :class:`Environment` is passed to the filter as first argument. - """ - f.environmentfilter = True - return f +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +K = t.TypeVar("K") +V = t.TypeVar("V") -def ignore_case(value): +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.""" - return value.lower() if isinstance(value, string_types) else value + if isinstance(value, str): + return t.cast(V, value.lower()) + return value -def make_attrgetter(environment, attribute, postprocess=None, default=None): + +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. """ - attribute = _prepare_attribute_parts(attribute) + parts = _prepare_attribute_parts(attribute) - def attrgetter(item): - for part in attribute: + def attrgetter(item: t.Any) -> t.Any: + for part in parts: item = environment.getitem(item, part) - if default and isinstance(item, Undefined): + if default is not None and isinstance(item, Undefined): item = default if postprocess is not None: @@ -84,7 +80,11 @@ def make_attrgetter(environment, attribute, postprocess=None, default=None): return attrgetter -def make_multi_attrgetter(environment, attribute, postprocess=None): +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 @@ -95,17 +95,19 @@ def make_multi_attrgetter(environment, attribute, postprocess=None): Examples of attribute: "attr1,attr2", "attr1.inner1.0,attr2.inner2.0", etc. """ - attribute_parts = ( - attribute.split(",") if isinstance(attribute, string_types) else [attribute] - ) - attribute = [ - _prepare_attribute_parts(attribute_part) for attribute_part in attribute_parts - ] + 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): - items = [None] * len(attribute) - for i, attribute_part in enumerate(attribute): + 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) @@ -113,28 +115,35 @@ def make_multi_attrgetter(environment, attribute, postprocess=None): item_i = postprocess(item_i) items[i] = item_i + return items return attrgetter -def _prepare_attribute_parts(attr): +def _prepare_attribute_parts( + attr: t.Optional[t.Union[str, int]] +) -> t.List[t.Union[str, int]]: if attr is None: return [] - elif isinstance(attr, string_types): + + if isinstance(attr, str): return [int(x) if x.isdigit() else x for x in attr.split(".")] - else: - return [attr] + return [attr] -def do_forceescape(value): + +def do_forceescape(value: "t.Union[str, HasHTML]") -> Markup: """Enforce HTML escaping. This will probably double escape variables.""" if hasattr(value, "__html__"): - value = value.__html__() - return escape(text_type(value)) + value = t.cast("HasHTML", value).__html__() + + return escape(str(value)) -def do_urlencode(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 @@ -150,22 +159,23 @@ def do_urlencode(value): .. versionadded:: 2.7 """ - if isinstance(value, string_types) or not isinstance(value, abc.Iterable): - return unicode_urlencode(value) + if isinstance(value, str) or not isinstance(value, abc.Iterable): + return url_quote(value) if isinstance(value, dict): - items = iteritems(value) + items: t.Iterable[t.Tuple[str, t.Any]] = value.items() else: - items = iter(value) + items = value # type: ignore - return u"&".join( - "%s=%s" % (unicode_urlencode(k, for_qs=True), unicode_urlencode(v, for_qs=True)) - for k, v in items + return "&".join( + f"{url_quote(k, for_qs=True)}={url_quote(v, for_qs=True)}" for k, v in items ) -@evalcontextfilter -def do_replace(eval_ctx, s, old, new, count=None): +@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. @@ -182,8 +192,10 @@ def do_replace(eval_ctx, s, old, new, count=None): """ if count is None: count = -1 + if not eval_ctx.autoescape: - return text_type(s).replace(text_type(old), text_type(new), count) + return str(s).replace(str(old), str(new), count) + if ( hasattr(old, "__html__") or hasattr(new, "__html__") @@ -191,22 +203,55 @@ def do_replace(eval_ctx, s, old, new, count=None): ): s = escape(s) else: - s = soft_unicode(s) - return s.replace(soft_unicode(old), soft_unicode(new), count) + s = soft_str(s) + + return s.replace(soft_str(old), soft_str(new), count) -def do_upper(s): +def do_upper(s: str) -> str: """Convert a value to uppercase.""" - return soft_unicode(s).upper() + return soft_str(s).upper() -def do_lower(s): +def do_lower(s: str) -> str: """Convert a value to lowercase.""" - return soft_unicode(s).lower() + 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() -@evalcontextfilter -def do_xmlattr(_eval_ctx, d, autospace=True): + +@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. All values that are neither `none` nor `undefined` are automatically escaped: @@ -229,42 +274,52 @@ def do_xmlattr(_eval_ctx, d, autospace=True): 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 iteritems(d) + rv = " ".join( + f'{escape(key)}="{escape(value)}"' + for key, value in d.items() if value is not None and not isinstance(value, Undefined) ) + if autospace and rv: - rv = u" " + rv - if _eval_ctx.autoescape: + rv = " " + rv + + if eval_ctx.autoescape: rv = Markup(rv) + return rv -def do_capitalize(s): +def do_capitalize(s: str) -> str: """Capitalize a value. The first character will be uppercase, all others lowercase. """ - return soft_unicode(s).capitalize() + return soft_str(s).capitalize() + + +_word_beginning_split_re = re.compile(r"([-\s({\[<]+)") -def do_title(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_unicode(s)) + for item in _word_beginning_split_re.split(soft_str(s)) if item ] ) -def do_dictsort(value, case_sensitive=False, by="key", reverse=False): - """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: +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 @@ -287,7 +342,7 @@ def do_dictsort(value, case_sensitive=False, by="key", reverse=False): else: raise FilterArgumentError('You can only sort by either "key" or "value"') - def sort_func(item): + def sort_func(item: t.Tuple[t.Any, t.Any]) -> t.Any: value = item[pos] if not case_sensitive: @@ -298,8 +353,14 @@ def do_dictsort(value, case_sensitive=False, by="key", reverse=False): return sorted(value.items(), key=sort_func, reverse=reverse) -@environmentfilter -def do_sort(environment, value, reverse=False, case_sensitive=False, attribute=None): +@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 @@ -331,7 +392,7 @@ def do_sort(environment, value, reverse=False, case_sensitive=False, attribute=N .. sourcecode:: jinja - {% for user users|sort(attribute="age,name") %} + {% for user in users|sort(attribute="age,name") %} ... {% endfor %} @@ -348,8 +409,13 @@ def do_sort(environment, value, reverse=False, case_sensitive=False, attribute=N return sorted(value, key=key_func, reverse=reverse) -@environmentfilter -def do_unique(environment, value, case_sensitive=False, attribute=None): +@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 @@ -376,7 +442,13 @@ def do_unique(environment, value, case_sensitive=False, attribute=None): yield item -def _min_or_max(environment, value, func, case_sensitive, attribute): +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: @@ -390,8 +462,13 @@ def _min_or_max(environment, value, func, case_sensitive, attribute): return func(chain([first], it), key=key_func) -@environmentfilter -def do_min(environment, value, case_sensitive=False, attribute=None): +@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 @@ -405,8 +482,13 @@ def do_min(environment, value, case_sensitive=False, attribute=None): return _min_or_max(environment, value, min, case_sensitive, attribute) -@environmentfilter -def do_max(environment, value, case_sensitive=False, attribute=None): +@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 @@ -420,7 +502,11 @@ def do_max(environment, value, case_sensitive=False, attribute=None): return _min_or_max(environment, value, max, case_sensitive, attribute) -def do_default(value, default_value=u"", boolean=False): +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: @@ -445,11 +531,17 @@ def do_default(value, default_value=u"", boolean=False): """ if isinstance(value, Undefined) or (boolean and not value): return default_value + return value -@evalcontextfilter -def do_join(eval_ctx, value, d=u"", attribute=None): +@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: @@ -472,39 +564,54 @@ def do_join(eval_ctx, value, d=u"", attribute=None): The `attribute` parameter was added. """ if attribute is not None: - value = imap(make_attrgetter(eval_ctx.environment, attribute), value) + value = map(make_attrgetter(eval_ctx.environment, attribute), value) # no automatic escaping? joining is a lot easier then if not eval_ctx.autoescape: - return text_type(d).join(imap(text_type, value)) + 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] = text_type(item) + value[idx] = str(item) + if do_escape: d = escape(d) else: - d = text_type(d) + d = str(d) + return d.join(value) # no html involved, to normal joining - return soft_unicode(d).join(imap(soft_unicode, value)) + 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, width=80): + +def do_center(value: str, width: int = 80) -> str: """Centers the value in a field of a given width.""" - return text_type(value).center(width) + return soft_str(value).center(width) -@environmentfilter -def do_first(environment, seq): +@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)) @@ -512,10 +619,21 @@ def do_first(environment, seq): return environment.undefined("No first item, sequence was empty.") -@environmentfilter -def do_last(environment, seq): - """ - Return the last item of a sequence. +@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: @@ -530,8 +648,11 @@ def do_last(environment, seq): return environment.undefined("No last item, sequence was empty.") -@contextfilter -def do_random(context, seq): +# 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) @@ -539,108 +660,151 @@ def do_random(context, seq): return context.environment.undefined("No random item, sequence was empty.") -def do_filesizeformat(value, binary=False): +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 = binary and 1024 or 1000 + base = 1024 if binary else 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"), + ("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 "%d Bytes" % bytes + return f"{int(bytes)} Bytes" else: 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) + return f"{base * bytes / unit:.1f} {prefix}" + return f"{base * bytes / unit:.1f} {prefix}" -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) +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})$") -@evalcontextfilter +@pass_eval_context def do_urlize( - eval_ctx, value, trim_url_limit=None, nofollow=False, target=None, rel=None -): - """Converts URLs in plain text into clickable links. + 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 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": + if nofollow: + rel_parts.add("nofollow") - .. sourcecode:: jinja + rel_parts.update((policies["urlize.rel"] or "").split()) + rel = " ".join(sorted(rel_parts)) or None - {{ mytext|urlize(40, true) }} - links are shortened to 40 chars and defined with rel="nofollow" + if target is None: + target = policies["urlize.target"] - If *target* is specified, the ``target`` attribute will be added to the - ``<a>`` tag: + if extra_schemes is None: + extra_schemes = policies["urlize.extra_schemes"] or () - .. sourcecode:: jinja + 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.") - {{ mytext|urlize(40, target='_blank') }} + rv = urlize( + value, + trim_url_limit=trim_url_limit, + rel=rel, + target=target, + extra_schemes=extra_schemes, + ) - .. versionchanged:: 2.8+ - The *target* parameter was added. - """ - policies = eval_ctx.environment.policies - rel = set((rel or "").split() or []) - if nofollow: - rel.add("nofollow") - rel.update((policies["urlize.rel"] or "").split()) - if target is None: - target = policies["urlize.target"] - rel = " ".join(sorted(rel)) or None - rv = urlize(value, trim_url_limit, rel=rel, target=target) if eval_ctx.autoescape: rv = Markup(rv) + return rv -def do_indent(s, width=4, first=False, blank=False, indentfirst=None): +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 to indent by. + :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 indentfirst is not None: - warnings.warn( - "The 'indentfirst' argument is renamed to 'first' and will" - " be removed in version 3.0.", - DeprecationWarning, - stacklevel=2, - ) - first = indentfirst + if isinstance(width, str): + indention = width + else: + indention = " " * width - indention = u" " * width - newline = u"\n" + newline = "\n" if isinstance(s, Markup): indention = Markup(indention) @@ -665,8 +829,15 @@ def do_indent(s, width=4, first=False, blank=False, indentfirst=None): return rv -@environmentfilter -def do_truncate(env, s, length=255, killwords=False, end="...", leeway=None): +@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 @@ -692,25 +863,29 @@ def do_truncate(env, s, length=255, killwords=False, end="...", leeway=None): """ if leeway is None: leeway = env.policies["truncate.leeway"] - assert length >= len(end), "expected length >= %s, got %s" % (len(end), length) - assert leeway >= 0, "expected leeway >= 0, got %s" % 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 -@environmentfilter +@pass_environment def do_wordwrap( - environment, - s, - width=79, - break_long_words=True, - wrapstring=None, - break_on_hyphens=True, -): + 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. @@ -732,10 +907,9 @@ def do_wordwrap( .. versionchanged:: 2.7 Added the ``wrapstring`` parameter. """ - import textwrap - if not wrapstring: + if wrapstring is None: wrapstring = environment.newline_sequence # textwrap.wrap doesn't consider existing newlines when wrapping. @@ -759,12 +933,15 @@ def do_wordwrap( ) -def do_wordcount(s): +_word_re = re.compile(r"\w+") + + +def do_wordcount(s: str) -> int: """Count the words in that string.""" - return len(_word_re.findall(soft_unicode(s))) + return len(_word_re.findall(soft_str(s))) -def do_int(value, default=0, base=10): +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 @@ -774,8 +951,9 @@ def do_int(value, default=0, base=10): The base is ignored for decimal numbers and non-string values. """ try: - if isinstance(value, string_types): + 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. @@ -785,7 +963,7 @@ def do_int(value, default=0, base=10): return default -def do_float(value, default=0.0): +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. @@ -796,7 +974,7 @@ def do_float(value, default=0.0): return default -def do_format(value, *args, **kwargs): +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``. @@ -820,22 +998,26 @@ def do_format(value, *args, **kwargs): raise FilterArgumentError( "can't handle positional and keyword arguments at the same time" ) - return soft_unicode(value) % (kwargs or args) + return soft_str(value) % (kwargs or args) -def do_trim(value, chars=None): + +def do_trim(value: str, chars: t.Optional[str] = None) -> str: """Strip leading and trailing characters, by default whitespace.""" - return soft_unicode(value).strip(chars) + return soft_str(value).strip(chars) -def do_striptags(value): +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 = value.__html__() - return Markup(text_type(value)).striptags() + value = t.cast("HasHTML", value).__html__() + + return Markup(str(value)).striptags() -def do_slice(value, slices, fill_with=None): +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: @@ -860,18 +1042,34 @@ def do_slice(value, slices, fill_with=None): 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 -def do_batch(value, linecount, fill_with=None): +@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 @@ -890,19 +1088,27 @@ def do_batch(value, linecount, fill_with=None): {%- endfor %} </table> """ - tmp = [] + 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, precision=0, method="common"): +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: @@ -930,24 +1136,35 @@ def do_round(value, precision=0, method="common"): """ 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 func(value * (10 ** precision)) / (10 ** precision) + 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) -# Use a regular tuple repr here. This is what we did in the past and we -# really want to hide this custom type as much as possible. In particular -# we do not want to accidentally expose an auto generated repr in case -# people start to print this out in comments or something similar for -# debugging. -_GroupTuple = namedtuple("_GroupTuple", ["grouper", "list"]) -_GroupTuple.__repr__ = tuple.__repr__ -_GroupTuple.__str__ = tuple.__str__ + def __str__(self) -> str: + return tuple.__str__(self) -@environmentfilter -def do_groupby(environment, value, attribute): +@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``, @@ -978,18 +1195,86 @@ def do_groupby(environment, value, attribute): <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) - return [ + 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] -@environmentfilter -def do_sum(environment, iterable, attribute=None, start=0): + 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. @@ -1001,52 +1286,93 @@ def do_sum(environment, iterable, attribute=None, start=0): Total: {{ items|sum(attribute='price') }} .. versionchanged:: 2.6 - The `attribute` parameter was added to allow suming up over - attributes. Also the `start` parameter was moved on to the right. + 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 = imap(make_attrgetter(environment, attribute), iterable) - return sum(iterable, start) + 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) -def do_list(value): + 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) -def do_mark_safe(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): +def do_mark_unsafe(value: str) -> str: """Mark a value as unsafe. This is the reverse operation for :func:`safe`.""" - return text_type(value) + return str(value) -def do_reverse(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, string_types): + if isinstance(value, str): return value[::-1] + try: - return reversed(value) + return reversed(value) # type: ignore except TypeError: try: rv = list(value) rv.reverse() return rv - except TypeError: - raise FilterArgumentError("argument must be iterable") + except TypeError as e: + raise FilterArgumentError("argument must be iterable") from e -@environmentfilter -def do_attr(environment, obj, name): +@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. @@ -1063,16 +1389,39 @@ def do_attr(environment, obj, name): except AttributeError: pass else: - if environment.sandboxed and not environment.is_safe_attribute( - obj, name, value - ): - return environment.unsafe_undefined(obj, name) + 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) -@contextfilter -def do_map(*args, **kwargs): +@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. @@ -1104,7 +1453,7 @@ def do_map(*args, **kwargs): .. code-block:: python (u.username for u in users) - (u.username or "Anonymous" for u in users) + (getattr(u, "username", "Anonymous") for u in users) (do_lower(x) for x in titles) .. versionchanged:: 2.11.0 @@ -1112,14 +1461,53 @@ def do_map(*args, **kwargs): .. versionadded:: 2.7 """ - seq, func = prepare_map(args, kwargs) - if seq: - for item in seq: + if value: + func = prepare_map(context, args, kwargs) + + for item in value: yield func(item) -@contextfilter -def do_select(*args, **kwargs): +@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. @@ -1144,11 +1532,23 @@ def do_select(*args, **kwargs): .. versionadded:: 2.7 """ - return select_or_reject(args, kwargs, lambda x: x, False) + 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) -@contextfilter -def do_reject(*args, **kwargs): +@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. @@ -1168,11 +1568,23 @@ def do_reject(*args, **kwargs): .. versionadded:: 2.7 """ - return select_or_reject(args, kwargs, lambda x: not x, False) + return select_or_reject(context, value, args, kwargs, lambda x: not x, False) -@contextfilter -def do_selectattr(*args, **kwargs): +@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. @@ -1196,11 +1608,23 @@ def do_selectattr(*args, **kwargs): .. versionadded:: 2.7 """ - return select_or_reject(args, kwargs, lambda x: x, True) + 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) -@contextfilter -def do_rejectattr(*args, **kwargs): + +@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. @@ -1222,105 +1646,138 @@ def do_rejectattr(*args, **kwargs): .. versionadded:: 2.7 """ - return select_or_reject(args, kwargs, lambda x: not x, True) - + return select_or_reject(context, value, args, kwargs, lambda x: not x, True) -@evalcontextfilter -def do_tojson(eval_ctx, value, indent=None): - """Dumps a structure to JSON so that it's safe to use in ``<script>`` - tags. It accepts the same arguments and returns a JSON string. Note that - this is available in templates through the ``|tojson`` filter which will - also mark the result as safe. Due to how this function escapes certain - characters this is safe even if used outside of ``<script>`` tags. - The following characters are escaped in strings: +@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) - - ``<`` - - ``>`` - - ``&`` - - ``'`` - This makes it safe to embed such strings in any place in HTML with the - notable exception of double quoted attributes. In that case single - quote your attributes or HTML escape it in addition. +@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 indent parameter can be used to enable pretty printing. Set it to - the number of spaces that the structures should be indented with. + 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. - Note that this filter is for use in HTML contexts only. + :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 - dumper = policies["json.dumps_function"] - options = policies["json.dumps_kwargs"] + dumps = policies["json.dumps_function"] + kwargs = policies["json.dumps_kwargs"] + if indent is not None: - options = dict(options) - options["indent"] = indent - return htmlsafe_json_dumps(value, dumper=dumper, **options) + kwargs = kwargs.copy() + kwargs["indent"] = indent + return htmlsafe_json_dumps(value, dumps=dumps, **kwargs) -def prepare_map(args, kwargs): - context = args[0] - seq = args[1] - default = None - if len(args) == 2 and "attribute" in 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( - "Unexpected keyword argument %r" % next(iter(kwargs)) + f"Unexpected keyword argument {next(iter(kwargs))!r}" ) + func = make_attrgetter(context.environment, attribute, default=default) else: try: - name = args[2] - args = args[3:] + name = args[0] + args = args[1:] except LookupError: - raise FilterArgumentError("map requires a filter argument") + raise FilterArgumentError("map requires a filter argument") from None - def func(item): + def func(item: t.Any) -> t.Any: return context.environment.call_filter( name, item, args, kwargs, context=context ) - return seq, func + return func -def prepare_select_or_reject(args, kwargs, modfunc, lookup_attr): - context = args[0] - seq = args[1] +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[2] + attr = args[0] except LookupError: - raise FilterArgumentError("Missing parameter for attribute name") + raise FilterArgumentError("Missing parameter for attribute name") from None + transfunc = make_attrgetter(context.environment, attr) off = 1 else: off = 0 - def transfunc(x): + def transfunc(x: V) -> V: return x try: - name = args[2 + off] - args = args[3 + off :] + name = args[off] + args = args[1 + off :] - def func(item): + def func(item: t.Any) -> t.Any: return context.environment.call_test(name, item, args, kwargs) except LookupError: - func = bool + 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 - return seq, lambda item: modfunc(func(transfunc(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) -def select_or_reject(args, kwargs, modfunc, lookup_attr): - seq, func = prepare_select_or_reject(args, kwargs, modfunc, lookup_attr) - if seq: - for item in seq: + async for item in auto_aiter(value): if func(item): yield item @@ -1350,6 +1807,7 @@ FILTERS = { "length": len, "list": do_list, "lower": do_lower, + "items": do_items, "map": do_map, "min": do_min, "max": do_max, @@ -1365,7 +1823,7 @@ FILTERS = { "selectattr": do_selectattr, "slice": do_slice, "sort": do_sort, - "string": soft_unicode, + "string": soft_str, "striptags": do_striptags, "sum": do_sum, "title": do_title, |