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/_filter.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import decimal
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Callable, Generator, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import TYPE_CHECKING, Concatenate
|
|
9
|
+
|
|
10
|
+
from ._drop import liquid_object, liquid_primitive
|
|
11
|
+
from ._missing import MISSING
|
|
12
|
+
from ._nothing import NOTHING
|
|
13
|
+
from .exceptions import LiquidFilterError, LiquidFilterInterrupt
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import datetime as dt
|
|
17
|
+
|
|
18
|
+
from ._context import RenderContext
|
|
19
|
+
from .environment import Environment
|
|
20
|
+
from .tokens import Token
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
type Filter = Callable[Concatenate[FilterContext, object, ...], object]
|
|
24
|
+
|
|
25
|
+
RE_DIGITS = re.compile(r"^[0-9]+")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class FilterContext:
|
|
30
|
+
"""Liquid config, state and utilities for filter implementations."""
|
|
31
|
+
|
|
32
|
+
context: RenderContext
|
|
33
|
+
span: Token
|
|
34
|
+
|
|
35
|
+
missing: object = field(init=False)
|
|
36
|
+
env: Environment = field(init=False)
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
self.missing = MISSING
|
|
40
|
+
self.env = self.context.env
|
|
41
|
+
|
|
42
|
+
def get_item(self, obj: object, key: object, default: object = MISSING) -> object:
|
|
43
|
+
"""Get `key` from `obj` with Shopify/liquid semantics."""
|
|
44
|
+
if isinstance(obj, str) and isinstance(key, str):
|
|
45
|
+
return key if key in obj else None
|
|
46
|
+
|
|
47
|
+
if isinstance(obj, (int, float, Decimal)) and isinstance(
|
|
48
|
+
key, (int, float, Decimal)
|
|
49
|
+
):
|
|
50
|
+
return obj == key
|
|
51
|
+
|
|
52
|
+
if self.is_nil(obj):
|
|
53
|
+
raise LiquidFilterInterrupt(
|
|
54
|
+
f"can't read property {obj}[{key}]",
|
|
55
|
+
token=self.span,
|
|
56
|
+
source=self.context.template.source,
|
|
57
|
+
name=self.context.template.name,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not (isinstance(obj, Mapping) or liquid_object(obj)):
|
|
61
|
+
raise LiquidFilterError(
|
|
62
|
+
f"can't read property {obj}[{key}]",
|
|
63
|
+
token=self.span,
|
|
64
|
+
source=self.context.template.source,
|
|
65
|
+
name=self.context.template.name,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
result, _ = self.context.resolve_path(obj, [key]) # pyright: ignore[reportUnknownArgumentType]
|
|
69
|
+
return default if result == NOTHING else result
|
|
70
|
+
|
|
71
|
+
def resolve(self, name: object) -> object:
|
|
72
|
+
return self.context.resolve(name)
|
|
73
|
+
|
|
74
|
+
def input_array(self, obj: object) -> list[object]:
|
|
75
|
+
"""Coerce `obj` to a list suitable for filters that expect an array input."""
|
|
76
|
+
if liquid_primitive(obj):
|
|
77
|
+
obj = obj.__liquid__("array", self.context)
|
|
78
|
+
|
|
79
|
+
if isinstance(obj, (list, tuple)):
|
|
80
|
+
return _flatten(obj) # pyright: ignore[reportUnknownArgumentType]
|
|
81
|
+
|
|
82
|
+
return [obj]
|
|
83
|
+
|
|
84
|
+
def is_nil(self, obj: object) -> bool:
|
|
85
|
+
"""Return true if `obj` is Liquid nullish."""
|
|
86
|
+
return self.context.env.is_nil(obj)
|
|
87
|
+
|
|
88
|
+
def truthy(self, obj: object) -> bool:
|
|
89
|
+
"""Return true if `obj` is Liquid truthy."""
|
|
90
|
+
return self.context.env.truthy(obj, self.context)
|
|
91
|
+
|
|
92
|
+
def to_int(self, obj: object) -> int:
|
|
93
|
+
"""Coerce `obj` to an integer."""
|
|
94
|
+
if self.is_nil(obj):
|
|
95
|
+
raise LiquidFilterError(
|
|
96
|
+
"invalid integer",
|
|
97
|
+
token=self.span,
|
|
98
|
+
source=self.context.template.source,
|
|
99
|
+
name=self.context.template.name,
|
|
100
|
+
)
|
|
101
|
+
return self.context.env.to_int(obj, self.context, self.span)
|
|
102
|
+
|
|
103
|
+
def to_numeric(self, obj: object, default: int | Decimal) -> float | int | Decimal:
|
|
104
|
+
"""Coerce `obj` to an integer, float or Decimal."""
|
|
105
|
+
if isinstance(obj, str):
|
|
106
|
+
try:
|
|
107
|
+
return int(obj)
|
|
108
|
+
except ValueError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
return float(obj)
|
|
113
|
+
except ValueError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
# This is different than env.to_numeric
|
|
117
|
+
if match := RE_DIGITS.match(obj):
|
|
118
|
+
return int(match.group())
|
|
119
|
+
|
|
120
|
+
return default
|
|
121
|
+
|
|
122
|
+
return self.context.env.to_numeric(obj, self.context, default)
|
|
123
|
+
|
|
124
|
+
def to_decimal(self, obj: object, default: Decimal | int) -> Decimal | int:
|
|
125
|
+
if self.is_nil(obj):
|
|
126
|
+
return default
|
|
127
|
+
|
|
128
|
+
if obj is True:
|
|
129
|
+
return default
|
|
130
|
+
|
|
131
|
+
if isinstance(obj, int):
|
|
132
|
+
return obj
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
return Decimal(str(obj))
|
|
136
|
+
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
137
|
+
if isinstance(obj, str):
|
|
138
|
+
match = RE_DIGITS.match(obj)
|
|
139
|
+
if match:
|
|
140
|
+
return Decimal(match.group())
|
|
141
|
+
return default
|
|
142
|
+
|
|
143
|
+
def to_str[T](self, obj: object, default: T) -> str | T:
|
|
144
|
+
"""Coerce `obj` to a string."""
|
|
145
|
+
return (
|
|
146
|
+
default
|
|
147
|
+
if obj is None
|
|
148
|
+
else self.context.env.to_str(obj, self.context, self.span)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def to_date(self, obj: object) -> dt.datetime | None:
|
|
152
|
+
"""Coerce `obj` to a datatime, or ``None`` if `obj` can not be interpreted as a datetime."""
|
|
153
|
+
return self.env.to_date(obj, self.context, self.span)
|
|
154
|
+
|
|
155
|
+
def error(self, message: str) -> LiquidFilterError:
|
|
156
|
+
"""Return a Liquid exception with suitable error context data."""
|
|
157
|
+
return LiquidFilterError(
|
|
158
|
+
message,
|
|
159
|
+
token=self.span,
|
|
160
|
+
source=self.context.template.source,
|
|
161
|
+
name=self.context.template.name,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _flatten(it: Iterable[object], level: int = 5) -> list[object]:
|
|
166
|
+
|
|
167
|
+
def flatten_(it: Iterable[object], level: int = 5) -> Generator[object]:
|
|
168
|
+
for obj in it:
|
|
169
|
+
if level < 1 or not isinstance(obj, (list, tuple)):
|
|
170
|
+
yield obj
|
|
171
|
+
else:
|
|
172
|
+
yield from flatten_(obj, level=level - 1) # pyright: ignore[reportUnknownArgumentType]
|
|
173
|
+
|
|
174
|
+
return list(flatten_(it, level))
|
liquid/_io.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class StringBuffer:
|
|
2
|
+
"""An output text buffer that keeps track of its own length."""
|
|
3
|
+
|
|
4
|
+
def __init__(self) -> None:
|
|
5
|
+
self._parts: list[str] = []
|
|
6
|
+
self._length = 0
|
|
7
|
+
|
|
8
|
+
def write(self, s: str) -> None:
|
|
9
|
+
"""Write `s` to the buffer."""
|
|
10
|
+
self._parts.append(s)
|
|
11
|
+
self._length += len(s)
|
|
12
|
+
|
|
13
|
+
def __len__(self) -> int:
|
|
14
|
+
return self._length
|
|
15
|
+
|
|
16
|
+
def getvalue(self) -> str:
|
|
17
|
+
"""Return the contents of the buffer."""
|
|
18
|
+
return "".join(self._parts)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol
|
|
5
|
+
|
|
6
|
+
from . import expressions, tokens
|
|
7
|
+
from ._parser import PRECEDENCE_LOWEST
|
|
8
|
+
from .exceptions import LiquidSyntaxError
|
|
9
|
+
from .tags import ErrorNode
|
|
10
|
+
from .tokens import Token, TokenKind, span
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._markup import Expression, Markup
|
|
14
|
+
from ._parser import Precedence
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _LegacyParserProtocol(Protocol):
|
|
18
|
+
def parse_output(self) -> Markup: ...
|
|
19
|
+
def parse_tag(self) -> Markup: ...
|
|
20
|
+
def parse_expression(
|
|
21
|
+
self,
|
|
22
|
+
precedence: Precedence = PRECEDENCE_LOWEST,
|
|
23
|
+
*,
|
|
24
|
+
infix: bool = False,
|
|
25
|
+
expect_end_of_expression: bool = False,
|
|
26
|
+
) -> Expression: ...
|
|
27
|
+
def current(self) -> Token: ...
|
|
28
|
+
def next(self) -> Token: ...
|
|
29
|
+
def kind(self) -> TokenKind: ...
|
|
30
|
+
def carry_whitespace_control(self) -> None: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LaxParserMixin(ABC, _LegacyParserProtocol):
|
|
34
|
+
"""A legacy Liquid parser that captures and suppresses syntax errors.
|
|
35
|
+
|
|
36
|
+
Other parse-time errors such as ``ResourceLimitError`` will still be raised.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ()
|
|
40
|
+
|
|
41
|
+
def parse_output(self) -> Markup:
|
|
42
|
+
try:
|
|
43
|
+
return super().parse_output() # type: ignore
|
|
44
|
+
except LiquidSyntaxError as err:
|
|
45
|
+
return self.error(err)
|
|
46
|
+
|
|
47
|
+
def parse_tag(self) -> Markup:
|
|
48
|
+
try:
|
|
49
|
+
return super().parse_tag() # type: ignore
|
|
50
|
+
except LiquidSyntaxError as err:
|
|
51
|
+
return self.error(err)
|
|
52
|
+
|
|
53
|
+
def parse_expression(
|
|
54
|
+
self,
|
|
55
|
+
precedence: Precedence = PRECEDENCE_LOWEST,
|
|
56
|
+
*,
|
|
57
|
+
infix: bool = False,
|
|
58
|
+
expect_end_of_expression: bool = False,
|
|
59
|
+
) -> Expression:
|
|
60
|
+
start_token = self.current()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
return super().parse_expression( # type: ignore
|
|
64
|
+
precedence,
|
|
65
|
+
infix=infix,
|
|
66
|
+
expect_end_of_expression=expect_end_of_expression,
|
|
67
|
+
)
|
|
68
|
+
except LiquidSyntaxError as err:
|
|
69
|
+
while self.kind() not in (
|
|
70
|
+
tokens.TOKEN_WC,
|
|
71
|
+
tokens.TOKEN_TAG_END,
|
|
72
|
+
tokens.TOKEN_OUT_END,
|
|
73
|
+
):
|
|
74
|
+
self.pos += 1 # type: ignore
|
|
75
|
+
|
|
76
|
+
return expressions.ErrorExpr(span(start_token, self.current()), err)
|
|
77
|
+
|
|
78
|
+
def error(self, err: LiquidSyntaxError) -> ErrorNode:
|
|
79
|
+
# Skip tokens up to the next delimiter.
|
|
80
|
+
start_token = self.current()
|
|
81
|
+
|
|
82
|
+
while 1:
|
|
83
|
+
match self.kind():
|
|
84
|
+
case (
|
|
85
|
+
tokens.TOKEN_EOI
|
|
86
|
+
| tokens.TOKEN_COMMENT_END
|
|
87
|
+
| tokens.TOKEN_COMMENT_START
|
|
88
|
+
| tokens.TOKEN_OUT_END
|
|
89
|
+
| tokens.TOKEN_OUT_START
|
|
90
|
+
| tokens.TOKEN_TAG_END
|
|
91
|
+
| tokens.TOKEN_TAG_START
|
|
92
|
+
):
|
|
93
|
+
break
|
|
94
|
+
case tokens.TOKEN_WC:
|
|
95
|
+
self.carry_whitespace_control()
|
|
96
|
+
case _:
|
|
97
|
+
self.pos += 1 # type: ignore
|
|
98
|
+
|
|
99
|
+
end_token = self.next()
|
|
100
|
+
return ErrorNode(span(start_token, end_token), err)
|
liquid/_lexer.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .exceptions import ResourceLimitError
|
|
7
|
+
from .tokens import TOKEN_WC
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from .environment import Environment
|
|
13
|
+
from .tokens import Token, TokenKind
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
type State = Callable[[], State | None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Lexer:
|
|
20
|
+
"""Template source tokenizer."""
|
|
21
|
+
|
|
22
|
+
__slots__ = (
|
|
23
|
+
"length",
|
|
24
|
+
"pos",
|
|
25
|
+
"source",
|
|
26
|
+
"start",
|
|
27
|
+
"tokens",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __init__(self, env: Environment, source: str) -> None:
|
|
31
|
+
self.source = source
|
|
32
|
+
self.length = len(source)
|
|
33
|
+
self.start = 0
|
|
34
|
+
self.pos = 0
|
|
35
|
+
self.tokens: list[Token] = []
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def tokenize(cls, env: Environment, source: str) -> list[Token]:
|
|
39
|
+
lexer = cls(env, source)
|
|
40
|
+
try:
|
|
41
|
+
lexer.run()
|
|
42
|
+
except RecursionError:
|
|
43
|
+
raise ResourceLimitError("maximum recursion depth exceeded")
|
|
44
|
+
return lexer.tokens
|
|
45
|
+
|
|
46
|
+
def run(self) -> None:
|
|
47
|
+
state: State | None = self.scan_markup
|
|
48
|
+
while state:
|
|
49
|
+
state = state()
|
|
50
|
+
|
|
51
|
+
def emit(self, kind: TokenKind) -> None:
|
|
52
|
+
self.tokens.append((kind, self.start, self.pos))
|
|
53
|
+
self.start = self.pos
|
|
54
|
+
|
|
55
|
+
def index(self, pattern: re.Pattern[str]) -> int | None:
|
|
56
|
+
if match := pattern.search(self.source, self.pos):
|
|
57
|
+
return match.start()
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def scan(self, pattern: re.Pattern[str]) -> str | None:
|
|
61
|
+
if match := pattern.match(self.source, self.pos):
|
|
62
|
+
self.pos = match.end()
|
|
63
|
+
return match[0]
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def scan_until(self, pattern: re.Pattern[str]) -> str | None:
|
|
67
|
+
if match := pattern.search(self.source, self.pos):
|
|
68
|
+
self.pos = match.start()
|
|
69
|
+
return self.source[self.start : match.start()]
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def skip(self, pattern: re.Pattern[str]) -> bool:
|
|
73
|
+
if match := pattern.match(self.source, self.pos):
|
|
74
|
+
self.pos = match.end()
|
|
75
|
+
self.start = self.pos
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def skip_until(self, pattern: re.Pattern[str]) -> re.Match[str] | None:
|
|
80
|
+
if match := pattern.search(self.source, self.pos):
|
|
81
|
+
self.pos = match.start()
|
|
82
|
+
return match
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def accept_whitespace_control(self) -> bool:
|
|
86
|
+
if self.pos < self.length and self.source[self.pos] == "-":
|
|
87
|
+
self.pos += 1
|
|
88
|
+
self.emit(TOKEN_WC)
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
def scan_markup(self) -> State | None:
|
|
93
|
+
raise NotImplementedError
|