python-liquid 2.3.5__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/loaders/file_system_loader.py +143 -0
- liquid/{builtin/loaders → loaders}/mixins.py +40 -52
- 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.5.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.5.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.5.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/file_system_loader.py +0 -146
- 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.5.dist-info/RECORD +0 -96
liquid/_nothing.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ._context import RenderContext
|
|
7
|
+
from ._drop import ContextHint
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Nothing:
|
|
11
|
+
def __str__(self) -> str:
|
|
12
|
+
return ""
|
|
13
|
+
|
|
14
|
+
def __liquid__(self, hint: ContextHint, context: RenderContext) -> object:
|
|
15
|
+
match hint:
|
|
16
|
+
case "output" | "string":
|
|
17
|
+
return ""
|
|
18
|
+
case "index" | "property" | "object":
|
|
19
|
+
return None
|
|
20
|
+
case "numeric":
|
|
21
|
+
return 0
|
|
22
|
+
case "boolean":
|
|
23
|
+
return False
|
|
24
|
+
case "array":
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
NOTHING = Nothing()
|
liquid/_parser.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Literal
|
|
5
|
+
|
|
6
|
+
from .exceptions import LiquidSyntaxError
|
|
7
|
+
from .tokens import *
|
|
8
|
+
from .tokens import TOKEN_NAMES, span, token_value
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Collection
|
|
12
|
+
|
|
13
|
+
from ._markup import Block, Expression
|
|
14
|
+
from .environment import Environment
|
|
15
|
+
from .expressions import KeywordArgument, Name, Parameter, StringLiteral
|
|
16
|
+
from .tokens import Token, TokenKind
|
|
17
|
+
|
|
18
|
+
PRECEDENCE_LOWEST: Literal[1] = 1
|
|
19
|
+
PRECEDENCE_LOGICAL_OR: Literal[2] = 2
|
|
20
|
+
PRECEDENCE_LOGICAL_AND: Literal[3] = 3
|
|
21
|
+
PRECEDENCE_LOGICAL_NOT: Literal[4] = 4
|
|
22
|
+
PRECEDENCE_LOGICAL_RIGHT: Literal[5] = 5
|
|
23
|
+
PRECEDENCE_COMPARISON: Literal[6] = 6
|
|
24
|
+
PRECEDENCE_MEMBERSHIP: Literal[7] = 7
|
|
25
|
+
PRECEDENCE_PIPE: Literal[8] = 8
|
|
26
|
+
PRECEDENCE_FILTER_ARG: Literal[9] = 9
|
|
27
|
+
PRECEDENCE_ADD: Literal[10] = 10
|
|
28
|
+
PRECEDENCE_MULL: Literal[11] = 11
|
|
29
|
+
PRECEDENCE_NEG: Literal[12] = 12
|
|
30
|
+
PRECEDENCE_PREFIX: Literal[13] = 13
|
|
31
|
+
|
|
32
|
+
type Precedence = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
TERMINATE_EXPRESSION: frozenset[TokenKind] = frozenset(
|
|
36
|
+
[
|
|
37
|
+
TOKEN_WC,
|
|
38
|
+
TOKEN_OUT_END,
|
|
39
|
+
TOKEN_TAG_END,
|
|
40
|
+
TOKEN_TEXT,
|
|
41
|
+
TOKEN_RPAREN,
|
|
42
|
+
TOKEN_EOI,
|
|
43
|
+
TOKEN_IF,
|
|
44
|
+
TOKEN_ELSE,
|
|
45
|
+
TOKEN_INTERPOLATION_END,
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Parser(ABC):
|
|
51
|
+
"""A base parser from which different parser behavior can be derived."""
|
|
52
|
+
|
|
53
|
+
__slots__ = (
|
|
54
|
+
"block_depth",
|
|
55
|
+
"env",
|
|
56
|
+
"eoi",
|
|
57
|
+
"expression_depth",
|
|
58
|
+
"length",
|
|
59
|
+
"pos",
|
|
60
|
+
"source",
|
|
61
|
+
"template_name",
|
|
62
|
+
"tokens",
|
|
63
|
+
"whitespace_control_carry",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
env: Environment,
|
|
69
|
+
source: str,
|
|
70
|
+
template_name: str,
|
|
71
|
+
tokens: list[Token],
|
|
72
|
+
) -> None:
|
|
73
|
+
self.env = env
|
|
74
|
+
self.source = source
|
|
75
|
+
self.template_name = template_name
|
|
76
|
+
self.tokens = tokens
|
|
77
|
+
self.length = len(tokens)
|
|
78
|
+
self.pos: int = 0
|
|
79
|
+
self.eoi: Token = (TOKEN_EOI, len(source), len(source))
|
|
80
|
+
self.whitespace_control_carry: Literal["-"] | None = None
|
|
81
|
+
self.block_depth: int = 0
|
|
82
|
+
self.expression_depth: int = 0
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def parse(cls, env: Environment, source: str, template_name: str) -> Block:
|
|
86
|
+
return cls(env, source, template_name, env.tokenize(source)).parse_block(())
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def parse_block(self, end: Collection[str]) -> Block:
|
|
90
|
+
"""Parse text and markup until reaching a named tag.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
end
|
|
95
|
+
Possible tag names that terminate the block.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
block
|
|
100
|
+
A sequence of nodes in the template syntax tree.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def parse_expression(
|
|
105
|
+
self,
|
|
106
|
+
precedence: Precedence = PRECEDENCE_LOWEST,
|
|
107
|
+
*,
|
|
108
|
+
infix: bool = False,
|
|
109
|
+
expect_end_of_expression: bool = False,
|
|
110
|
+
) -> Expression:
|
|
111
|
+
"""Parse a "common" expression from the token stream.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
precedence
|
|
116
|
+
The binding power of this sub expression.
|
|
117
|
+
infix
|
|
118
|
+
When true, accept infix operators when parsing the expression.
|
|
119
|
+
expect_end_of_expression
|
|
120
|
+
When ``True``, raise a syntax error if we don't finish on an expression
|
|
121
|
+
termination token.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def parse_line_statements(self) -> Block:
|
|
126
|
+
"""Parse newline terminated markup from the `{% liquid %}` tag."""
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def parse_filtered_expression(
|
|
130
|
+
self, precedence: Precedence = PRECEDENCE_LOWEST, *, infix: bool = False
|
|
131
|
+
) -> Expression:
|
|
132
|
+
"""Parse a "common" expression optionally followed by one or more filters.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
precedence
|
|
137
|
+
The binding power of this sub expression.
|
|
138
|
+
infix
|
|
139
|
+
When true, accept infix operators when parsing the expression.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def parse_identifier(self) -> Name:
|
|
144
|
+
"""Parse an identifier
|
|
145
|
+
|
|
146
|
+
Raises
|
|
147
|
+
------
|
|
148
|
+
LiquidSyntaxError
|
|
149
|
+
If the identifier is followed by path segments.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def parse_name(self) -> Name:
|
|
154
|
+
"""Parse an identifier, possibly surrounded by quotes.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
LiquidSyntaxError
|
|
159
|
+
If the identifier is followed by path segments.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def parse_string_literal(self) -> StringLiteral:
|
|
164
|
+
"""Parse a literal string surrounded by single or double quotes."""
|
|
165
|
+
|
|
166
|
+
@abstractmethod
|
|
167
|
+
def parse_arguments(
|
|
168
|
+
self, require_commas: bool = True
|
|
169
|
+
) -> list[Expression | KeywordArgument]:
|
|
170
|
+
"""Parse positional and/or keyword arguments from the token stream.
|
|
171
|
+
|
|
172
|
+
Assumes any leading commas have been consumed by the caller, if they are allowed.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
require_commas
|
|
177
|
+
When true, throw a syntax error if arguments are not separated by a comma.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def parse_keyword_arguments(
|
|
182
|
+
self, require_commas: bool = True
|
|
183
|
+
) -> list[KeywordArgument]:
|
|
184
|
+
"""Parse zero or more keyword arguments from the token stream.
|
|
185
|
+
|
|
186
|
+
Assumes any leading commas have been consumed by the caller, if they are allowed.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
require_commas
|
|
191
|
+
When true, throw a syntax error if arguments are not separated by a comma.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def parse_positional_arguments(
|
|
196
|
+
self, require_commas: bool = True
|
|
197
|
+
) -> list[Expression]:
|
|
198
|
+
"""Parse zero or more positional arguments from the token stream.
|
|
199
|
+
|
|
200
|
+
Assumes any leading commas have been consumed by the caller, if they are allowed.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
require_commas
|
|
205
|
+
When true, throw a syntax error if arguments are not separated by a comma.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def parse_parameters(self, require_commas: bool = True) -> list[Parameter]:
|
|
210
|
+
"""Parse zero or more parameters from the token stream.
|
|
211
|
+
|
|
212
|
+
A parameter is a Name with an optional default expression.
|
|
213
|
+
|
|
214
|
+
Assumes any leading commas have been consumed by the caller, if they are allowed.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
require_commas
|
|
219
|
+
When true, throw a syntax error if arguments are not separated by a comma.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def current(self) -> Token:
|
|
223
|
+
"""Return the current token without advancing the pointer."""
|
|
224
|
+
if self.pos < self.length:
|
|
225
|
+
return self.tokens[self.pos]
|
|
226
|
+
return self.eoi
|
|
227
|
+
|
|
228
|
+
def kind(self) -> TokenKind:
|
|
229
|
+
"""Return the kind of the current token without advancing the pointer."""
|
|
230
|
+
if self.pos < self.length:
|
|
231
|
+
return self.tokens[self.pos][0]
|
|
232
|
+
return TOKEN_EOI
|
|
233
|
+
|
|
234
|
+
def value(self) -> str:
|
|
235
|
+
"""Return the string value associated with the current token."""
|
|
236
|
+
if self.pos < self.length:
|
|
237
|
+
return token_value(self.tokens[self.pos], self.source)
|
|
238
|
+
return ""
|
|
239
|
+
|
|
240
|
+
def next(self) -> Token:
|
|
241
|
+
"""Return the current token and advance the pointer."""
|
|
242
|
+
if self.pos < self.length:
|
|
243
|
+
token = self.tokens[self.pos]
|
|
244
|
+
self.pos += 1
|
|
245
|
+
return token
|
|
246
|
+
return self.eoi
|
|
247
|
+
|
|
248
|
+
def peek(self, n: int = 1) -> Token:
|
|
249
|
+
"""Return the token at pos+n without advancing the pointer."""
|
|
250
|
+
if self.pos + n < self.length:
|
|
251
|
+
return self.tokens[self.pos + n]
|
|
252
|
+
return self.eoi
|
|
253
|
+
|
|
254
|
+
def peek_kind(self, n: int = 1) -> TokenKind:
|
|
255
|
+
"""Return the token kind at pos+n without advancing the pointer."""
|
|
256
|
+
if self.pos + n < self.length:
|
|
257
|
+
return self.tokens[self.pos + n][0]
|
|
258
|
+
return TOKEN_EOI
|
|
259
|
+
|
|
260
|
+
def eat(self, kind: TokenKind, message: str | None = None) -> Token:
|
|
261
|
+
"""Consume and return a token matching `kind`.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
kind
|
|
266
|
+
The expected type of the next token in the stream.
|
|
267
|
+
message
|
|
268
|
+
An custom error message to use in the event of a syntax error.
|
|
269
|
+
|
|
270
|
+
Raises
|
|
271
|
+
------
|
|
272
|
+
LiquidSyntaxError
|
|
273
|
+
If the current token's kind does not match `kind`.
|
|
274
|
+
"""
|
|
275
|
+
token = self.current()
|
|
276
|
+
if token[0] != kind:
|
|
277
|
+
raise LiquidSyntaxError(
|
|
278
|
+
message or f"unexpected {token_value(token, self.source)!r}",
|
|
279
|
+
token=token,
|
|
280
|
+
source=self.source,
|
|
281
|
+
name=self.template_name,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
self.pos += 1
|
|
285
|
+
return token
|
|
286
|
+
|
|
287
|
+
def eat_one_of(self, kinds: Collection[TokenKind]) -> Token:
|
|
288
|
+
token = self.next()
|
|
289
|
+
if token[0] not in kinds:
|
|
290
|
+
raise LiquidSyntaxError(
|
|
291
|
+
f"unexpected {TOKEN_NAMES[token[0]]!r}",
|
|
292
|
+
token=token,
|
|
293
|
+
source=self.source,
|
|
294
|
+
name=self.template_name,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return token
|
|
298
|
+
|
|
299
|
+
def skip(self, kind: int) -> bool:
|
|
300
|
+
if self.kind() == kind:
|
|
301
|
+
self.pos += 1
|
|
302
|
+
return True
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def carry_whitespace_control(self) -> None:
|
|
306
|
+
if self.kind() == TOKEN_WC:
|
|
307
|
+
self.pos += 1
|
|
308
|
+
self.whitespace_control_carry = "-"
|
|
309
|
+
else:
|
|
310
|
+
self.whitespace_control_carry = None
|
|
311
|
+
|
|
312
|
+
def eat_tag(self, tag_name: str) -> Token:
|
|
313
|
+
self.eat(TOKEN_TAG_START, message=f"expected tag {tag_name!r}")
|
|
314
|
+
|
|
315
|
+
if self.kind() == TOKEN_WC:
|
|
316
|
+
self.pos += 1
|
|
317
|
+
|
|
318
|
+
token = self.eat(TOKEN_TAG_NAME, message=f"expected tag {tag_name!r}")
|
|
319
|
+
value = token_value(token, self.source)
|
|
320
|
+
|
|
321
|
+
if not value == tag_name:
|
|
322
|
+
raise LiquidSyntaxError(
|
|
323
|
+
f"unexpected tag {value!r}",
|
|
324
|
+
token=token,
|
|
325
|
+
source=self.source,
|
|
326
|
+
name=self.template_name,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Ignore everything between the tag name and the closing tag delimiter.
|
|
330
|
+
while self.kind() not in TERMINATE_EXPRESSION:
|
|
331
|
+
self.pos += 1
|
|
332
|
+
|
|
333
|
+
self.carry_whitespace_control()
|
|
334
|
+
self.eat(TOKEN_TAG_END, message="expected a closing tag delimiter")
|
|
335
|
+
return token
|
|
336
|
+
|
|
337
|
+
def eat_tag_prefix(self, prefix: str) -> Token:
|
|
338
|
+
self.eat(TOKEN_TAG_START, message=f"expected tag {prefix!r}")
|
|
339
|
+
|
|
340
|
+
if self.kind() == TOKEN_WC:
|
|
341
|
+
self.pos += 1
|
|
342
|
+
|
|
343
|
+
token = self.eat(TOKEN_TAG_NAME, message=f"expected tag {prefix!r}")
|
|
344
|
+
value = token_value(token, self.source)
|
|
345
|
+
|
|
346
|
+
if not value.startswith(prefix):
|
|
347
|
+
raise LiquidSyntaxError(
|
|
348
|
+
f"unexpected tag {value!r}",
|
|
349
|
+
token=token,
|
|
350
|
+
source=self.source,
|
|
351
|
+
name=self.template_name,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Ignore everything between the tag name and the closing tag delimiter.
|
|
355
|
+
while self.kind() not in TERMINATE_EXPRESSION:
|
|
356
|
+
self.pos += 1
|
|
357
|
+
|
|
358
|
+
self.carry_whitespace_control()
|
|
359
|
+
self.eat(TOKEN_TAG_END, message="expected a closing tag delimiter")
|
|
360
|
+
return token
|
|
361
|
+
|
|
362
|
+
def eat_empty_tag(self, tag_name: str) -> Token:
|
|
363
|
+
self.eat(TOKEN_TAG_START, message=f"expected tag {tag_name!r}")
|
|
364
|
+
|
|
365
|
+
if self.kind() == TOKEN_WC:
|
|
366
|
+
self.pos += 1
|
|
367
|
+
|
|
368
|
+
token = self.eat(TOKEN_TAG_NAME, message=f"expected tag {tag_name!r}")
|
|
369
|
+
value = token_value(token, self.source)
|
|
370
|
+
|
|
371
|
+
if value != tag_name:
|
|
372
|
+
raise LiquidSyntaxError(
|
|
373
|
+
f"expected tag {tag_name!r}, found {value!r}",
|
|
374
|
+
token=token,
|
|
375
|
+
source=self.source,
|
|
376
|
+
name=self.template_name,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
self.carry_whitespace_control()
|
|
380
|
+
self.eat(TOKEN_TAG_END, message="expected a closing tag delimiter")
|
|
381
|
+
return token
|
|
382
|
+
|
|
383
|
+
def expect_expression(self) -> None:
|
|
384
|
+
if self.kind() in TERMINATE_EXPRESSION:
|
|
385
|
+
raise LiquidSyntaxError(
|
|
386
|
+
"missing expression",
|
|
387
|
+
token=self.current(),
|
|
388
|
+
source=self.source,
|
|
389
|
+
name=self.template_name,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def raw_expression(self) -> str:
|
|
393
|
+
"""Return source text up to but not including the next closing tag delimiter.
|
|
394
|
+
|
|
395
|
+
Calls ``carry_whitespace_control()`` and consumes the closing tag delimiter,
|
|
396
|
+
leaving the parser ready to parse the next block.
|
|
397
|
+
"""
|
|
398
|
+
start = self.current()
|
|
399
|
+
|
|
400
|
+
while self.kind() not in (TOKEN_WC, TOKEN_TAG_END, TOKEN_EOI):
|
|
401
|
+
self.pos += 1
|
|
402
|
+
|
|
403
|
+
end = self.tokens[self.pos - 1]
|
|
404
|
+
|
|
405
|
+
self.carry_whitespace_control()
|
|
406
|
+
self.eat(TOKEN_TAG_END)
|
|
407
|
+
|
|
408
|
+
if start == end:
|
|
409
|
+
return ""
|
|
410
|
+
|
|
411
|
+
return token_value(span(start, end), self.source)
|
|
412
|
+
|
|
413
|
+
def peek_tag_name(self) -> str:
|
|
414
|
+
token = self.current()
|
|
415
|
+
if token[0] == TOKEN_WC:
|
|
416
|
+
token = self.peek()
|
|
417
|
+
|
|
418
|
+
if token[0] != TOKEN_TAG_NAME:
|
|
419
|
+
raise LiquidSyntaxError(
|
|
420
|
+
"missing tag name",
|
|
421
|
+
token=token,
|
|
422
|
+
source=self.source,
|
|
423
|
+
name=self.template_name,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return token_value(token, self.source)
|
|
427
|
+
|
|
428
|
+
def peek_whitespace_control(self) -> str | None:
|
|
429
|
+
token = self.peek()
|
|
430
|
+
if token[0] == TOKEN_WC:
|
|
431
|
+
return token_value(token, self.source)
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
def skip_whitespace_control(self) -> None:
|
|
435
|
+
if self.kind() == TOKEN_WC:
|
|
436
|
+
self.pos += 1
|
|
437
|
+
|
|
438
|
+
def tag(self, name: str) -> bool:
|
|
439
|
+
"""Return true if we're at the start of a tag named `name`."""
|
|
440
|
+
token = self.peek()
|
|
441
|
+
if token[0] == TOKEN_WC:
|
|
442
|
+
token = self.peek(2)
|
|
443
|
+
|
|
444
|
+
return token[0] == TOKEN_TAG_NAME and token_value(token, self.source) == name
|
|
445
|
+
|
|
446
|
+
def tags(self, names: Collection[str]) -> str | None:
|
|
447
|
+
"""Return true if we're at the start of a tag and that tag's name is in `names`."""
|
|
448
|
+
token = self.peek()
|
|
449
|
+
if token[0] == TOKEN_WC:
|
|
450
|
+
token = self.peek(2)
|
|
451
|
+
|
|
452
|
+
if token[0] != TOKEN_TAG_NAME:
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
name = token_value(token, self.source)
|
|
456
|
+
|
|
457
|
+
if name in names:
|
|
458
|
+
return name
|
|
459
|
+
|
|
460
|
+
return None
|