python-fragments 0.29__tar.gz → 0.31__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.29 → python_fragments-0.31}/PKG-INFO +1 -1
- {python_fragments-0.29 → python_fragments-0.31}/fragments/ast_nodes.py +45 -23
- {python_fragments-0.29 → python_fragments-0.31}/fragments/grammar.py +15 -3
- {python_fragments-0.29 → python_fragments-0.31}/fragments/html/elements.py +4 -6
- {python_fragments-0.29 → python_fragments-0.31}/fragments/types.py +1 -2
- {python_fragments-0.29 → python_fragments-0.31}/pyproject.toml +1 -1
- {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/PKG-INFO +1 -1
- {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/SOURCES.txt +1 -0
- python_fragments-0.31/tests/test_end_to_end.py +158 -0
- {python_fragments-0.29 → python_fragments-0.31}/tests/test_grammar.py +48 -0
- {python_fragments-0.29 → python_fragments-0.31}/tests/test_html_elements.py +2 -2
- {python_fragments-0.29 → python_fragments-0.31}/tests/test_source_map.py +1 -1
- {python_fragments-0.29 → python_fragments-0.31}/LICENSE +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/README.md +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/__init__.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/cli.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/html/__init__.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/loader.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/__init__.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/based_proxy.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/__init__.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/completion.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/definition.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/hover.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/references.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/rename.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/file_state.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/message_queue.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/types.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/source.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/fragments/transpiler.py +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/dependency_links.txt +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/entry_points.txt +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/requires.txt +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/top_level.txt +0 -0
- {python_fragments-0.29 → python_fragments-0.31}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from typing import Sequence
|
|
3
3
|
|
|
4
|
-
type ASTHTMLChild = ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation | ASTComponent | ASTControlNode | ASTDoctype
|
|
4
|
+
type ASTHTMLChild = ASTHTMLElement | ASTHTMLComment | ASTHTMLText | ASTInterpolation | ASTComponent | ASTControlNode | ASTDoctype | ASTChildrenSlot
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
@dataclass(slots=True)
|
|
@@ -15,7 +15,7 @@ class ASTModule:
|
|
|
15
15
|
transpiled_start: int = field(init=False)
|
|
16
16
|
transpiled_end: int = field(init=False)
|
|
17
17
|
|
|
18
|
-
__template__: str = "from fragments.html.elements import el,
|
|
18
|
+
__template__: str = "from fragments.html.elements import el, _sequence, comment\n{}"
|
|
19
19
|
|
|
20
20
|
def transpile(self, transpiled_start: int = 0) -> None:
|
|
21
21
|
"""Build transpiled outputs for the module."""
|
|
@@ -83,18 +83,15 @@ class ASTFragment:
|
|
|
83
83
|
transpiled_start: int = field(init=False)
|
|
84
84
|
transpiled_end: int = field(init=False)
|
|
85
85
|
|
|
86
|
-
__template__: str = """sequence([{}])"""
|
|
87
|
-
|
|
88
86
|
def transpile(self, transpiled_start: int) -> None:
|
|
89
87
|
self.transpiled_start = transpiled_start
|
|
90
|
-
transpiled_start += 10
|
|
91
88
|
for child in self.children:
|
|
92
89
|
child.transpile(transpiled_start)
|
|
93
90
|
transpiled_start = child.transpiled_end + 1
|
|
94
|
-
if
|
|
91
|
+
if self.children:
|
|
95
92
|
transpiled_start -= 1
|
|
96
93
|
|
|
97
|
-
self.transpiled_content =
|
|
94
|
+
self.transpiled_content = "+".join(child.transpiled_content for child in self.children) if self.children else '""'
|
|
98
95
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
99
96
|
|
|
100
97
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -126,26 +123,28 @@ class ASTHTMLElement:
|
|
|
126
123
|
transpiled_start: int = field(init=False)
|
|
127
124
|
transpiled_end: int = field(init=False)
|
|
128
125
|
|
|
129
|
-
__element_template__: str = """el("{}",
|
|
126
|
+
__element_template__: str = """el("{}",{},oneline={},attributes={})"""
|
|
130
127
|
|
|
131
128
|
def transpile(self, transpiled_start: int) -> None:
|
|
132
129
|
self.transpiled_start = transpiled_start
|
|
133
|
-
transpiled_start = transpiled_start + len(self.name) +
|
|
130
|
+
transpiled_start = transpiled_start + len(self.name) + 6 # el("name",
|
|
134
131
|
for child in self.children:
|
|
135
132
|
child.transpile(transpiled_start)
|
|
136
133
|
transpiled_start = child.transpiled_end + 1
|
|
137
|
-
if
|
|
134
|
+
if self.children:
|
|
138
135
|
transpiled_start -= 1
|
|
139
|
-
|
|
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 # ""
|
|
140
139
|
oneline_offset = len(str(self.one_line))
|
|
141
|
-
transpiled_start +=
|
|
140
|
+
transpiled_start += 9 + oneline_offset + 12 + 1 # ,oneline= + oneline + ,attributes= + {
|
|
142
141
|
for attribute in self.attributes.values():
|
|
143
142
|
attribute.transpile(transpiled_start)
|
|
144
143
|
transpiled_start = attribute.transpiled_end + 1
|
|
145
144
|
transpiled_start -= 1
|
|
146
145
|
|
|
147
146
|
attributes = "{" + ",".join(attribute.transpiled_content for attribute in self.attributes.values()) + "}"
|
|
148
|
-
self.transpiled_content = self.__element_template__.format(self.name,
|
|
147
|
+
self.transpiled_content = self.__element_template__.format(self.name, children_expr, self.one_line, attributes)
|
|
149
148
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
150
149
|
|
|
151
150
|
def map_offset(self, offset: int) -> int | None:
|
|
@@ -184,18 +183,18 @@ class ASTControlNode[T: (ASTHTMLElement, ASTComponent)]:
|
|
|
184
183
|
transpiled_start: int = field(init=False)
|
|
185
184
|
transpiled_end: int = field(init=False)
|
|
186
185
|
|
|
187
|
-
__for_template__: str = "
|
|
188
|
-
__if_template__: str = "{} if {} else ''"
|
|
186
|
+
__for_template__: str = "_sequence([{} for {}])"
|
|
187
|
+
__if_template__: str = "({} if {} else '')"
|
|
189
188
|
|
|
190
189
|
def transpile(self, transpiled_start: int) -> None:
|
|
191
190
|
self.transpiled_start = transpiled_start
|
|
192
191
|
if self.for_interpolation is not None:
|
|
193
|
-
self.child.transpile(transpiled_start +
|
|
192
|
+
self.child.transpile(transpiled_start + 11) # _sequence([
|
|
194
193
|
self.for_interpolation.transpile(self.child.transpiled_end + 5) # child + for
|
|
195
194
|
self.transpiled_content = self.__for_template__.format(self.child.transpiled_content, self.for_interpolation.transpiled_content)
|
|
196
195
|
elif self.if_interpolation is not None:
|
|
197
|
-
self.child.transpile(transpiled_start)
|
|
198
|
-
self.if_interpolation.transpile(self.child.transpiled_end + 4) #
|
|
196
|
+
self.child.transpile(transpiled_start + 1) # ( before child
|
|
197
|
+
self.if_interpolation.transpile(self.child.transpiled_end + 4) # " if "
|
|
199
198
|
self.transpiled_content = self.__if_template__.format(self.child.transpiled_content, self.if_interpolation.transpiled_content)
|
|
200
199
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
201
200
|
|
|
@@ -250,25 +249,27 @@ class ASTComponent:
|
|
|
250
249
|
transpiled_start: int = field(init=False)
|
|
251
250
|
transpiled_end: int = field(init=False)
|
|
252
251
|
|
|
253
|
-
__template__: str = """{}(
|
|
252
|
+
__template__: str = """{}({},{})"""
|
|
254
253
|
|
|
255
254
|
def transpile(self, transpiled_start: int) -> None:
|
|
256
255
|
self.transpiled_start = transpiled_start
|
|
257
256
|
self.name.transpile(self.transpiled_start)
|
|
258
|
-
transpiled_start = self.name.transpiled_end +
|
|
257
|
+
transpiled_start = self.name.transpiled_end + 1 # (
|
|
259
258
|
for child in self.children:
|
|
260
259
|
child.transpile(transpiled_start)
|
|
261
|
-
transpiled_start = child.transpiled_end + 1
|
|
260
|
+
transpiled_start = child.transpiled_end + 1 # + between children
|
|
262
261
|
if self.children:
|
|
263
262
|
transpiled_start -= 1
|
|
264
|
-
children = "
|
|
263
|
+
children = "+".join(child.transpiled_content for child in self.children) if self.children else '""'
|
|
264
|
+
if not self.children:
|
|
265
|
+
transpiled_start += 2 # ""
|
|
265
266
|
|
|
266
267
|
if len(self.arguments) == 0:
|
|
267
268
|
self.transpiled_content = self.__template__.format(self.name.transpiled_content, children, "")
|
|
268
269
|
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
269
270
|
return
|
|
270
271
|
|
|
271
|
-
transpiled_start +=
|
|
272
|
+
transpiled_start += 1 # , before arguments
|
|
272
273
|
for attribute in self.arguments.values():
|
|
273
274
|
attribute.transpile(transpiled_start)
|
|
274
275
|
transpiled_start = attribute.transpiled_end + 1
|
|
@@ -546,3 +547,24 @@ class ASTInterpolation:
|
|
|
546
547
|
return self.source_start + specific_offset + 2 + self.leading_whitespace
|
|
547
548
|
|
|
548
549
|
return None
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@dataclass(slots=True)
|
|
553
|
+
class ASTChildrenSlot:
|
|
554
|
+
source_start: int = field(compare=False)
|
|
555
|
+
source_end: int = field(compare=False)
|
|
556
|
+
|
|
557
|
+
transpiled_content: str = field(init=False)
|
|
558
|
+
transpiled_start: int = field(init=False)
|
|
559
|
+
transpiled_end: int = field(init=False)
|
|
560
|
+
|
|
561
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
562
|
+
self.transpiled_start = transpiled_start
|
|
563
|
+
self.transpiled_content = "children"
|
|
564
|
+
self.transpiled_end = transpiled_start + len("children")
|
|
565
|
+
|
|
566
|
+
def map_offset(self, offset: int) -> None:
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
def unmap_offset(self, offset: int) -> None:
|
|
570
|
+
return None
|
|
@@ -2,6 +2,7 @@ from re import Match
|
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
4
|
from fragments.ast_nodes import (
|
|
5
|
+
ASTChildrenSlot,
|
|
5
6
|
ASTComponent,
|
|
6
7
|
ASTComponentArgument,
|
|
7
8
|
ASTComponentName,
|
|
@@ -22,7 +23,6 @@ from fragments.source import Source
|
|
|
22
23
|
HTML_IDENTIFIER = r"[a-zA-Z][a-zA-Z0-9_:.-]*"
|
|
23
24
|
HTML_ATTRIBUTE_NAME = r"[a-zA-Z:-_][a-zA-Z0-9_:.-]*"
|
|
24
25
|
HTML_TEXT = r"([\s\S]*?)(?=<|{{)"
|
|
25
|
-
CHILDREN_META_COMPONENT = r"<[\S]*Children\.\.\.[\S]*/>"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class ParsingError(Exception):
|
|
@@ -103,7 +103,9 @@ def expect_fragment(source: Source) -> tuple[Source, ASTFragment]:
|
|
|
103
103
|
|
|
104
104
|
children: list[ASTHTMLChild] = []
|
|
105
105
|
while not source.remaining().startswith("</>"):
|
|
106
|
-
|
|
106
|
+
source_after_whitespace, _ = source.eat_whitespace()
|
|
107
|
+
if source_after_whitespace.remaining().startswith("<"):
|
|
108
|
+
source = source_after_whitespace
|
|
107
109
|
if source.remaining().startswith("</>"):
|
|
108
110
|
break
|
|
109
111
|
source, child = expect_child(source)
|
|
@@ -116,6 +118,12 @@ def expect_fragment(source: Source) -> tuple[Source, ASTFragment]:
|
|
|
116
118
|
return source, ASTFragment(source_start, source_end, children)
|
|
117
119
|
|
|
118
120
|
|
|
121
|
+
def expect_children_slot(source: Source) -> tuple[Source, ASTChildrenSlot]:
|
|
122
|
+
source_start = source.offset
|
|
123
|
+
source = expect_string(source, "<Children... />")
|
|
124
|
+
return source, ASTChildrenSlot(source_start, source.offset)
|
|
125
|
+
|
|
126
|
+
|
|
119
127
|
def expect_child(source: Source) -> tuple[Source, ASTHTMLChild]:
|
|
120
128
|
"""Any HTML / functional block that might appear as part of the fragment."""
|
|
121
129
|
if source.remaining().startswith("<!DOCTYPE html>"):
|
|
@@ -126,6 +134,10 @@ def expect_child(source: Source) -> tuple[Source, ASTHTMLChild]:
|
|
|
126
134
|
source, html_comment = expect_html_comment(source)
|
|
127
135
|
return source, html_comment
|
|
128
136
|
|
|
137
|
+
if source.remaining().startswith("<Children... />"):
|
|
138
|
+
source, children_slot = expect_children_slot(source)
|
|
139
|
+
return source, children_slot
|
|
140
|
+
|
|
129
141
|
if source.start_matches(r"<[A-Z]"):
|
|
130
142
|
source, component = expect_component(source)
|
|
131
143
|
return source, component
|
|
@@ -281,7 +293,7 @@ def expect_children(source: Source) -> tuple[Source, list[ASTHTMLChild]]:
|
|
|
281
293
|
source, child = expect_child(source)
|
|
282
294
|
children.append(child)
|
|
283
295
|
source_after_whitespace, _ = source.eat_whitespace()
|
|
284
|
-
if
|
|
296
|
+
if source_after_whitespace.remaining().startswith("<"):
|
|
285
297
|
source = source_after_whitespace
|
|
286
298
|
return source, children
|
|
287
299
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import Any
|
|
3
|
-
from fragments.types import Children
|
|
3
|
+
from fragments.types import Children, Stringable
|
|
4
4
|
import html
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
return "".join(str(
|
|
7
|
+
def _sequence(items: list[str | Stringable]) -> str:
|
|
8
|
+
return "".join(str(item) for item in items)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def el(
|
|
@@ -26,9 +26,7 @@ def el(
|
|
|
26
26
|
if oneline:
|
|
27
27
|
return f"""<{tag_contents_string} />"""
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return f"""<{tag_contents_string}>{children_string}</{name}>"""
|
|
29
|
+
return f"""<{tag_contents_string}>{children}</{name}>"""
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
def comment(content: str) -> 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.31"
|
|
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"
|
|
@@ -41,6 +41,7 @@ python_fragments.egg-info/dependency_links.txt
|
|
|
41
41
|
python_fragments.egg-info/entry_points.txt
|
|
42
42
|
python_fragments.egg-info/requires.txt
|
|
43
43
|
python_fragments.egg-info/top_level.txt
|
|
44
|
+
tests/test_end_to_end.py
|
|
44
45
|
tests/test_grammar.py
|
|
45
46
|
tests/test_html_elements.py
|
|
46
47
|
tests/test_source_map.py
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from fragments.grammar import expect_module
|
|
2
|
+
from fragments.source import Source
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def render(source_str: str, **variables: object) -> str:
|
|
6
|
+
"""Transpile a fragment source string, execute it, and return the result."""
|
|
7
|
+
source = Source.from_string(f"result = {source_str.strip()}")
|
|
8
|
+
_, module = expect_module(source)
|
|
9
|
+
module.transpile(0)
|
|
10
|
+
namespace: dict[str, object] = dict(variables)
|
|
11
|
+
exec(module.transpiled_content, namespace)
|
|
12
|
+
return str(namespace["result"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Basic elements
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_single_element():
|
|
21
|
+
assert render("<><h1>Hello</h1></>") == "<h1>Hello</h1>"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_multiple_siblings():
|
|
25
|
+
assert render("<><h1>Title</h1><p>Body</p></>") == "<h1>Title</h1><p>Body</p>"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_nested_elements():
|
|
29
|
+
assert render("<><div><p>text</p></div></>") == "<div><p>text</p></div>"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_self_closing_element():
|
|
33
|
+
assert render("<><br /></>") == "<br />"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_empty_fragment():
|
|
37
|
+
assert render('<></>') == ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Interpolation
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_interpolation():
|
|
46
|
+
assert render("<><p>{{ name }}</p></>", name="World") == "<p>World</p>"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_multiple_interpolations():
|
|
50
|
+
assert render("<><p>{{ a }} and {{ b }}</p></>", a="foo", b="bar") == "<p>foo and bar</p>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_text_space_before_element():
|
|
54
|
+
assert render("<>Icon: <i></i></>") == "Icon: <i></i>"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_text_space_after_element():
|
|
58
|
+
assert render("<><i></i> text</>") == "<i></i> text"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_text_space_around_element():
|
|
62
|
+
assert render("<>before <i>em</i> after</>") == "before <i>em</i> after"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Control flow — if
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_if_true():
|
|
71
|
+
assert render("<><p if={{ cond }}>yes</p></>", cond=True) == "<p>yes</p>"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_if_false():
|
|
75
|
+
assert render("<><p if={{ cond }}>yes</p></>", cond=False) == ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_if_true_with_sibling():
|
|
79
|
+
result = render("<><p if={{ cond }}>yes</p><p>always</p></>", cond=True)
|
|
80
|
+
assert result == "<p>yes</p><p>always</p>"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_if_false_with_sibling():
|
|
84
|
+
result = render("<><p if={{ cond }}>yes</p><p>always</p></>", cond=False)
|
|
85
|
+
assert result == "<p>always</p>"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_if_true_with_preceding_sibling():
|
|
89
|
+
result = render("<><p>always</p><p if={{ cond }}>maybe</p></>", cond=True)
|
|
90
|
+
assert result == "<p>always</p><p>maybe</p>"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_if_false_with_preceding_sibling():
|
|
94
|
+
result = render("<><p>always</p><p if={{ cond }}>maybe</p></>", cond=False)
|
|
95
|
+
assert result == "<p>always</p>"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_if_between_siblings():
|
|
99
|
+
result = render("<><p>before</p><p if={{ cond }}>maybe</p><p>after</p></>", cond=True)
|
|
100
|
+
assert result == "<p>before</p><p>maybe</p><p>after</p>"
|
|
101
|
+
|
|
102
|
+
result = render("<><p>before</p><p if={{ cond }}>maybe</p><p>after</p></>", cond=False)
|
|
103
|
+
assert result == "<p>before</p><p>after</p>"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Control flow — for
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_for_loop():
|
|
112
|
+
result = render("<><li for={{ item in items }}>{{ item }}</li></>", items=["a", "b", "c"])
|
|
113
|
+
assert result == "<li>a</li><li>b</li><li>c</li>"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_for_loop_with_sibling():
|
|
117
|
+
result = render("<><h1>List</h1><li for={{ item in items }}>{{ item }}</li></>", items=["x", "y"])
|
|
118
|
+
assert result == "<h1>List</h1><li>x</li><li>y</li>"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_for_loop_empty():
|
|
122
|
+
assert render("<><li for={{ item in items }}>{{ item }}</li></>", items=[]) == ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Components
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_component_no_children():
|
|
131
|
+
def Badge(children: str, label: str) -> str:
|
|
132
|
+
return f"<span>{label}</span>"
|
|
133
|
+
|
|
134
|
+
result = render("<><Badge label=\"hi\" /></>", Badge=Badge)
|
|
135
|
+
assert result == "<span>hi</span>"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_component_with_children():
|
|
139
|
+
from fragments.types import Children
|
|
140
|
+
|
|
141
|
+
def Wrapper(children: Children) -> str:
|
|
142
|
+
return f"<div>{children}</div>"
|
|
143
|
+
|
|
144
|
+
result = render("<><Wrapper><p>content</p></Wrapper></>", Wrapper=Wrapper)
|
|
145
|
+
assert result == "<div><p>content</p></div>"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_component_children_pre_joined():
|
|
149
|
+
from fragments.types import Children
|
|
150
|
+
|
|
151
|
+
def Wrapper(children: Children) -> str:
|
|
152
|
+
return f"<div>{children}</div>"
|
|
153
|
+
|
|
154
|
+
result = render(
|
|
155
|
+
"<><Wrapper><p>one</p><p>two</p></Wrapper></>",
|
|
156
|
+
Wrapper=Wrapper,
|
|
157
|
+
)
|
|
158
|
+
assert result == "<div><p>one</p><p>two</p></div>"
|
|
@@ -2,6 +2,7 @@ import pytest
|
|
|
2
2
|
|
|
3
3
|
from fragments import grammar
|
|
4
4
|
from fragments.ast_nodes import (
|
|
5
|
+
ASTChildrenSlot,
|
|
5
6
|
ASTComponent,
|
|
6
7
|
ASTComponentArgument,
|
|
7
8
|
ASTComponentName,
|
|
@@ -367,6 +368,53 @@ def test_lowercase_not_parsed_as_component():
|
|
|
367
368
|
assert isinstance(fragment.children[0], ASTHTMLElement)
|
|
368
369
|
|
|
369
370
|
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# Children slot
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_children_slot_parses():
|
|
377
|
+
source = Source.from_string("<><Children... /></>")
|
|
378
|
+
source, fragment = grammar.expect_fragment(source)
|
|
379
|
+
assert source.at_end()
|
|
380
|
+
assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
|
|
381
|
+
ASTChildrenSlot(-1, -1)
|
|
382
|
+
]))
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_children_slot_transpiles_to_children():
|
|
386
|
+
source = Source.from_string("<><Children... /></>")
|
|
387
|
+
_, fragment = grammar.expect_fragment(source)
|
|
388
|
+
fragment.transpile(0)
|
|
389
|
+
slot = fragment.children[0]
|
|
390
|
+
assert isinstance(slot, ASTChildrenSlot)
|
|
391
|
+
assert slot.transpiled_content == "children"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_children_slot_multiple_in_one_fragment():
|
|
395
|
+
source = Source.from_string("<><Children... /><Children... /></>")
|
|
396
|
+
source, fragment = grammar.expect_fragment(source)
|
|
397
|
+
assert source.at_end()
|
|
398
|
+
assert len(fragment.children) == 2
|
|
399
|
+
assert all(isinstance(child, ASTChildrenSlot) for child in fragment.children)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_children_slot_not_confused_with_component():
|
|
403
|
+
source = Source.from_string("<><Children /></>")
|
|
404
|
+
source, fragment = grammar.expect_fragment(source)
|
|
405
|
+
assert source.at_end()
|
|
406
|
+
assert isinstance(fragment.children[0], ASTComponent)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_children_slot_inside_element():
|
|
410
|
+
source = Source.from_string("<><div><Children... /></div></>")
|
|
411
|
+
source, fragment = grammar.expect_fragment(source)
|
|
412
|
+
assert source.at_end()
|
|
413
|
+
element = fragment.children[0]
|
|
414
|
+
assert isinstance(element, ASTHTMLElement)
|
|
415
|
+
assert isinstance(element.children[0], ASTChildrenSlot)
|
|
416
|
+
|
|
417
|
+
|
|
370
418
|
# ---------------------------------------------------------------------------
|
|
371
419
|
# HTML text
|
|
372
420
|
# ---------------------------------------------------------------------------
|
|
@@ -22,10 +22,10 @@ def test_className_string_passthrough():
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def test_el_className_list_attribute():
|
|
25
|
-
result = el("div",
|
|
25
|
+
result = el("div", "content", False, {"className": ["foo", "bar"]})
|
|
26
26
|
assert result == '<div class="foo bar">content</div>'
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def test_el_className_empty_list():
|
|
30
|
-
result = el("div",
|
|
30
|
+
result = el("div", "", True, {"className": []})
|
|
31
31
|
assert result == '<div class="" />'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from fragments.lsp.file_state import FileState
|
|
2
2
|
from lsprotocol import types
|
|
3
3
|
|
|
4
|
-
IMPORT_PREFIX = "from fragments.html.elements import el,
|
|
4
|
+
IMPORT_PREFIX = "from fragments.html.elements import el, _sequence, comment\n"
|
|
5
5
|
IMPORT_PREFIX_LEN = len(IMPORT_PREFIX)
|
|
6
6
|
|
|
7
7
|
|
|
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.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/completion.py
RENAMED
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/definition.py
RENAMED
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/hover.py
RENAMED
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/inlay_hints.py
RENAMED
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/lifecycle.py
RENAMED
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/references.py
RENAMED
|
File without changes
|
{python_fragments-0.29 → python_fragments-0.31}/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
|
{python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|