messageformat2 0.1.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.
- messageformat2/__init__.py +5 -0
- messageformat2/builtins.py +323 -0
- messageformat2/cli.py +51 -0
- messageformat2/datamodel.py +507 -0
- messageformat2/errors.py +151 -0
- messageformat2/message.py +127 -0
- messageformat2/parser.py +557 -0
- messageformat2/runtime.py +301 -0
- messageformat2-0.1.0.dist-info/LICENSE +21 -0
- messageformat2-0.1.0.dist-info/METADATA +148 -0
- messageformat2-0.1.0.dist-info/RECORD +14 -0
- messageformat2-0.1.0.dist-info/WHEEL +5 -0
- messageformat2-0.1.0.dist-info/entry_points.txt +2 -0
- messageformat2-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from babel import Locale
|
|
4
|
+
|
|
5
|
+
from messageformat2.builtins import Formatter, Selector, default_registry
|
|
6
|
+
from messageformat2.datamodel import Node
|
|
7
|
+
from messageformat2.parser import parse
|
|
8
|
+
from messageformat2.runtime import format_message as _format_message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_message(
|
|
12
|
+
msg: str,
|
|
13
|
+
inputs: dict[str, Any] | None = None,
|
|
14
|
+
locale: Locale | str | None = None,
|
|
15
|
+
*,
|
|
16
|
+
formatters: dict[str, Formatter] | None = None,
|
|
17
|
+
selectors: dict[str, Selector] | None = None,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Format a message.
|
|
20
|
+
|
|
21
|
+
Use this function if you only need to format a message once.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
>>> message = Message("Hello, {$name}!")
|
|
25
|
+
>>> message.format({"name": "Alice"})
|
|
26
|
+
'Hello, Alice!'
|
|
27
|
+
>>> message = Message('''\\
|
|
28
|
+
... .match {$count :integer}
|
|
29
|
+
... one {{You have one notification}}
|
|
30
|
+
... * {{You have {$count} notifications}}''')
|
|
31
|
+
>>> message.format({"count": 3}, "en")
|
|
32
|
+
'You have 3 notifications'
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
msg: The message to format.
|
|
36
|
+
locale: The locale in which to format the message. Defaults to the system locale.
|
|
37
|
+
inputs: Input variables referenced by the message.
|
|
38
|
+
formatters: Additional formatters.
|
|
39
|
+
selectors: Additional selectors.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The formatted message.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ParseError: If the message contains a syntax error.
|
|
46
|
+
DataModelError: If the message contains a semantic error.
|
|
47
|
+
FormatError: If the message cannot be formatted.
|
|
48
|
+
"""
|
|
49
|
+
return Message(msg).format(inputs, locale, formatters=formatters, selectors=selectors)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Message:
|
|
53
|
+
def __init__(self, msg: str):
|
|
54
|
+
"""Create a new Message object that can be formatted.
|
|
55
|
+
|
|
56
|
+
Use this class if you need to format the same message multiple times as
|
|
57
|
+
the message is only parsed once.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
msg: The message.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ParseError: If the message contains a syntax error.
|
|
64
|
+
DataModelError: If the message contains a semantic error.
|
|
65
|
+
"""
|
|
66
|
+
self.msg = msg
|
|
67
|
+
self._ast = parse(msg)
|
|
68
|
+
|
|
69
|
+
def format(
|
|
70
|
+
self,
|
|
71
|
+
inputs: dict[str, Any] | None = None,
|
|
72
|
+
locale: Locale | str | None = None,
|
|
73
|
+
*,
|
|
74
|
+
formatters: dict[str, Formatter] | None = None,
|
|
75
|
+
selectors: dict[str, Selector] | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Format the message.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
>>> message = Message("Hello, {$name}!")
|
|
81
|
+
>>> message.format({"name": "Alice"})
|
|
82
|
+
'Hello, Alice!'
|
|
83
|
+
>>> message = Message('''\\
|
|
84
|
+
... .match {$count :integer}
|
|
85
|
+
... one {{You have one notification}}
|
|
86
|
+
... * {{You have {$count} notifications}}''')
|
|
87
|
+
>>> message.format({"count": 3}, "en")
|
|
88
|
+
'You have 3 notifications'
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
locale: The locale in which to format the message. Defaults to the system locale.
|
|
92
|
+
inputs: Input variables referenced by the message.
|
|
93
|
+
formatters: Additional formatters.
|
|
94
|
+
selectors: Additional selectors.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The formatted message.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
FormatError: If the message cannot be formatted.
|
|
101
|
+
"""
|
|
102
|
+
if locale is None:
|
|
103
|
+
locale = Locale.default()
|
|
104
|
+
elif not isinstance(locale, Locale):
|
|
105
|
+
locale = Locale.parse(locale)
|
|
106
|
+
if inputs is None:
|
|
107
|
+
inputs = {}
|
|
108
|
+
registry = (
|
|
109
|
+
default_registry
|
|
110
|
+
if not formatters and not selectors
|
|
111
|
+
else default_registry.extend(formatters=formatters, selectors=selectors)
|
|
112
|
+
)
|
|
113
|
+
return _format_message(self._ast, locale, inputs, registry)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def datamodel(self) -> Node:
|
|
117
|
+
"""Return the data model representation of the message.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
>>> message = Message("Hello, {$name}!")
|
|
121
|
+
>>> message.datamodel # doctest: +ELLIPSIS
|
|
122
|
+
PatternMessage(..., pattern=['Hello, ', VariableExpression(arg=VariableRef(name='name'), ...), '!'])
|
|
123
|
+
"""
|
|
124
|
+
return self._ast
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
return f"Message({self.msg})"
|
messageformat2/parser.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from messageformat2.datamodel import (
|
|
4
|
+
Attribute,
|
|
5
|
+
CatchallKey,
|
|
6
|
+
DataModelValidator,
|
|
7
|
+
Declaration,
|
|
8
|
+
Expression,
|
|
9
|
+
FunctionAnnotation,
|
|
10
|
+
FunctionExpression,
|
|
11
|
+
InputDeclaration,
|
|
12
|
+
Literal,
|
|
13
|
+
LiteralExpression,
|
|
14
|
+
LocalDeclaration,
|
|
15
|
+
Markup,
|
|
16
|
+
Message,
|
|
17
|
+
Option,
|
|
18
|
+
Pattern,
|
|
19
|
+
PatternMessage,
|
|
20
|
+
SelectMessage,
|
|
21
|
+
UnsupportedAnnotation,
|
|
22
|
+
UnsupportedExpression,
|
|
23
|
+
UnsupportedStatement,
|
|
24
|
+
VariableExpression,
|
|
25
|
+
VariableRef,
|
|
26
|
+
Variant,
|
|
27
|
+
_Matcher,
|
|
28
|
+
)
|
|
29
|
+
from messageformat2.errors import ParseError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_name_start = (
|
|
33
|
+
"^"
|
|
34
|
+
"[a-zA-Z_"
|
|
35
|
+
"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff"
|
|
36
|
+
"\u0370-\u037d\u037f-\u1fff\u200c-\u200d"
|
|
37
|
+
"\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff"
|
|
38
|
+
"\uf900-\ufdcf\ufdf0-\ufffd\U00010000-\U000effff]"
|
|
39
|
+
)
|
|
40
|
+
name_start = re.compile(_name_start)
|
|
41
|
+
name_char = re.compile(rf"{_name_start}|[0-9-.\u00B7\u0300-\u036F\u203F-\u2040]")
|
|
42
|
+
|
|
43
|
+
number_literal = re.compile(r"^-?(?:(?:0|[1-9])\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?")
|
|
44
|
+
|
|
45
|
+
_whitespace = r"[\s\u3000]"
|
|
46
|
+
whitespace = re.compile(f"^{_whitespace}")
|
|
47
|
+
|
|
48
|
+
text_escape = re.compile(r"^\\[\\{}]")
|
|
49
|
+
quoted_escape = re.compile(r"^\\[\\|]")
|
|
50
|
+
_reserved_escape = r"^\\[\\{|}]"
|
|
51
|
+
reserved_escape = re.compile(f"^{_reserved_escape}")
|
|
52
|
+
|
|
53
|
+
_content_char = (
|
|
54
|
+
"[\u0001-\u0008]|[\u000b-\u000c]|[\u000e-\u001f]|"
|
|
55
|
+
"[\u0021-\\\u002d]|" # \u002d is '-' so needs to be escaped
|
|
56
|
+
"[\u002f-\u003f]|[\u0041-\u005b]|"
|
|
57
|
+
"[\u005d-\u007a]|[\u007e-\u2fff]|[\u3001-\ud7ff]|[\ue000-\U0010ffff]"
|
|
58
|
+
)
|
|
59
|
+
content_char = re.compile(f"^{_content_char}")
|
|
60
|
+
|
|
61
|
+
simple_start_char = re.compile(rf"^{_content_char}|{_whitespace}|[@|]")
|
|
62
|
+
text_char = re.compile(rf"^{_content_char}|{_whitespace}|[.@|]")
|
|
63
|
+
quoted_char = re.compile(rf"^{_content_char}|{_whitespace}|[.@{{}}]")
|
|
64
|
+
_reserved_char = rf"{_content_char}|[.]"
|
|
65
|
+
reserved_char = re.compile(rf"^{_reserved_char}")
|
|
66
|
+
|
|
67
|
+
markup_start = re.compile(rf"{{{_whitespace}?[#/]")
|
|
68
|
+
annotation_start = re.compile(r"^[:^&!%*+<>?~]")
|
|
69
|
+
input_start = re.compile(r"^\.input")
|
|
70
|
+
local_start = re.compile(r"^\.local")
|
|
71
|
+
match_start = re.compile(r"^\.match")
|
|
72
|
+
quoted_pattern_start = re.compile(r"^\{\{")
|
|
73
|
+
quoted_pattern_end = re.compile(r"^\}\}")
|
|
74
|
+
reserved_body_part_start = re.compile(rf"^{_reserved_char}|{_reserved_escape}|\|")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Queue:
|
|
78
|
+
def __init__(self, text: str) -> None:
|
|
79
|
+
self.queue = text
|
|
80
|
+
|
|
81
|
+
def pop(self, char: str | None = None) -> str:
|
|
82
|
+
if not self.queue:
|
|
83
|
+
msg = "Unexpect end of input"
|
|
84
|
+
raise ParseError(msg)
|
|
85
|
+
|
|
86
|
+
s, rest = self.queue[0], self.queue[1:]
|
|
87
|
+
self.queue = rest
|
|
88
|
+
|
|
89
|
+
if char is not None and s != char:
|
|
90
|
+
msg = f"Expected: {char}, got: {s}"
|
|
91
|
+
raise ParseError(msg)
|
|
92
|
+
|
|
93
|
+
return s
|
|
94
|
+
|
|
95
|
+
def peek(self) -> str | None:
|
|
96
|
+
if self.queue:
|
|
97
|
+
return self.queue[0]
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def peek_after_whitespace(self) -> str | None:
|
|
101
|
+
if self.queue:
|
|
102
|
+
i = 0
|
|
103
|
+
while i < len(self.queue) and whitespace.match(self.queue[i]):
|
|
104
|
+
i += 1
|
|
105
|
+
return self.queue[i]
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def matches(self, pattern: re.Pattern) -> bool:
|
|
109
|
+
if self.queue:
|
|
110
|
+
return bool(pattern.match(self.queue))
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def matches_after_whitespace(self, pattern: re.Pattern) -> bool:
|
|
114
|
+
if self.queue:
|
|
115
|
+
i = 0
|
|
116
|
+
while i < len(self.queue) and whitespace.match(self.queue[i]):
|
|
117
|
+
i += 1
|
|
118
|
+
return bool(pattern.match(self.queue[i:]))
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
def pop_match(self, pattern: re.Pattern) -> str:
|
|
122
|
+
if self.queue:
|
|
123
|
+
match = pattern.match(self.queue)
|
|
124
|
+
if not match:
|
|
125
|
+
msg = f"Did not match: {self.queue[0]}"
|
|
126
|
+
raise ParseError(msg)
|
|
127
|
+
s, rest = match.group(), self.queue[match.end() :]
|
|
128
|
+
self.queue = rest
|
|
129
|
+
return s
|
|
130
|
+
msg = "Unexpected end of input"
|
|
131
|
+
raise ParseError(msg)
|
|
132
|
+
|
|
133
|
+
def __len__(self) -> int:
|
|
134
|
+
return len(self.queue)
|
|
135
|
+
|
|
136
|
+
def __bool__(self) -> bool:
|
|
137
|
+
return bool(self.queue)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def parse(msg: str) -> Message:
|
|
141
|
+
ast = parse_message(msg)
|
|
142
|
+
DataModelValidator().visit(ast)
|
|
143
|
+
return ast
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_message(msg: str) -> Message:
|
|
147
|
+
queue = Queue(msg)
|
|
148
|
+
message = parse_complex_message(queue) if queue.peek() == "." else parse_simple_message(queue)
|
|
149
|
+
if queue:
|
|
150
|
+
msg = f"Expected end of message but instead got: {queue.peek()}"
|
|
151
|
+
raise ParseError(msg)
|
|
152
|
+
return message
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_simple_message(queue: Queue) -> PatternMessage:
|
|
156
|
+
if not queue:
|
|
157
|
+
return PatternMessage(declarations=[], pattern=[])
|
|
158
|
+
simple_start = parse_simple_start(queue)
|
|
159
|
+
pattern = parse_pattern(queue)
|
|
160
|
+
|
|
161
|
+
if not pattern:
|
|
162
|
+
return PatternMessage(declarations=[], pattern=simple_start)
|
|
163
|
+
|
|
164
|
+
x = simple_start[-1]
|
|
165
|
+
y = pattern[0]
|
|
166
|
+
match (x, y):
|
|
167
|
+
case (str(), str()):
|
|
168
|
+
pattern = simple_start[:-1] + [x + y] + pattern[1:]
|
|
169
|
+
return PatternMessage(declarations=[], pattern=pattern)
|
|
170
|
+
case _:
|
|
171
|
+
return PatternMessage(declarations=[], pattern=simple_start + pattern)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def parse_complex_message(queue: Queue) -> Message:
|
|
175
|
+
declarations = []
|
|
176
|
+
while queue.peek() == "." and not queue.matches(match_start):
|
|
177
|
+
declarations.append(parse_declaration(queue))
|
|
178
|
+
parse_optional_whitespace(queue)
|
|
179
|
+
|
|
180
|
+
body = parse_complex_body(queue)
|
|
181
|
+
match body:
|
|
182
|
+
case _Matcher(selectors=selectors, variants=variants):
|
|
183
|
+
return SelectMessage(declarations=declarations, selectors=selectors, variants=variants)
|
|
184
|
+
case _:
|
|
185
|
+
return PatternMessage(declarations=declarations, pattern=body)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_declaration(queue: Queue) -> Declaration:
|
|
189
|
+
if queue.matches(input_start):
|
|
190
|
+
return parse_input_declaration(queue)
|
|
191
|
+
if queue.matches(local_start):
|
|
192
|
+
return parse_local_declaration(queue)
|
|
193
|
+
return parse_reserved_statement(queue)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def parse_input_declaration(queue: Queue) -> InputDeclaration:
|
|
197
|
+
queue.pop_match(input_start)
|
|
198
|
+
parse_optional_whitespace(queue)
|
|
199
|
+
queue.pop("{")
|
|
200
|
+
expression = parse_variable_expression(queue)
|
|
201
|
+
queue.pop("}")
|
|
202
|
+
return InputDeclaration(name=expression.arg.name, value=expression)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def parse_local_declaration(queue: Queue) -> LocalDeclaration:
|
|
206
|
+
queue.pop_match(local_start)
|
|
207
|
+
parse_whitespace(queue)
|
|
208
|
+
variable = parse_variable(queue)
|
|
209
|
+
parse_optional_whitespace(queue)
|
|
210
|
+
queue.pop("=")
|
|
211
|
+
parse_optional_whitespace(queue)
|
|
212
|
+
return LocalDeclaration(name=variable.name, value=parse_expression(queue))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def parse_complex_body(queue: Queue) -> _Matcher | Pattern:
|
|
216
|
+
if queue.matches(quoted_pattern_start):
|
|
217
|
+
return parse_quoted_pattern(queue)
|
|
218
|
+
return parse_matcher(queue)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_quoted_pattern(queue: Queue) -> Pattern:
|
|
222
|
+
queue.pop_match(quoted_pattern_start)
|
|
223
|
+
pattern = parse_pattern(queue)
|
|
224
|
+
queue.pop_match(quoted_pattern_end)
|
|
225
|
+
return pattern
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parse_matcher(queue: Queue) -> _Matcher:
|
|
229
|
+
queue.pop_match(match_start)
|
|
230
|
+
parse_optional_whitespace(queue)
|
|
231
|
+
selectors = [parse_selector(queue)]
|
|
232
|
+
while queue.peek_after_whitespace() == "{":
|
|
233
|
+
parse_optional_whitespace(queue)
|
|
234
|
+
selectors.append(parse_selector(queue))
|
|
235
|
+
|
|
236
|
+
parse_optional_whitespace(queue)
|
|
237
|
+
variants = [parse_variant(queue)]
|
|
238
|
+
|
|
239
|
+
while queue.peek_after_whitespace():
|
|
240
|
+
parse_optional_whitespace(queue)
|
|
241
|
+
variants.append(parse_variant(queue))
|
|
242
|
+
|
|
243
|
+
return _Matcher(selectors=selectors, variants=variants)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def parse_selector(queue: Queue) -> Expression:
|
|
247
|
+
return parse_expression(queue)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def parse_variant(queue: Queue) -> Variant:
|
|
251
|
+
keys = [parse_key(queue)]
|
|
252
|
+
|
|
253
|
+
while queue.peek_after_whitespace() and not queue.matches_after_whitespace(quoted_pattern_start):
|
|
254
|
+
parse_whitespace(queue)
|
|
255
|
+
keys.append(parse_key(queue))
|
|
256
|
+
|
|
257
|
+
parse_optional_whitespace(queue)
|
|
258
|
+
return Variant(keys=keys, value=parse_quoted_pattern(queue))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def parse_key(queue: Queue) -> Literal | CatchallKey:
|
|
262
|
+
if queue.peek() == "*":
|
|
263
|
+
queue.pop()
|
|
264
|
+
return CatchallKey(value=None)
|
|
265
|
+
return parse_literal(queue)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def parse_simple_start(queue: Queue) -> Pattern:
|
|
269
|
+
if queue.peek() == "{":
|
|
270
|
+
return [parse_placeholder(queue)]
|
|
271
|
+
if queue.matches(simple_start_char):
|
|
272
|
+
return [queue.pop()]
|
|
273
|
+
if queue.matches(text_escape):
|
|
274
|
+
text = queue.pop_match(text_escape)
|
|
275
|
+
text = text.replace(r"\{", "{").replace(r"\}", "}").replace(r"\\", "\\")
|
|
276
|
+
return [text]
|
|
277
|
+
msg = f"Invalid character: {queue.peek()}"
|
|
278
|
+
raise ParseError(msg)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def parse_pattern(queue: Queue) -> Pattern:
|
|
282
|
+
pattern = []
|
|
283
|
+
string = ""
|
|
284
|
+
while queue and not queue.matches(quoted_pattern_end):
|
|
285
|
+
if queue.matches(text_char):
|
|
286
|
+
string += queue.pop()
|
|
287
|
+
elif queue.matches(text_escape):
|
|
288
|
+
escape = queue.pop_match(text_escape)
|
|
289
|
+
string += escape.replace(r"\{", "{").replace(r"\}", "}").replace(r"\\", "\\")
|
|
290
|
+
else:
|
|
291
|
+
if string:
|
|
292
|
+
pattern.append(string)
|
|
293
|
+
string = ""
|
|
294
|
+
pattern.append(parse_placeholder(queue))
|
|
295
|
+
if string:
|
|
296
|
+
pattern.append(string)
|
|
297
|
+
string = ""
|
|
298
|
+
return pattern
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def parse_placeholder(queue: Queue) -> Markup | Expression:
|
|
302
|
+
if queue.matches(markup_start):
|
|
303
|
+
return parse_markup(queue)
|
|
304
|
+
return parse_expression(queue)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def parse_markup(queue: Queue) -> Markup:
|
|
308
|
+
queue.pop("{")
|
|
309
|
+
parse_optional_whitespace(queue)
|
|
310
|
+
|
|
311
|
+
type_ = queue.pop()
|
|
312
|
+
standalone = False
|
|
313
|
+
|
|
314
|
+
name = parse_identifier(queue)
|
|
315
|
+
options = []
|
|
316
|
+
attributes = []
|
|
317
|
+
|
|
318
|
+
while queue.peek_after_whitespace() not in {"/", "}"}:
|
|
319
|
+
parse_whitespace(queue)
|
|
320
|
+
if queue.peek() == "@":
|
|
321
|
+
attributes.append(parse_attribute(queue))
|
|
322
|
+
else:
|
|
323
|
+
options.append(parse_option(queue))
|
|
324
|
+
|
|
325
|
+
parse_optional_whitespace(queue)
|
|
326
|
+
|
|
327
|
+
if type_ == "#" and queue.peek() == "/":
|
|
328
|
+
standalone = True
|
|
329
|
+
queue.pop()
|
|
330
|
+
|
|
331
|
+
queue.pop("}")
|
|
332
|
+
kind = "standalone" if standalone else "open" if type_ == "#" else "close"
|
|
333
|
+
return Markup(kind=kind, name=name, options=options, attributes=attributes)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def parse_expression(queue: Queue) -> Expression:
|
|
337
|
+
queue.pop("{")
|
|
338
|
+
parse_optional_whitespace(queue)
|
|
339
|
+
|
|
340
|
+
if queue.peek() == "$":
|
|
341
|
+
expression = parse_variable_expression(queue)
|
|
342
|
+
elif queue.peek() == ":":
|
|
343
|
+
annotation = parse_function_annotation(queue)
|
|
344
|
+
attributes = []
|
|
345
|
+
while queue.peek_after_whitespace() != "}":
|
|
346
|
+
parse_whitespace(queue)
|
|
347
|
+
attributes.append(parse_attribute(queue))
|
|
348
|
+
expression = FunctionExpression(annotation=annotation, attributes=attributes)
|
|
349
|
+
elif queue.peek() in {"^", "&", "!", "%", "*", "+", "<", ">", "?", "~"}:
|
|
350
|
+
annotation = parse_unsupported_annotation(queue)
|
|
351
|
+
attributes = []
|
|
352
|
+
while queue.peek_after_whitespace() != "}":
|
|
353
|
+
parse_whitespace(queue)
|
|
354
|
+
attributes.append(parse_attribute(queue))
|
|
355
|
+
expression = UnsupportedExpression(annotation=annotation, attributes=attributes)
|
|
356
|
+
else:
|
|
357
|
+
literal = parse_literal(queue)
|
|
358
|
+
annotation = None
|
|
359
|
+
attributes = []
|
|
360
|
+
|
|
361
|
+
if queue.peek_after_whitespace() in {":", "^", "&", "!", "%", "*", "+", "<", ">", "?", "~"}:
|
|
362
|
+
parse_whitespace(queue)
|
|
363
|
+
annotation = parse_annotation(queue)
|
|
364
|
+
|
|
365
|
+
while queue.peek_after_whitespace() != "}":
|
|
366
|
+
parse_whitespace(queue)
|
|
367
|
+
attributes.append(parse_attribute(queue))
|
|
368
|
+
expression = LiteralExpression(arg=literal, annotation=annotation, attributes=attributes)
|
|
369
|
+
|
|
370
|
+
parse_optional_whitespace(queue)
|
|
371
|
+
queue.pop("}")
|
|
372
|
+
return expression
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def parse_variable_expression(queue: Queue) -> VariableExpression:
|
|
376
|
+
variable = parse_variable(queue)
|
|
377
|
+
annotation = None
|
|
378
|
+
attributes = []
|
|
379
|
+
|
|
380
|
+
if queue.peek_after_whitespace() in {":", "^", "&", "!", "%", "*", "+", "<", ">", "?", "~"}:
|
|
381
|
+
parse_whitespace(queue)
|
|
382
|
+
annotation = parse_annotation(queue)
|
|
383
|
+
|
|
384
|
+
while queue.peek_after_whitespace() != "}":
|
|
385
|
+
parse_whitespace(queue)
|
|
386
|
+
attributes.append(parse_attribute(queue))
|
|
387
|
+
return VariableExpression(arg=variable, annotation=annotation, attributes=attributes)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def parse_identifier(queue: Queue) -> str:
|
|
391
|
+
namespace = parse_namespace(queue)
|
|
392
|
+
if queue.peek() == ":":
|
|
393
|
+
queue.pop()
|
|
394
|
+
name = parse_name(queue)
|
|
395
|
+
return f"{namespace}:{name}"
|
|
396
|
+
return namespace
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def parse_namespace(queue: Queue) -> str:
|
|
400
|
+
return parse_name(queue)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def parse_name(queue: Queue) -> str:
|
|
404
|
+
if not queue.matches(name_start):
|
|
405
|
+
msg = f"Invalid name start: {queue.peek()}"
|
|
406
|
+
raise ParseError(msg)
|
|
407
|
+
name = queue.pop()
|
|
408
|
+
|
|
409
|
+
while queue.matches(name_char):
|
|
410
|
+
name += queue.pop()
|
|
411
|
+
return name
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def parse_annotation(queue: Queue) -> FunctionAnnotation | UnsupportedAnnotation:
|
|
415
|
+
if queue.peek() == ":":
|
|
416
|
+
return parse_function_annotation(queue)
|
|
417
|
+
return parse_unsupported_annotation(queue)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def parse_function_annotation(queue: Queue) -> FunctionAnnotation:
|
|
421
|
+
queue.pop(":")
|
|
422
|
+
name = parse_identifier(queue)
|
|
423
|
+
options = []
|
|
424
|
+
while queue.peek_after_whitespace() and queue.matches_after_whitespace(name_start):
|
|
425
|
+
parse_whitespace(queue)
|
|
426
|
+
options.append(parse_option(queue))
|
|
427
|
+
|
|
428
|
+
return FunctionAnnotation(name=name, options=options)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def parse_unsupported_annotation(queue: Queue) -> UnsupportedAnnotation:
|
|
432
|
+
source = queue.pop()
|
|
433
|
+
if queue.matches_after_whitespace(reserved_body_part_start):
|
|
434
|
+
source += parse_optional_whitespace(queue)
|
|
435
|
+
source += parse_reserved_body(queue)
|
|
436
|
+
return UnsupportedAnnotation(source=source)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def parse_reserved_statement(queue: Queue) -> UnsupportedStatement:
|
|
440
|
+
queue.pop(".")
|
|
441
|
+
keyword = parse_name(queue)
|
|
442
|
+
|
|
443
|
+
body = ""
|
|
444
|
+
if queue.matches_after_whitespace(reserved_body_part_start):
|
|
445
|
+
body += parse_whitespace(queue)
|
|
446
|
+
body += parse_reserved_body(queue)
|
|
447
|
+
|
|
448
|
+
parse_optional_whitespace(queue)
|
|
449
|
+
expressions = [parse_expression(queue)]
|
|
450
|
+
|
|
451
|
+
while queue.peek_after_whitespace() == "{":
|
|
452
|
+
parse_optional_whitespace(queue)
|
|
453
|
+
expressions.append(parse_expression(queue))
|
|
454
|
+
|
|
455
|
+
return UnsupportedStatement(keyword=keyword, body=body or None, expressions=expressions)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def parse_reserved_body(queue: Queue) -> str:
|
|
459
|
+
body = parse_reserved_body_part(queue)
|
|
460
|
+
while queue.matches_after_whitespace(reserved_body_part_start):
|
|
461
|
+
body += parse_optional_whitespace(queue)
|
|
462
|
+
body += parse_reserved_body_part(queue)
|
|
463
|
+
return body
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def parse_reserved_body_part(queue: Queue) -> str:
|
|
467
|
+
part = ""
|
|
468
|
+
while True:
|
|
469
|
+
if queue.matches(reserved_char):
|
|
470
|
+
part += queue.pop()
|
|
471
|
+
elif queue.matches(reserved_escape):
|
|
472
|
+
part += queue.pop_match(reserved_escape)
|
|
473
|
+
elif queue.peek() == "|":
|
|
474
|
+
part += str(parse_quoted_literal(queue))
|
|
475
|
+
else:
|
|
476
|
+
break
|
|
477
|
+
return part
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def parse_attribute(queue: Queue) -> Attribute:
|
|
481
|
+
queue.pop("@")
|
|
482
|
+
name = parse_identifier(queue)
|
|
483
|
+
|
|
484
|
+
if queue.peek_after_whitespace() != "=":
|
|
485
|
+
return Attribute(name=name, value=None)
|
|
486
|
+
|
|
487
|
+
parse_optional_whitespace(queue)
|
|
488
|
+
queue.pop() # '='
|
|
489
|
+
parse_optional_whitespace(queue)
|
|
490
|
+
|
|
491
|
+
value = parse_variable(queue) if queue.peek() == "$" else parse_literal(queue)
|
|
492
|
+
return Attribute(name=name, value=value)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def parse_option(queue: Queue) -> Option:
|
|
496
|
+
name = parse_identifier(queue)
|
|
497
|
+
|
|
498
|
+
parse_optional_whitespace(queue)
|
|
499
|
+
if queue.peek() != "=":
|
|
500
|
+
msg = f"Invalid option: {name}"
|
|
501
|
+
raise ParseError(msg)
|
|
502
|
+
queue.pop()
|
|
503
|
+
parse_optional_whitespace(queue)
|
|
504
|
+
|
|
505
|
+
value = parse_variable(queue) if queue.peek() == "$" else parse_literal(queue)
|
|
506
|
+
return Option(name=name, value=value)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def parse_variable(queue: Queue) -> VariableRef:
|
|
510
|
+
if queue.peek() != "$":
|
|
511
|
+
msg = f"Invalid variable: {queue.peek()}"
|
|
512
|
+
raise ParseError(msg)
|
|
513
|
+
queue.pop()
|
|
514
|
+
return VariableRef(name=parse_name(queue))
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def parse_literal(queue: Queue) -> Literal:
|
|
518
|
+
return parse_quoted_literal(queue) if queue.peek() == "|" else parse_unquoted_literal(queue)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def parse_quoted_literal(queue: Queue) -> Literal:
|
|
522
|
+
literal = ""
|
|
523
|
+
queue.pop("|")
|
|
524
|
+
while True:
|
|
525
|
+
if queue.matches(quoted_escape):
|
|
526
|
+
escape = queue.pop_match(quoted_escape)
|
|
527
|
+
if escape == r"\\":
|
|
528
|
+
literal += "\\"
|
|
529
|
+
else:
|
|
530
|
+
literal += "|"
|
|
531
|
+
elif queue.matches(quoted_char):
|
|
532
|
+
literal += queue.pop()
|
|
533
|
+
else:
|
|
534
|
+
break
|
|
535
|
+
|
|
536
|
+
queue.pop("|")
|
|
537
|
+
return Literal(value=literal)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def parse_unquoted_literal(queue: Queue) -> Literal:
|
|
541
|
+
if queue.matches(number_literal):
|
|
542
|
+
return Literal(value=queue.pop_match(number_literal))
|
|
543
|
+
return Literal(value=parse_name(queue))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def parse_whitespace(queue: Queue) -> str:
|
|
547
|
+
if not queue.matches(whitespace):
|
|
548
|
+
msg = f"Expected whitespace: {queue.peek()}"
|
|
549
|
+
raise ParseError(msg)
|
|
550
|
+
return parse_optional_whitespace(queue)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def parse_optional_whitespace(queue: Queue) -> str:
|
|
554
|
+
ws = ""
|
|
555
|
+
while queue.matches(whitespace):
|
|
556
|
+
ws += queue.pop()
|
|
557
|
+
return ws
|