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.
Files changed (212) hide show
  1. liquid/__about__.py +1 -0
  2. liquid/__init__.py +161 -187
  3. liquid/_base_tag.py +66 -0
  4. liquid/{utils/chain_map.py → _chain_map.py} +18 -19
  5. liquid/_context.py +465 -0
  6. liquid/_drop.py +108 -0
  7. liquid/_filter.py +174 -0
  8. liquid/_io.py +18 -0
  9. liquid/_lax_parser_mixin.py +100 -0
  10. liquid/_lexer.py +93 -0
  11. liquid/_lexer_extended.py +623 -0
  12. liquid/_lexer_legacy.py +536 -0
  13. liquid/_loader.py +84 -0
  14. liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
  15. liquid/_markup.py +253 -0
  16. liquid/_messages.py +292 -0
  17. liquid/_missing.py +3 -0
  18. liquid/_nothing.py +28 -0
  19. liquid/_parser.py +460 -0
  20. liquid/_parser_extended.py +293 -0
  21. liquid/_parser_legacy.py +819 -0
  22. liquid/_resource_limits.py +50 -0
  23. liquid/_slice.py +41 -0
  24. liquid/_static_analysis.py +503 -0
  25. liquid/_strftime.py +186 -0
  26. liquid/_template.py +536 -0
  27. liquid/drops/__init__.py +20 -0
  28. liquid/drops/forloop.py +78 -0
  29. liquid/drops/iterable.py +25 -0
  30. liquid/drops/range.py +76 -0
  31. liquid/drops/self_.py +45 -0
  32. liquid/drops/tablerowloop.py +142 -0
  33. liquid/drops/undefined.py +228 -0
  34. liquid/environment.py +717 -551
  35. liquid/exceptions.py +67 -214
  36. liquid/expressions.py +1184 -0
  37. liquid/filters/__init__.py +134 -0
  38. liquid/filters/abs.py +7 -0
  39. liquid/filters/append.py +5 -0
  40. liquid/filters/at_least.py +9 -0
  41. liquid/filters/at_most.py +9 -0
  42. liquid/filters/babel.py +560 -0
  43. liquid/filters/base64.py +26 -0
  44. liquid/filters/capitalize.py +5 -0
  45. liquid/filters/ceil.py +8 -0
  46. liquid/filters/compact.py +25 -0
  47. liquid/filters/concat.py +11 -0
  48. liquid/filters/date.py +27 -0
  49. liquid/filters/default.py +25 -0
  50. liquid/filters/divided_by.py +21 -0
  51. liquid/filters/downcase.py +5 -0
  52. liquid/filters/escape.py +56 -0
  53. liquid/filters/find.py +56 -0
  54. liquid/filters/first.py +19 -0
  55. liquid/filters/floor.py +8 -0
  56. liquid/filters/html.py +23 -0
  57. liquid/filters/index.py +12 -0
  58. liquid/filters/join.py +13 -0
  59. liquid/filters/json_.py +30 -0
  60. liquid/filters/last.py +17 -0
  61. liquid/filters/map.py +5 -0
  62. liquid/filters/minus.py +13 -0
  63. liquid/filters/modulo.py +18 -0
  64. liquid/filters/newline_to_br.py +16 -0
  65. liquid/filters/plus.py +13 -0
  66. liquid/filters/prepend.py +5 -0
  67. liquid/filters/reject.py +19 -0
  68. liquid/filters/remove.py +36 -0
  69. liquid/filters/replace.py +32 -0
  70. liquid/filters/reverse.py +5 -0
  71. liquid/filters/round.py +35 -0
  72. liquid/filters/size.py +25 -0
  73. liquid/filters/slice.py +39 -0
  74. liquid/filters/sort.py +148 -0
  75. liquid/filters/split.py +34 -0
  76. liquid/filters/squish.py +9 -0
  77. liquid/filters/strip.py +13 -0
  78. liquid/filters/strip_html.py +19 -0
  79. liquid/filters/strip_newlines.py +17 -0
  80. liquid/filters/sum.py +21 -0
  81. liquid/filters/times.py +13 -0
  82. liquid/filters/translate.py +380 -0
  83. liquid/filters/truncate.py +39 -0
  84. liquid/filters/uniq.py +11 -0
  85. liquid/filters/upcase.py +5 -0
  86. liquid/filters/url.py +18 -0
  87. liquid/filters/where.py +18 -0
  88. liquid/loaders/__init__.py +17 -0
  89. liquid/loaders/caching_file_system_loader.py +64 -0
  90. liquid/loaders/choice_loader.py +93 -0
  91. liquid/loaders/dict_loader.py +56 -0
  92. liquid/{builtin/loaders → loaders}/file_system_loader.py +51 -55
  93. liquid/{builtin/loaders → loaders}/mixins.py +39 -51
  94. liquid/loaders/package_loader.py +110 -0
  95. liquid/tags/__init__.py +61 -0
  96. liquid/tags/assign.py +68 -0
  97. liquid/tags/capture.py +83 -0
  98. liquid/tags/case.py +199 -0
  99. liquid/tags/comment.py +56 -0
  100. liquid/tags/cycle.py +131 -0
  101. liquid/tags/decrement.py +55 -0
  102. liquid/tags/doc.py +56 -0
  103. liquid/tags/echo.py +65 -0
  104. liquid/tags/else_.py +53 -0
  105. liquid/tags/error.py +51 -0
  106. liquid/tags/extends.py +448 -0
  107. liquid/tags/for_.py +421 -0
  108. liquid/tags/if_.py +156 -0
  109. liquid/tags/ifchanged.py +69 -0
  110. liquid/tags/include.py +223 -0
  111. liquid/tags/increment.py +55 -0
  112. liquid/tags/inline_comment.py +70 -0
  113. liquid/tags/liquid.py +56 -0
  114. liquid/tags/macro.py +281 -0
  115. liquid/tags/output.py +58 -0
  116. liquid/tags/raw.py +60 -0
  117. liquid/tags/render.py +245 -0
  118. liquid/tags/tablerow.py +254 -0
  119. liquid/tags/template_comment.py +56 -0
  120. liquid/tags/translate.py +316 -0
  121. liquid/tags/unless.py +121 -0
  122. liquid/tags/with_.py +83 -0
  123. liquid/tokens.py +370 -0
  124. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
  125. python_liquid-3.0.0.dist-info/RECORD +128 -0
  126. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
  127. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
  128. liquid/analyze_tags.py +0 -214
  129. liquid/ast.py +0 -252
  130. liquid/builtin/__init__.py +0 -203
  131. liquid/builtin/content.py +0 -41
  132. liquid/builtin/drops/__init__.py +0 -3
  133. liquid/builtin/drops/drops.py +0 -13
  134. liquid/builtin/expressions/__init__.py +0 -47
  135. liquid/builtin/expressions/_tokenize.py +0 -186
  136. liquid/builtin/expressions/arguments.py +0 -207
  137. liquid/builtin/expressions/filtered.py +0 -387
  138. liquid/builtin/expressions/logical.py +0 -672
  139. liquid/builtin/expressions/loop.py +0 -275
  140. liquid/builtin/expressions/path.py +0 -163
  141. liquid/builtin/expressions/primitive.py +0 -417
  142. liquid/builtin/filters/__init__.py +0 -0
  143. liquid/builtin/filters/array.py +0 -329
  144. liquid/builtin/filters/extra.py +0 -83
  145. liquid/builtin/filters/math.py +0 -129
  146. liquid/builtin/filters/misc.py +0 -137
  147. liquid/builtin/filters/string.py +0 -416
  148. liquid/builtin/illegal.py +0 -28
  149. liquid/builtin/loaders/__init__.py +0 -0
  150. liquid/builtin/loaders/caching_file_system_loader.py +0 -61
  151. liquid/builtin/loaders/choice_loader.py +0 -94
  152. liquid/builtin/loaders/dict_loader.py +0 -64
  153. liquid/builtin/loaders/package_loader.py +0 -132
  154. liquid/builtin/output.py +0 -74
  155. liquid/builtin/tags/__init__.py +0 -0
  156. liquid/builtin/tags/assign_tag.py +0 -75
  157. liquid/builtin/tags/capture_tag.py +0 -98
  158. liquid/builtin/tags/case_tag.py +0 -287
  159. liquid/builtin/tags/comment_tag.py +0 -77
  160. liquid/builtin/tags/cycle_tag.py +0 -132
  161. liquid/builtin/tags/decrement_tag.py +0 -59
  162. liquid/builtin/tags/doc_tag.py +0 -68
  163. liquid/builtin/tags/echo_tag.py +0 -45
  164. liquid/builtin/tags/for_tag.py +0 -337
  165. liquid/builtin/tags/if_tag.py +0 -209
  166. liquid/builtin/tags/ifchanged_tag.py +0 -94
  167. liquid/builtin/tags/include_tag.py +0 -251
  168. liquid/builtin/tags/increment_tag.py +0 -60
  169. liquid/builtin/tags/inline_comment_tag.py +0 -47
  170. liquid/builtin/tags/liquid_tag.py +0 -183
  171. liquid/builtin/tags/render_tag.py +0 -371
  172. liquid/builtin/tags/tablerow_tag.py +0 -320
  173. liquid/builtin/tags/unless_tag.py +0 -218
  174. liquid/context.py +0 -615
  175. liquid/expression.py +0 -41
  176. liquid/extra/__init__.py +0 -98
  177. liquid/extra/filters/__init__.py +0 -33
  178. liquid/extra/filters/_json.py +0 -33
  179. liquid/extra/filters/array.py +0 -60
  180. liquid/extra/filters/babel.py +0 -552
  181. liquid/extra/filters/html.py +0 -32
  182. liquid/extra/filters/translate.py +0 -448
  183. liquid/extra/tags/__init__.py +0 -17
  184. liquid/extra/tags/_with.py +0 -80
  185. liquid/extra/tags/extends_tag.py +0 -576
  186. liquid/extra/tags/macro_tag.py +0 -270
  187. liquid/extra/tags/snippet_tag.py +0 -107
  188. liquid/extra/tags/translate_tag.py +0 -388
  189. liquid/filter.py +0 -230
  190. liquid/future/__init__.py +0 -3
  191. liquid/future/environment.py +0 -30
  192. liquid/future/filters/__init__.py +0 -1
  193. liquid/future/tags/__init__.py +0 -1
  194. liquid/lex.py +0 -260
  195. liquid/limits.py +0 -66
  196. liquid/loader.py +0 -141
  197. liquid/messages.py +0 -357
  198. liquid/mode.py +0 -12
  199. liquid/output.py +0 -35
  200. liquid/parser.py +0 -119
  201. liquid/span.py +0 -35
  202. liquid/static_analysis.py +0 -443
  203. liquid/stream.py +0 -148
  204. liquid/stringify.py +0 -31
  205. liquid/tag.py +0 -47
  206. liquid/template.py +0 -658
  207. liquid/token.py +0 -145
  208. liquid/undefined.py +0 -218
  209. liquid/utils/__init__.py +0 -15
  210. liquid/utils/html.py +0 -57
  211. liquid/utils/text.py +0 -28
  212. python_liquid-2.3.6.dist-info/RECORD +0 -96
@@ -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()
@@ -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
@@ -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
@@ -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