python-liquid 2.3.6__py3-none-any.whl → 3.0.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/__about__.py +1 -0
- liquid/__init__.py +161 -187
- liquid/_base_tag.py +66 -0
- liquid/{utils/chain_map.py → _chain_map.py} +18 -19
- liquid/_context.py +465 -0
- liquid/_drop.py +108 -0
- liquid/_filter.py +174 -0
- liquid/_io.py +18 -0
- liquid/_lax_parser_mixin.py +100 -0
- liquid/_lexer.py +93 -0
- liquid/_lexer_extended.py +623 -0
- liquid/_lexer_legacy.py +536 -0
- liquid/_loader.py +84 -0
- liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
- liquid/_markup.py +253 -0
- liquid/_messages.py +292 -0
- liquid/_missing.py +3 -0
- liquid/_nothing.py +28 -0
- liquid/_parser.py +460 -0
- liquid/_parser_extended.py +293 -0
- liquid/_parser_legacy.py +819 -0
- liquid/_resource_limits.py +50 -0
- liquid/_slice.py +41 -0
- liquid/_static_analysis.py +503 -0
- liquid/_strftime.py +186 -0
- liquid/_template.py +536 -0
- liquid/drops/__init__.py +20 -0
- liquid/drops/forloop.py +78 -0
- liquid/drops/iterable.py +25 -0
- liquid/drops/range.py +76 -0
- liquid/drops/self_.py +45 -0
- liquid/drops/tablerowloop.py +142 -0
- liquid/drops/undefined.py +228 -0
- liquid/environment.py +717 -551
- liquid/exceptions.py +67 -214
- liquid/expressions.py +1184 -0
- liquid/filters/__init__.py +134 -0
- liquid/filters/abs.py +7 -0
- liquid/filters/append.py +5 -0
- liquid/filters/at_least.py +9 -0
- liquid/filters/at_most.py +9 -0
- liquid/filters/babel.py +560 -0
- liquid/filters/base64.py +26 -0
- liquid/filters/capitalize.py +5 -0
- liquid/filters/ceil.py +8 -0
- liquid/filters/compact.py +25 -0
- liquid/filters/concat.py +11 -0
- liquid/filters/date.py +27 -0
- liquid/filters/default.py +25 -0
- liquid/filters/divided_by.py +21 -0
- liquid/filters/downcase.py +5 -0
- liquid/filters/escape.py +56 -0
- liquid/filters/find.py +56 -0
- liquid/filters/first.py +19 -0
- liquid/filters/floor.py +8 -0
- liquid/filters/html.py +23 -0
- liquid/filters/index.py +12 -0
- liquid/filters/join.py +13 -0
- liquid/filters/json_.py +30 -0
- liquid/filters/last.py +17 -0
- liquid/filters/map.py +5 -0
- liquid/filters/minus.py +13 -0
- liquid/filters/modulo.py +18 -0
- liquid/filters/newline_to_br.py +16 -0
- liquid/filters/plus.py +13 -0
- liquid/filters/prepend.py +5 -0
- liquid/filters/reject.py +19 -0
- liquid/filters/remove.py +36 -0
- liquid/filters/replace.py +32 -0
- liquid/filters/reverse.py +5 -0
- liquid/filters/round.py +35 -0
- liquid/filters/size.py +25 -0
- liquid/filters/slice.py +39 -0
- liquid/filters/sort.py +148 -0
- liquid/filters/split.py +34 -0
- liquid/filters/squish.py +9 -0
- liquid/filters/strip.py +13 -0
- liquid/filters/strip_html.py +19 -0
- liquid/filters/strip_newlines.py +17 -0
- liquid/filters/sum.py +21 -0
- liquid/filters/times.py +13 -0
- liquid/filters/translate.py +380 -0
- liquid/filters/truncate.py +39 -0
- liquid/filters/uniq.py +11 -0
- liquid/filters/upcase.py +5 -0
- liquid/filters/url.py +18 -0
- liquid/filters/where.py +18 -0
- liquid/loaders/__init__.py +17 -0
- liquid/loaders/caching_file_system_loader.py +64 -0
- liquid/loaders/choice_loader.py +93 -0
- liquid/loaders/dict_loader.py +56 -0
- liquid/{builtin/loaders → loaders}/file_system_loader.py +51 -55
- liquid/{builtin/loaders → loaders}/mixins.py +39 -51
- liquid/loaders/package_loader.py +110 -0
- liquid/tags/__init__.py +61 -0
- liquid/tags/assign.py +68 -0
- liquid/tags/capture.py +83 -0
- liquid/tags/case.py +199 -0
- liquid/tags/comment.py +56 -0
- liquid/tags/cycle.py +131 -0
- liquid/tags/decrement.py +55 -0
- liquid/tags/doc.py +56 -0
- liquid/tags/echo.py +65 -0
- liquid/tags/else_.py +53 -0
- liquid/tags/error.py +51 -0
- liquid/tags/extends.py +448 -0
- liquid/tags/for_.py +421 -0
- liquid/tags/if_.py +156 -0
- liquid/tags/ifchanged.py +69 -0
- liquid/tags/include.py +223 -0
- liquid/tags/increment.py +55 -0
- liquid/tags/inline_comment.py +70 -0
- liquid/tags/liquid.py +56 -0
- liquid/tags/macro.py +281 -0
- liquid/tags/output.py +58 -0
- liquid/tags/raw.py +60 -0
- liquid/tags/render.py +245 -0
- liquid/tags/tablerow.py +254 -0
- liquid/tags/template_comment.py +56 -0
- liquid/tags/translate.py +316 -0
- liquid/tags/unless.py +121 -0
- liquid/tags/with_.py +83 -0
- liquid/tokens.py +370 -0
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
- python_liquid-3.0.0.dist-info/RECORD +128 -0
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
- liquid/analyze_tags.py +0 -214
- liquid/ast.py +0 -252
- liquid/builtin/__init__.py +0 -203
- liquid/builtin/content.py +0 -41
- liquid/builtin/drops/__init__.py +0 -3
- liquid/builtin/drops/drops.py +0 -13
- liquid/builtin/expressions/__init__.py +0 -47
- liquid/builtin/expressions/_tokenize.py +0 -186
- liquid/builtin/expressions/arguments.py +0 -207
- liquid/builtin/expressions/filtered.py +0 -387
- liquid/builtin/expressions/logical.py +0 -672
- liquid/builtin/expressions/loop.py +0 -275
- liquid/builtin/expressions/path.py +0 -163
- liquid/builtin/expressions/primitive.py +0 -417
- liquid/builtin/filters/__init__.py +0 -0
- liquid/builtin/filters/array.py +0 -329
- liquid/builtin/filters/extra.py +0 -83
- liquid/builtin/filters/math.py +0 -129
- liquid/builtin/filters/misc.py +0 -137
- liquid/builtin/filters/string.py +0 -416
- liquid/builtin/illegal.py +0 -28
- liquid/builtin/loaders/__init__.py +0 -0
- liquid/builtin/loaders/caching_file_system_loader.py +0 -61
- liquid/builtin/loaders/choice_loader.py +0 -94
- liquid/builtin/loaders/dict_loader.py +0 -64
- liquid/builtin/loaders/package_loader.py +0 -132
- liquid/builtin/output.py +0 -74
- liquid/builtin/tags/__init__.py +0 -0
- liquid/builtin/tags/assign_tag.py +0 -75
- liquid/builtin/tags/capture_tag.py +0 -98
- liquid/builtin/tags/case_tag.py +0 -287
- liquid/builtin/tags/comment_tag.py +0 -77
- liquid/builtin/tags/cycle_tag.py +0 -132
- liquid/builtin/tags/decrement_tag.py +0 -59
- liquid/builtin/tags/doc_tag.py +0 -68
- liquid/builtin/tags/echo_tag.py +0 -45
- liquid/builtin/tags/for_tag.py +0 -337
- liquid/builtin/tags/if_tag.py +0 -209
- liquid/builtin/tags/ifchanged_tag.py +0 -94
- liquid/builtin/tags/include_tag.py +0 -251
- liquid/builtin/tags/increment_tag.py +0 -60
- liquid/builtin/tags/inline_comment_tag.py +0 -47
- liquid/builtin/tags/liquid_tag.py +0 -183
- liquid/builtin/tags/render_tag.py +0 -371
- liquid/builtin/tags/tablerow_tag.py +0 -320
- liquid/builtin/tags/unless_tag.py +0 -218
- liquid/context.py +0 -615
- liquid/expression.py +0 -41
- liquid/extra/__init__.py +0 -98
- liquid/extra/filters/__init__.py +0 -33
- liquid/extra/filters/_json.py +0 -33
- liquid/extra/filters/array.py +0 -60
- liquid/extra/filters/babel.py +0 -552
- liquid/extra/filters/html.py +0 -32
- liquid/extra/filters/translate.py +0 -448
- liquid/extra/tags/__init__.py +0 -17
- liquid/extra/tags/_with.py +0 -80
- liquid/extra/tags/extends_tag.py +0 -576
- liquid/extra/tags/macro_tag.py +0 -270
- liquid/extra/tags/snippet_tag.py +0 -107
- liquid/extra/tags/translate_tag.py +0 -388
- liquid/filter.py +0 -230
- liquid/future/__init__.py +0 -3
- liquid/future/environment.py +0 -30
- liquid/future/filters/__init__.py +0 -1
- liquid/future/tags/__init__.py +0 -1
- liquid/lex.py +0 -260
- liquid/limits.py +0 -66
- liquid/loader.py +0 -141
- liquid/messages.py +0 -357
- liquid/mode.py +0 -12
- liquid/output.py +0 -35
- liquid/parser.py +0 -119
- liquid/span.py +0 -35
- liquid/static_analysis.py +0 -443
- liquid/stream.py +0 -148
- liquid/stringify.py +0 -31
- liquid/tag.py +0 -47
- liquid/template.py +0 -658
- liquid/token.py +0 -145
- liquid/undefined.py +0 -218
- liquid/utils/__init__.py +0 -15
- liquid/utils/html.py +0 -57
- liquid/utils/text.py +0 -28
- python_liquid-2.3.6.dist-info/RECORD +0 -96
liquid/builtin/filters/array.py
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
"""Filter functions that operate on arrays."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from decimal import Decimal
|
|
6
|
-
from functools import partial
|
|
7
|
-
from itertools import chain
|
|
8
|
-
from itertools import islice
|
|
9
|
-
from operator import getitem
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
11
|
-
from typing import Any
|
|
12
|
-
from typing import Iterable
|
|
13
|
-
from typing import Sequence
|
|
14
|
-
from typing import Union
|
|
15
|
-
|
|
16
|
-
from liquid import Markup
|
|
17
|
-
from liquid.builtin.expressions import Nil
|
|
18
|
-
from liquid.exceptions import FilterArgumentError
|
|
19
|
-
from liquid.exceptions import FilterError
|
|
20
|
-
from liquid.exceptions import FilterItemTypeError
|
|
21
|
-
from liquid.filter import decimal_arg
|
|
22
|
-
from liquid.filter import liquid_filter
|
|
23
|
-
from liquid.filter import sequence_filter
|
|
24
|
-
from liquid.filter import with_environment
|
|
25
|
-
from liquid.undefined import is_undefined
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from liquid import Environment
|
|
29
|
-
|
|
30
|
-
ArrayT = Union[list[Any], tuple[Any, ...]]
|
|
31
|
-
"""Array-like objects."""
|
|
32
|
-
|
|
33
|
-
# Send objects with missing keys to the end when sorting a list.
|
|
34
|
-
MAX_CH = chr(0x10FFFF)
|
|
35
|
-
|
|
36
|
-
# Unique object for use with the uniq filter.
|
|
37
|
-
MISSING = object()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@with_environment
|
|
41
|
-
@sequence_filter
|
|
42
|
-
def join(
|
|
43
|
-
left: Iterable[object],
|
|
44
|
-
separator: object = " ",
|
|
45
|
-
*,
|
|
46
|
-
environment: Environment,
|
|
47
|
-
) -> str:
|
|
48
|
-
"""Return concatenated items in _left_ separated by _separator_.
|
|
49
|
-
|
|
50
|
-
Items in _left_ will be coerced to a string if they are not already
|
|
51
|
-
strings, as will _separator_.
|
|
52
|
-
"""
|
|
53
|
-
if not isinstance(separator, str):
|
|
54
|
-
separator = str(separator)
|
|
55
|
-
|
|
56
|
-
if environment.autoescape and separator == " ":
|
|
57
|
-
separator = Markup(" ")
|
|
58
|
-
|
|
59
|
-
return separator.join(_str_if_not(item) for item in left)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@with_environment
|
|
63
|
-
@liquid_filter
|
|
64
|
-
def first(left: Any, *, environment: Environment) -> object:
|
|
65
|
-
"""Return the first item in collection _left_.
|
|
66
|
-
|
|
67
|
-
If _left_ is not a collection or it is empty, `None` is returned.
|
|
68
|
-
"""
|
|
69
|
-
if isinstance(left, str):
|
|
70
|
-
# Honor the opt-in Ruby-compat flag, matching the `.first` property path.
|
|
71
|
-
if environment.string_first_and_last:
|
|
72
|
-
return left[0] if left else None
|
|
73
|
-
return None
|
|
74
|
-
|
|
75
|
-
if isinstance(left, dict):
|
|
76
|
-
left = list(islice(left.items(), 1))
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
return getitem(left, 0)
|
|
80
|
-
except (TypeError, KeyError, IndexError):
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@with_environment
|
|
85
|
-
@liquid_filter
|
|
86
|
-
def last(obj: Sequence[Any], *, environment: Environment) -> object:
|
|
87
|
-
"""Return the last item in collection _left_.
|
|
88
|
-
|
|
89
|
-
If _left_ is not a collection or it is empty, `None` is returned.
|
|
90
|
-
"""
|
|
91
|
-
if isinstance(obj, str):
|
|
92
|
-
# Honor the opt-in Ruby-compat flag, matching the `.last` property path.
|
|
93
|
-
if environment.string_first_and_last:
|
|
94
|
-
return obj[-1] if obj else None
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
return getitem(obj, -1)
|
|
99
|
-
except (TypeError, KeyError, IndexError):
|
|
100
|
-
return None
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@sequence_filter
|
|
104
|
-
def concat(sequence: ArrayT, second_array: ArrayT) -> ArrayT:
|
|
105
|
-
"""Return the concatenation of _sequence_ and _second_array_."""
|
|
106
|
-
if not isinstance(second_array, (list, tuple)):
|
|
107
|
-
raise FilterArgumentError(
|
|
108
|
-
f"concat expected an array, found {type(second_array).__name__}"
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
if is_undefined(sequence):
|
|
112
|
-
return second_array
|
|
113
|
-
|
|
114
|
-
return list(chain(sequence, second_array))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@sequence_filter
|
|
118
|
-
def map_(sequence: ArrayT, key: object) -> list[object]:
|
|
119
|
-
"""Return an array/list of items in _sequence_ selected by _key_."""
|
|
120
|
-
try:
|
|
121
|
-
return [_getitem(itm, str(key), default=_NULL) for itm in sequence]
|
|
122
|
-
except TypeError as err:
|
|
123
|
-
raise FilterError("can't map sequence", token=None) from err
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@sequence_filter
|
|
127
|
-
def reverse(array: ArrayT) -> list[object]:
|
|
128
|
-
"""Reverses the order of the items in an array."""
|
|
129
|
-
return list(reversed(array))
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@sequence_filter
|
|
133
|
-
def sort(sequence: ArrayT, key: object = None) -> list[object]:
|
|
134
|
-
"""Return a copy of _sequence_ in ascending order.
|
|
135
|
-
|
|
136
|
-
When a key string is provided, objects without the key property will
|
|
137
|
-
be at the end of the output list/array.
|
|
138
|
-
"""
|
|
139
|
-
if key:
|
|
140
|
-
key_func = partial(_getitem, key=str(key), default=MAX_CH)
|
|
141
|
-
return sorted(sequence, key=key_func)
|
|
142
|
-
|
|
143
|
-
try:
|
|
144
|
-
return sorted(sequence)
|
|
145
|
-
except TypeError as err:
|
|
146
|
-
raise FilterError("can't sort sequence", token=None) from err
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@sequence_filter
|
|
150
|
-
def sort_natural(sequence: ArrayT, key: object = None) -> list[object]:
|
|
151
|
-
"""Return a copy of _sequence_ in ascending order, with case-insensitive comparison.
|
|
152
|
-
|
|
153
|
-
When a key string is provided, objects without the key property will
|
|
154
|
-
be at the end of the output list/array.
|
|
155
|
-
"""
|
|
156
|
-
if key:
|
|
157
|
-
item_getter = partial(_getitem, key=str(key), default=MAX_CH)
|
|
158
|
-
return sorted(sequence, key=lambda obj: _lower(item_getter(obj)))
|
|
159
|
-
|
|
160
|
-
return sorted(sequence, key=_lower)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@sequence_filter
|
|
164
|
-
def where(sequence: ArrayT, attr: object, value: object = None) -> list[object]:
|
|
165
|
-
"""Return a list of items from _sequence_ where _attr_ equals _value_."""
|
|
166
|
-
if value is not None and not is_undefined(value):
|
|
167
|
-
return [itm for itm in sequence if _getitem(itm, attr) == value]
|
|
168
|
-
|
|
169
|
-
return [itm for itm in sequence if _getitem(itm, attr) not in (False, None)]
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@sequence_filter
|
|
173
|
-
def reject(sequence: ArrayT, attr: object, value: object = None) -> list[object]:
|
|
174
|
-
"""Return a list of items from _sequence_ where _attr_ is not equal to _value_."""
|
|
175
|
-
if attr is None or is_undefined(attr):
|
|
176
|
-
return []
|
|
177
|
-
|
|
178
|
-
if value is not None and not is_undefined(value):
|
|
179
|
-
return [itm for itm in sequence if _getitem(itm, attr) != value]
|
|
180
|
-
|
|
181
|
-
return [itm for itm in sequence if _getitem(itm, attr) in (False, None)]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@sequence_filter
|
|
185
|
-
def find(sequence: ArrayT, attr: object, value: object = None) -> object:
|
|
186
|
-
"""Return the first item from _sequence_ where _attr_ is equal to _value_."""
|
|
187
|
-
if value is not None and not is_undefined(value):
|
|
188
|
-
return next((itm for itm in sequence if _getitem(itm, attr) == value), None)
|
|
189
|
-
|
|
190
|
-
return next(
|
|
191
|
-
(itm for itm in sequence if _getitem(itm, attr) not in (False, None)), None
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
@sequence_filter
|
|
196
|
-
def find_index(
|
|
197
|
-
sequence: ArrayT, attr: object, value: object = None
|
|
198
|
-
) -> Union[int, None]:
|
|
199
|
-
"""Return the index of first item from _sequence_ where _attr_ equals _value_."""
|
|
200
|
-
if value is not None and not is_undefined(value):
|
|
201
|
-
return next(
|
|
202
|
-
(i for i, itm in enumerate(sequence) if _getitem(itm, attr) == value), None
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
return next(
|
|
206
|
-
(
|
|
207
|
-
i
|
|
208
|
-
for i, itm in enumerate(sequence)
|
|
209
|
-
if _getitem(itm, attr) not in (False, None)
|
|
210
|
-
),
|
|
211
|
-
None,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
@sequence_filter
|
|
216
|
-
def has(sequence: ArrayT, attr: object, value: object = None) -> bool:
|
|
217
|
-
"""Return true if any items in _sequence_ have _attr_ equal to _value_."""
|
|
218
|
-
if value is not None and not is_undefined(value):
|
|
219
|
-
return any((itm for itm in sequence if _getitem(itm, attr) == value))
|
|
220
|
-
|
|
221
|
-
return any((itm for itm in sequence if _getitem(itm, attr) not in (False, None)))
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
@sequence_filter
|
|
225
|
-
def uniq(sequence: ArrayT, key: object = None) -> list[object]:
|
|
226
|
-
"""Return a copy of _sequence_ with duplicate elements removed."""
|
|
227
|
-
# Note that we're not using a dict or set for deduplication because we need
|
|
228
|
-
# to handle sequences containing unhashable objects, like dictionaries.
|
|
229
|
-
|
|
230
|
-
# This is probably quite slow.
|
|
231
|
-
if key is not None:
|
|
232
|
-
keys = []
|
|
233
|
-
result = []
|
|
234
|
-
for obj in sequence:
|
|
235
|
-
try:
|
|
236
|
-
item = obj[key]
|
|
237
|
-
except KeyError:
|
|
238
|
-
item = MISSING
|
|
239
|
-
except TypeError as err:
|
|
240
|
-
raise FilterArgumentError(
|
|
241
|
-
f"can't read property '{key}' of {obj}", token=None
|
|
242
|
-
) from err
|
|
243
|
-
|
|
244
|
-
if item not in keys:
|
|
245
|
-
keys.append(item)
|
|
246
|
-
result.append(obj)
|
|
247
|
-
|
|
248
|
-
return result
|
|
249
|
-
|
|
250
|
-
return [obj for i, obj in enumerate(sequence) if sequence.index(obj) == i]
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@sequence_filter
|
|
254
|
-
def compact(sequence: ArrayT, key: object = None) -> list[object]:
|
|
255
|
-
"""Return a copy of _sequence_ with any nil values removed."""
|
|
256
|
-
if key is not None:
|
|
257
|
-
try:
|
|
258
|
-
return [itm for itm in sequence if itm[key] is not None]
|
|
259
|
-
except TypeError as err:
|
|
260
|
-
raise FilterArgumentError(
|
|
261
|
-
f"can't read property '{key}'", token=None
|
|
262
|
-
) from err
|
|
263
|
-
return [itm for itm in sequence if itm is not None]
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
@sequence_filter
|
|
267
|
-
def sum_(sequence: ArrayT, key: object = None) -> Union[float, int, Decimal]:
|
|
268
|
-
"""Return the sum of all numeric elements in _sequence_.
|
|
269
|
-
|
|
270
|
-
If _key_ is given, it is assumed that sequence items are mapping-like,
|
|
271
|
-
and the values at _item[key]_ will be summed instead.
|
|
272
|
-
"""
|
|
273
|
-
if key is not None and not is_undefined(key):
|
|
274
|
-
rv = sum(decimal_arg(_getitem(elem, key, 0), 0) for elem in sequence)
|
|
275
|
-
else:
|
|
276
|
-
rv = sum(decimal_arg(elem, 0) for elem in sequence)
|
|
277
|
-
if isinstance(rv, Decimal):
|
|
278
|
-
return float(rv)
|
|
279
|
-
return rv
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def _str_if_not(val: object) -> str:
|
|
283
|
-
if not isinstance(val, str):
|
|
284
|
-
return str(val)
|
|
285
|
-
return val
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _getitem(sequence: Any, key: object, default: object = None) -> Any:
|
|
289
|
-
"""Helper for the map filter.
|
|
290
|
-
|
|
291
|
-
Same as sequence[key], but returns a default value if key does not exist
|
|
292
|
-
in sequence, and handles some corner cases so as to mimic Shopify/Liquid
|
|
293
|
-
behavior.
|
|
294
|
-
"""
|
|
295
|
-
try:
|
|
296
|
-
return getitem(sequence, key)
|
|
297
|
-
except (KeyError, IndexError):
|
|
298
|
-
return default
|
|
299
|
-
except TypeError as err:
|
|
300
|
-
if sequence is None:
|
|
301
|
-
raise FilterItemTypeError(str(err), token=None) from err
|
|
302
|
-
if isinstance(sequence, str) and isinstance(key, str) and key in sequence:
|
|
303
|
-
return key
|
|
304
|
-
if isinstance(sequence, int) and isinstance(key, int):
|
|
305
|
-
return sequence == key
|
|
306
|
-
if not hasattr(sequence, "__getitem__"):
|
|
307
|
-
raise
|
|
308
|
-
return default
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _lower(obj: Any) -> str:
|
|
312
|
-
"""Helper for the sort filter."""
|
|
313
|
-
try:
|
|
314
|
-
return str(obj).lower()
|
|
315
|
-
except AttributeError:
|
|
316
|
-
return ""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
class _Null:
|
|
320
|
-
"""A null without a token for use in the map filter."""
|
|
321
|
-
|
|
322
|
-
def __eq__(self, other: object) -> bool:
|
|
323
|
-
return other is None or isinstance(other, (_Null, Nil))
|
|
324
|
-
|
|
325
|
-
def __str__(self) -> str: # pragma: no cover
|
|
326
|
-
return ""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
_NULL = _Null()
|
liquid/builtin/filters/extra.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""Filters that don't exist in the reference implementation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
from liquid import Markup
|
|
9
|
-
from liquid.filter import string_filter
|
|
10
|
-
from liquid.filter import with_environment
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from liquid import Environment
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@with_environment
|
|
17
|
-
@string_filter
|
|
18
|
-
def safe(val: str, *, environment: Environment) -> str:
|
|
19
|
-
"""Return a copy of _val_ that will not be automatically HTML escaped on output."""
|
|
20
|
-
if environment.autoescape:
|
|
21
|
-
return Markup(val)
|
|
22
|
-
return val
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# `escapejs` is inspired by https://github.com/salesforce/secure-filters and Django's
|
|
26
|
-
# escapejs filter, https://github.com/django/django/blob/485f483d49144a2ea5401442bc3b937a370b3ca6/django/utils/html.py#L63
|
|
27
|
-
|
|
28
|
-
_ESCAPE_MAP = {
|
|
29
|
-
"\\": "\\u005C",
|
|
30
|
-
"'": "\\u0027",
|
|
31
|
-
'"': "\\u0022",
|
|
32
|
-
">": "\\u003E",
|
|
33
|
-
"<": "\\u003C",
|
|
34
|
-
"&": "\\u0026",
|
|
35
|
-
"=": "\\u003D",
|
|
36
|
-
"-": "\\u002D",
|
|
37
|
-
";": "\\u003B",
|
|
38
|
-
"`": "\\u0060",
|
|
39
|
-
"\u2028": "\\u2028",
|
|
40
|
-
"\u2029": "\\u2029",
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
_ESCAPE_MAP.update({chr(c): f"\\u{c:04X}" for c in range(32)})
|
|
44
|
-
_ESCAPE_RE = re.compile("[" + re.escape("".join(_ESCAPE_MAP.keys())) + "]")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@with_environment
|
|
48
|
-
@string_filter
|
|
49
|
-
def escapejs(val: str, *, environment: Environment) -> str:
|
|
50
|
-
"""Escape characters for safe use in JavaScript string literals.
|
|
51
|
-
|
|
52
|
-
This filter escapes a string for embedding inside **JavaScript string
|
|
53
|
-
literals**, using either single or double quotes (e.g. `'...'` or `"..."`).
|
|
54
|
-
It replaces control characters and potentially dangerous symbols with
|
|
55
|
-
their corresponding Unicode escape sequences.
|
|
56
|
-
|
|
57
|
-
**Important:** This filter does **not** make strings safe for use in
|
|
58
|
-
JavaScript template literals (backtick strings), or in raw JavaScript
|
|
59
|
-
expressions. Use it only when placing data inside quoted JS strings
|
|
60
|
-
within inline `<script>` blocks or event handlers.
|
|
61
|
-
|
|
62
|
-
**Recommended alternatives:**
|
|
63
|
-
- Pass data using HTML `data-*` attributes and read them in JS via
|
|
64
|
-
`element.dataset`.
|
|
65
|
-
- For structured data, prefer a JSON-serialization approach using the
|
|
66
|
-
JSON filter.
|
|
67
|
-
|
|
68
|
-
Escaped characters include:
|
|
69
|
-
- ASCII control characters (U+0000 to U+001F)
|
|
70
|
-
- Characters like quotes, angle brackets, ampersands, equals signs
|
|
71
|
-
- Line/paragraph separators (U+2028, U+2029)
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
val: The input string to escape.
|
|
75
|
-
environment: The active Liquid environment
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
A JavaScript-safe string, with problematic characters escaped as Unicode.
|
|
79
|
-
"""
|
|
80
|
-
escaped = _ESCAPE_RE.sub(lambda m: _ESCAPE_MAP[m.group()], val)
|
|
81
|
-
if environment.autoescape:
|
|
82
|
-
return Markup(escaped)
|
|
83
|
-
return escaped
|
liquid/builtin/filters/math.py
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"""Maths related filter functions."""
|
|
2
|
-
|
|
3
|
-
import decimal
|
|
4
|
-
import math
|
|
5
|
-
from typing import Optional
|
|
6
|
-
from typing import Union
|
|
7
|
-
|
|
8
|
-
from liquid.exceptions import FilterArgumentError
|
|
9
|
-
from liquid.filter import math_filter
|
|
10
|
-
from liquid.filter import num_arg
|
|
11
|
-
from liquid.undefined import is_undefined
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@math_filter
|
|
15
|
-
def abs_(num: Union[float, int]) -> Union[float, int]:
|
|
16
|
-
"""Return the absolute value of number _num_."""
|
|
17
|
-
return abs(num)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@math_filter
|
|
21
|
-
def at_most(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
22
|
-
"""Return _val_ or _other_, whichever is smaller."""
|
|
23
|
-
other = num_arg(other, default=0)
|
|
24
|
-
return min(num, other)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@math_filter
|
|
28
|
-
def at_least(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
29
|
-
"""Return _val_ or _other_, whichever is greater."""
|
|
30
|
-
other = num_arg(other, default=0)
|
|
31
|
-
return max(num, other)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@math_filter
|
|
35
|
-
def ceil(num: Union[float, int]) -> Union[float, int]:
|
|
36
|
-
"""Return _num_ rounded up to the next integer."""
|
|
37
|
-
return math.ceil(num)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@math_filter
|
|
41
|
-
def divided_by(num: Union[float, int], other: object) -> Union[float, int]:
|
|
42
|
-
"""Return the result of dividing _num_ by _other_.
|
|
43
|
-
|
|
44
|
-
If both _num_ and _other_ are integers, integer division is performed.
|
|
45
|
-
"""
|
|
46
|
-
other = num_arg(other, default=0)
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
if isinstance(other, int) and isinstance(num, int):
|
|
50
|
-
return num // other
|
|
51
|
-
return num / other
|
|
52
|
-
except ZeroDivisionError as err:
|
|
53
|
-
raise FilterArgumentError(
|
|
54
|
-
f"divided_by: can't divide by {other}", token=None
|
|
55
|
-
) from err
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@math_filter
|
|
59
|
-
def floor(num: Union[float, int]) -> Union[float, int]:
|
|
60
|
-
"""Return _num_ rounded down to the next integer."""
|
|
61
|
-
return math.floor(num)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@math_filter
|
|
65
|
-
def minus(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
66
|
-
"""Return the result of subtracting _other_ from _num_."""
|
|
67
|
-
other = num_arg(other, default=0)
|
|
68
|
-
|
|
69
|
-
if isinstance(num, int) and isinstance(other, int):
|
|
70
|
-
return num - other
|
|
71
|
-
return float(decimal.Decimal(str(num)) - decimal.Decimal(str(other)))
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@math_filter
|
|
75
|
-
def plus(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
76
|
-
"""Return the result of adding _other_ to _num_."""
|
|
77
|
-
other = num_arg(other, default=0)
|
|
78
|
-
|
|
79
|
-
if isinstance(num, int) and isinstance(other, int):
|
|
80
|
-
return num + other
|
|
81
|
-
return float(decimal.Decimal(str(num)) + decimal.Decimal(str(other)))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@math_filter
|
|
85
|
-
def round_(num: Union[float, int], ndigits: Optional[int] = None) -> Union[float, int]:
|
|
86
|
-
"""Returns the result of rounding _num_ to _ndigits_ decimal digits."""
|
|
87
|
-
if ndigits is None or is_undefined(ndigits):
|
|
88
|
-
return round(num)
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
_ndigits = num_arg(ndigits)
|
|
92
|
-
except FilterArgumentError:
|
|
93
|
-
# Probably a string that can't be case to an int or float
|
|
94
|
-
return round(num)
|
|
95
|
-
|
|
96
|
-
if isinstance(_ndigits, float):
|
|
97
|
-
_ndigits = int(_ndigits)
|
|
98
|
-
|
|
99
|
-
if _ndigits < 0:
|
|
100
|
-
return 0
|
|
101
|
-
if _ndigits == 0:
|
|
102
|
-
return round(num)
|
|
103
|
-
|
|
104
|
-
return round(num, _ndigits)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@math_filter
|
|
108
|
-
def times(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
109
|
-
"""Return the result of multiplying _num_ by _other_."""
|
|
110
|
-
other = num_arg(other, default=0)
|
|
111
|
-
|
|
112
|
-
if isinstance(num, int) and isinstance(other, int):
|
|
113
|
-
return num * other
|
|
114
|
-
return float(decimal.Decimal(str(num)) * decimal.Decimal(str(other)))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@math_filter
|
|
118
|
-
def modulo(num: Union[float, int], other: Union[float, int]) -> Union[float, int]:
|
|
119
|
-
"""Return the remainder of dividing _num_ by _other_."""
|
|
120
|
-
other = num_arg(other, default=0)
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
if isinstance(num, int) and isinstance(other, int):
|
|
124
|
-
return num % other
|
|
125
|
-
return float(decimal.Decimal(str(num)) % decimal.Decimal(str(other)))
|
|
126
|
-
except ZeroDivisionError as err:
|
|
127
|
-
raise FilterArgumentError(
|
|
128
|
-
f"modulo: can't divide by {other}", token=None
|
|
129
|
-
) from err
|
liquid/builtin/filters/misc.py
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"""Miscellaneous filter functions."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import datetime as dt
|
|
6
|
-
import decimal
|
|
7
|
-
import functools
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
-
from typing import Any
|
|
10
|
-
from typing import Union
|
|
11
|
-
|
|
12
|
-
from dateutil import parser
|
|
13
|
-
|
|
14
|
-
from liquid import Markup
|
|
15
|
-
from liquid.builtin.expressions import is_empty
|
|
16
|
-
from liquid.exceptions import FilterArgumentError
|
|
17
|
-
from liquid.filter import liquid_filter
|
|
18
|
-
from liquid.filter import with_environment
|
|
19
|
-
from liquid.undefined import is_undefined
|
|
20
|
-
|
|
21
|
-
if TYPE_CHECKING:
|
|
22
|
-
from liquid import Environment
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@liquid_filter
|
|
26
|
-
def size(obj: Any) -> int:
|
|
27
|
-
"""Return the length of _obj_.
|
|
28
|
-
|
|
29
|
-
_obj_ could be a dict, list, string or any class implementing _len_.
|
|
30
|
-
"""
|
|
31
|
-
try:
|
|
32
|
-
return len(obj)
|
|
33
|
-
except TypeError:
|
|
34
|
-
return 0
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@liquid_filter
|
|
38
|
-
def default(obj: Any, default_: object = "", *, allow_false: bool = False) -> Any:
|
|
39
|
-
"""Return _obj_, or _default_ if _obj_ is nil, false, or empty."""
|
|
40
|
-
_obj = obj
|
|
41
|
-
|
|
42
|
-
# Return the default value immediately if the object defines a
|
|
43
|
-
# `force_liquid_default` property.
|
|
44
|
-
if hasattr(obj, "force_liquid_default") and obj.force_liquid_default:
|
|
45
|
-
return default_
|
|
46
|
-
|
|
47
|
-
if hasattr(obj, "__liquid__"):
|
|
48
|
-
_obj = obj.__liquid__()
|
|
49
|
-
|
|
50
|
-
# Liquid 0, 0.0, 0b0, 0X0, 0o0 and Decimal("0") are not falsy.
|
|
51
|
-
if not isinstance(obj, bool) and isinstance(obj, (int, float, decimal.Decimal)):
|
|
52
|
-
return obj
|
|
53
|
-
|
|
54
|
-
if allow_false is True and _obj is False:
|
|
55
|
-
return obj
|
|
56
|
-
|
|
57
|
-
if _obj in (None, False) or is_empty(_obj):
|
|
58
|
-
return default_
|
|
59
|
-
|
|
60
|
-
return obj
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@with_environment
|
|
64
|
-
@liquid_filter
|
|
65
|
-
def date(
|
|
66
|
-
dat: Union[dt.datetime, str, int],
|
|
67
|
-
fmt: str,
|
|
68
|
-
*,
|
|
69
|
-
environment: Environment,
|
|
70
|
-
) -> str:
|
|
71
|
-
"""Return a string representation of _dat_ using format string _fmt_."""
|
|
72
|
-
if is_undefined(dat):
|
|
73
|
-
return ""
|
|
74
|
-
|
|
75
|
-
if is_undefined(fmt):
|
|
76
|
-
return str(dat)
|
|
77
|
-
|
|
78
|
-
date_ = dt.datetime.now() if dat in ("now", "today") else _to_date(dat)
|
|
79
|
-
|
|
80
|
-
if date_ is None:
|
|
81
|
-
if isinstance(dat, (str, int)):
|
|
82
|
-
# Input is returned unchanged. This is consistent
|
|
83
|
-
# with the reference implementation.
|
|
84
|
-
return str(dat)
|
|
85
|
-
raise FilterArgumentError(
|
|
86
|
-
f"date expected datetime.datetime, found {type(dat).__name__}",
|
|
87
|
-
token=None,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
rv = date_.strftime(fmt)
|
|
92
|
-
except ValueError as err:
|
|
93
|
-
# This is not uncommon on Windows when a format string contains
|
|
94
|
-
# directives that are not officially supported by Python.
|
|
95
|
-
|
|
96
|
-
# Handle "%s" as a special case.
|
|
97
|
-
if fmt == r"%s":
|
|
98
|
-
return str(date_.timestamp()).split(".")[0]
|
|
99
|
-
raise FilterArgumentError(str(err), token=None) from err
|
|
100
|
-
|
|
101
|
-
if environment.autoescape and isinstance(fmt, Markup):
|
|
102
|
-
return Markup(rv)
|
|
103
|
-
return rv
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@functools.lru_cache(maxsize=10)
|
|
107
|
-
def _to_date( # noqa: PLR0911
|
|
108
|
-
d: object,
|
|
109
|
-
) -> Union[dt.datetime, None]:
|
|
110
|
-
if isinstance(d, dt.datetime):
|
|
111
|
-
return d
|
|
112
|
-
|
|
113
|
-
if isinstance(d, dt.date):
|
|
114
|
-
return dt.datetime.combine(d, dt.time.min)
|
|
115
|
-
|
|
116
|
-
if isinstance(d, str):
|
|
117
|
-
if d.isdigit():
|
|
118
|
-
# The reference implementation does not support string
|
|
119
|
-
# representations of negative integers either.
|
|
120
|
-
return dt.datetime.fromtimestamp(int(d))
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
return parser.parse(d)
|
|
124
|
-
except parser.ParserError:
|
|
125
|
-
# Input is returned unchanged. This is consistent
|
|
126
|
-
# with the reference implementation.
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
if isinstance(d, int):
|
|
130
|
-
try:
|
|
131
|
-
return dt.datetime.fromtimestamp(d)
|
|
132
|
-
except (OverflowError, OSError):
|
|
133
|
-
# Testing on Windows shows that it can't handle some
|
|
134
|
-
# negative integers.
|
|
135
|
-
return None
|
|
136
|
-
|
|
137
|
-
return None
|