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/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
|
liquid/filters/split.py
ADDED
|
@@ -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
|
liquid/filters/squish.py
ADDED
liquid/filters/strip.py
ADDED
|
@@ -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
|
+
)
|
liquid/filters/times.py
ADDED
|
@@ -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_
|