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.
Files changed (51) hide show
  1. {python_fragments-0.31 → python_fragments-0.33}/PKG-INFO +1 -1
  2. {python_fragments-0.31 → python_fragments-0.33}/fragments/ast_nodes.py +59 -60
  3. python_fragments-0.33/fragments/html/elements.py +47 -0
  4. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/hover.py +1 -0
  5. python_fragments-0.33/fragments/types.py +1 -0
  6. {python_fragments-0.31 → python_fragments-0.33}/pyproject.toml +1 -1
  7. {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/PKG-INFO +1 -1
  8. {python_fragments-0.31 → python_fragments-0.33}/tests/test_end_to_end.py +65 -2
  9. {python_fragments-0.31 → python_fragments-0.33}/tests/test_html_elements.py +1 -11
  10. {python_fragments-0.31 → python_fragments-0.33}/tests/test_source_map.py +6 -5
  11. python_fragments-0.31/fragments/html/elements.py +0 -71
  12. python_fragments-0.31/fragments/types.py +0 -8
  13. {python_fragments-0.31 → python_fragments-0.33}/LICENSE +0 -0
  14. {python_fragments-0.31 → python_fragments-0.33}/README.md +0 -0
  15. {python_fragments-0.31 → python_fragments-0.33}/fragments/__init__.py +0 -0
  16. {python_fragments-0.31 → python_fragments-0.33}/fragments/cli.py +0 -0
  17. {python_fragments-0.31 → python_fragments-0.33}/fragments/grammar.py +0 -0
  18. {python_fragments-0.31 → python_fragments-0.33}/fragments/html/__init__.py +0 -0
  19. {python_fragments-0.31 → python_fragments-0.33}/fragments/loader.py +0 -0
  20. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/__init__.py +0 -0
  21. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/based_proxy.py +0 -0
  22. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/__init__.py +0 -0
  23. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
  24. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/completion.py +0 -0
  25. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/definition.py +0 -0
  26. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
  27. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
  28. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
  29. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
  30. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
  31. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
  32. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/references.py +0 -0
  33. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/rename.py +0 -0
  34. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
  35. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
  36. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/file_state.py +0 -0
  37. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/message_queue.py +0 -0
  38. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
  39. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
  40. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
  41. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
  42. {python_fragments-0.31 → python_fragments-0.33}/fragments/lsp/types.py +0 -0
  43. {python_fragments-0.31 → python_fragments-0.33}/fragments/source.py +0 -0
  44. {python_fragments-0.31 → python_fragments-0.33}/fragments/transpiler.py +0 -0
  45. {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/SOURCES.txt +0 -0
  46. {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/dependency_links.txt +0 -0
  47. {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/entry_points.txt +0 -0
  48. {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/requires.txt +0 -0
  49. {python_fragments-0.31 → python_fragments-0.33}/python_fragments.egg-info/top_level.txt +0 -0
  50. {python_fragments-0.31 → python_fragments-0.33}/setup.cfg +0 -0
  51. {python_fragments-0.31 → python_fragments-0.33}/tests/test_grammar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fragments
3
- Version: 0.31
3
+ Version: 0.33
4
4
  Summary: Modern HTML template rendering in Python
5
5
  Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
6
  License: MIT License
@@ -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 = "from fragments.html.elements import el, _sequence, comment\n{}"
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
- transpiled_start += len(self.__template__) - 2
26
+ self.transpiled_content = IMPORT_PREFIX
27
+
24
28
  for child in self.children:
25
- child.transpile(transpiled_start)
26
- transpiled_start = child.transpiled_end
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
- for child in self.children:
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
- self.transpiled_content = "+".join(child.transpiled_content for child in self.children) if self.children else '""'
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
- transpiled_start = transpiled_start + len(self.name) + 6 # el("name",
131
- for child in self.children:
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
- attribute.transpile(transpiled_start)
143
- transpiled_start = attribute.transpiled_end + 1
144
- transpiled_start -= 1
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
- attributes = "{" + ",".join(attribute.transpiled_content for attribute in self.attributes.values()) + "}"
147
- self.transpiled_content = self.__element_template__.format(self.name, children_expr, self.one_line, attributes)
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 = "_sequence([{} for {}])"
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 + 11) # _sequence([
193
- self.for_interpolation.transpile(self.child.transpiled_end + 5) # child + for
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
- transpiled_start = self.name.transpiled_end + 1 # (
266
+ self.transpiled_content = self.name.transpiled_content + '(""'
267
+
258
268
  for child in self.children:
259
- child.transpile(transpiled_start)
260
- transpiled_start = child.transpiled_end + 1 # + between children
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
- transpiled_start += 1 # , before arguments
273
- for attribute in self.arguments.values():
274
- attribute.transpile(transpiled_start)
275
- transpiled_start = attribute.transpiled_end + 1
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
- attributes = ",".join(attribute.transpiled_content for attribute in self.arguments.values())
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").replace('"', '"')
463
- self.transpiled_content = self.__template__.format(self.name, escaped_literal)
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.__template__.format(self.name, "None")
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 + 1 + len(self.name) + 2) # " + name + ":
473
- self.transpiled_content = self.__template__.format(self.name, self.interpolation.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 - 2 - self.leading_whitespace
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}"'
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  from lsprotocol import types
2
3
  from lsprotocol.types import REQUESTS, NOTIFICATIONS
3
4
  from typing import cast
@@ -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.31"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fragments
3
- Version: 0.31
3
+ Version: 0.33
4
4
  Summary: Modern HTML template rendering in Python
5
5
  Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
6
  License: MIT License
@@ -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("<><Badge label=\"hi\" /></>", Badge=Badge)
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 el, className_to_string
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}"'
@@ -1,8 +0,0 @@
1
- from typing import Protocol
2
-
3
-
4
- class Stringable(Protocol):
5
- def __str__(self) -> str: ...
6
-
7
-
8
- type Children = str
File without changes