python-liquid 1.12.1__py3-none-any.whl → 1.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- liquid/__init__.py +1 -1
- liquid/builtin/filters/array.py +21 -13
- liquid/builtin/filters/extra.py +1 -1
- liquid/builtin/filters/math.py +38 -45
- liquid/builtin/filters/misc.py +7 -4
- liquid/builtin/filters/string.py +36 -28
- liquid/builtin/loaders/base_loader.py +2 -1
- liquid/builtin/statement.py +3 -1
- liquid/builtin/tags/assign_tag.py +7 -3
- liquid/builtin/tags/case_tag.py +5 -2
- liquid/builtin/tags/decrement_tag.py +3 -1
- liquid/builtin/tags/echo_tag.py +3 -3
- liquid/builtin/tags/for_tag.py +4 -1
- liquid/builtin/tags/if_tag.py +4 -1
- liquid/builtin/tags/increment_tag.py +3 -1
- liquid/builtin/tags/render_tag.py +4 -1
- liquid/builtin/tags/tablerow_tag.py +42 -5
- liquid/builtin/tags/unless_tag.py +3 -1
- liquid/context.py +1 -3
- liquid/environment.py +74 -19
- liquid/expression.py +5 -10
- liquid/expressions/boolean/parse.py +15 -4
- liquid/expressions/common.py +29 -6
- liquid/expressions/conditional/parse.py +26 -11
- liquid/expressions/filtered/parse.py +29 -12
- liquid/expressions/loop/parse.py +5 -2
- liquid/expressions/stream.py +3 -1
- liquid/extra/tags/if_expressions.py +13 -10
- liquid/extra/tags/if_not.py +3 -2
- liquid/future/environment.py +4 -0
- liquid/future/tags/__init__.py +5 -1
- liquid/future/tags/_case_tag.py +182 -0
- liquid/future/tags/_tablerow_tag.py +14 -0
- liquid/golden/case_tag.py +46 -11
- liquid/golden/if_tag.py +28 -1
- liquid/golden/tablerow_tag.py +55 -0
- liquid/parse.py +0 -1
- {python_liquid-1.12.1.dist-info → python_liquid-1.13.0.dist-info}/METADATA +11 -4
- {python_liquid-1.12.1.dist-info → python_liquid-1.13.0.dist-info}/RECORD +41 -39
- {python_liquid-1.12.1.dist-info → python_liquid-1.13.0.dist-info}/WHEEL +1 -1
- {python_liquid-1.12.1.dist-info → python_liquid-1.13.0.dist-info}/licenses/LICENSE +0 -0
liquid/__init__.py
CHANGED
liquid/builtin/filters/array.py
CHANGED
|
@@ -33,6 +33,7 @@ if TYPE_CHECKING:
|
|
|
33
33
|
from liquid import Environment
|
|
34
34
|
|
|
35
35
|
ArrayT = Union[List[Any], Tuple[Any, ...]]
|
|
36
|
+
"""Array-like objects."""
|
|
36
37
|
|
|
37
38
|
# Send objects with missing keys to the end when sorting a list.
|
|
38
39
|
MAX_CH = chr(0x10FFFF)
|
|
@@ -74,9 +75,12 @@ def _lower(obj: Any) -> str:
|
|
|
74
75
|
@with_environment
|
|
75
76
|
@sequence_filter
|
|
76
77
|
def join(
|
|
77
|
-
sequence: Iterable[object],
|
|
78
|
+
sequence: Iterable[object],
|
|
79
|
+
separator: object = " ",
|
|
80
|
+
*,
|
|
81
|
+
environment: Environment,
|
|
78
82
|
) -> str:
|
|
79
|
-
"""
|
|
83
|
+
"""Return a string by joining items in _sequence_, separated by _separator_."""
|
|
80
84
|
if not isinstance(separator, str):
|
|
81
85
|
separator = str(separator)
|
|
82
86
|
|
|
@@ -88,7 +92,7 @@ def join(
|
|
|
88
92
|
|
|
89
93
|
@liquid_filter
|
|
90
94
|
def first(obj: Any) -> object:
|
|
91
|
-
"""Return the first item of
|
|
95
|
+
"""Return the first item of collection _obj_."""
|
|
92
96
|
if isinstance(obj, str):
|
|
93
97
|
return None
|
|
94
98
|
|
|
@@ -103,7 +107,7 @@ def first(obj: Any) -> object:
|
|
|
103
107
|
|
|
104
108
|
@liquid_filter
|
|
105
109
|
def last(obj: Sequence[Any]) -> object:
|
|
106
|
-
"""Return the last item of
|
|
110
|
+
"""Return the last item of array-like object _obj_."""
|
|
107
111
|
if isinstance(obj, str):
|
|
108
112
|
return None
|
|
109
113
|
|
|
@@ -115,7 +119,7 @@ def last(obj: Sequence[Any]) -> object:
|
|
|
115
119
|
|
|
116
120
|
@sequence_filter
|
|
117
121
|
def concat(sequence: ArrayT, second_array: ArrayT) -> ArrayT:
|
|
118
|
-
"""Return
|
|
122
|
+
"""Return the concatenation of _sequence_ and _second_array_."""
|
|
119
123
|
if not isinstance(second_array, (list, tuple)):
|
|
120
124
|
raise FilterArgumentError(
|
|
121
125
|
f"concat expected an array, found {type(second_array).__name__}"
|
|
@@ -129,7 +133,7 @@ def concat(sequence: ArrayT, second_array: ArrayT) -> ArrayT:
|
|
|
129
133
|
|
|
130
134
|
@sequence_filter
|
|
131
135
|
def map_(sequence: ArrayT, key: object) -> List[object]:
|
|
132
|
-
"""
|
|
136
|
+
"""Return an array/list of items in _sequence_ selected by _key_."""
|
|
133
137
|
try:
|
|
134
138
|
return [_getitem(itm, str(key), default=NIL) for itm in sequence]
|
|
135
139
|
except TypeError as err:
|
|
@@ -144,9 +148,9 @@ def reverse(array: ArrayT) -> List[object]:
|
|
|
144
148
|
|
|
145
149
|
@array_filter
|
|
146
150
|
def sort(sequence: ArrayT, key: object = None) -> List[object]:
|
|
147
|
-
"""
|
|
151
|
+
"""Return a copy of _sequence_ in ascending order.
|
|
148
152
|
|
|
149
|
-
When a key string is provided, objects without the key property
|
|
153
|
+
When a key string is provided, objects without the key property will
|
|
150
154
|
be at the end of the output list/array.
|
|
151
155
|
"""
|
|
152
156
|
if key:
|
|
@@ -161,7 +165,11 @@ def sort(sequence: ArrayT, key: object = None) -> List[object]:
|
|
|
161
165
|
|
|
162
166
|
@array_filter
|
|
163
167
|
def sort_natural(sequence: ArrayT, key: object = None) -> List[object]:
|
|
164
|
-
"""
|
|
168
|
+
"""Return a copy of _sequence_ in ascending order, with case-insensitive comparison.
|
|
169
|
+
|
|
170
|
+
When a key string is provided, objects without the key property will
|
|
171
|
+
be at the end of the output list/array.
|
|
172
|
+
"""
|
|
165
173
|
if key:
|
|
166
174
|
item_getter = partial(_getitem, key=str(key), default=MAX_CH)
|
|
167
175
|
return sorted(sequence, key=lambda obj: _lower(item_getter(obj)))
|
|
@@ -171,7 +179,7 @@ def sort_natural(sequence: ArrayT, key: object = None) -> List[object]:
|
|
|
171
179
|
|
|
172
180
|
@sequence_filter
|
|
173
181
|
def where(sequence: ArrayT, attr: object, value: object = None) -> List[object]:
|
|
174
|
-
"""
|
|
182
|
+
"""Return a list of items from _sequence_ where _attr_ equals _value_."""
|
|
175
183
|
if value is not None and not is_undefined(value):
|
|
176
184
|
return [itm for itm in sequence if _getitem(itm, attr) == value]
|
|
177
185
|
|
|
@@ -180,7 +188,7 @@ def where(sequence: ArrayT, attr: object, value: object = None) -> List[object]:
|
|
|
180
188
|
|
|
181
189
|
@sequence_filter
|
|
182
190
|
def uniq(sequence: ArrayT, key: object = None) -> List[object]:
|
|
183
|
-
"""
|
|
191
|
+
"""Return a copy of _sequence_ with duplicate elements removed."""
|
|
184
192
|
# Note that we're not using a dict or set for deduplication because we need
|
|
185
193
|
# to handle sequences containing unhashable objects, like dictionaries.
|
|
186
194
|
|
|
@@ -209,7 +217,7 @@ def uniq(sequence: ArrayT, key: object = None) -> List[object]:
|
|
|
209
217
|
|
|
210
218
|
@sequence_filter
|
|
211
219
|
def compact(sequence: ArrayT, key: object = None) -> List[object]:
|
|
212
|
-
"""
|
|
220
|
+
"""Return a copy of _sequence_ with any nil values removed."""
|
|
213
221
|
if key is not None:
|
|
214
222
|
try:
|
|
215
223
|
return [itm for itm in sequence if itm[key] is not None]
|
|
@@ -220,7 +228,7 @@ def compact(sequence: ArrayT, key: object = None) -> List[object]:
|
|
|
220
228
|
|
|
221
229
|
@sequence_filter
|
|
222
230
|
def sum_(sequence: ArrayT, key: object = None) -> Union[float, int, Decimal]:
|
|
223
|
-
"""Return the sum of all numeric elements in
|
|
231
|
+
"""Return the sum of all numeric elements in _sequence_.
|
|
224
232
|
|
|
225
233
|
If _key_ is given, it is assumed that sequence items are mapping-like,
|
|
226
234
|
and the values at _item[key]_ will be summed instead.
|
liquid/builtin/filters/extra.py
CHANGED
|
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
|
|
18
18
|
@with_environment
|
|
19
19
|
@string_filter
|
|
20
20
|
def safe(val: str, *, environment: Environment) -> str:
|
|
21
|
-
"""
|
|
21
|
+
"""Return a copy of _val_ that will not be automatically HTML escaped on output."""
|
|
22
22
|
if environment.autoescape:
|
|
23
23
|
return Markup(val)
|
|
24
24
|
return val
|
liquid/builtin/filters/math.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Maths related filter
|
|
1
|
+
"""Maths related filter functions."""
|
|
2
2
|
|
|
3
3
|
import decimal
|
|
4
4
|
import math
|
|
@@ -10,51 +10,44 @@ from liquid.exceptions import FilterArgumentError
|
|
|
10
10
|
from liquid.filter import math_filter
|
|
11
11
|
from liquid.filter import num_arg
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# TODO: Version 2 - Either handle all filter function argument type
|
|
14
|
+
# conversions in a decorator or all in the function itself. Having these type
|
|
15
|
+
# conversions split between decorators and calls to helper functions does not
|
|
16
|
+
# help with readability, or make it easy to write good doc strings.
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
@math_filter
|
|
18
|
-
def abs_(num:
|
|
19
|
-
"""Return the absolute value of
|
|
20
|
-
|
|
21
|
-
Accepts an int, float or a string representations of an int or float.
|
|
22
|
-
"""
|
|
20
|
+
def abs_(num: Union[float, int]) -> Union[float, int]:
|
|
21
|
+
"""Return the absolute value of number _num_."""
|
|
23
22
|
return abs(num)
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@math_filter
|
|
27
|
-
def at_most(num:
|
|
28
|
-
"""Return
|
|
29
|
-
|
|
30
|
-
Accepts an int, float or a string representations of an int or float.
|
|
31
|
-
"""
|
|
26
|
+
def at_most(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
27
|
+
"""Return _val_ or _other_, whichever is smaller."""
|
|
32
28
|
other = num_arg(other, default=0)
|
|
33
29
|
return min(num, other)
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
@math_filter
|
|
37
|
-
def at_least(num:
|
|
38
|
-
"""Return
|
|
39
|
-
|
|
40
|
-
Accepts an int, float or a string representations of an int or float.
|
|
41
|
-
"""
|
|
33
|
+
def at_least(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
34
|
+
"""Return _val_ or _other_, whichever is greater."""
|
|
42
35
|
other = num_arg(other, default=0)
|
|
43
36
|
return max(num, other)
|
|
44
37
|
|
|
45
38
|
|
|
46
39
|
@math_filter
|
|
47
|
-
def ceil(num:
|
|
48
|
-
"""Return
|
|
49
|
-
|
|
50
|
-
Accepts an int, float or a string representations of an int or float.
|
|
51
|
-
"""
|
|
40
|
+
def ceil(num: Union[float, int]) -> Union[float, int]:
|
|
41
|
+
"""Return _num_ rounded up to the next integer."""
|
|
52
42
|
return math.ceil(num)
|
|
53
43
|
|
|
54
44
|
|
|
55
45
|
@math_filter
|
|
56
|
-
def divided_by(num:
|
|
57
|
-
"""
|
|
46
|
+
def divided_by(num: Union[float, int], other: object) -> Union[float, int]:
|
|
47
|
+
"""Return the result of dividing _num_ by _other_.
|
|
48
|
+
|
|
49
|
+
If both _num_ and _other_ are integers, integer division is performed.
|
|
50
|
+
"""
|
|
58
51
|
other = num_arg(other, default=0)
|
|
59
52
|
|
|
60
53
|
try:
|
|
@@ -62,41 +55,40 @@ def divided_by(num: NumberT, other: object) -> NumberT:
|
|
|
62
55
|
return num // other
|
|
63
56
|
return num / other
|
|
64
57
|
except ZeroDivisionError as err:
|
|
58
|
+
# TODO: [VERSION_2] move inclusion of filter name in error messages to
|
|
59
|
+
# FilteredExpression, where the filter is applied.
|
|
65
60
|
raise FilterArgumentError(f"divided_by: can't divide by {other}") from err
|
|
66
61
|
|
|
67
62
|
|
|
68
63
|
@math_filter
|
|
69
|
-
def floor(num:
|
|
70
|
-
"""Return
|
|
71
|
-
|
|
72
|
-
Accepts an int, float or a string representations of an int or float.
|
|
73
|
-
"""
|
|
64
|
+
def floor(num: Union[float, int]) -> Union[float, int]:
|
|
65
|
+
"""Return _num_ rounded down to the next integer."""
|
|
74
66
|
return math.floor(num)
|
|
75
67
|
|
|
76
68
|
|
|
77
69
|
@math_filter
|
|
78
|
-
def minus(num:
|
|
79
|
-
"""
|
|
70
|
+
def minus(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
71
|
+
"""Return the result of subtracting _other_ from _num_."""
|
|
80
72
|
other = num_arg(other, default=0)
|
|
81
73
|
|
|
82
74
|
if isinstance(num, int) and isinstance(other, int):
|
|
83
75
|
return num - other
|
|
84
|
-
return float(
|
|
76
|
+
return float(decimal.Decimal(str(num)) - decimal.Decimal(str(other)))
|
|
85
77
|
|
|
86
78
|
|
|
87
79
|
@math_filter
|
|
88
|
-
def plus(num:
|
|
89
|
-
"""
|
|
80
|
+
def plus(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
81
|
+
"""Return the result of adding _other_ to _num_."""
|
|
90
82
|
other = num_arg(other, default=0)
|
|
91
83
|
|
|
92
84
|
if isinstance(num, int) and isinstance(other, int):
|
|
93
85
|
return num + other
|
|
94
|
-
return float(
|
|
86
|
+
return float(decimal.Decimal(str(num)) + decimal.Decimal(str(other)))
|
|
95
87
|
|
|
96
88
|
|
|
97
89
|
@math_filter
|
|
98
|
-
def round_(num:
|
|
99
|
-
"""
|
|
90
|
+
def round_(num: Union[float, int], ndigits: Optional[int] = None) -> Union[float, int]:
|
|
91
|
+
"""Returns the result of rounding _num_ to _ndigits_ decimal digits."""
|
|
100
92
|
if ndigits is None or is_undefined(ndigits):
|
|
101
93
|
return round(num)
|
|
102
94
|
|
|
@@ -118,24 +110,25 @@ def round_(num: NumberT, ndigits: Optional[int] = None) -> NumberT:
|
|
|
118
110
|
|
|
119
111
|
|
|
120
112
|
@math_filter
|
|
121
|
-
def times(num:
|
|
122
|
-
"""
|
|
113
|
+
def times(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
114
|
+
"""Return the result of multiplying _num_ by _other_."""
|
|
123
115
|
other = num_arg(other, default=0)
|
|
124
116
|
|
|
125
117
|
if isinstance(num, int) and isinstance(other, int):
|
|
126
118
|
return num * other
|
|
127
|
-
return float(
|
|
119
|
+
return float(decimal.Decimal(str(num)) * decimal.Decimal(str(other)))
|
|
128
120
|
|
|
129
121
|
|
|
130
122
|
@math_filter
|
|
131
|
-
def modulo(num:
|
|
132
|
-
"""
|
|
123
|
+
def modulo(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
124
|
+
"""Return the remainder of dividing _num_ by _other_."""
|
|
133
125
|
other = num_arg(other, default=0)
|
|
134
126
|
|
|
135
127
|
try:
|
|
136
128
|
if isinstance(num, int) and isinstance(other, int):
|
|
137
129
|
return num % other
|
|
138
|
-
return float(
|
|
139
|
-
|
|
130
|
+
return float(decimal.Decimal(str(num)) % decimal.Decimal(str(other)))
|
|
140
131
|
except ZeroDivisionError as err:
|
|
132
|
+
# TODO: [VERSION_2] move inclusion of filter name in error messages to
|
|
133
|
+
# FilteredExpression, where the filter is applied.
|
|
141
134
|
raise FilterArgumentError(f"modulo: can't divide by {other}") from err
|
liquid/builtin/filters/misc.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Miscellaneous
|
|
1
|
+
"""Miscellaneous filter functions."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import datetime
|
|
@@ -27,7 +27,10 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
@liquid_filter
|
|
29
29
|
def size(obj: Any) -> int:
|
|
30
|
-
"""Return the length of
|
|
30
|
+
"""Return the length of _obj_.
|
|
31
|
+
|
|
32
|
+
_obj_ could be a dict, list, string or any class implementing _len_.
|
|
33
|
+
"""
|
|
31
34
|
try:
|
|
32
35
|
return len(obj)
|
|
33
36
|
except TypeError:
|
|
@@ -36,7 +39,7 @@ def size(obj: Any) -> int:
|
|
|
36
39
|
|
|
37
40
|
@liquid_filter
|
|
38
41
|
def default(obj: Any, default_: object = "", *, allow_false: bool = False) -> Any:
|
|
39
|
-
"""Return
|
|
42
|
+
"""Return _obj_, or _default_ if _obj_ is nil, false, or empty."""
|
|
40
43
|
_obj = obj
|
|
41
44
|
|
|
42
45
|
# Return the default value immediately if the object defines a
|
|
@@ -69,7 +72,7 @@ def date( # noqa: PLR0912 PLR0911
|
|
|
69
72
|
*,
|
|
70
73
|
environment: Environment,
|
|
71
74
|
) -> str:
|
|
72
|
-
"""
|
|
75
|
+
"""Return a string representation of _dat_ using format string _fmt_."""
|
|
73
76
|
if is_undefined(dat):
|
|
74
77
|
return ""
|
|
75
78
|
|
liquid/builtin/filters/string.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Filter functions that operate on strings."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import base64
|
|
@@ -38,7 +39,10 @@ if TYPE_CHECKING:
|
|
|
38
39
|
|
|
39
40
|
@string_filter
|
|
40
41
|
def append(val: str, arg: object) -> str:
|
|
41
|
-
"""
|
|
42
|
+
"""Return a copy of _val_ concatenated with _arg_.
|
|
43
|
+
|
|
44
|
+
If _arg_ is not a string, it will be converted to one before concatenation.
|
|
45
|
+
"""
|
|
42
46
|
if not isinstance(arg, str):
|
|
43
47
|
arg = str(arg)
|
|
44
48
|
return val + arg
|
|
@@ -46,20 +50,20 @@ def append(val: str, arg: object) -> str:
|
|
|
46
50
|
|
|
47
51
|
@string_filter
|
|
48
52
|
def capitalize(val: str) -> str:
|
|
49
|
-
"""
|
|
53
|
+
"""Return _val_ with the first character in uppercase and the rest lowercase."""
|
|
50
54
|
return val.capitalize()
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
@string_filter
|
|
54
58
|
def downcase(val: str) -> str:
|
|
55
|
-
"""
|
|
59
|
+
"""Return a copy of _val_ with all characters converted to lowercase."""
|
|
56
60
|
return val.lower()
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
@with_environment
|
|
60
64
|
@string_filter
|
|
61
65
|
def escape(val: str, *, environment: Environment) -> str:
|
|
62
|
-
"""
|
|
66
|
+
"""Return _val_ with the characters &, < and > converted to HTML-safe sequences."""
|
|
63
67
|
if environment.autoescape:
|
|
64
68
|
return markupsafe_escape(str(val))
|
|
65
69
|
return html.escape(val)
|
|
@@ -68,7 +72,11 @@ def escape(val: str, *, environment: Environment) -> str:
|
|
|
68
72
|
@with_environment
|
|
69
73
|
@string_filter
|
|
70
74
|
def escape_once(val: str, *, environment: Environment) -> str:
|
|
71
|
-
"""
|
|
75
|
+
"""Return _val_ with the characters &, < and > converted to HTML-safe sequences.
|
|
76
|
+
|
|
77
|
+
It is safe to use `escape_one` on string values that already contain HTML escape
|
|
78
|
+
sequences.
|
|
79
|
+
"""
|
|
72
80
|
if environment.autoescape:
|
|
73
81
|
return Markup(val).unescape()
|
|
74
82
|
return html.escape(html.unescape(val))
|
|
@@ -76,7 +84,7 @@ def escape_once(val: str, *, environment: Environment) -> str:
|
|
|
76
84
|
|
|
77
85
|
@string_filter
|
|
78
86
|
def lstrip(val: str) -> str:
|
|
79
|
-
"""
|
|
87
|
+
"""Return a copy of _val_ with leading whitespace removed."""
|
|
80
88
|
return val.lstrip()
|
|
81
89
|
|
|
82
90
|
|
|
@@ -86,7 +94,7 @@ RE_LINETERM = re.compile(r"\r?\n")
|
|
|
86
94
|
@with_environment
|
|
87
95
|
@string_filter
|
|
88
96
|
def newline_to_br(val: str, *, environment: Environment) -> str:
|
|
89
|
-
"""
|
|
97
|
+
"""Return a copy of _val_ with LF or CRLF converted to `<br />`, plus a newline."""
|
|
90
98
|
# The reference implementation was changed to replace "\r\n" as well as "\n",
|
|
91
99
|
# but they don't seem to care about "\r" (Mac OS).
|
|
92
100
|
if environment.autoescape:
|
|
@@ -97,25 +105,25 @@ def newline_to_br(val: str, *, environment: Environment) -> str:
|
|
|
97
105
|
|
|
98
106
|
@string_filter
|
|
99
107
|
def prepend(val: str, arg: str) -> str:
|
|
100
|
-
"""
|
|
108
|
+
"""Return a copy of _arg_ concatenated with _val_."""
|
|
101
109
|
return soft_str(arg) + val
|
|
102
110
|
|
|
103
111
|
|
|
104
112
|
@string_filter
|
|
105
113
|
def remove(val: str, arg: str) -> str:
|
|
106
|
-
"""
|
|
114
|
+
"""Return a copy of _val_ with all occurrences of _arg_ removed."""
|
|
107
115
|
return val.replace(soft_str(arg), "")
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
@string_filter
|
|
111
119
|
def remove_first(val: str, arg: str) -> str:
|
|
112
|
-
"""
|
|
120
|
+
"""Return a copy of _val_ with the first occurrence of _arg_ removed."""
|
|
113
121
|
return val.replace(soft_str(arg), "", 1)
|
|
114
122
|
|
|
115
123
|
|
|
116
124
|
@string_filter
|
|
117
125
|
def remove_last(val: str, arg: str) -> str:
|
|
118
|
-
"""
|
|
126
|
+
"""Return a copy of _val_ with last occurrence of _arg_ removed."""
|
|
119
127
|
try:
|
|
120
128
|
before, _, after = val.rpartition(soft_str(arg))
|
|
121
129
|
except ValueError:
|
|
@@ -128,19 +136,19 @@ def remove_last(val: str, arg: str) -> str:
|
|
|
128
136
|
|
|
129
137
|
@string_filter
|
|
130
138
|
def replace(val: str, seq: str, sub: str = "") -> str:
|
|
131
|
-
"""
|
|
139
|
+
"""Return a copy of _val_ with each occurrence of _seq_ replaced with _sub_."""
|
|
132
140
|
return val.replace(soft_str(seq), soft_str(sub))
|
|
133
141
|
|
|
134
142
|
|
|
135
143
|
@string_filter
|
|
136
144
|
def replace_first(val: str, seq: str, sub: str = "") -> str:
|
|
137
|
-
"""
|
|
145
|
+
"""Return a copy of _val_ with the first occurrence of _seq_ replaced with _sub_."""
|
|
138
146
|
return val.replace(soft_str(seq), soft_str(sub), 1)
|
|
139
147
|
|
|
140
148
|
|
|
141
149
|
@string_filter
|
|
142
150
|
def replace_last(val: str, seq: str, sub: str) -> str:
|
|
143
|
-
"""
|
|
151
|
+
"""Return a copy of _val_ with the last occurrence of _seq_ replaced with _sub_."""
|
|
144
152
|
try:
|
|
145
153
|
before, _, after = val.rpartition(soft_str(seq))
|
|
146
154
|
except ValueError:
|
|
@@ -153,7 +161,7 @@ def replace_last(val: str, seq: str, sub: str) -> str:
|
|
|
153
161
|
|
|
154
162
|
@string_filter
|
|
155
163
|
def upcase(val: str) -> str:
|
|
156
|
-
"""
|
|
164
|
+
"""Return a copy of _val_ with all characters converted to uppercase."""
|
|
157
165
|
return val.upper()
|
|
158
166
|
|
|
159
167
|
|
|
@@ -211,7 +219,7 @@ def slice_(val: Any, start: Any, length: Any = 1) -> Union[str, List[object]]:
|
|
|
211
219
|
|
|
212
220
|
@string_filter
|
|
213
221
|
def split(val: str, seq: str) -> List[str]:
|
|
214
|
-
"""
|
|
222
|
+
"""Return a list of strings from splitting _value_ on _seq_."""
|
|
215
223
|
if not seq:
|
|
216
224
|
return list(val)
|
|
217
225
|
|
|
@@ -220,20 +228,20 @@ def split(val: str, seq: str) -> List[str]:
|
|
|
220
228
|
|
|
221
229
|
@string_filter
|
|
222
230
|
def strip(val: str) -> str:
|
|
223
|
-
"""
|
|
231
|
+
"""Return a copy of _val_ with leading and trailing whitespace removed."""
|
|
224
232
|
return val.strip()
|
|
225
233
|
|
|
226
234
|
|
|
227
235
|
@string_filter
|
|
228
236
|
def rstrip(val: str) -> str:
|
|
229
|
-
"""
|
|
237
|
+
"""Return a copy of _val_ with trailing whitespace removed."""
|
|
230
238
|
return val.rstrip()
|
|
231
239
|
|
|
232
240
|
|
|
233
241
|
@with_environment
|
|
234
242
|
@string_filter
|
|
235
243
|
def strip_html(val: str, *, environment: Environment) -> str:
|
|
236
|
-
"""Return
|
|
244
|
+
"""Return a copy of _val_ with all HTML tags removed."""
|
|
237
245
|
stripped = strip_tags(val)
|
|
238
246
|
if environment.autoescape and isinstance(val, Markup):
|
|
239
247
|
return Markup(stripped)
|
|
@@ -243,7 +251,7 @@ def strip_html(val: str, *, environment: Environment) -> str:
|
|
|
243
251
|
@with_environment
|
|
244
252
|
@string_filter
|
|
245
253
|
def strip_newlines(val: str, *, environment: Environment) -> str:
|
|
246
|
-
"""Return
|
|
254
|
+
"""Return ta copy of _val_ with all newline characters removed."""
|
|
247
255
|
if environment.autoescape:
|
|
248
256
|
val = markupsafe_escape(val)
|
|
249
257
|
return Markup(RE_LINETERM.sub("", val))
|
|
@@ -252,7 +260,7 @@ def strip_newlines(val: str, *, environment: Environment) -> str:
|
|
|
252
260
|
|
|
253
261
|
@string_filter
|
|
254
262
|
def truncate(val: str, num: Any = 50, end: str = "...") -> str:
|
|
255
|
-
"""
|
|
263
|
+
"""Return a copy of _val_ truncated to _num_ characters."""
|
|
256
264
|
if is_undefined(num):
|
|
257
265
|
raise FilterArgumentError("truncate expected an integer, found Undefined")
|
|
258
266
|
|
|
@@ -273,7 +281,7 @@ MAX_TRUNC_WORDS = (1 << 31) - 1
|
|
|
273
281
|
|
|
274
282
|
@string_filter
|
|
275
283
|
def truncatewords(val: str, num: Any = 15, end: str = "...") -> str:
|
|
276
|
-
"""
|
|
284
|
+
"""Return a copy of _val_ truncated to at most _num_ words."""
|
|
277
285
|
if is_undefined(num):
|
|
278
286
|
raise FilterArgumentError("truncate expected an integer, found Undefined")
|
|
279
287
|
|
|
@@ -306,7 +314,7 @@ def truncatewords(val: str, num: Any = 15, end: str = "...") -> str:
|
|
|
306
314
|
@with_environment
|
|
307
315
|
@string_filter
|
|
308
316
|
def url_encode(val: str, *, environment: Environment) -> str:
|
|
309
|
-
"""
|
|
317
|
+
"""Return a percent-encoded copy of _val_ so it is useable in a URL."""
|
|
310
318
|
if environment.autoescape:
|
|
311
319
|
return Markup(urllib.parse.quote_plus(val))
|
|
312
320
|
return urllib.parse.quote_plus(val)
|
|
@@ -314,20 +322,20 @@ def url_encode(val: str, *, environment: Environment) -> str:
|
|
|
314
322
|
|
|
315
323
|
@string_filter
|
|
316
324
|
def url_decode(val: str) -> str:
|
|
317
|
-
"""
|
|
325
|
+
"""Return a copy of _val_ after decoding percent-encoded sequences."""
|
|
318
326
|
# Assuming URL decoded strings are all unsafe.
|
|
319
327
|
return urllib.parse.unquote_plus(val)
|
|
320
328
|
|
|
321
329
|
|
|
322
330
|
@string_filter
|
|
323
331
|
def base64_encode(val: str) -> str:
|
|
324
|
-
"""
|
|
332
|
+
"""Return _val_ encoded in base64."""
|
|
325
333
|
return base64.b64encode(val.encode()).decode()
|
|
326
334
|
|
|
327
335
|
|
|
328
336
|
@string_filter
|
|
329
337
|
def base64_decode(val: str) -> str:
|
|
330
|
-
"""
|
|
338
|
+
"""Return _val_ decoded as base64.
|
|
331
339
|
|
|
332
340
|
The decoded value is assumed to be UTF-8 and will be decoded as UTF-8.
|
|
333
341
|
"""
|
|
@@ -339,13 +347,13 @@ def base64_decode(val: str) -> str:
|
|
|
339
347
|
|
|
340
348
|
@string_filter
|
|
341
349
|
def base64_url_safe_encode(val: str) -> str:
|
|
342
|
-
"""
|
|
350
|
+
"""Return _val_ encoded in URL-safe base64."""
|
|
343
351
|
return base64.urlsafe_b64encode(val.encode()).decode()
|
|
344
352
|
|
|
345
353
|
|
|
346
354
|
@string_filter
|
|
347
355
|
def base64_url_safe_decode(val: str) -> str:
|
|
348
|
-
"""
|
|
356
|
+
"""Return _val_ decoded as URL-safe base64.
|
|
349
357
|
|
|
350
358
|
The decoded value is assumed to be UTF-8 and will be decoded as UTF-8.
|
|
351
359
|
"""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Base template loader."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
from abc import ABC
|
|
@@ -48,7 +49,7 @@ class BaseLoader(ABC): # noqa: B024
|
|
|
48
49
|
|
|
49
50
|
Attributes:
|
|
50
51
|
caching_loader (bool): Indicates if this loader implements its own cache.
|
|
51
|
-
Setting this
|
|
52
|
+
Setting this to `True` will cause the `Environment` to disable its cache
|
|
52
53
|
when initialized with a caching loader.
|
|
53
54
|
"""
|
|
54
55
|
|
liquid/builtin/statement.py
CHANGED
|
@@ -60,4 +60,6 @@ class Statement(Tag):
|
|
|
60
60
|
def parse(self, stream: TokenStream) -> StatementNode:
|
|
61
61
|
tok = stream.current
|
|
62
62
|
expect(stream, TOKEN_STATEMENT)
|
|
63
|
-
return self.node_class(
|
|
63
|
+
return self.node_class(
|
|
64
|
+
tok, self.env.parse_filtered_expression_value(tok.value, tok.linenum)
|
|
65
|
+
)
|
|
@@ -65,8 +65,8 @@ class AssignTag(Tag):
|
|
|
65
65
|
block = False
|
|
66
66
|
node_class = AssignNode
|
|
67
67
|
|
|
68
|
-
def _parse_expression(self, value: str) -> Expression:
|
|
69
|
-
return self.env.parse_filtered_expression_value(value)
|
|
68
|
+
def _parse_expression(self, value: str, linenum: int) -> Expression:
|
|
69
|
+
return self.env.parse_filtered_expression_value(value, linenum)
|
|
70
70
|
|
|
71
71
|
def parse(self, stream: TokenStream) -> AssignNode:
|
|
72
72
|
expect(stream, TOKEN_TAG, value=TAG_ASSIGN)
|
|
@@ -83,5 +83,9 @@ class AssignTag(Tag):
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
return self.node_class(
|
|
86
|
-
tok,
|
|
86
|
+
tok,
|
|
87
|
+
AssignmentExpression(
|
|
88
|
+
name,
|
|
89
|
+
self._parse_expression(right, stream.current.linenum),
|
|
90
|
+
),
|
|
87
91
|
)
|
liquid/builtin/tags/case_tag.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Tag and node definition for the built-in "case" tag."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import sys
|
|
@@ -193,14 +194,16 @@ class CaseTag(Tag):
|
|
|
193
194
|
|
|
194
195
|
def _parse_case_expression(self, expr: str, linenum: int) -> Expression:
|
|
195
196
|
stream = ExpressionTokenStream(
|
|
196
|
-
tokenize_common_expression(expr, linenum=linenum)
|
|
197
|
+
tokenize_common_expression(expr, linenum=linenum),
|
|
198
|
+
shorthand_indexes=self.env.shorthand_indexes,
|
|
197
199
|
)
|
|
198
200
|
return parse_common_expression(stream)
|
|
199
201
|
|
|
200
202
|
def _parse_when_expression(self, expr: str, linenum: int) -> List[Expression]:
|
|
201
203
|
expressions = []
|
|
202
204
|
stream = ExpressionTokenStream(
|
|
203
|
-
tokenize_common_expression(expr, linenum=linenum)
|
|
205
|
+
tokenize_common_expression(expr, linenum=linenum),
|
|
206
|
+
shorthand_indexes=self.env.shorthand_indexes,
|
|
204
207
|
)
|
|
205
208
|
|
|
206
209
|
while True:
|