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
liquid/filters/sort.py ADDED
@@ -0,0 +1,148 @@
1
+ import math
2
+ import re
3
+ from decimal import Decimal
4
+ from functools import cmp_to_key
5
+ from operator import getitem
6
+ from typing import Any, Literal
7
+
8
+ from .._filter import FilterContext
9
+
10
+
11
+ def sort(context: FilterContext, left: object, key: object = None) -> list[object]:
12
+ left = context.input_array(left)
13
+
14
+ def compare(a: object, b: object) -> Literal[-1, 0, 1]:
15
+ nil_a = context.is_nil(a)
16
+ nil_b = context.is_nil(b)
17
+
18
+ if nil_a and nil_b:
19
+ return 0
20
+
21
+ if nil_a and not nil_b:
22
+ return 1
23
+
24
+ if not nil_a and nil_b:
25
+ return -1
26
+
27
+ try:
28
+ if a < b: # type: ignore
29
+ return -1
30
+
31
+ if a > b: # type: ignore
32
+ return 1
33
+
34
+ return 0
35
+ except TypeError:
36
+ raise context.error(
37
+ f"comparison with {type(a).__class__!r} and {type(b).__class__!r} failed"
38
+ ) from None
39
+
40
+ if context.is_nil(key):
41
+ return sorted(context.input_array(left), key=cmp_to_key(compare))
42
+
43
+ return sorted(
44
+ context.input_array(left),
45
+ key=cmp_to_key(
46
+ lambda a, b: compare(
47
+ context.get_item(a, key, None), context.get_item(b, key, None)
48
+ )
49
+ ),
50
+ )
51
+
52
+
53
+ def sort_natural(
54
+ context: FilterContext, left: object, key: object = None
55
+ ) -> list[object]:
56
+ left = context.input_array(left)
57
+
58
+ def compare(a: object, b: object) -> Literal[-1, 0, 1]:
59
+ nil_a = context.is_nil(a)
60
+ nil_b = context.is_nil(b)
61
+
62
+ if nil_a and nil_b:
63
+ return 0
64
+
65
+ if nil_a and not nil_b:
66
+ return 1
67
+
68
+ if not nil_a and nil_b:
69
+ return -1
70
+
71
+ a = context.to_str(a, "").lower()
72
+ b = context.to_str(b, "").lower()
73
+
74
+ if a < b:
75
+ return -1
76
+
77
+ if a > b:
78
+ return 1
79
+
80
+ return 0
81
+
82
+ if context.is_nil(key):
83
+ return sorted(context.input_array(left), key=cmp_to_key(compare))
84
+
85
+ return sorted(
86
+ context.input_array(left),
87
+ key=cmp_to_key(
88
+ lambda a, b: compare(
89
+ context.get_item(a, key, None), context.get_item(b, key, None)
90
+ )
91
+ ),
92
+ )
93
+
94
+
95
+ RE_NUMERIC = re.compile(r"-?\d+")
96
+
97
+
98
+ def sort_numeric(
99
+ context: FilterContext, left: object, key: object = None
100
+ ) -> list[object]:
101
+ left = context.input_array(left)
102
+
103
+ def compare(a: object, b: object) -> Literal[-1, 0, 1]:
104
+ a = _ints(a)
105
+ b = _ints(b)
106
+
107
+ try:
108
+ if a < b:
109
+ return -1
110
+
111
+ if a > b:
112
+ return 1
113
+
114
+ return 0
115
+ except TypeError:
116
+ raise context.error(
117
+ f"comparison with {type(a).__class__!r} and {type(b).__class__!r} failed"
118
+ ) from None
119
+
120
+ if context.is_nil(key):
121
+ return sorted(context.input_array(left), key=cmp_to_key(compare))
122
+
123
+ return sorted(
124
+ context.input_array(left),
125
+ key=cmp_to_key(
126
+ lambda a, b: compare(_getitem(a, key, None), _getitem(b, key, None))
127
+ ),
128
+ )
129
+
130
+
131
+ def _getitem(sequence: Any, key: object, default: object = None) -> Any:
132
+ try:
133
+ return getitem(sequence, key)
134
+ except (KeyError, IndexError, TypeError):
135
+ return default
136
+
137
+
138
+ def _ints(obj: object) -> tuple[int | float | Decimal, ...]:
139
+ if isinstance(obj, bool):
140
+ return (math.inf,)
141
+ if isinstance(obj, (int, float, Decimal)):
142
+ return (obj,)
143
+
144
+ ints = tuple(int(n) for n in RE_NUMERIC.findall(str(obj)))
145
+
146
+ if not ints:
147
+ return (math.inf,)
148
+ return ints
@@ -0,0 +1,34 @@
1
+ import re
2
+
3
+ import markupsafe
4
+
5
+ from .._filter import FilterContext
6
+
7
+ RE_S = re.compile(r"\s")
8
+
9
+
10
+ def split(context: FilterContext, left: object, sep: object) -> list[str]:
11
+ left_ = context.to_str(left, "")
12
+
13
+ if not left_:
14
+ return []
15
+
16
+ sep_ = context.to_str(sep, "")
17
+
18
+ if not sep_:
19
+ return list(left_)
20
+
21
+ if left_ == sep_:
22
+ return []
23
+
24
+ result: list[str]
25
+
26
+ if sep_ == " ":
27
+ result = [markupsafe.Markup(s) for s in RE_S.split(left_)]
28
+ else:
29
+ result = left_.split(sep_)
30
+
31
+ if result[-1] == "":
32
+ result.pop()
33
+
34
+ return result
@@ -0,0 +1,9 @@
1
+ import re
2
+
3
+ from .._filter import FilterContext
4
+
5
+ RE_WHITESPACE = re.compile(r"\s+")
6
+
7
+
8
+ def squish(context: FilterContext, left: object) -> str:
9
+ return RE_WHITESPACE.sub(" ", context.to_str(left, "").strip())
@@ -0,0 +1,13 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def strip(context: FilterContext, left: object) -> str:
5
+ return context.to_str(left, "").strip()
6
+
7
+
8
+ def lstrip(context: FilterContext, left: object) -> str:
9
+ return context.to_str(left, "").lstrip()
10
+
11
+
12
+ def rstrip(context: FilterContext, left: object) -> str:
13
+ return context.to_str(left, "").rstrip()
@@ -0,0 +1,19 @@
1
+ import re
2
+
3
+ import markupsafe
4
+
5
+ from .._filter import FilterContext
6
+
7
+ RE_HTML_BLOCKS = re.compile(
8
+ r"<script.*?</script>|<!--.*?-->|<style.*?</style>", re.DOTALL
9
+ )
10
+
11
+ RE_HTML_TAGS = re.compile("<.*?>", re.DOTALL)
12
+
13
+
14
+ def strip_html(context: FilterContext, left: object) -> str:
15
+ left = context.to_str(left, "")
16
+ stripped = RE_HTML_TAGS.sub("", RE_HTML_BLOCKS.sub("", context.to_str(left, "")))
17
+ return (
18
+ markupsafe.Markup(stripped) if isinstance(left, markupsafe.Markup) else stripped
19
+ )
@@ -0,0 +1,17 @@
1
+ import re
2
+
3
+ import markupsafe
4
+
5
+ from .._filter import FilterContext
6
+
7
+ RE_NEWLINES = re.compile(r"\r?\n")
8
+
9
+
10
+ def strip_newlines(context: FilterContext, left: object) -> str:
11
+ left_ = context.to_str(left, "")
12
+ stripped = RE_NEWLINES.sub("", left_)
13
+ return (
14
+ markupsafe.Markup(stripped)
15
+ if isinstance(left_, markupsafe.Markup)
16
+ else stripped
17
+ )
liquid/filters/sum.py ADDED
@@ -0,0 +1,21 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def sum_(
7
+ context: FilterContext, left: object, prop: object = None
8
+ ) -> int | float | Decimal:
9
+ if type(left) is bool:
10
+ return 0
11
+
12
+ if isinstance(left, (int, float, Decimal)):
13
+ return left
14
+
15
+ if context.is_nil(prop):
16
+ return sum(context.to_decimal(obj, 0) for obj in context.input_array(left))
17
+
18
+ return sum(
19
+ context.to_decimal(context.get_item(obj, prop), 0)
20
+ for obj in context.input_array(left)
21
+ )
@@ -0,0 +1,13 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+
5
+
6
+ def times(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,380 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from gettext import NullTranslations
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ from markupsafe import Markup
8
+
9
+ from .. import expressions
10
+ from .._messages import MESSAGES, MessageText, TranslatableFilter, Translations
11
+
12
+ if TYPE_CHECKING:
13
+ from .._filter import FilterContext
14
+ from .._markup import Expression
15
+
16
+
17
+ class BaseTranslateFilter:
18
+ """Base class for the default translation filters.
19
+
20
+ Parameters
21
+ ----------
22
+ translations_var
23
+ The name of a render context variable that resolves to a gettext `Translations`
24
+ class. Defaults to ``"translations"``.
25
+ default_translations
26
+ A fallback translations class to use if `translations_var` can not be resolves.
27
+ Defaults to ``NullTranslations``.
28
+ message_interpolation
29
+ If `True` (default), perform printf-style string interpolation on the translated
30
+ message, using keyword arguments passed to the filter function.
31
+ """
32
+
33
+ re_vars = re.compile(r"(?<!%)%\((\w+)\)s")
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ translations_var: str = "translations",
39
+ default_translations: Translations | None = None,
40
+ message_interpolation: bool = True,
41
+ ) -> None:
42
+ self.translations_var = translations_var
43
+ self.default_translations = default_translations or NullTranslations()
44
+ self.message_interpolation = message_interpolation
45
+
46
+ def format_message(
47
+ self, context: FilterContext, message_text: str, message_vars: dict[str, Any]
48
+ ) -> str:
49
+ """Return the message string formatted with the given message variables."""
50
+ with context.context.extend(namespace=message_vars):
51
+ _vars = {
52
+ k: context.env.serialize(
53
+ context.resolve(k), context.context, context.span
54
+ )
55
+ for k in self.re_vars.findall(message_text)
56
+ }
57
+
58
+ # Missing variables get replaced by the current `Undefined` type and we're
59
+ # converting all values to a string, so a KeyError or a ValueError should
60
+ # be impossible.
61
+ return message_text % _vars
62
+
63
+ def _resolve_translations(
64
+ self, context: FilterContext
65
+ ) -> Translations | NullTranslations:
66
+ result = context.resolve(self.translations_var)
67
+ if context.is_nil(result):
68
+ return self.default_translations
69
+ return cast("Translations", result)
70
+
71
+
72
+ class Translate(BaseTranslateFilter, TranslatableFilter):
73
+ """A Liquid filter for translating strings to other languages.
74
+
75
+ Depending on the keyword arguments provided when the resulting filter
76
+ is called, it could behave like gettext, ngettext, pgettext or npgettext.
77
+ """
78
+
79
+ def __call__(
80
+ self,
81
+ context: FilterContext,
82
+ left: object,
83
+ message_context: object = None,
84
+ **kwargs: Any,
85
+ ) -> str:
86
+ """Apply the filter and return the result."""
87
+ autoescape = context.env.autoescape
88
+ left_ = context.to_str(left, "")
89
+
90
+ translations = self._resolve_translations(context)
91
+
92
+ plural = kwargs.pop("plural", None)
93
+ n = _count(kwargs.get("count"))
94
+
95
+ if plural is not None and n is not None:
96
+ plural = context.to_str(plural, "")
97
+
98
+ if message_context is not None:
99
+ text = translations.npgettext(
100
+ context.to_str(message_context, ""),
101
+ left_,
102
+ plural,
103
+ n,
104
+ )
105
+ else:
106
+ text = translations.ngettext(left_, plural, n)
107
+ elif message_context is not None:
108
+ text = translations.pgettext(
109
+ context.to_str(message_context, ""),
110
+ left_,
111
+ )
112
+ else:
113
+ text = translations.gettext(left_)
114
+
115
+ if autoescape:
116
+ text = Markup(text)
117
+
118
+ if self.message_interpolation:
119
+ text = self.format_message(context, text, kwargs)
120
+
121
+ return text
122
+
123
+ def message(
124
+ self,
125
+ left: Expression,
126
+ filter_: expressions.Filtered,
127
+ lineno: int,
128
+ ) -> MessageText | None:
129
+ if not isinstance(left, expressions.StringLiteral):
130
+ return None
131
+
132
+ if not filter_.args:
133
+ return MessageText(
134
+ lineno=lineno,
135
+ funcname="gettext",
136
+ message=(left.value,),
137
+ )
138
+
139
+ if not isinstance(filter_.args[0], expressions.KeywordArgument):
140
+ context_: Expression | None = filter_.args[0]
141
+ else:
142
+ context_ = None
143
+
144
+ plural: Expression | None = None
145
+ for arg in filter_.args:
146
+ if (
147
+ isinstance(arg, expressions.KeywordArgument)
148
+ and arg.name.value == "plural"
149
+ ):
150
+ plural = arg.expr
151
+
152
+ # Translate our filters into standard *gettext argument specs.
153
+
154
+ if isinstance(plural, expressions.StringLiteral):
155
+ if isinstance(context_, expressions.StringLiteral):
156
+ funcname = "npgettext"
157
+ message: MESSAGES = ((context_.value, "c"), left.value, plural.value)
158
+ else:
159
+ funcname = "ngettext"
160
+ message = (left.value, plural.value)
161
+ elif plural is not None:
162
+ # Don't attempt to extract any messages if plural is given but not a string
163
+ # literal.
164
+ return None
165
+ elif isinstance(context_, expressions.StringLiteral):
166
+ funcname = "pgettext"
167
+ message = ((context_.value, "c"), left.value)
168
+ else:
169
+ funcname = "gettext"
170
+ message = (left.value,)
171
+
172
+ return MessageText(
173
+ lineno=lineno,
174
+ funcname=funcname,
175
+ message=message,
176
+ )
177
+
178
+
179
+ class GetText(BaseTranslateFilter, TranslatableFilter):
180
+ """A Liquid filter equivalent of `gettext.gettext`."""
181
+
182
+ def __call__(
183
+ self,
184
+ context: FilterContext,
185
+ left: object,
186
+ **kwargs: Any,
187
+ ) -> str:
188
+ autoescape = context.env.autoescape
189
+ left_ = context.to_str(left, "")
190
+ translations = self._resolve_translations(context)
191
+ text = translations.gettext(left_)
192
+
193
+ if autoescape:
194
+ text = Markup(text)
195
+
196
+ if self.message_interpolation:
197
+ text = self.format_message(context, text, kwargs)
198
+
199
+ return text
200
+
201
+ def message(
202
+ self,
203
+ left: Expression,
204
+ filter_: expressions.Filtered,
205
+ lineno: int,
206
+ ) -> MessageText | None:
207
+ if not isinstance(left, expressions.StringLiteral):
208
+ return None
209
+
210
+ return MessageText(
211
+ lineno=lineno,
212
+ funcname="gettext",
213
+ message=(left.value,),
214
+ )
215
+
216
+
217
+ class NGetText(BaseTranslateFilter, TranslatableFilter):
218
+ """A Liquid filter equivalent of `gettext.ngettext`."""
219
+
220
+ def __call__(
221
+ self,
222
+ context: FilterContext,
223
+ left: object,
224
+ plural: str,
225
+ count: object,
226
+ **kwargs: Any,
227
+ ) -> str:
228
+ """Apply the filter and return the result."""
229
+ left_ = context.to_str(left, "")
230
+
231
+ plural = context.to_str(plural, "")
232
+ count = int(context.to_numeric(count, 1))
233
+
234
+ translations = self._resolve_translations(context)
235
+ text = translations.ngettext(left_, plural, count)
236
+
237
+ if context.env.autoescape:
238
+ text = Markup(text)
239
+
240
+ if self.message_interpolation:
241
+ text = self.format_message(context, text, kwargs)
242
+
243
+ return text
244
+
245
+ def message(
246
+ self,
247
+ left: Expression,
248
+ filter_: expressions.Filtered,
249
+ lineno: int,
250
+ ) -> MessageText | None:
251
+ if len(filter_.args) < 1:
252
+ return None
253
+
254
+ plural = filter_.args[0]
255
+
256
+ if not isinstance(left, expressions.StringLiteral) or not isinstance(
257
+ plural, expressions.StringLiteral
258
+ ):
259
+ return None
260
+
261
+ return MessageText(
262
+ lineno=lineno,
263
+ funcname="ngettext",
264
+ message=(left.value, plural.value),
265
+ )
266
+
267
+
268
+ class PGetText(BaseTranslateFilter, TranslatableFilter):
269
+ """A Liquid filter equivalent of `gettext.pgettext`."""
270
+
271
+ def __call__(
272
+ self,
273
+ context: FilterContext,
274
+ left: object,
275
+ message_context: str,
276
+ **kwargs: Any,
277
+ ) -> str:
278
+ left_ = context.to_str(left, "")
279
+ message_context = context.to_str(message_context, "")
280
+
281
+ translations = self._resolve_translations(context)
282
+ text = translations.pgettext(message_context, left_)
283
+
284
+ if context.env.autoescape:
285
+ text = Markup(text)
286
+
287
+ if self.message_interpolation:
288
+ text = self.format_message(context, text, kwargs)
289
+
290
+ return text
291
+
292
+ def message(
293
+ self,
294
+ left: Expression,
295
+ filter_: expressions.Filtered,
296
+ lineno: int,
297
+ ) -> MessageText | None:
298
+ if len(filter_.args) < 1:
299
+ return None
300
+
301
+ ctx = filter_.args[0]
302
+
303
+ if not isinstance(left, expressions.StringLiteral) or not isinstance(
304
+ ctx, expressions.StringLiteral
305
+ ):
306
+ return None
307
+
308
+ return MessageText(
309
+ lineno=lineno,
310
+ funcname="pgettext",
311
+ message=((ctx.value, "c"), left.value),
312
+ )
313
+
314
+
315
+ class NPGetText(BaseTranslateFilter, TranslatableFilter):
316
+ """A Liquid filter equivalent of `gettext.npgettext`."""
317
+
318
+ def __call__(
319
+ self,
320
+ context: FilterContext,
321
+ left: object,
322
+ message_context: str,
323
+ plural: str,
324
+ count: object,
325
+ **kwargs: Any,
326
+ ) -> str:
327
+ left_ = context.to_str(left, "")
328
+ message_context = context.to_str(message_context, "")
329
+ plural = context.to_str(plural, "")
330
+ count = int(context.to_numeric(count, default=1))
331
+
332
+ translations = self._resolve_translations(context)
333
+ text = translations.npgettext(
334
+ message_context,
335
+ left_,
336
+ plural,
337
+ count,
338
+ )
339
+
340
+ if context.env.autoescape:
341
+ text = Markup(text)
342
+
343
+ if self.message_interpolation:
344
+ text = self.format_message(context, text, kwargs)
345
+
346
+ return text
347
+
348
+ def message(
349
+ self,
350
+ left: Expression,
351
+ filter_: expressions.Filtered,
352
+ lineno: int,
353
+ ) -> MessageText | None:
354
+ if len(filter_.args) < 2:
355
+ return None
356
+
357
+ ctx = filter_.args[0]
358
+ plural = filter_.args[1]
359
+
360
+ if (
361
+ not isinstance(left, expressions.StringLiteral)
362
+ or not isinstance(plural, expressions.StringLiteral)
363
+ or not isinstance(ctx, expressions.StringLiteral)
364
+ ):
365
+ return None
366
+
367
+ return MessageText(
368
+ lineno=lineno,
369
+ funcname="npgettext",
370
+ message=((ctx.value, "c"), left.value, plural.value),
371
+ )
372
+
373
+
374
+ def _count(val: Any) -> int | None:
375
+ if val in (None, False, True):
376
+ return None
377
+ try:
378
+ return int(val)
379
+ except ValueError:
380
+ return None
@@ -0,0 +1,39 @@
1
+ import re
2
+
3
+ from .._filter import FilterContext
4
+
5
+ MAX_TRUNC_WORDS = 2147483647
6
+
7
+ RE_S = re.compile(r"\s+")
8
+
9
+
10
+ def truncate(
11
+ context: FilterContext, left: object, length: object = 50, end: object = "..."
12
+ ) -> str:
13
+ left_ = context.to_str(left, "")
14
+ length_ = context.to_int(length)
15
+
16
+ if len(left_) <= length_:
17
+ return left_
18
+
19
+ end_ = context.to_str(end, "")
20
+ stop = 0 if length_ < len(end_) else length_ - len(end_)
21
+ return left_[0:stop] + end_
22
+
23
+
24
+ def truncate_words(
25
+ context: FilterContext, left: object, word_count: object = 15, end: object = "..."
26
+ ) -> str:
27
+ left_ = context.to_str(left, "")
28
+ word_count_ = max(context.to_int(word_count), 1)
29
+
30
+ if word_count_ >= MAX_TRUNC_WORDS:
31
+ return left_
32
+
33
+ end_ = context.to_str(end, "")
34
+ words = RE_S.split(left_.strip())
35
+
36
+ if len(words) <= word_count_:
37
+ return left_
38
+
39
+ return " ".join(words[:word_count_]) + end_