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.
Files changed (49) hide show
  1. {python_fragments-0.29 → python_fragments-0.31}/PKG-INFO +1 -1
  2. {python_fragments-0.29 → python_fragments-0.31}/fragments/ast_nodes.py +45 -23
  3. {python_fragments-0.29 → python_fragments-0.31}/fragments/grammar.py +15 -3
  4. {python_fragments-0.29 → python_fragments-0.31}/fragments/html/elements.py +4 -6
  5. {python_fragments-0.29 → python_fragments-0.31}/fragments/types.py +1 -2
  6. {python_fragments-0.29 → python_fragments-0.31}/pyproject.toml +1 -1
  7. {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/PKG-INFO +1 -1
  8. {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/SOURCES.txt +1 -0
  9. python_fragments-0.31/tests/test_end_to_end.py +158 -0
  10. {python_fragments-0.29 → python_fragments-0.31}/tests/test_grammar.py +48 -0
  11. {python_fragments-0.29 → python_fragments-0.31}/tests/test_html_elements.py +2 -2
  12. {python_fragments-0.29 → python_fragments-0.31}/tests/test_source_map.py +1 -1
  13. {python_fragments-0.29 → python_fragments-0.31}/LICENSE +0 -0
  14. {python_fragments-0.29 → python_fragments-0.31}/README.md +0 -0
  15. {python_fragments-0.29 → python_fragments-0.31}/fragments/__init__.py +0 -0
  16. {python_fragments-0.29 → python_fragments-0.31}/fragments/cli.py +0 -0
  17. {python_fragments-0.29 → python_fragments-0.31}/fragments/html/__init__.py +0 -0
  18. {python_fragments-0.29 → python_fragments-0.31}/fragments/loader.py +0 -0
  19. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/__init__.py +0 -0
  20. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/based_proxy.py +0 -0
  21. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/__init__.py +0 -0
  22. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
  23. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/completion.py +0 -0
  24. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/definition.py +0 -0
  25. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
  26. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
  27. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
  28. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
  29. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/hover.py +0 -0
  30. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
  31. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
  32. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/references.py +0 -0
  33. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/rename.py +0 -0
  34. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
  35. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
  36. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/file_state.py +0 -0
  37. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/message_queue.py +0 -0
  38. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
  39. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
  40. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
  41. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
  42. {python_fragments-0.29 → python_fragments-0.31}/fragments/lsp/types.py +0 -0
  43. {python_fragments-0.29 → python_fragments-0.31}/fragments/source.py +0 -0
  44. {python_fragments-0.29 → python_fragments-0.31}/fragments/transpiler.py +0 -0
  45. {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/dependency_links.txt +0 -0
  46. {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/entry_points.txt +0 -0
  47. {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/requires.txt +0 -0
  48. {python_fragments-0.29 → python_fragments-0.31}/python_fragments.egg-info/top_level.txt +0 -0
  49. {python_fragments-0.29 → python_fragments-0.31}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fragments
3
- Version: 0.29
3
+ Version: 0.31
4
4
  Summary: Modern HTML template rendering in Python
5
5
  Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
6
  License: MIT License
@@ -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, sequence, comment\n{}"
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 len(self.children) > 0:
91
+ if self.children:
95
92
  transpiled_start -= 1
96
93
 
97
- self.transpiled_content = self.__template__.format(",".join(child.transpiled_content for child in self.children))
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("{}",[{}],oneline={},attributes={})"""
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) + 7
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 len(self.children) > 0:
134
+ if self.children:
138
135
  transpiled_start -= 1
139
- children = ",".join(child.transpiled_content for child in self.children)
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 += 10 + oneline_offset + 12 + 1 # ],oneline= + oneline + ,attributes= + {
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, children, self.one_line, attributes)
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 = "sequence([{} for {}])"
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 + 10) # sequence([
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) # child + if
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 + 2
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 = ",".join(child.transpiled_content for child in self.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 += 2
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
- source, _ = source.eat_whitespace()
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 not source_after_whitespace.remaining().startswith("{{"):
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 sequence(children: Children) -> str:
8
- return "".join(str(child) for child in children)
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
- children_string = sequence(children)
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:
@@ -5,5 +5,4 @@ class Stringable(Protocol):
5
5
  def __str__(self) -> str: ...
6
6
 
7
7
 
8
- type Child = str | Stringable
9
- type Children = list[Child]
8
+ 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.29"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fragments
3
- Version: 0.29
3
+ Version: 0.31
4
4
  Summary: Modern HTML template rendering in Python
5
5
  Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
6
  License: MIT License
@@ -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", ["content"], False, {"className": ["foo", "bar"]})
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", [], True, {"className": []})
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, sequence, comment\n"
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