python-fragments 0.31__tar.gz → 0.33__tar.gz
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.
- {python_fragments-0.31 → python_fragments-0.33}/PKG-INFO +1 -1
- {python_fragments-0.31 → python_fragments-0.33}/fragments/ast_nodes.py +59 -60
- python_fragments-0.33/fragments/html/elements.py +47 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/hover.py +1 -0
- python_fragments-0.33/fragments/types.py +1 -0
- {python_fragments-0.31 → python_fragments-0.33}/pyproject.toml +1 -1
- {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/PKG-INFO +1 -1
- {python_fragments-0.31 → python_fragments-0.33}/tests/test_end_to_end.py +65 -2
- {python_fragments-0.31 → python_fragments-0.33}/tests/test_html_elements.py +1 -11
- {python_fragments-0.31 → python_fragments-0.33}/tests/test_source_map.py +6 -5
- python_fragments-0.31/fragments/html/elements.py +0 -71
- python_fragments-0.31/fragments/types.py +0 -8
- {python_fragments-0.31 → python_fragments-0.33}/LICENSE +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/README.md +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/__init__.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/cli.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/grammar.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/html/__init__.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/loader.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/__init__.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/based_proxy.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/__init__.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/completion.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/definition.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/references.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/rename.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/file_state.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/message_queue.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/types.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/source.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/fragments/transpiler.py +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/SOURCES.txt +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/dependency_links.txt +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/entry_points.txt +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/requires.txt +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/top_level.txt +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/setup.cfg +0 -0
- {python_fragments-0.31 → python_fragments-0.33}/tests/test_grammar.py +0 -0
|
@@ -3,6 +3,9 @@ from typing import Sequence
|
|
|
3
3
|
|
|
4
4
|
type ASTHTMLChild = ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation | ASTComponent | ASTControlNode | ASTDoctype | ASTChildrenSlot
|
|
5
5
|
|
|
6
|
+
IMPORT_PREFIX = "from fragments.html.elements import attribute_to_string, comment\n"
|
|
7
|
+
IMPORT_PREFIX_LEN = len(IMPORT_PREFIX)
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
@dataclass(slots=True)
|
|
8
11
|
class ASTModule:
|
|
@@ -15,18 +18,17 @@ class ASTModule:
|
|
|
15
18
|
transpiled_start: int = field(init=False)
|
|
16
19
|
transpiled_end: int = field(init=False)
|
|
17
20
|
|
|
18
|
-
__template__: str = "
|
|
21
|
+
__template__: str = "{}\n{}"
|
|
19
22
|
|
|
20
23
|
def transpile(self, transpiled_start: int = 0) -> None:
|
|
21
24
|
"""Build transpiled outputs for the module."""
|
|
22
25
|
self.transpiled_start = transpiled_start
|
|
23
|
-
|
|
26
|
+
self.transpiled_content = IMPORT_PREFIX
|
|
27
|
+
|
|
24
28
|
for child in self.children:
|
|
25
|
-
child.transpile(transpiled_start)
|
|
26
|
-
|
|
29
|
+
child.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
30
|
+
self.transpiled_content += child.transpiled_content
|
|
27
31
|
|
|
28
|
-
children: str = "".join(child.transpiled_content for child in self.children)
|
|
29
|
-
self.transpiled_content = self.__template__.format(children)
|
|
30
32
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
31
33
|
|
|
32
34
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -85,13 +87,16 @@ class ASTFragment:
|
|
|
85
87
|
|
|
86
88
|
def transpile(self, transpiled_start: int) -> None:
|
|
87
89
|
self.transpiled_start = transpiled_start
|
|
88
|
-
|
|
89
|
-
child.transpile(transpiled_start)
|
|
90
|
-
transpiled_start = child.transpiled_end + 1
|
|
91
|
-
if self.children:
|
|
92
|
-
transpiled_start -= 1
|
|
90
|
+
self.transpiled_content = '""'
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
for child in self.children:
|
|
93
|
+
self.transpiled_content += "+"
|
|
94
|
+
if isinstance(child, ASTInterpolation):
|
|
95
|
+
self.transpiled_content += "str("
|
|
96
|
+
child.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
97
|
+
self.transpiled_content += child.transpiled_content
|
|
98
|
+
if isinstance(child, ASTInterpolation):
|
|
99
|
+
self.transpiled_content += ")"
|
|
95
100
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
96
101
|
|
|
97
102
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -123,28 +128,32 @@ class ASTHTMLElement:
|
|
|
123
128
|
transpiled_start: int = field(init=False)
|
|
124
129
|
transpiled_end: int = field(init=False)
|
|
125
130
|
|
|
126
|
-
__element_template__: str = """el("{}",{},oneline={},attributes={})"""
|
|
127
|
-
|
|
128
131
|
def transpile(self, transpiled_start: int) -> None:
|
|
129
132
|
self.transpiled_start = transpiled_start
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
child.transpile(transpiled_start)
|
|
133
|
-
transpiled_start = child.transpiled_end + 1
|
|
134
|
-
if self.children:
|
|
135
|
-
transpiled_start -= 1
|
|
136
|
-
children_expr = "+".join(child.transpiled_content for child in self.children) if self.children else '""'
|
|
137
|
-
if not self.children:
|
|
138
|
-
transpiled_start += 2 # ""
|
|
139
|
-
oneline_offset = len(str(self.one_line))
|
|
140
|
-
transpiled_start += 9 + oneline_offset + 12 + 1 # ,oneline= + oneline + ,attributes= + {
|
|
133
|
+
self.transpiled_content = f'f"<{self.name}'
|
|
134
|
+
|
|
141
135
|
for attribute in self.attributes.values():
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
136
|
+
self.transpiled_content += " "
|
|
137
|
+
attribute.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
138
|
+
self.transpiled_content += attribute.transpiled_content
|
|
139
|
+
|
|
140
|
+
if self.one_line:
|
|
141
|
+
self.transpiled_content += ' />"'
|
|
142
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
143
|
+
return
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
self.transpiled_content += '>"'
|
|
146
|
+
|
|
147
|
+
for child in self.children:
|
|
148
|
+
self.transpiled_content += "+"
|
|
149
|
+
if isinstance(child, ASTInterpolation):
|
|
150
|
+
self.transpiled_content += "str("
|
|
151
|
+
child.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
152
|
+
self.transpiled_content += child.transpiled_content
|
|
153
|
+
if isinstance(child, ASTInterpolation):
|
|
154
|
+
self.transpiled_content += ")"
|
|
155
|
+
|
|
156
|
+
self.transpiled_content += f'+"</{self.name}>"'
|
|
148
157
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
149
158
|
|
|
150
159
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -183,14 +192,14 @@ class ASTControlNode[T: (ASTHTMLElement, ASTComponent)]:
|
|
|
183
192
|
transpiled_start: int = field(init=False)
|
|
184
193
|
transpiled_end: int = field(init=False)
|
|
185
194
|
|
|
186
|
-
__for_template__: str = "
|
|
195
|
+
__for_template__: str = "''.join(str({}) for {})"
|
|
187
196
|
__if_template__: str = "({} if {} else '')"
|
|
188
197
|
|
|
189
198
|
def transpile(self, transpiled_start: int) -> None:
|
|
190
199
|
self.transpiled_start = transpiled_start
|
|
191
200
|
if self.for_interpolation is not None:
|
|
192
|
-
self.child.transpile(transpiled_start +
|
|
193
|
-
self.for_interpolation.transpile(self.child.transpiled_end +
|
|
201
|
+
self.child.transpile(transpiled_start + 12) # ''.join(str(
|
|
202
|
+
self.for_interpolation.transpile(self.child.transpiled_end + 6) # child + ) for
|
|
194
203
|
self.transpiled_content = self.__for_template__.format(self.child.transpiled_content, self.for_interpolation.transpiled_content)
|
|
195
204
|
elif self.if_interpolation is not None:
|
|
196
205
|
self.child.transpile(transpiled_start + 1) # ( before child
|
|
@@ -254,28 +263,19 @@ class ASTComponent:
|
|
|
254
263
|
def transpile(self, transpiled_start: int) -> None:
|
|
255
264
|
self.transpiled_start = transpiled_start
|
|
256
265
|
self.name.transpile(self.transpiled_start)
|
|
257
|
-
|
|
266
|
+
self.transpiled_content = self.name.transpiled_content + '(""'
|
|
267
|
+
|
|
258
268
|
for child in self.children:
|
|
259
|
-
child.transpile(transpiled_start)
|
|
260
|
-
|
|
261
|
-
if self.children:
|
|
262
|
-
transpiled_start -= 1
|
|
263
|
-
children = "+".join(child.transpiled_content for child in self.children) if self.children else '""'
|
|
264
|
-
if not self.children:
|
|
265
|
-
transpiled_start += 2 # ""
|
|
266
|
-
|
|
267
|
-
if len(self.arguments) == 0:
|
|
268
|
-
self.transpiled_content = self.__template__.format(self.name.transpiled_content, children, "")
|
|
269
|
-
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
270
|
-
return
|
|
269
|
+
child.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
270
|
+
self.transpiled_content += "+" + child.transpiled_content
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
transpiled_start
|
|
272
|
+
self.transpiled_content += ","
|
|
273
|
+
|
|
274
|
+
for argument in self.arguments.values():
|
|
275
|
+
argument.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
276
|
+
self.transpiled_content += argument.transpiled_content + ","
|
|
276
277
|
|
|
277
|
-
|
|
278
|
-
self.transpiled_content = self.__template__.format(self.name.transpiled_content, children, attributes)
|
|
278
|
+
self.transpiled_content += ")"
|
|
279
279
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
280
280
|
|
|
281
281
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -453,24 +453,23 @@ class ASTHTMLAttribute:
|
|
|
453
453
|
transpiled_start: int = field(init=False)
|
|
454
454
|
transpiled_end: int = field(init=False)
|
|
455
455
|
|
|
456
|
-
__template__: str = '"{}":{}'
|
|
457
|
-
|
|
458
456
|
def transpile(self, transpiled_start: int) -> None:
|
|
459
457
|
self.transpiled_start = transpiled_start
|
|
458
|
+
self.transpiled_content = "{" + f"attribute_to_string('{self.name}',"
|
|
460
459
|
|
|
461
460
|
if self.string_literal is not None:
|
|
462
|
-
escaped_literal = self.string_literal.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r")
|
|
463
|
-
self.transpiled_content
|
|
461
|
+
escaped_literal = self.string_literal.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r")
|
|
462
|
+
self.transpiled_content += escaped_literal + ")}"
|
|
464
463
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
465
464
|
return
|
|
466
465
|
|
|
467
466
|
if self.interpolation is None:
|
|
468
|
-
self.transpiled_content = self.
|
|
467
|
+
self.transpiled_content = f"{self.name}"
|
|
469
468
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
470
469
|
return
|
|
471
470
|
|
|
472
|
-
self.interpolation.transpile(self.transpiled_start +
|
|
473
|
-
self.transpiled_content
|
|
471
|
+
self.interpolation.transpile(self.transpiled_start + len(self.transpiled_content))
|
|
472
|
+
self.transpiled_content += self.interpolation.transpiled_content + ")}"
|
|
474
473
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
475
474
|
|
|
476
475
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -536,7 +535,7 @@ class ASTInterpolation:
|
|
|
536
535
|
|
|
537
536
|
def map_offset(self, offset: int) -> int | None:
|
|
538
537
|
if self.source_start + 2 + self.leading_whitespace <= offset <= self.source_end - 2 - self.trailing_whitespace:
|
|
539
|
-
specific_offset = offset - self.source_start
|
|
538
|
+
specific_offset = offset - (self.source_start + self.leading_whitespace + 2)
|
|
540
539
|
return self.transpiled_start + specific_offset
|
|
541
540
|
|
|
542
541
|
return None
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
import html
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def comment(content: str) -> str:
|
|
7
|
+
return f"<!-- {content} -->"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def attribute_to_string(name: str, value: Any) -> str:
|
|
11
|
+
if value is None:
|
|
12
|
+
return ""
|
|
13
|
+
|
|
14
|
+
if isinstance(value, tuple):
|
|
15
|
+
value = list(value)
|
|
16
|
+
|
|
17
|
+
if name == "className":
|
|
18
|
+
return className_to_string(value)
|
|
19
|
+
|
|
20
|
+
if name == "style":
|
|
21
|
+
return style_to_string(value)
|
|
22
|
+
|
|
23
|
+
if isinstance(value, dict) or isinstance(value, list):
|
|
24
|
+
value = html.escape(json.dumps(value))
|
|
25
|
+
|
|
26
|
+
if isinstance(value, bool):
|
|
27
|
+
value = str(value).lower()
|
|
28
|
+
|
|
29
|
+
return f'{name}="{value}"'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def className_to_string(contents: list[str] | str) -> str:
|
|
33
|
+
if isinstance(contents, list):
|
|
34
|
+
inner = " ".join(contents)
|
|
35
|
+
else:
|
|
36
|
+
inner = contents
|
|
37
|
+
|
|
38
|
+
return f'class="{inner}"'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def style_to_string(style: dict[str, str] | str) -> str:
|
|
42
|
+
if isinstance(style, dict):
|
|
43
|
+
inner = ";".join(": ".join(pair) for pair in style.items())
|
|
44
|
+
else:
|
|
45
|
+
inner = style
|
|
46
|
+
|
|
47
|
+
return f'style="{inner}"'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
type Children = str
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-fragments"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.33"
|
|
8
8
|
description = "Modern HTML template rendering in Python"
|
|
9
9
|
authors = [{ name = "The Running Algorithm", email = "services@therunningalgorithm.info" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -34,7 +34,7 @@ def test_self_closing_element():
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def test_empty_fragment():
|
|
37
|
-
assert render(
|
|
37
|
+
assert render("<></>") == ""
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
# ---------------------------------------------------------------------------
|
|
@@ -131,7 +131,7 @@ def test_component_no_children():
|
|
|
131
131
|
def Badge(children: str, label: str) -> str:
|
|
132
132
|
return f"<span>{label}</span>"
|
|
133
133
|
|
|
134
|
-
result = render(
|
|
134
|
+
result = render('<><Badge label="hi" /></>', Badge=Badge)
|
|
135
135
|
assert result == "<span>hi</span>"
|
|
136
136
|
|
|
137
137
|
|
|
@@ -156,3 +156,66 @@ def test_component_children_pre_joined():
|
|
|
156
156
|
Wrapper=Wrapper,
|
|
157
157
|
)
|
|
158
158
|
assert result == "<div><p>one</p><p>two</p></div>"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Special attributes — className and style
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_interpolated_classname_list():
|
|
167
|
+
result = render("<><div className={{ classes }}>text</div></>", classes=["foo", "bar"])
|
|
168
|
+
assert result == '<div class="foo bar">text</div>'
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_interpolated_classname_string():
|
|
172
|
+
result = render("<><div className={{ classes }}>text</div></>", classes="foo bar")
|
|
173
|
+
assert result == '<div class="foo bar">text</div>'
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_interpolated_style_dict():
|
|
177
|
+
result = render("<><div style={{ styles }}>text</div></>", styles={"color": "red", "font-size": "12px"})
|
|
178
|
+
assert result == '<div style="color: red;font-size: 12px">text</div>'
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_interpolated_style_string():
|
|
182
|
+
result = render("<><div style={{ styles }}>text</div></>", styles="color: red")
|
|
183
|
+
assert result == '<div style="color: red">text</div>'
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Boolean attributes
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_bare_boolean_attribute():
|
|
192
|
+
assert render("<><input disabled /></>") == "<input disabled />"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_bare_boolean_attribute_with_regular_attribute():
|
|
196
|
+
result = render('<><input type="checkbox" checked /></>')
|
|
197
|
+
assert result == '<input type="checkbox" checked />'
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_multiple_bare_boolean_attributes():
|
|
201
|
+
assert render("<><input disabled required /></>") == "<input disabled required />"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_boolean_attribute_none_value():
|
|
205
|
+
assert render("<><input disabled={{ None }} /></>") == "<input />"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_boolean_attribute_true_value():
|
|
209
|
+
assert render("<><input disabled={{ True }} /></>") == '<input disabled="true" />'
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_boolean_attribute_false_value():
|
|
213
|
+
assert render("<><input disabled={{ False }} /></>") == '<input disabled="false" />'
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_boolean_attribute_true_or_none_value():
|
|
217
|
+
assert render("<><input disabled={{ True or None }} /></>") == '<input disabled="true" />'
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_boolean_attribute_false_or_none_value():
|
|
221
|
+
assert render("<><input disabled={{ False or None }} /></>") == "<input />"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from fragments.html.elements import
|
|
1
|
+
from fragments.html.elements import className_to_string
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def test_className_list_joined_with_spaces():
|
|
@@ -19,13 +19,3 @@ def test_className_single_item_list():
|
|
|
19
19
|
def test_className_string_passthrough():
|
|
20
20
|
result = className_to_string("already-a-string")
|
|
21
21
|
assert result == 'class="already-a-string"'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_el_className_list_attribute():
|
|
25
|
-
result = el("div", "content", False, {"className": ["foo", "bar"]})
|
|
26
|
-
assert result == '<div class="foo bar">content</div>'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_el_className_empty_list():
|
|
30
|
-
result = el("div", "", True, {"className": []})
|
|
31
|
-
assert result == '<div class="" />'
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from fragments.lsp.file_state import FileState
|
|
2
2
|
from lsprotocol import types
|
|
3
|
-
|
|
4
|
-
IMPORT_PREFIX = "from fragments.html.elements import el, _sequence, comment\n"
|
|
5
|
-
IMPORT_PREFIX_LEN = len(IMPORT_PREFIX)
|
|
3
|
+
from fragments.ast_nodes import IMPORT_PREFIX_LEN
|
|
6
4
|
|
|
7
5
|
|
|
8
6
|
def make_state(source_str: str) -> FileState:
|
|
@@ -60,19 +58,22 @@ def test_no_fragments_position_mapping():
|
|
|
60
58
|
|
|
61
59
|
|
|
62
60
|
def test_interpolation_is_mappable():
|
|
63
|
-
source = "<>\n <p>{{ title }}</p>\n</>"
|
|
61
|
+
source = "<>\n <p>{{ title in titles }}</p>\n</>"
|
|
64
62
|
state = make_state(source)
|
|
65
63
|
interp_start = source.index("{{")
|
|
66
64
|
# expression 'title' starts after '{{ ' (2 + 1 space)
|
|
67
65
|
expr_start = interp_start + 3
|
|
68
|
-
expr = "title"
|
|
66
|
+
expr = "title in titles"
|
|
69
67
|
|
|
70
68
|
# Characters of the expression are mappable (first char excluded by exclusive-start).
|
|
71
69
|
for k in range(1, len(expr)):
|
|
72
70
|
t = state.ast_module.map_offset(expr_start + k)
|
|
73
71
|
assert t is not None, f"offset {expr_start + k} should be mappable"
|
|
72
|
+
assert state.ast_module.map_offset(expr_start + k - 1) == t - 1
|
|
74
73
|
assert state.transpiled[t] == expr[k]
|
|
75
74
|
|
|
75
|
+
# Characters in the white space after an expression are not mappable
|
|
76
|
+
|
|
76
77
|
# Round-trip
|
|
77
78
|
for k in range(1, len(expr)):
|
|
78
79
|
t = state.ast_module.map_offset(expr_start + k)
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Any
|
|
3
|
-
from fragments.types import Children, Stringable
|
|
4
|
-
import html
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def _sequence(items: list[str | Stringable]) -> str:
|
|
8
|
-
return "".join(str(item) for item in items)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def el(
|
|
12
|
-
name: str,
|
|
13
|
-
children: Children,
|
|
14
|
-
oneline: bool,
|
|
15
|
-
attributes: dict[str, Any],
|
|
16
|
-
) -> str:
|
|
17
|
-
tag_contents = [
|
|
18
|
-
name,
|
|
19
|
-
className_to_string(attributes.pop("className")) if "className" in attributes else None,
|
|
20
|
-
style_to_string(attributes.pop("style")) if "style" in attributes else None,
|
|
21
|
-
attributes_to_string(attributes) if attributes else None,
|
|
22
|
-
]
|
|
23
|
-
tag_contents = [item for item in tag_contents if item is not None]
|
|
24
|
-
tag_contents_string = " ".join(tag_contents)
|
|
25
|
-
|
|
26
|
-
if oneline:
|
|
27
|
-
return f"""<{tag_contents_string} />"""
|
|
28
|
-
|
|
29
|
-
return f"""<{tag_contents_string}>{children}</{name}>"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def comment(content: str) -> str:
|
|
33
|
-
return f"<!-- {content} -->"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def attributes_to_string(attributes: dict[str, Any]) -> str:
|
|
37
|
-
return " ".join(attribute_to_string(name, value) for name, value in attributes.items())
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def attribute_to_string(name: str, value: Any) -> str:
|
|
41
|
-
if value is None:
|
|
42
|
-
return name
|
|
43
|
-
|
|
44
|
-
if isinstance(value, tuple):
|
|
45
|
-
value = list(value)
|
|
46
|
-
|
|
47
|
-
if isinstance(value, dict) or isinstance(value, list):
|
|
48
|
-
value = html.escape(json.dumps(value))
|
|
49
|
-
|
|
50
|
-
if isinstance(value, bool):
|
|
51
|
-
value = str(value).lower()
|
|
52
|
-
|
|
53
|
-
return f'{name}="{value}"'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def className_to_string(contents: list[str] | str) -> str:
|
|
57
|
-
if isinstance(contents, list):
|
|
58
|
-
inner = " ".join(contents)
|
|
59
|
-
else:
|
|
60
|
-
inner = contents
|
|
61
|
-
|
|
62
|
-
return f'class="{inner}"'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def style_to_string(style: dict[str, str] | str) -> str:
|
|
66
|
-
if isinstance(style, dict):
|
|
67
|
-
inner = ";".join(": ".join(pair) for pair in style.items())
|
|
68
|
-
else:
|
|
69
|
-
inner = style
|
|
70
|
-
|
|
71
|
-
return f'style="{inner}"'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/completion.py
RENAMED
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/definition.py
RENAMED
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/inlay_hints.py
RENAMED
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/lifecycle.py
RENAMED
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/references.py
RENAMED
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/rename.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|