python-fragments 0.28__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.31/LICENSE +21 -0
- python_fragments-0.31/PKG-INFO +88 -0
- {python_fragments-0.28 → python_fragments-0.31}/README.md +11 -1
- {python_fragments-0.28 → python_fragments-0.31}/fragments/ast_nodes.py +66 -23
- {python_fragments-0.28 → python_fragments-0.31}/fragments/grammar.py +26 -3
- {python_fragments-0.28 → python_fragments-0.31}/fragments/html/elements.py +4 -6
- {python_fragments-0.28 → python_fragments-0.31}/fragments/types.py +1 -2
- {python_fragments-0.28 → python_fragments-0.31}/pyproject.toml +2 -2
- python_fragments-0.31/python_fragments.egg-info/PKG-INFO +88 -0
- {python_fragments-0.28 → python_fragments-0.31}/python_fragments.egg-info/SOURCES.txt +2 -0
- python_fragments-0.31/tests/test_end_to_end.py +158 -0
- {python_fragments-0.28 → python_fragments-0.31}/tests/test_grammar.py +90 -0
- {python_fragments-0.28 → python_fragments-0.31}/tests/test_html_elements.py +2 -2
- {python_fragments-0.28 → python_fragments-0.31}/tests/test_source_map.py +1 -1
- python_fragments-0.28/PKG-INFO +0 -55
- python_fragments-0.28/python_fragments.egg-info/PKG-INFO +0 -55
- {python_fragments-0.28 → python_fragments-0.31}/fragments/__init__.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/cli.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/html/__init__.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/loader.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/__init__.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/based_proxy.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/__init__.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/completion.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/definition.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/hover.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/references.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/rename.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/file_state.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/message_queue.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/types.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/source.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/fragments/transpiler.py +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/python_fragments.egg-info/dependency_links.txt +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/python_fragments.egg-info/entry_points.txt +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/python_fragments.egg-info/requires.txt +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/python_fragments.egg-info/top_level.txt +0 -0
- {python_fragments-0.28 → python_fragments-0.31}/setup.cfg +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Running Algorithm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-fragments
|
|
3
|
+
Version: 0.31
|
|
4
|
+
Summary: Modern HTML template rendering in Python
|
|
5
|
+
Author-email: The Running Algorithm <services@therunningalgorithm.info>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 The Running Algorithm
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.12
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: lsp
|
|
32
|
+
Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
|
|
33
|
+
Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Python Fragments
|
|
39
|
+
|
|
40
|
+
> **This package is in early development and not yet stable. The API may change without notice between releases.**
|
|
41
|
+
|
|
42
|
+
Modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from fragments import loader # isort: skip
|
|
46
|
+
|
|
47
|
+
from fastapi import FastAPI
|
|
48
|
+
from fastapi.responses import HTMLResponse
|
|
49
|
+
from components import Layout, PostCard
|
|
50
|
+
|
|
51
|
+
app = FastAPI()
|
|
52
|
+
|
|
53
|
+
POSTS = [...]
|
|
54
|
+
|
|
55
|
+
@app.get("/", response_class=HTMLResponse)
|
|
56
|
+
async def index() -> str:
|
|
57
|
+
published = [p for p in POSTS if p.published]
|
|
58
|
+
return <>
|
|
59
|
+
<Layout title="My Blog">
|
|
60
|
+
<h1>Latest Posts</h1>
|
|
61
|
+
<PostCard for={{ post in published }} post={{ post }} />
|
|
62
|
+
</Layout>
|
|
63
|
+
</>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## IDE Support
|
|
67
|
+
|
|
68
|
+
Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting — all working inside fragment syntax.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install python-fragments
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Register the loader at your application's entry point, before importing any modules that contain fragments:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from fragments import loader # isort: skip
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
|
|
85
|
+
|
|
86
|
+
## Feedback and feature requests
|
|
87
|
+
|
|
88
|
+
Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/TheRunningAlgorithm2/python-fragments/issues). The project is not currently accepting code contributions.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **This package is in early development and not yet stable. The API may change without notice between releases.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
|
|
6
6
|
|
|
7
7
|
```python
|
|
8
8
|
from fragments import loader # isort: skip
|
|
@@ -26,6 +26,12 @@ async def index() -> str:
|
|
|
26
26
|
</>
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## IDE Support
|
|
30
|
+
|
|
31
|
+
Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting — all working inside fragment syntax.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
29
35
|
## Installation
|
|
30
36
|
|
|
31
37
|
```bash
|
|
@@ -39,3 +45,7 @@ from fragments import loader # isort: skip
|
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
|
|
48
|
+
|
|
49
|
+
## Feedback and feature requests
|
|
50
|
+
|
|
51
|
+
Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/TheRunningAlgorithm2/python-fragments/issues). The project is not currently accepting code contributions.
|
|
@@ -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
|
|
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
|
|
@@ -392,6 +393,27 @@ class ASTComponentArgument:
|
|
|
392
393
|
return None
|
|
393
394
|
|
|
394
395
|
|
|
396
|
+
@dataclass(slots=True)
|
|
397
|
+
class ASTDoctype:
|
|
398
|
+
source_start: int = field(compare=False)
|
|
399
|
+
source_end: int = field(compare=False)
|
|
400
|
+
|
|
401
|
+
transpiled_content: str = field(init=False)
|
|
402
|
+
transpiled_start: int = field(init=False)
|
|
403
|
+
transpiled_end: int = field(init=False)
|
|
404
|
+
|
|
405
|
+
def transpile(self, transpiled_start: int) -> None:
|
|
406
|
+
self.transpiled_start = transpiled_start
|
|
407
|
+
self.transpiled_content = '"<!DOCTYPE html>"'
|
|
408
|
+
self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
|
|
409
|
+
|
|
410
|
+
def map_offset(self, offset: int) -> None:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def unmap_offset(self, offset: int) -> None:
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
|
|
395
417
|
@dataclass(slots=True)
|
|
396
418
|
class ASTHTMLComment:
|
|
397
419
|
source_start: int = field(compare=False)
|
|
@@ -525,3 +547,24 @@ class ASTInterpolation:
|
|
|
525
547
|
return self.source_start + specific_offset + 2 + self.leading_whitespace
|
|
526
548
|
|
|
527
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,10 +2,12 @@ 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,
|
|
8
9
|
ASTControlNode,
|
|
10
|
+
ASTDoctype,
|
|
9
11
|
ASTFragment,
|
|
10
12
|
ASTHTMLAttribute,
|
|
11
13
|
ASTHTMLComment,
|
|
@@ -21,7 +23,6 @@ from fragments.source import Source
|
|
|
21
23
|
HTML_IDENTIFIER = r"[a-zA-Z][a-zA-Z0-9_:.-]*"
|
|
22
24
|
HTML_ATTRIBUTE_NAME = r"[a-zA-Z:-_][a-zA-Z0-9_:.-]*"
|
|
23
25
|
HTML_TEXT = r"([\s\S]*?)(?=<|{{)"
|
|
24
|
-
CHILDREN_META_COMPONENT = r"<[\S]*Children\.\.\.[\S]*/>"
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class ParsingError(Exception):
|
|
@@ -102,7 +103,9 @@ def expect_fragment(source: Source) -> tuple[Source, ASTFragment]:
|
|
|
102
103
|
|
|
103
104
|
children: list[ASTHTMLChild] = []
|
|
104
105
|
while not source.remaining().startswith("</>"):
|
|
105
|
-
|
|
106
|
+
source_after_whitespace, _ = source.eat_whitespace()
|
|
107
|
+
if source_after_whitespace.remaining().startswith("<"):
|
|
108
|
+
source = source_after_whitespace
|
|
106
109
|
if source.remaining().startswith("</>"):
|
|
107
110
|
break
|
|
108
111
|
source, child = expect_child(source)
|
|
@@ -115,12 +118,26 @@ def expect_fragment(source: Source) -> tuple[Source, ASTFragment]:
|
|
|
115
118
|
return source, ASTFragment(source_start, source_end, children)
|
|
116
119
|
|
|
117
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
|
+
|
|
118
127
|
def expect_child(source: Source) -> tuple[Source, ASTHTMLChild]:
|
|
119
128
|
"""Any HTML / functional block that might appear as part of the fragment."""
|
|
129
|
+
if source.remaining().startswith("<!DOCTYPE html>"):
|
|
130
|
+
source, doctype = expect_doctype(source)
|
|
131
|
+
return source, doctype
|
|
132
|
+
|
|
120
133
|
if source.remaining().startswith("<!--"):
|
|
121
134
|
source, html_comment = expect_html_comment(source)
|
|
122
135
|
return source, html_comment
|
|
123
136
|
|
|
137
|
+
if source.remaining().startswith("<Children... />"):
|
|
138
|
+
source, children_slot = expect_children_slot(source)
|
|
139
|
+
return source, children_slot
|
|
140
|
+
|
|
124
141
|
if source.start_matches(r"<[A-Z]"):
|
|
125
142
|
source, component = expect_component(source)
|
|
126
143
|
return source, component
|
|
@@ -192,6 +209,12 @@ def expect_component_name(source: Source) -> tuple[Source, ASTComponentName]:
|
|
|
192
209
|
return source, ASTComponentName(source_start, source.offset, name)
|
|
193
210
|
|
|
194
211
|
|
|
212
|
+
def expect_doctype(source: Source) -> tuple[Source, ASTDoctype]:
|
|
213
|
+
source_start = source.offset
|
|
214
|
+
source = expect_string(source, "<!DOCTYPE html>")
|
|
215
|
+
return source, ASTDoctype(source_start, source.offset)
|
|
216
|
+
|
|
217
|
+
|
|
195
218
|
def expect_html_comment(source: Source) -> tuple[Source, ASTHTMLComment]:
|
|
196
219
|
source_start = source.offset
|
|
197
220
|
source = expect_string(source, "<!--")
|
|
@@ -270,7 +293,7 @@ def expect_children(source: Source) -> tuple[Source, list[ASTHTMLChild]]:
|
|
|
270
293
|
source, child = expect_child(source)
|
|
271
294
|
children.append(child)
|
|
272
295
|
source_after_whitespace, _ = source.eat_whitespace()
|
|
273
|
-
if
|
|
296
|
+
if source_after_whitespace.remaining().startswith("<"):
|
|
274
297
|
source = source_after_whitespace
|
|
275
298
|
return source, children
|
|
276
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,11 +4,11 @@ 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"
|
|
11
|
-
license = {
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
12
|
requires-python = ">=3.12"
|
|
13
13
|
dependencies = []
|
|
14
14
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-fragments
|
|
3
|
+
Version: 0.31
|
|
4
|
+
Summary: Modern HTML template rendering in Python
|
|
5
|
+
Author-email: The Running Algorithm <services@therunningalgorithm.info>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 The Running Algorithm
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.12
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: lsp
|
|
32
|
+
Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
|
|
33
|
+
Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Python Fragments
|
|
39
|
+
|
|
40
|
+
> **This package is in early development and not yet stable. The API may change without notice between releases.**
|
|
41
|
+
|
|
42
|
+
Modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from fragments import loader # isort: skip
|
|
46
|
+
|
|
47
|
+
from fastapi import FastAPI
|
|
48
|
+
from fastapi.responses import HTMLResponse
|
|
49
|
+
from components import Layout, PostCard
|
|
50
|
+
|
|
51
|
+
app = FastAPI()
|
|
52
|
+
|
|
53
|
+
POSTS = [...]
|
|
54
|
+
|
|
55
|
+
@app.get("/", response_class=HTMLResponse)
|
|
56
|
+
async def index() -> str:
|
|
57
|
+
published = [p for p in POSTS if p.published]
|
|
58
|
+
return <>
|
|
59
|
+
<Layout title="My Blog">
|
|
60
|
+
<h1>Latest Posts</h1>
|
|
61
|
+
<PostCard for={{ post in published }} post={{ post }} />
|
|
62
|
+
</Layout>
|
|
63
|
+
</>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## IDE Support
|
|
67
|
+
|
|
68
|
+
Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting — all working inside fragment syntax.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install python-fragments
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Register the loader at your application's entry point, before importing any modules that contain fragments:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from fragments import loader # isort: skip
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
|
|
85
|
+
|
|
86
|
+
## Feedback and feature requests
|
|
87
|
+
|
|
88
|
+
Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/TheRunningAlgorithm2/python-fragments/issues). The project is not currently accepting code contributions.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
fragments/__init__.py
|
|
@@ -40,6 +41,7 @@ python_fragments.egg-info/dependency_links.txt
|
|
|
40
41
|
python_fragments.egg-info/entry_points.txt
|
|
41
42
|
python_fragments.egg-info/requires.txt
|
|
42
43
|
python_fragments.egg-info/top_level.txt
|
|
44
|
+
tests/test_end_to_end.py
|
|
43
45
|
tests/test_grammar.py
|
|
44
46
|
tests/test_html_elements.py
|
|
45
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,10 +2,12 @@ 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,
|
|
8
9
|
ASTControlNode,
|
|
10
|
+
ASTDoctype,
|
|
9
11
|
ASTFragment,
|
|
10
12
|
ASTHTMLAttribute,
|
|
11
13
|
ASTHTMLComment,
|
|
@@ -23,6 +25,47 @@ def _transpiled(fragment: ASTFragment) -> ASTFragment:
|
|
|
23
25
|
return fragment
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Doctype
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_doctype_standalone():
|
|
34
|
+
source = Source.from_string("<><!DOCTYPE html></>")
|
|
35
|
+
source, fragment = grammar.expect_fragment(source)
|
|
36
|
+
assert source.at_end()
|
|
37
|
+
assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
|
|
38
|
+
ASTDoctype(-1, -1)
|
|
39
|
+
]))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_doctype_transpiles_to_literal_string():
|
|
43
|
+
source = Source.from_string("<><!DOCTYPE html></>")
|
|
44
|
+
_, fragment = grammar.expect_fragment(source)
|
|
45
|
+
fragment.transpile(0)
|
|
46
|
+
doctype = fragment.children[0]
|
|
47
|
+
assert isinstance(doctype, ASTDoctype)
|
|
48
|
+
assert doctype.transpiled_content == '"<!DOCTYPE html>"'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_doctype_followed_by_element():
|
|
52
|
+
source = Source.from_string("<><!DOCTYPE html><html></html></>")
|
|
53
|
+
source, fragment = grammar.expect_fragment(source)
|
|
54
|
+
assert source.at_end()
|
|
55
|
+
assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
|
|
56
|
+
ASTDoctype(-1, -1),
|
|
57
|
+
ASTHTMLElement(-1, -1, "html", {}, [], False),
|
|
58
|
+
]))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_doctype_not_confused_with_comment():
|
|
62
|
+
source = Source.from_string("<><!DOCTYPE html><!-- a note --></>")
|
|
63
|
+
source, fragment = grammar.expect_fragment(source)
|
|
64
|
+
assert source.at_end()
|
|
65
|
+
assert isinstance(fragment.children[0], ASTDoctype)
|
|
66
|
+
assert isinstance(fragment.children[1], ASTHTMLComment)
|
|
67
|
+
|
|
68
|
+
|
|
26
69
|
# ---------------------------------------------------------------------------
|
|
27
70
|
# Comments
|
|
28
71
|
# ---------------------------------------------------------------------------
|
|
@@ -325,6 +368,53 @@ def test_lowercase_not_parsed_as_component():
|
|
|
325
368
|
assert isinstance(fragment.children[0], ASTHTMLElement)
|
|
326
369
|
|
|
327
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
|
+
|
|
328
418
|
# ---------------------------------------------------------------------------
|
|
329
419
|
# HTML text
|
|
330
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
|
|
python_fragments-0.28/PKG-INFO
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-fragments
|
|
3
|
-
Version: 0.28
|
|
4
|
-
Summary: Modern HTML template rendering in Python
|
|
5
|
-
Author-email: The Running Algorithm <services@therunningalgorithm.info>
|
|
6
|
-
License: Proprietary
|
|
7
|
-
Requires-Python: >=3.12
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
Provides-Extra: lsp
|
|
10
|
-
Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
|
|
11
|
-
Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
|
|
12
|
-
Provides-Extra: dev
|
|
13
|
-
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
14
|
-
|
|
15
|
-
# Python Fragments
|
|
16
|
-
|
|
17
|
-
> **This package is in early development and not yet stable. The API may change without notice between releases.**
|
|
18
|
-
|
|
19
|
-
Production-ready, modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
from fragments import loader # isort: skip
|
|
23
|
-
|
|
24
|
-
from fastapi import FastAPI
|
|
25
|
-
from fastapi.responses import HTMLResponse
|
|
26
|
-
from components import Layout, PostCard
|
|
27
|
-
|
|
28
|
-
app = FastAPI()
|
|
29
|
-
|
|
30
|
-
POSTS = [...]
|
|
31
|
-
|
|
32
|
-
@app.get("/", response_class=HTMLResponse)
|
|
33
|
-
async def index() -> str:
|
|
34
|
-
published = [p for p in POSTS if p.published]
|
|
35
|
-
return <>
|
|
36
|
-
<Layout title="My Blog">
|
|
37
|
-
<h1>Latest Posts</h1>
|
|
38
|
-
<PostCard for={{ post in published }} post={{ post }} />
|
|
39
|
-
</Layout>
|
|
40
|
-
</>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Installation
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
pip install python-fragments
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Register the loader at your application's entry point, before importing any modules that contain fragments:
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
from fragments import loader # isort: skip
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-fragments
|
|
3
|
-
Version: 0.28
|
|
4
|
-
Summary: Modern HTML template rendering in Python
|
|
5
|
-
Author-email: The Running Algorithm <services@therunningalgorithm.info>
|
|
6
|
-
License: Proprietary
|
|
7
|
-
Requires-Python: >=3.12
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
Provides-Extra: lsp
|
|
10
|
-
Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
|
|
11
|
-
Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
|
|
12
|
-
Provides-Extra: dev
|
|
13
|
-
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
14
|
-
|
|
15
|
-
# Python Fragments
|
|
16
|
-
|
|
17
|
-
> **This package is in early development and not yet stable. The API may change without notice between releases.**
|
|
18
|
-
|
|
19
|
-
Production-ready, modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
from fragments import loader # isort: skip
|
|
23
|
-
|
|
24
|
-
from fastapi import FastAPI
|
|
25
|
-
from fastapi.responses import HTMLResponse
|
|
26
|
-
from components import Layout, PostCard
|
|
27
|
-
|
|
28
|
-
app = FastAPI()
|
|
29
|
-
|
|
30
|
-
POSTS = [...]
|
|
31
|
-
|
|
32
|
-
@app.get("/", response_class=HTMLResponse)
|
|
33
|
-
async def index() -> str:
|
|
34
|
-
published = [p for p in POSTS if p.published]
|
|
35
|
-
return <>
|
|
36
|
-
<Layout title="My Blog">
|
|
37
|
-
<h1>Latest Posts</h1>
|
|
38
|
-
<PostCard for={{ post in published }} post={{ post }} />
|
|
39
|
-
</Layout>
|
|
40
|
-
</>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Installation
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
pip install python-fragments
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Register the loader at your application's entry point, before importing any modules that contain fragments:
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
from fragments import loader # isort: skip
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/completion.py
RENAMED
|
File without changes
|
{python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/definition.py
RENAMED
|
File without changes
|
{python_fragments-0.28 → 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.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/hover.py
RENAMED
|
File without changes
|
{python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/inlay_hints.py
RENAMED
|
File without changes
|
{python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/lifecycle.py
RENAMED
|
File without changes
|
{python_fragments-0.28 → python_fragments-0.31}/fragments/lsp/client_message_handlers/references.py
RENAMED
|
File without changes
|
{python_fragments-0.28 → 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.28 → 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
|