diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
index b697d34..bac541b 100644
--- a/src/jinja2/filters.py
+++ b/src/jinja2/filters.py
@@ -66,7 +66,7 @@ def make_multi_attrgetter(environment: 'Environment', attribute: t.Optional[t.Un
def do_forceescape(value: 't.Union[str, HasHTML]') -> Markup:
"""Enforce HTML escaping. This will probably double escape variables."""
- pass
+ 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.
@@ -84,7 +84,16 @@ def do_urlencode(value: t.Union[str, t.Mapping[str, t.Any], t.Iterable[t.Tuple[s
.. versionadded:: 2.7
"""
- pass
+ from urllib.parse import quote, urlencode
+
+ if isinstance(value, str):
+ return quote(value, safe='/')
+ elif isinstance(value, t.Mapping):
+ return urlencode(value)
+ elif isinstance(value, t.Iterable):
+ return urlencode(list(value))
+ else:
+ raise TypeError("Expected string, mapping, or iterable.")
@pass_eval_context
def do_replace(eval_ctx: 'EvalContext', s: str, old: str, new: str, count: t.Optional[int]=None) -> str:
@@ -110,7 +119,7 @@ def do_upper(s: str) -> str:
def do_lower(s: str) -> str:
"""Convert a value to lowercase."""
- pass
+ return 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.
@@ -174,7 +183,17 @@ def do_xmlattr(eval_ctx: 'EvalContext', d: t.Mapping[str, t.Any], autospace: boo
.. versionchanged:: 3.1.3
Keys with spaces are not allowed.
"""
- pass
+ result = []
+ for key, value in d.items():
+ if _attr_key_re.search(key):
+ raise ValueError(f"unsafe attribute name: {key!r}")
+ if value is not None and not isinstance(value, Undefined):
+ result.append(f'{key}="{escape(value)}"')
+
+ rv = ' '.join(result)
+ if autospace and rv:
+ rv = ' ' + rv
+ return rv
def do_capitalize(s: str) -> str:
"""Capitalize a value. The first character will be uppercase, all others
@@ -207,7 +226,20 @@ def do_dictsort(value: t.Mapping[K, V], case_sensitive: bool=False, by: 'te.Lite
{% for key, value in mydict|dictsort(false, 'value') %}
sort the dict by value, case insensitive
"""
- pass
+ if by == 'key':
+ pos = 0
+ elif by == 'value':
+ pos = 1
+ else:
+ raise ValueError('Can only sort by either "key" or "value"')
+
+ def sort_func(item):
+ value = item[pos]
+ if not case_sensitive and isinstance(value, str):
+ value = value.lower()
+ 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]':
@@ -244,7 +276,31 @@ def do_sort(environment: 'Environment', value: 't.Iterable[V]', reverse: bool=Fa
{% for user in users|sort(attribute="age,name") %}
...
- {% endfor %}
+ if not case_sensitive and isinstance(value[0], str):
+ def prepare(val):
+ if isinstance(val, str):
+ return val.lower()
+ return val
+ else:
+ prepare = lambda val: val
+
+ if attribute is None:
+ key_func = prepare
+ elif isinstance(attribute, str):
+ if '.' in attribute:
+ def key_func(val):
+ for attr in attribute.split('.'):
+ val = environment.getattr(val, attr)
+ return prepare(val)
+ else:
+ key_func = lambda val: prepare(environment.getattr(val, attribute))
+ elif isinstance(attribute, (list, tuple)):
+ def key_func(val):
+ return tuple(prepare(environment.getattr(val, attr)) for attr in attribute)
+ else:
+ raise TypeError('attribute must be a string or a list of strings')
+
+ return sorted(value, key=key_func, reverse=reverse)
.. versionchanged:: 2.11.0
The ``attribute`` parameter can be a comma separated list of
@@ -270,7 +326,19 @@ def do_unique(environment: 'Environment', value: 't.Iterable[V]', case_sensitive
:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Filter objects with unique values for this attribute.
"""
- pass
+ getter = make_attrgetter(environment, attribute)
+ seen = set()
+
+ def _unique(item):
+ key = getter(item)
+ if not case_sensitive and isinstance(key, str):
+ key = key.lower()
+ if key not in seen:
+ seen.add(key)
+ return True
+ return False
+
+ return (item for item in value if _unique(item))
@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]':
@@ -294,7 +362,18 @@ def do_max(environment: 'Environment', value: 't.Iterable[V]', case_sensitive: b
{{ [1, 2, 3]|max }}
-> 3
-
+ if not value:
+ return Undefined(name='max')
+
+ key = make_attrgetter(environment, attribute)
+
+ def prepare(val):
+ val = key(val)
+ if not case_sensitive and isinstance(val, str):
+ val = val.lower()
+ return val
+
+ return max(value, key=prepare)
:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Get the object with the max value of this attribute.
"""
@@ -323,7 +402,9 @@ def do_default(value: V, default_value: V='', boolean: bool=False) -> V:
on nested elements and attributes that may contain undefined values
in the chain without getting an :exc:`~jinja2.UndefinedError`.
"""
- pass
+ 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[t.Any], d: str='', attribute: t.Optional[t.Union[str, int]]=None) -> str:
@@ -370,12 +451,17 @@ def do_last(environment: 'Environment', seq: 't.Reversible[V]') -> 't.Union[V, U
{{ data | selectattr('name', '==', 'Jinja') | list | last }}
"""
- pass
+ try:
+ return next(reversed(seq))
+ except StopIteration:
+ return Undefined(name='last')
@pass_context
def do_random(context: 'Context', seq: 't.Sequence[V]') -> 't.Union[V, Undefined]':
"""Return a random item from the sequence."""
- pass
+ if seq:
+ return random.choice(seq)
+ return Undefined(name='random')
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,
@@ -383,11 +469,33 @@ def do_filesizeformat(value: t.Union[str, float, int], binary: bool=False) -> st
Giga, etc.), if the second parameter is set to `True` the binary
prefixes are used (Mebi, Gibi).
"""
- pass
+ bytes = float(value)
+ 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")
+ ]
+ if bytes == 1:
+ return "1 Byte"
+ elif bytes < base:
+ return f"{bytes:3.0f} 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."""
- pass
+ from pprint import pformat
+ return pformat(value)
_uri_scheme_re = re.compile('^([\\w.+-]{2,}:(/){0,2})$')
@pass_eval_context
@@ -429,7 +537,19 @@ def do_urlize(eval_ctx: 'EvalContext', value: str, trim_url_limit: t.Optional[in
.. versionchanged:: 2.8
The ``target`` parameter was added.
"""
- pass
+ from markupsafe import escape
+ from jinja2.utils import urlize
+
+ if extra_schemes is None:
+ extra_schemes = eval_ctx.environment.policies["urlize.extra_schemes"]
+
+ return urlize(
+ value,
+ trim_url_limit=trim_url_limit,
+ rel=rel,
+ target=target,
+ extra_schemes=extra_schemes,
+ )
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
@@ -447,7 +567,23 @@ def do_indent(s: str, width: t.Union[int, str]=4, first: bool=False, blank: bool
Rename the ``indentfirst`` argument to ``first``.
"""
- pass
+ indention = ' ' * width if isinstance(width, int) else width
+
+ newline = '\n'
+ if isinstance(s, Markup):
+ newline = Markup(newline)
+ indention = Markup(indention)
+
+ result = []
+ for idx, line in enumerate(s.splitlines(True)):
+ if idx == 0 and not first:
+ result.append(line)
+ elif line.strip() or blank:
+ result.append(indention + line)
+ else:
+ result.append(line)
+
+ return newline.join(result)
@pass_environment
def do_truncate(env: 'Environment', s: str, length: int=255, killwords: bool=False, end: str='...', leeway: t.Optional[int]=None) -> str:
@@ -474,7 +610,20 @@ def do_truncate(env: 'Environment', s: str, length: int=255, killwords: bool=Fal
The default leeway on newer Jinja versions is 5 and was 0 before but
can be reconfigured globally.
"""
- pass
+ if leeway is None:
+ leeway = env.policies['truncate.leeway']
+
+ assert length >= len(end), 'expected length >= len(end)'
+ assert leeway >= 0, 'expected leeway >= 0'
+
+ 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:
@@ -511,7 +660,12 @@ def do_int(value: t.Any, default: int=0, base: int=10) -> int:
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
+ try:
+ if isinstance(value, str):
+ return int(value, base)
+ return int(value)
+ except (ValueError, TypeError):
+ return default
0b, 0o and 0x for bases 2, 8 and 16 respectively.
The base is ignored for decimal numbers and non-string values.
"""
@@ -548,7 +702,7 @@ def do_format(value: str, *args: t.Any, **kwargs: t.Any) -> str:
def do_trim(value: str, chars: t.Optional[str]=None) -> str:
"""Strip leading and trailing characters, by default whitespace."""
- pass
+ return value.strip(chars)
def do_striptags(value: 't.Union[str, HasHTML]') -> str:
"""Strip SGML/XML tags and replace adjacent whitespace by one space."""
@@ -574,7 +728,24 @@ def sync_do_slice(value: 't.Collection[V]', slices: int, fill_with: 't.Optional[
If you pass it a second argument it's used to fill missing
values on the last iteration.
"""
- pass
+ 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
def do_batch(value: 't.Iterable[V]', linecount: int, fill_with: 't.Optional[V]'=None) -> 't.Iterator[t.List[V]]':
"""
@@ -595,7 +766,17 @@ def do_batch(value: 't.Iterable[V]', linecount: int, fill_with: 't.Optional[V]'=
{%- endfor %}
</table>
"""
- pass
+ result = []
+ tmp = []
+ for item in value:
+ if len(tmp) == linecount:
+ yield tmp
+ tmp = []
+ tmp.append(item)
+ if tmp:
+ if fill_with is not None and len(tmp) < linecount:
+ tmp += [fill_with] * (linecount - len(tmp))
+ yield tmp
def do_round(value: float, precision: int=0, method: 'te.Literal["common", "ceil", "floor"]'='common') -> float:
"""Round the number to a given precision. The first
@@ -607,7 +788,14 @@ def do_round(value: float, precision: int=0, method: 'te.Literal["common", "ceil
- ``'floor'`` always rounds down
If you don't specify a method ``'common'`` is used.
-
+ if method == 'common':
+ return round(value, precision)
+ elif method == 'ceil':
+ return math.ceil(value * (10 ** precision)) / (10 ** precision)
+ elif method == 'floor':
+ return math.floor(value * (10 ** precision)) / (10 ** precision)
+ else:
+ raise FilterArgumentError('method must be common, ceil or floor')
.. sourcecode:: jinja
{{ 42.55|round }}
@@ -623,7 +811,14 @@ def do_round(value: float, precision: int=0, method: 'te.Literal["common", "ceil
{{ 42.55|round|int }}
-> 43
"""
- pass
+ if method == 'common':
+ return round(value, precision)
+ elif method == 'ceil':
+ return math.ceil(value * (10 ** precision)) / (10 ** precision)
+ elif method == 'floor':
+ return math.floor(value * (10 ** precision)) / (10 ** precision)
+ else:
+ raise FilterArgumentError('method must be common, ceil or floor')
class _GroupTuple(t.NamedTuple):
grouper: t.Any
@@ -909,4 +1104,4 @@ def do_tojson(eval_ctx: 'EvalContext', value: t.Any, indent: t.Optional[int]=Non
.. versionadded:: 2.9
"""
pass
-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}
\ No newline at end of file
+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}