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
@@ -0,0 +1,21 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def divided_by(
7
+ context: FilterContext, left: object, right: object
8
+ ) -> int | float | Decimal:
9
+ left = context.to_numeric(left, 0)
10
+ right = context.to_numeric(right, 0)
11
+
12
+ if right == 0:
13
+ raise context.error("divide by zero")
14
+
15
+ if isinstance(left, int) and isinstance(right, int):
16
+ return left // right
17
+
18
+ if isinstance(left, Decimal) or isinstance(right, Decimal):
19
+ return float(Decimal(str(left)) / Decimal(str(right)))
20
+
21
+ return left / right
@@ -0,0 +1,5 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def downcase(context: FilterContext, left: object) -> str:
5
+ return context.to_str(left, "").lower()
@@ -0,0 +1,56 @@
1
+ import html
2
+ import re
3
+
4
+ import markupsafe
5
+
6
+ from .._filter import FilterContext
7
+
8
+
9
+ def escape(context: FilterContext, left: object) -> str:
10
+ left_ = context.to_str(left, "")
11
+ return (
12
+ markupsafe.escape(str(left_))
13
+ if context.env.autoescape
14
+ else html.escape(left_).replace("'", "'")
15
+ )
16
+
17
+
18
+ def escape_once(context: FilterContext, left: object) -> str:
19
+ left_ = context.to_str(left, "")
20
+ return (
21
+ markupsafe.Markup(left_).unescape()
22
+ if context.env.autoescape
23
+ else html.escape(html.unescape(left_)).replace("'", "'")
24
+ )
25
+
26
+
27
+ _ESCAPE_MAP = {
28
+ "\\": "\\u005C",
29
+ "'": "\\u0027",
30
+ '"': "\\u0022",
31
+ ">": "\\u003E",
32
+ "<": "\\u003C",
33
+ "&": "\\u0026",
34
+ "=": "\\u003D",
35
+ "-": "\\u002D",
36
+ ";": "\\u003B",
37
+ "`": "\\u0060",
38
+ "\u2028": "\\u2028",
39
+ "\u2029": "\\u2029",
40
+ }
41
+
42
+ _ESCAPE_MAP.update({chr(c): f"\\u{c:04X}" for c in range(32)})
43
+ _ESCAPE_RE = re.compile("[" + re.escape("".join(_ESCAPE_MAP.keys())) + "]")
44
+
45
+
46
+ def escapejs(context: FilterContext, left: object) -> str:
47
+ escaped = _ESCAPE_RE.sub(lambda m: _ESCAPE_MAP[m.group()], context.to_str(left, ""))
48
+ if context.env.autoescape:
49
+ return markupsafe.Markup(escaped)
50
+ return escaped
51
+
52
+
53
+ def safe(context: FilterContext, left: object) -> str:
54
+ return (
55
+ markupsafe.Markup(left) if context.env.autoescape else context.to_str(left, "")
56
+ )
liquid/filters/find.py ADDED
@@ -0,0 +1,56 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def find(
5
+ context: FilterContext, left: object, prop: object, value: object = None
6
+ ) -> object:
7
+ left = context.input_array(left)
8
+
9
+ if context.is_nil(value):
10
+ for obj in left:
11
+ val = context.get_item(obj, prop)
12
+ if not context.is_nil(val) and val is not False:
13
+ return obj
14
+ return None
15
+
16
+ for obj in left:
17
+ if context.get_item(obj, prop) == value:
18
+ return obj
19
+
20
+ return None
21
+
22
+
23
+ def find_index(
24
+ context: FilterContext, left: object, prop: object, value: object = None
25
+ ) -> int | None:
26
+ left = context.input_array(left)
27
+
28
+ if context.is_nil(value):
29
+ for i, obj in enumerate(left):
30
+ val = context.get_item(obj, prop)
31
+ if not context.is_nil(val) and val is not False:
32
+ return i
33
+
34
+ return None
35
+
36
+ for i, obj in enumerate(left):
37
+ if context.get_item(obj, prop) == value:
38
+ return i
39
+
40
+ return None
41
+
42
+
43
+ def has(
44
+ context: FilterContext, left: object, prop: object, value: object = None
45
+ ) -> bool:
46
+ left = context.input_array(left)
47
+
48
+ if context.is_nil(value):
49
+ for obj in left:
50
+ val = context.get_item(obj, prop)
51
+ if not context.is_nil(val) and val is not False:
52
+ return True
53
+
54
+ return False
55
+
56
+ return any(context.get_item(obj, prop) == value for obj in left)
@@ -0,0 +1,19 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from itertools import islice
3
+
4
+ from .._drop import liquid_iterable
5
+ from .._filter import FilterContext
6
+
7
+
8
+ def first(context: FilterContext, left: object) -> object:
9
+ if liquid_iterable(left):
10
+ it = left.__liquid_iter__(0, None, False)
11
+ return next(iter(it), None)
12
+
13
+ if isinstance(left, Sequence):
14
+ return left[0] if left else None # pyright: ignore[reportUnknownVariableType]
15
+
16
+ if isinstance(left, Mapping):
17
+ return next(islice(left.items(), 1), None) # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType]
18
+
19
+ return None
@@ -0,0 +1,8 @@
1
+ import math
2
+ from decimal import Decimal
3
+
4
+ from .._filter import FilterContext
5
+
6
+
7
+ def floor(context: FilterContext, left: object) -> int | float | Decimal:
8
+ return math.floor(context.to_numeric(left, 0))
liquid/filters/html.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ from typing import TYPE_CHECKING
5
+
6
+ import markupsafe
7
+
8
+ if TYPE_CHECKING:
9
+ from .._filter import FilterContext
10
+
11
+
12
+ def stylesheet_tag(context: FilterContext, left: object) -> str:
13
+ tag = '<link href="{}" rel="stylesheet" type="text/css" media="all" />'
14
+ if context.env.autoescape:
15
+ return markupsafe.Markup(tag).format(str(left))
16
+ return tag.format(html.escape(context.to_str(left, "")))
17
+
18
+
19
+ def script_tag(context: FilterContext, left: object) -> str:
20
+ tag = '<script src="{}" type="text/javascript"></script>'
21
+ if context.env.autoescape:
22
+ return markupsafe.Markup(tag).format(str(left))
23
+ return tag.format(html.escape(context.to_str(left, "")))
@@ -0,0 +1,12 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def index(context: FilterContext, left: object, obj: object) -> object:
5
+ """Return the zero-based index of an array item.
6
+
7
+ `None` is returned if `obj` is not in `left`.
8
+ """
9
+ try:
10
+ return context.input_array(left).index(obj)
11
+ except ValueError:
12
+ return None
liquid/filters/join.py ADDED
@@ -0,0 +1,13 @@
1
+ import markupsafe
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def join(context: FilterContext, left: object, sep: object = " ") -> str:
7
+ left_ = context.input_array(left)
8
+ sep_ = context.to_str(sep, " ")
9
+
10
+ if context.env.autoescape and sep_ == " ":
11
+ sep_ = markupsafe.Markup(sep_)
12
+
13
+ return sep_.join(context.to_str(obj, "") for obj in left_)
@@ -0,0 +1,30 @@
1
+ import json
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+ from .._filter import FilterContext
6
+
7
+
8
+ class JSON:
9
+ """Serialize an object to a JSON formatted string.
10
+
11
+ Parameters
12
+ ----------
13
+ default
14
+ A function passed to ``json.dumps``. This function is called in the event that the
15
+ JSONEncoder does not know how to serialize an object. Defaults to `None`.
16
+ """
17
+
18
+ name = "json"
19
+
20
+ def __init__(self, default: Callable[[Any], Any] | None = None):
21
+ self.default = default
22
+
23
+ def __call__(
24
+ self,
25
+ context: FilterContext,
26
+ left: object,
27
+ indent: object | None = None,
28
+ ) -> str:
29
+ indent = context.to_int(indent) if indent else None
30
+ return json.dumps(left, default=self.default, indent=indent)
liquid/filters/last.py ADDED
@@ -0,0 +1,17 @@
1
+ from collections.abc import Sequence
2
+
3
+ from .._filter import FilterContext
4
+ from ..drops import Range, Undefined
5
+
6
+
7
+ def last(context: FilterContext, left: object) -> object:
8
+ if isinstance(left, Range):
9
+ return left.stop
10
+
11
+ if isinstance(left, Sequence):
12
+ return left[-1] if left else None # pyright: ignore[reportUnknownVariableType]
13
+
14
+ if isinstance(left, Undefined):
15
+ left.poke()
16
+
17
+ return None
liquid/filters/map.py ADDED
@@ -0,0 +1,5 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def map_(context: FilterContext, left: object, prop: object) -> list[object]:
5
+ return [context.get_item(obj, prop) for obj in context.input_array(left)]
@@ -0,0 +1,13 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def minus(context: FilterContext, left: object, right: object) -> int | float:
7
+ left = context.to_numeric(left, 0)
8
+ right = context.to_numeric(right, 0)
9
+
10
+ if isinstance(left, int) and isinstance(right, int):
11
+ return left - right
12
+
13
+ return float(Decimal(str(left)) - Decimal(str(right)))
@@ -0,0 +1,18 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def modulo(
7
+ context: FilterContext, left: object, right: object
8
+ ) -> int | float | Decimal:
9
+ left = context.to_numeric(left, 0)
10
+ right = context.to_numeric(right, 0)
11
+
12
+ if right == 0:
13
+ raise context.error("divide by zero")
14
+
15
+ if isinstance(left, int) and isinstance(right, int):
16
+ return left % right
17
+
18
+ return float(Decimal(str(left)) % Decimal(str(right)))
@@ -0,0 +1,16 @@
1
+ import re
2
+
3
+ import markupsafe
4
+
5
+ from .._filter import FilterContext
6
+
7
+ RE_LINETERM = re.compile(r"\r?\n")
8
+
9
+
10
+ def newline_to_br(context: FilterContext, left: object) -> str:
11
+ if context.env.autoescape:
12
+ return markupsafe.Markup(
13
+ RE_LINETERM.sub("<br />\n", markupsafe.escape(context.to_str(left, "")))
14
+ )
15
+
16
+ return RE_LINETERM.sub("<br />\n", context.to_str(left, ""))
liquid/filters/plus.py ADDED
@@ -0,0 +1,13 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def plus(context: FilterContext, left: object, right: object) -> int | float:
7
+ left = context.to_numeric(left, 0)
8
+ right = context.to_numeric(right, 0)
9
+
10
+ if isinstance(left, int) and isinstance(right, int):
11
+ return left + right
12
+
13
+ return float(Decimal(str(left)) + Decimal(str(right)))
@@ -0,0 +1,5 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def prepend(context: FilterContext, left: object, right: object) -> str:
5
+ return context.to_str(right, "") + context.to_str(left, "")
@@ -0,0 +1,19 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def reject(
5
+ context: FilterContext, left: object, prop: object, value: object = None
6
+ ) -> list[object]:
7
+ left = context.input_array(left)
8
+
9
+ if context.is_nil(prop):
10
+ return []
11
+
12
+ def _filter(obj: object) -> bool:
13
+ val = context.get_item(obj, prop)
14
+ return not context.truthy(val)
15
+
16
+ if context.is_nil(value):
17
+ return list(filter(_filter, left))
18
+
19
+ return [obj for obj in left if context.get_item(obj, prop) != value]
@@ -0,0 +1,36 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def remove(context: FilterContext, left: object, right: object) -> str:
5
+ left_ = context.to_str(left, "")
6
+ right_ = context.to_str(right, "")
7
+
8
+ if not right_:
9
+ return left_
10
+
11
+ return left_.replace(right_, "")
12
+
13
+
14
+ def remove_first(context: FilterContext, left: object, right: object) -> str:
15
+ left_ = context.to_str(left, "")
16
+ right_ = context.to_str(right, "")
17
+
18
+ if not right_:
19
+ return left_
20
+
21
+ return left_.replace(right_, "", 1)
22
+
23
+
24
+ def remove_last(context: FilterContext, left: object, right: object) -> str:
25
+ left_ = context.to_str(left, "")
26
+ right_ = context.to_str(right, "")
27
+
28
+ if not right_:
29
+ return left_
30
+
31
+ before, _, after = left_.rpartition(right_)
32
+
33
+ if before:
34
+ return before + after
35
+
36
+ return left_
@@ -0,0 +1,32 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def replace(
5
+ context: FilterContext, left: object, sub: object, rep: object = None
6
+ ) -> str:
7
+ return context.to_str(left, "").replace(
8
+ context.to_str(sub, ""), context.to_str(rep, "")
9
+ )
10
+
11
+
12
+ def replace_first(
13
+ context: FilterContext, left: object, sub: object, rep: object = None
14
+ ) -> str:
15
+ return context.to_str(left, "").replace(
16
+ context.to_str(sub, ""), context.to_str(rep, ""), 1
17
+ )
18
+
19
+
20
+ def replace_last(context: FilterContext, left: object, sub: object, rep: object) -> str:
21
+ left_ = context.to_str(left, "")
22
+ sub_ = context.to_str(sub, "")
23
+
24
+ if not sub_:
25
+ return left_ + context.to_str(rep, "")
26
+
27
+ before, _, after = left_.rpartition(sub_)
28
+
29
+ if before:
30
+ return before + context.to_str(rep, "") + after
31
+
32
+ return left_
@@ -0,0 +1,5 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def reverse(context: FilterContext, left: object) -> list[object]:
5
+ return list(reversed(context.input_array(left)))
@@ -0,0 +1,35 @@
1
+ import math
2
+ from decimal import ROUND_HALF_UP, Decimal
3
+
4
+ from .._filter import FilterContext
5
+
6
+
7
+ def round_(
8
+ context: FilterContext, left: object, decimal_places: object = None
9
+ ) -> int | float | Decimal:
10
+ left = context.to_numeric(left, 0)
11
+
12
+ if context.is_nil(decimal_places):
13
+ return _round_up(left)
14
+
15
+ to = context.to_numeric(decimal_places, 0)
16
+
17
+ if to == 0:
18
+ return _round_up(left)
19
+
20
+ if to < 0:
21
+ return 0
22
+
23
+ if left == 0:
24
+ return 0
25
+
26
+ return _round_up(left, math.floor(to))
27
+
28
+
29
+ def _round_up(x: float | Decimal, digits: int = 0) -> int | float:
30
+ if isinstance(x, int):
31
+ return x
32
+
33
+ quantum = Decimal(1).scaleb(-digits)
34
+ result = Decimal(str(x)).quantize(quantum, rounding=ROUND_HALF_UP)
35
+ return int(result) if digits == 0 else float(result)
liquid/filters/size.py ADDED
@@ -0,0 +1,25 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from decimal import Decimal
3
+
4
+ from .._drop import liquid_iterable
5
+ from .._filter import FilterContext
6
+ from ..drops import Undefined
7
+
8
+
9
+ def size(context: FilterContext, left: object) -> int:
10
+ if isinstance(left, (float, bool)):
11
+ return 0
12
+
13
+ if isinstance(left, (int, Decimal)):
14
+ return 8
15
+
16
+ if isinstance(left, Undefined):
17
+ return 0
18
+
19
+ if isinstance(left, (Sequence, Mapping)):
20
+ return len(left) # pyright: ignore[reportUnknownArgumentType]
21
+
22
+ if liquid_iterable(left):
23
+ return left.__liquid_iter__(0, None, False).length
24
+
25
+ return 0
@@ -0,0 +1,39 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+ MAX_SLICE_ARG: int = (2**53) - 1
6
+ MIN_SLICE_ARG: int = -(2**53) + 1
7
+
8
+
9
+ def slice_(
10
+ context: FilterContext, left: object, offset: object, length: object = None
11
+ ) -> str | list[object]:
12
+ if not isinstance(left, (list, tuple, str)):
13
+ left = context.to_str(left, "")
14
+
15
+ offset = _clamp(_to_integer(context, offset))
16
+ length = 1 if context.is_nil(length) else _clamp(_to_integer(context, length))
17
+
18
+ start = len(left) + offset if offset < 0 else offset # type: ignore
19
+ return [] if length < 1 else left[start : start + length] # type: ignore
20
+
21
+
22
+ def _clamp(i: int) -> int:
23
+ return max(min(i, MAX_SLICE_ARG), MIN_SLICE_ARG)
24
+
25
+
26
+ def _to_integer(context: FilterContext, obj: object) -> int:
27
+ if isinstance(obj, int):
28
+ return obj
29
+
30
+ if isinstance(obj, (float, Decimal)) and "." not in str(obj):
31
+ return int(obj)
32
+
33
+ if context.is_nil(obj):
34
+ raise context.error("invalid integer")
35
+
36
+ try:
37
+ return int(context.to_str(obj, "").lstrip())
38
+ except ValueError:
39
+ raise context.error("invalid integer") from None