python-fragments 0.1__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.
- fragments/__init__.py +0 -0
- fragments/ast_nodes.py +237 -0
- fragments/cli.py +20 -0
- fragments/grammar.py +206 -0
- fragments/html/__init__.py +0 -0
- fragments/html/elements.py +54 -0
- fragments/loader.py +32 -0
- fragments/lsp/__init__.py +0 -0
- fragments/lsp/completion.py +79 -0
- fragments/lsp/definition.py +49 -0
- fragments/lsp/file_state.py +109 -0
- fragments/lsp/hover.py +44 -0
- fragments/lsp/lifecycle.py +116 -0
- fragments/lsp/pyright.py +94 -0
- fragments/lsp/rename.py +106 -0
- fragments/lsp/semantic_tokens.py +42 -0
- fragments/lsp/server.py +129 -0
- fragments/source.py +26 -0
- fragments/transpiler.py +10 -0
- python_fragments-0.1.dist-info/METADATA +55 -0
- python_fragments-0.1.dist-info/RECORD +24 -0
- python_fragments-0.1.dist-info/WHEEL +5 -0
- python_fragments-0.1.dist-info/entry_points.txt +3 -0
- python_fragments-0.1.dist-info/top_level.txt +1 -0
fragments/__init__.py
ADDED
|
File without changes
|
fragments/ast_nodes.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(slots=True)
|
|
6
|
+
class ASTModule:
|
|
7
|
+
source_start: int = field(compare=False)
|
|
8
|
+
source_end: int = field(compare=False)
|
|
9
|
+
|
|
10
|
+
children: list["ASTPython | ASTFragment"]
|
|
11
|
+
|
|
12
|
+
transpiled_content: str = field(init=False)
|
|
13
|
+
transpiled_start: int = field(init=False)
|
|
14
|
+
transpiled_end: int = field(init=False)
|
|
15
|
+
|
|
16
|
+
__template__: str = "from fragments.html.elements import el, sequence, comment\n{}"
|
|
17
|
+
|
|
18
|
+
def transpile(self, transpiled_start: int = 0) -> None:
|
|
19
|
+
self.transpiled_start = transpiled_start
|
|
20
|
+
transpiled_start += len(self.__template__) - 2
|
|
21
|
+
for child in self.children:
|
|
22
|
+
child.transpile(transpiled_start)
|
|
23
|
+
transpiled_start = child.transpiled_end
|
|
24
|
+
|
|
25
|
+
children: str = "".join(child.transpiled_content for child in self.children)
|
|
26
|
+
self.transpiled_content = self.__template__.format(children)
|
|
27
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class ASTPython:
|
|
32
|
+
source_start: int = field(compare=False)
|
|
33
|
+
source_end: int = field(compare=False)
|
|
34
|
+
content: str
|
|
35
|
+
|
|
36
|
+
transpiled_content: str = field(init=False)
|
|
37
|
+
transpiled_start: int = field(init=False)
|
|
38
|
+
transpiled_end: int = field(init=False)
|
|
39
|
+
|
|
40
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
41
|
+
self.transpiled_start = transpiled_start
|
|
42
|
+
self.transpiled_content = self.content
|
|
43
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class ASTFragment:
|
|
48
|
+
source_start: int = field(compare=False)
|
|
49
|
+
source_end: int = field(compare=False)
|
|
50
|
+
|
|
51
|
+
children: list["ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation"]
|
|
52
|
+
|
|
53
|
+
transpiled_content: str = field(init=False)
|
|
54
|
+
transpiled_start: int = field(init=False)
|
|
55
|
+
transpiled_end: int = field(init=False)
|
|
56
|
+
|
|
57
|
+
__template__: str = """sequence([{}])"""
|
|
58
|
+
|
|
59
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
60
|
+
self.transpiled_start = transpiled_start
|
|
61
|
+
transpiled_start += 10
|
|
62
|
+
for child in self.children:
|
|
63
|
+
child.transpile(transpiled_start)
|
|
64
|
+
transpiled_start = child.transpiled_end + 1
|
|
65
|
+
transpiled_start -= 1
|
|
66
|
+
|
|
67
|
+
self.transpiled_content = self.__template__.format(",".join(child.transpiled_content for child in self.children))
|
|
68
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(slots=True)
|
|
72
|
+
class ASTHTMLElement:
|
|
73
|
+
source_start: int = field(compare=False)
|
|
74
|
+
source_end: int = field(compare=False)
|
|
75
|
+
|
|
76
|
+
name: str
|
|
77
|
+
attributes: dict[str, "ASTHTMLAttribute"]
|
|
78
|
+
if_attribute: "ASTInterpolation | None"
|
|
79
|
+
for_attribute: "ASTInterpolation | None"
|
|
80
|
+
children: Sequence["ASTHTMLElement | ASTHTMLText | ASTInterpolation"]
|
|
81
|
+
one_line: bool
|
|
82
|
+
|
|
83
|
+
transpiled_content: str = field(init=False)
|
|
84
|
+
transpiled_start: int = field(init=False)
|
|
85
|
+
transpiled_end: int = field(init=False)
|
|
86
|
+
|
|
87
|
+
__for_template__: str = """sequence([{} for {}])"""
|
|
88
|
+
__if_template__: str = """{} if {} else ''"""
|
|
89
|
+
__element_template__: str = """el("{}",[{}],oneline={},{})"""
|
|
90
|
+
__component_template__: str = """{}([{}],{})"""
|
|
91
|
+
|
|
92
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
93
|
+
self.transpiled_start = transpiled_start
|
|
94
|
+
|
|
95
|
+
if self.for_attribute is not None:
|
|
96
|
+
transpiled_start += 10
|
|
97
|
+
|
|
98
|
+
if self.name[0].capitalize() == self.name[0]:
|
|
99
|
+
self._transpile_component_call(transpiled_start)
|
|
100
|
+
else:
|
|
101
|
+
self._transpile_element_call(transpiled_start)
|
|
102
|
+
|
|
103
|
+
if self.if_attribute is not None:
|
|
104
|
+
self.if_attribute.transpile(self.transpiled_end + 4)
|
|
105
|
+
self.transpiled_content = self.__if_template__.format(self.transpiled_content, self.if_attribute.transpiled_content)
|
|
106
|
+
self.transpiled_end = self.if_attribute.transpiled_end + 8
|
|
107
|
+
elif self.for_attribute is not None:
|
|
108
|
+
self.for_attribute.transpile(self.transpiled_end + 5)
|
|
109
|
+
self.transpiled_content = self.__for_template__.format(self.transpiled_content, self.for_attribute.transpiled_content)
|
|
110
|
+
self.transpiled_end = self.for_attribute.transpiled_end + 2
|
|
111
|
+
|
|
112
|
+
def _transpile_element_call(self, start: int) -> None:
|
|
113
|
+
transpiled_start = start + len(self.name) + 7
|
|
114
|
+
for child in self.children:
|
|
115
|
+
child.transpile(transpiled_start)
|
|
116
|
+
transpiled_start = child.transpiled_end + 1
|
|
117
|
+
transpiled_start -= 1
|
|
118
|
+
children = ",".join(child.transpiled_content for child in self.children)
|
|
119
|
+
|
|
120
|
+
oneline_offset = len(str(self.one_line))
|
|
121
|
+
|
|
122
|
+
transpiled_start += 10 + oneline_offset + 1
|
|
123
|
+
|
|
124
|
+
for attribute in self.attributes.values():
|
|
125
|
+
attribute.transpile(transpiled_start)
|
|
126
|
+
transpiled_start = attribute.transpiled_end + 1
|
|
127
|
+
transpiled_start -= 1
|
|
128
|
+
|
|
129
|
+
attributes = ",".join(attribute.transpiled_content for attribute in self.attributes.values())
|
|
130
|
+
self.transpiled_content = self.__element_template__.format(self.name, children, self.one_line, attributes)
|
|
131
|
+
self.transpiled_end = start + len(self.transpiled_content)
|
|
132
|
+
|
|
133
|
+
def _transpile_component_call(self, start: int) -> None:
|
|
134
|
+
transpiled_start = start + len(self.name) + 2
|
|
135
|
+
for child in self.children:
|
|
136
|
+
child.transpile(transpiled_start)
|
|
137
|
+
transpiled_start = child.transpiled_end + 1
|
|
138
|
+
transpiled_start -= 1
|
|
139
|
+
children = ",".join(child.transpiled_content for child in self.children)
|
|
140
|
+
|
|
141
|
+
if len(self.attributes) == 0:
|
|
142
|
+
self.transpiled_content = self.__component_template__.format(self.name, children, "")
|
|
143
|
+
self.transpiled_end = start + len(self.transpiled_content)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
transpiled_start += 2
|
|
147
|
+
for attribute in self.attributes.values():
|
|
148
|
+
attribute.transpile(transpiled_start)
|
|
149
|
+
transpiled_start = attribute.transpiled_end + 1
|
|
150
|
+
|
|
151
|
+
attributes = ",".join(attribute.transpiled_content for attribute in self.attributes.values())
|
|
152
|
+
self.transpiled_content = self.__component_template__.format(self.name, children, attributes)
|
|
153
|
+
self.transpiled_end = start + len(self.transpiled_content)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(slots=True)
|
|
157
|
+
class ASTHTMLComment:
|
|
158
|
+
source_start: int = field(compare=False)
|
|
159
|
+
source_end: int = field(compare=False)
|
|
160
|
+
|
|
161
|
+
content: str
|
|
162
|
+
|
|
163
|
+
transpiled_content: str = field(init=False)
|
|
164
|
+
transpiled_start: int = field(init=False)
|
|
165
|
+
transpiled_end: int = field(init=False)
|
|
166
|
+
|
|
167
|
+
__template__: str = """comment("{}")"""
|
|
168
|
+
|
|
169
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
170
|
+
self.transpiled_start = transpiled_start
|
|
171
|
+
self.transpiled_content = self.__template__.format(self.content)
|
|
172
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(slots=True)
|
|
176
|
+
class ASTHTMLAttribute:
|
|
177
|
+
source_start: int = field(compare=False)
|
|
178
|
+
source_end: int = field(compare=False)
|
|
179
|
+
|
|
180
|
+
name: str
|
|
181
|
+
value: str | None
|
|
182
|
+
interpolation: "ASTInterpolation | None"
|
|
183
|
+
|
|
184
|
+
transpiled_content: str = field(init=False)
|
|
185
|
+
transpiled_start: int = field(init=False)
|
|
186
|
+
transpiled_end: int = field(init=False)
|
|
187
|
+
|
|
188
|
+
__value_template__: str = '{}="{}"'
|
|
189
|
+
__interpolation_template__: str = "{}={}"
|
|
190
|
+
|
|
191
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
192
|
+
self.transpiled_start = transpiled_start
|
|
193
|
+
|
|
194
|
+
if self.value is not None:
|
|
195
|
+
self.transpiled_content = self.__value_template__.format(self.name, self.value)
|
|
196
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
assert self.interpolation is not None
|
|
200
|
+
self.interpolation.transpile(self.transpiled_start + len(self.name) + 1)
|
|
201
|
+
self.transpiled_content = self.__interpolation_template__.format(self.name, self.interpolation.transpiled_content)
|
|
202
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(slots=True)
|
|
206
|
+
class ASTHTMLText:
|
|
207
|
+
source_start: int = field(compare=False)
|
|
208
|
+
source_end: int = field(compare=False)
|
|
209
|
+
|
|
210
|
+
text: str
|
|
211
|
+
|
|
212
|
+
transpiled_content: str = field(init=False)
|
|
213
|
+
transpiled_start: int = field(init=False)
|
|
214
|
+
transpiled_end: int = field(init=False)
|
|
215
|
+
|
|
216
|
+
__template__: str = '"{}"'
|
|
217
|
+
|
|
218
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
219
|
+
self.transpiled_content = self.__template__.format(self.text)
|
|
220
|
+
self.transpiled_start = transpiled_start
|
|
221
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(slots=True)
|
|
225
|
+
class ASTInterpolation:
|
|
226
|
+
source_start: int = field(compare=False)
|
|
227
|
+
source_end: int = field(compare=False)
|
|
228
|
+
expression: str
|
|
229
|
+
|
|
230
|
+
transpiled_content: str = field(init=False)
|
|
231
|
+
transpiled_start: int = field(init=False)
|
|
232
|
+
transpiled_end: int = field(init=False)
|
|
233
|
+
|
|
234
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
235
|
+
self.transpiled_content = self.expression
|
|
236
|
+
self.transpiled_start = transpiled_start
|
|
237
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
fragments/cli.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from fragments import transpiler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_args() -> argparse.Namespace:
|
|
7
|
+
parser = argparse.ArgumentParser("fragments", description="Fragments transpilation CLI tool")
|
|
8
|
+
parser.add_argument("--input", type=str, required=True)
|
|
9
|
+
return parser.parse_args()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
args = parse_args()
|
|
14
|
+
with open(args.input, "r") as f:
|
|
15
|
+
source = f.read()
|
|
16
|
+
print(transpiler.transpile(source))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
fragments/grammar.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from re import Match
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from fragments.ast_nodes import ASTFragment, ASTHTMLAttribute, ASTHTMLComment, ASTHTMLElement, ASTHTMLText, ASTInterpolation, ASTModule, ASTPython
|
|
5
|
+
from fragments.source import Source
|
|
6
|
+
|
|
7
|
+
PYTHON = r"([\s\S]*?)(?=<>)|[\s\S]*$"
|
|
8
|
+
IDENTIFIER = r"[a-zA-Z_][a-zA-Z0-9_]*"
|
|
9
|
+
STRING_CONTENTS = r"(.*?)(?=\")"
|
|
10
|
+
INTERPOLATION_EXPRESSION = r"(.*?)(?= }})"
|
|
11
|
+
HTML_IDENTIFIER = r"[a-zA-Z][a-zA-Z0-9_-]*"
|
|
12
|
+
HTML_TEXT = r"(.*?)(?=<)"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ParsingError(Exception):
|
|
16
|
+
def __init__(self, message: str, source_start: int) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.source_start: int = source_start
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def optional_regex(source: Source, pattern: str) -> tuple[Source, str | None]:
|
|
22
|
+
"""Expect the source to start with the given pattern, return the source after the pattern and the found match (if any)."""
|
|
23
|
+
result: Match[str] | None = re.match(pattern, source.remaining())
|
|
24
|
+
if result is None:
|
|
25
|
+
return source, None
|
|
26
|
+
return source.eat(result.end()), result.group(0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def expect_regex(source: Source, pattern: str, label: str) -> tuple[Source, str]:
|
|
30
|
+
"""Expect the source to start with the given pattern, return the source after the pattern and the found match."""
|
|
31
|
+
source, result = optional_regex(source, pattern)
|
|
32
|
+
if result is None:
|
|
33
|
+
raise ParsingError(f"Expected {label}", source.offset)
|
|
34
|
+
return source, result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def expect_string(source: Source, string: str) -> Source:
|
|
38
|
+
"""Expect the source to start with the given string, return the source after the string."""
|
|
39
|
+
if not source.remaining().startswith(string):
|
|
40
|
+
raise ParsingError(f"Expected {string}", source.offset)
|
|
41
|
+
return source.eat(len(string))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def expect_module(source: Source) -> tuple[Source, ASTModule]:
|
|
45
|
+
"""The top level of the recursive descent grammar."""
|
|
46
|
+
children: list["ASTPython | ASTFragment"] = []
|
|
47
|
+
source_start: int = source.offset
|
|
48
|
+
while not source.at_end():
|
|
49
|
+
if source.remaining().startswith("<>"):
|
|
50
|
+
source, fragment = expect_fragment(source)
|
|
51
|
+
children.append(fragment)
|
|
52
|
+
else:
|
|
53
|
+
source, python = expect_python(source)
|
|
54
|
+
children.append(python)
|
|
55
|
+
return source, ASTModule(source_start, source.offset, children)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def expect_python(source: Source) -> tuple[Source, ASTPython]:
|
|
59
|
+
"""Vanilla Python code."""
|
|
60
|
+
source_start: int = source.offset
|
|
61
|
+
source, python = expect_regex(source, PYTHON, "python source")
|
|
62
|
+
return source, ASTPython(source_start, source.offset, python)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def expect_fragment(source: Source) -> tuple[Source, ASTFragment]:
|
|
66
|
+
"""A fragment - the top level of things created by this library."""
|
|
67
|
+
source_start: int = source.offset
|
|
68
|
+
source = expect_string(source, "<>")
|
|
69
|
+
|
|
70
|
+
children: list[ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation] = []
|
|
71
|
+
while not source.remaining().startswith("</>"):
|
|
72
|
+
source, _ = source.eat_whitespace()
|
|
73
|
+
if source.remaining().startswith("</>"):
|
|
74
|
+
break
|
|
75
|
+
source, child = expect_expression(source)
|
|
76
|
+
children.append(child)
|
|
77
|
+
|
|
78
|
+
source, _ = source.eat_whitespace()
|
|
79
|
+
source = expect_string(source, "</>")
|
|
80
|
+
source_end: int = source.offset
|
|
81
|
+
|
|
82
|
+
return source, ASTFragment(source_start, source_end, children)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def expect_expression(source: Source) -> tuple[Source, ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation]:
|
|
86
|
+
"""Any HTML / functional block that might appear as part of the fragment."""
|
|
87
|
+
if source.remaining().startswith("<!--"):
|
|
88
|
+
source, html_comment = expect_html_comment(source)
|
|
89
|
+
return source, html_comment
|
|
90
|
+
|
|
91
|
+
if source.remaining().startswith("<"):
|
|
92
|
+
source, html_element = expect_html_element(source)
|
|
93
|
+
return source, html_element
|
|
94
|
+
|
|
95
|
+
if source.remaining().startswith("{{"):
|
|
96
|
+
source, interpolation = expect_interpolation(source)
|
|
97
|
+
return source, interpolation
|
|
98
|
+
|
|
99
|
+
source, html_text = expect_html_text(source)
|
|
100
|
+
return source, html_text
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def expect_html_comment(source: Source) -> tuple[Source, ASTHTMLComment]:
|
|
104
|
+
source_start = source.offset
|
|
105
|
+
source = expect_string(source, "<!--")
|
|
106
|
+
source, content = expect_regex(source, r"[\s\S]*?(?=-->)", "comment content")
|
|
107
|
+
source = expect_string(source, "-->")
|
|
108
|
+
return source, ASTHTMLComment(source_start, source.offset, content)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def expect_html_element(source: Source) -> tuple[Source, ASTHTMLElement]:
|
|
112
|
+
"""An HTML element that may appear in the fragment tree."""
|
|
113
|
+
element_source_start = source.offset
|
|
114
|
+
source = expect_string(source, "<")
|
|
115
|
+
source, _ = source.eat_whitespace()
|
|
116
|
+
source, name = expect_regex(source, HTML_IDENTIFIER, "element name")
|
|
117
|
+
source, _ = source.eat_whitespace()
|
|
118
|
+
|
|
119
|
+
attributes: dict[str, ASTHTMLAttribute] = {}
|
|
120
|
+
while not (source.remaining().startswith(">") or source.remaining().startswith("/>")):
|
|
121
|
+
attribute_source_start = source.offset
|
|
122
|
+
source, attribute_name = expect_regex(source, HTML_IDENTIFIER, "attribute name")
|
|
123
|
+
if not source.remaining().startswith("="):
|
|
124
|
+
attribute = ASTHTMLAttribute(attribute_source_start, source.offset, attribute_name, None, None)
|
|
125
|
+
attributes[attribute_name] = attribute
|
|
126
|
+
continue
|
|
127
|
+
source = expect_string(source, "=")
|
|
128
|
+
|
|
129
|
+
if source.remaining().startswith('"'):
|
|
130
|
+
source = expect_string(source, '"')
|
|
131
|
+
source, attribute_value = expect_regex(source, STRING_CONTENTS, "attribute value")
|
|
132
|
+
source = expect_string(source, '"')
|
|
133
|
+
attribute = ASTHTMLAttribute(attribute_source_start, source.offset, attribute_name, attribute_value, None)
|
|
134
|
+
attributes[attribute_name] = attribute
|
|
135
|
+
elif source.remaining().startswith("{{"):
|
|
136
|
+
source, interpolation = expect_interpolation(source)
|
|
137
|
+
attribute = ASTHTMLAttribute(attribute_source_start, source.offset, attribute_name, None, interpolation)
|
|
138
|
+
attributes[attribute_name] = attribute
|
|
139
|
+
|
|
140
|
+
source, _ = source.eat_whitespace()
|
|
141
|
+
|
|
142
|
+
if_attribute = attributes.pop("if") if "if" in attributes else None
|
|
143
|
+
for_attribute = attributes.pop("for") if "for" in attributes else None
|
|
144
|
+
|
|
145
|
+
if source.remaining().startswith("/>"):
|
|
146
|
+
source = expect_string(source, "/>")
|
|
147
|
+
html_element = ASTHTMLElement(
|
|
148
|
+
element_source_start,
|
|
149
|
+
source.offset,
|
|
150
|
+
name,
|
|
151
|
+
attributes,
|
|
152
|
+
if_attribute.interpolation if if_attribute is not None else None,
|
|
153
|
+
for_attribute.interpolation if for_attribute is not None else None,
|
|
154
|
+
[],
|
|
155
|
+
True,
|
|
156
|
+
)
|
|
157
|
+
return source, html_element
|
|
158
|
+
|
|
159
|
+
source = expect_string(source, ">")
|
|
160
|
+
source, _ = source.eat_whitespace()
|
|
161
|
+
|
|
162
|
+
children: list[ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation] = []
|
|
163
|
+
while not source.remaining().startswith("</"):
|
|
164
|
+
source, child = expect_expression(source)
|
|
165
|
+
children.append(child)
|
|
166
|
+
source, _ = source.eat_whitespace()
|
|
167
|
+
|
|
168
|
+
source = expect_string(source, "</")
|
|
169
|
+
source, _ = source.eat_whitespace()
|
|
170
|
+
source, closing_name = expect_regex(source, IDENTIFIER, "element name")
|
|
171
|
+
|
|
172
|
+
if name != closing_name:
|
|
173
|
+
raise ParsingError(f"Element closed ({closing_name!r}) is not the same as currently opened element ({name!r})", source.offset)
|
|
174
|
+
|
|
175
|
+
source, _ = source.eat_whitespace()
|
|
176
|
+
source = expect_string(source, ">")
|
|
177
|
+
|
|
178
|
+
html_element = ASTHTMLElement(
|
|
179
|
+
element_source_start,
|
|
180
|
+
source.offset,
|
|
181
|
+
name,
|
|
182
|
+
attributes,
|
|
183
|
+
if_attribute.interpolation if if_attribute is not None else None,
|
|
184
|
+
for_attribute.interpolation if for_attribute is not None else None,
|
|
185
|
+
children,
|
|
186
|
+
False,
|
|
187
|
+
)
|
|
188
|
+
return source, html_element
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def expect_interpolation(source: Source) -> tuple[Source, ASTInterpolation]:
|
|
192
|
+
"""An interpolation block."""
|
|
193
|
+
source_start = source.offset
|
|
194
|
+
source = expect_string(source, "{{")
|
|
195
|
+
source, _ = source.eat_whitespace()
|
|
196
|
+
source, expression = expect_regex(source, INTERPOLATION_EXPRESSION, "expression")
|
|
197
|
+
source, _ = source.eat_whitespace()
|
|
198
|
+
source = expect_string(source, "}}")
|
|
199
|
+
return source, ASTInterpolation(source_start, source.offset, expression)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def expect_html_text(source: Source) -> tuple[Source, ASTHTMLText]:
|
|
203
|
+
"""Text as the child of an HTML expression."""
|
|
204
|
+
source_start = source.offset
|
|
205
|
+
source, text = expect_regex(source, HTML_TEXT, "text")
|
|
206
|
+
return source, ASTHTMLText(source_start, source.offset, text)
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sequence(children: list[str]) -> str:
|
|
5
|
+
return "".join(children)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def el(
|
|
9
|
+
name: str,
|
|
10
|
+
children: list[str],
|
|
11
|
+
oneline: bool,
|
|
12
|
+
**attributes: dict[str, Any],
|
|
13
|
+
) -> str:
|
|
14
|
+
tag_contents = [
|
|
15
|
+
name,
|
|
16
|
+
classes_to_string(attributes.pop("classes")) if "classes" in attributes else None,
|
|
17
|
+
style_to_string(attributes.pop("style")) if "style" in attributes else None,
|
|
18
|
+
attributes_to_string(attributes) if attributes is not None else None,
|
|
19
|
+
]
|
|
20
|
+
tag_contents = [item for item in tag_contents if item is not None]
|
|
21
|
+
tag_contents_string = " ".join(tag_contents)
|
|
22
|
+
|
|
23
|
+
if oneline:
|
|
24
|
+
return f"""<{tag_contents_string} />"""
|
|
25
|
+
|
|
26
|
+
children_string = "".join(str(child) for child in children)
|
|
27
|
+
|
|
28
|
+
return f"""<{tag_contents_string}>{children_string}</{name}>"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def comment(content: str) -> str:
|
|
32
|
+
return f"<!-- {content} -->"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def attributes_to_string(attributes: dict[str, Any]) -> str:
|
|
36
|
+
return " ".join("=".join([key, f'"{value}"']) if value is not None else key for key, value in attributes.items())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def classes_to_string(classes: list[str] | str) -> str:
|
|
40
|
+
if isinstance(classes, list):
|
|
41
|
+
inner = " ".join(classes)
|
|
42
|
+
else:
|
|
43
|
+
inner = classes
|
|
44
|
+
|
|
45
|
+
return f'class="{inner}"'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def style_to_string(style: dict[str, str] | str) -> str:
|
|
49
|
+
if isinstance(style, dict):
|
|
50
|
+
inner = ";".join(": ".join(pair) for pair in style.items())
|
|
51
|
+
else:
|
|
52
|
+
inner = style
|
|
53
|
+
|
|
54
|
+
return f'style="{inner}"'
|
fragments/loader.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import importlib.abc
|
|
2
|
+
import importlib.machinery
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from fragments import transpiler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TranspilingLoader(importlib.machinery.SourceFileLoader):
|
|
9
|
+
def get_data(self, path: str) -> bytes:
|
|
10
|
+
if not path.endswith(".py"):
|
|
11
|
+
return super().get_data(path)
|
|
12
|
+
|
|
13
|
+
source = super().get_data(path).decode("utf-8")
|
|
14
|
+
|
|
15
|
+
if "<>" not in source:
|
|
16
|
+
return source.encode("utf-8")
|
|
17
|
+
|
|
18
|
+
transpiled = transpiler.transpile(source)
|
|
19
|
+
return transpiled.encode("utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TranspilingFinder(importlib.abc.MetaPathFinder):
|
|
23
|
+
def find_spec(self, fullname: str, path, target=None):
|
|
24
|
+
spec = importlib.machinery.PathFinder.find_spec(fullname, path)
|
|
25
|
+
if spec is None or not isinstance(spec.loader, importlib.machinery.SourceFileLoader):
|
|
26
|
+
return None
|
|
27
|
+
assert spec.origin is not None
|
|
28
|
+
spec.loader = TranspilingLoader(fullname, spec.origin)
|
|
29
|
+
return spec
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
sys.meta_path.insert(0, TranspilingFinder())
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lsprotocol import types
|
|
4
|
+
|
|
5
|
+
from fragments.lsp.server import FragmentsServer, _converter, _remap_text_edits, server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@server.feature(types.TEXT_DOCUMENT_COMPLETION, types.CompletionOptions(trigger_characters=["."]))
|
|
9
|
+
async def completion(language_server: FragmentsServer, params: types.CompletionParams) -> types.CompletionList | None:
|
|
10
|
+
if language_server._pyright is None or params.text_document.uri not in language_server._files:
|
|
11
|
+
return None
|
|
12
|
+
state = language_server._files[params.text_document.uri]
|
|
13
|
+
|
|
14
|
+
context = None
|
|
15
|
+
if params.context is not None:
|
|
16
|
+
context = {
|
|
17
|
+
"triggerKind": params.context.trigger_kind.value,
|
|
18
|
+
"triggerCharacter": params.context.trigger_character,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if state is None:
|
|
22
|
+
result = await language_server._pyright.request(
|
|
23
|
+
"textDocument/completion",
|
|
24
|
+
{
|
|
25
|
+
"textDocument": {"uri": params.text_document.uri},
|
|
26
|
+
"position": {"line": params.position.line, "character": params.position.character},
|
|
27
|
+
"context": context,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
raw_result = result.get("result")
|
|
31
|
+
if not raw_result:
|
|
32
|
+
return None
|
|
33
|
+
if isinstance(raw_result, list):
|
|
34
|
+
return types.CompletionList(is_incomplete=False, items=_converter.structure(raw_result, list[types.CompletionItem]))
|
|
35
|
+
return _converter.structure(raw_result, types.CompletionList)
|
|
36
|
+
|
|
37
|
+
transpiled_position = state.original_to_transpiled_position(params.position)
|
|
38
|
+
if transpiled_position is None:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
result = await language_server._pyright.request(
|
|
42
|
+
"textDocument/completion",
|
|
43
|
+
{
|
|
44
|
+
"textDocument": {"uri": params.text_document.uri},
|
|
45
|
+
"position": {"line": transpiled_position.line, "character": transpiled_position.character},
|
|
46
|
+
"context": context,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
raw_result = result.get("result")
|
|
51
|
+
if not raw_result:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if isinstance(raw_result, list):
|
|
55
|
+
raw_items, is_incomplete = raw_result, False
|
|
56
|
+
else:
|
|
57
|
+
raw_items = raw_result.get("items", [])
|
|
58
|
+
is_incomplete = raw_result.get("isIncomplete", False)
|
|
59
|
+
|
|
60
|
+
remapped_items: list[types.CompletionItem] = []
|
|
61
|
+
for item in _converter.structure(raw_items, list[types.CompletionItem]):
|
|
62
|
+
if item.text_edit is not None:
|
|
63
|
+
if isinstance(item.text_edit, types.InsertReplaceEdit):
|
|
64
|
+
insert = state.transpiled_to_original_range(item.text_edit.insert)
|
|
65
|
+
replace = state.transpiled_to_original_range(item.text_edit.replace)
|
|
66
|
+
if insert is None or replace is None:
|
|
67
|
+
continue
|
|
68
|
+
item.text_edit.insert = insert
|
|
69
|
+
item.text_edit.replace = replace
|
|
70
|
+
else:
|
|
71
|
+
remapped = state.transpiled_to_original_range(item.text_edit.range)
|
|
72
|
+
if remapped is None:
|
|
73
|
+
continue
|
|
74
|
+
item.text_edit.range = remapped
|
|
75
|
+
if item.additional_text_edits:
|
|
76
|
+
item.additional_text_edits = _remap_text_edits(item.additional_text_edits, state)
|
|
77
|
+
remapped_items.append(item)
|
|
78
|
+
|
|
79
|
+
return types.CompletionList(is_incomplete=is_incomplete, items=remapped_items)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lsprotocol import types
|
|
4
|
+
|
|
5
|
+
from fragments.lsp.server import FragmentsServer, _converter, server
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@server.feature(types.TEXT_DOCUMENT_DEFINITION)
|
|
9
|
+
async def definition(language_server: FragmentsServer, params: types.DefinitionParams) -> list[types.Location] | None:
|
|
10
|
+
if language_server._pyright is None or params.text_document.uri not in language_server._files:
|
|
11
|
+
return None
|
|
12
|
+
state = language_server._files[params.text_document.uri]
|
|
13
|
+
|
|
14
|
+
if state is None:
|
|
15
|
+
result = await language_server._pyright.request(
|
|
16
|
+
"textDocument/definition",
|
|
17
|
+
{
|
|
18
|
+
"textDocument": {"uri": params.text_document.uri},
|
|
19
|
+
"position": {"line": params.position.line, "character": params.position.character},
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
else:
|
|
23
|
+
transpiled_position = state.original_to_transpiled_position(params.position)
|
|
24
|
+
if transpiled_position is None:
|
|
25
|
+
return None
|
|
26
|
+
result = await language_server._pyright.request(
|
|
27
|
+
"textDocument/definition",
|
|
28
|
+
{
|
|
29
|
+
"textDocument": {"uri": params.text_document.uri},
|
|
30
|
+
"position": {"line": transpiled_position.line, "character": transpiled_position.character},
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
raw_result = result.get("result")
|
|
35
|
+
if not raw_result:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
raw_locations = [raw_result] if isinstance(raw_result, dict) else raw_result
|
|
39
|
+
locations: list[types.Location] = []
|
|
40
|
+
for location in _converter.structure(raw_locations, list[types.Location]):
|
|
41
|
+
target_state = language_server._files.get(location.uri)
|
|
42
|
+
if target_state is not None:
|
|
43
|
+
remapped = target_state.transpiled_to_original_range(location.range)
|
|
44
|
+
if remapped is None:
|
|
45
|
+
continue
|
|
46
|
+
location.range = remapped
|
|
47
|
+
locations.append(location)
|
|
48
|
+
|
|
49
|
+
return locations or None
|