jx 0.2.0__tar.gz → 0.4.0__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.
- {jx-0.2.0/src/jx.egg-info → jx-0.4.0}/PKG-INFO +12 -16
- {jx-0.2.0 → jx-0.4.0}/README.md +8 -9
- {jx-0.2.0 → jx-0.4.0}/pyproject.toml +17 -12
- {jx-0.2.0 → jx-0.4.0}/src/jx/__init__.py +2 -1
- {jx-0.2.0 → jx-0.4.0}/src/jx/attrs.py +57 -25
- {jx-0.2.0 → jx-0.4.0}/src/jx/catalog.py +94 -42
- {jx-0.2.0 → jx-0.4.0}/src/jx/component.py +3 -6
- {jx-0.2.0 → jx-0.4.0}/src/jx/exceptions.py +3 -1
- {jx-0.2.0 → jx-0.4.0}/src/jx/meta.py +4 -3
- {jx-0.2.0 → jx-0.4.0}/src/jx/parser.py +2 -2
- {jx-0.2.0 → jx-0.4.0}/src/jx/utils.py +2 -1
- {jx-0.2.0 → jx-0.4.0/src/jx.egg-info}/PKG-INFO +12 -16
- jx-0.4.0/src/jx.egg-info/requires.txt +1 -0
- {jx-0.2.0 → jx-0.4.0}/tests/test_attrs.py +23 -18
- {jx-0.2.0 → jx-0.4.0}/tests/test_catalog.py +23 -1
- {jx-0.2.0 → jx-0.4.0}/tests/test_component.py +68 -1
- {jx-0.2.0 → jx-0.4.0}/tests/test_meta.py +2 -1
- {jx-0.2.0 → jx-0.4.0}/tests/test_parser.py +2 -1
- {jx-0.2.0 → jx-0.4.0}/tests/test_slots.py +2 -1
- {jx-0.2.0 → jx-0.4.0}/tests/test_thread_safety.py +2 -1
- jx-0.2.0/src/jx.egg-info/requires.txt +0 -3
- {jx-0.2.0 → jx-0.4.0}/LICENSE +0 -0
- {jx-0.2.0 → jx-0.4.0}/setup.cfg +0 -0
- {jx-0.2.0 → jx-0.4.0}/src/jx.egg-info/SOURCES.txt +0 -0
- {jx-0.2.0 → jx-0.4.0}/src/jx.egg-info/dependency_links.txt +0 -0
- {jx-0.2.0 → jx-0.4.0}/src/jx.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Replace your HTML templates with Python server-Side components
|
|
5
5
|
Author-email: Juan Pablo Scaletti <juanpablo@jpscaletti.com>
|
|
6
|
-
Project-URL:
|
|
7
|
-
Project-URL:
|
|
8
|
-
Project-URL: documentation, https://jx.scaletti.dev/
|
|
6
|
+
Project-URL: Code, https://github.com/jpsca/jx
|
|
7
|
+
Project-URL: Documentation, https://jx.scaletti.dev/
|
|
9
8
|
Classifier: Development Status :: 4 - Beta
|
|
10
9
|
Classifier: Environment :: Web Environment
|
|
11
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -19,25 +18,22 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
19
18
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
19
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
21
20
|
Classifier: Typing :: Typed
|
|
22
|
-
Requires-Python: <4,>=3.
|
|
21
|
+
Requires-Python: <4,>=3.12
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
License-File: LICENSE
|
|
25
24
|
Requires-Dist: jinja2>=3.0
|
|
26
|
-
Requires-Dist: markupsafe>=2.0
|
|
27
|
-
Requires-Dist: ty>=0.0.1a15
|
|
28
25
|
Dynamic: license-file
|
|
29
26
|
|
|
30
|
-
<
|
|
31
|
-
<img src="https://
|
|
32
|
-
</
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<p>
|
|
37
|
-
<img alt="python: 3.11, 3.12, 3.13, 3.14" src="https://github.com/jpsca/jx/raw/main/docs/python.svg">
|
|
38
|
-
<img alt="license: MIT" src="https://github.com/jpsca/jx/raw/main/docs/license.svg">
|
|
27
|
+
<div align="center">
|
|
28
|
+
<h1><img alt="Jx" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/assets/images/logo-jx.png" height="100" align="top"></h1>
|
|
29
|
+
</div>
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img alt="python: 3.12, 3.13, 3.14" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/python.svg">
|
|
32
|
+
<img alt="license: MIT" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/license.svg">
|
|
39
33
|
</p>
|
|
40
34
|
|
|
35
|
+
### Python server-side components
|
|
36
|
+
|
|
41
37
|
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
42
38
|
|
|
43
39
|
<!-- Documentation: https://jx.scaletti.dev/ -->
|
{jx-0.2.0 → jx-0.4.0}/README.md
RENAMED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
<
|
|
2
|
-
<img src="https://
|
|
3
|
-
</
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<p>
|
|
8
|
-
<img alt="python: 3.11, 3.12, 3.13, 3.14" src="https://github.com/jpsca/jx/raw/main/docs/python.svg">
|
|
9
|
-
<img alt="license: MIT" src="https://github.com/jpsca/jx/raw/main/docs/license.svg">
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1><img alt="Jx" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/assets/images/logo-jx.png" height="100" align="top"></h1>
|
|
3
|
+
</div>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<img alt="python: 3.12, 3.13, 3.14" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/python.svg">
|
|
6
|
+
<img alt="license: MIT" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/license.svg">
|
|
10
7
|
</p>
|
|
11
8
|
|
|
9
|
+
### Python server-side components
|
|
10
|
+
|
|
12
11
|
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
13
12
|
|
|
14
13
|
<!-- Documentation: https://jx.scaletti.dev/ -->
|
|
@@ -4,7 +4,7 @@ requires = ["setuptools"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Replace your HTML templates with Python server-Side components"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Juan Pablo Scaletti", email = "juanpablo@jpscaletti.com"},
|
|
@@ -26,25 +26,24 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Text Processing :: Markup :: HTML",
|
|
27
27
|
"Typing :: Typed",
|
|
28
28
|
]
|
|
29
|
-
requires-python = ">=3.
|
|
29
|
+
requires-python = ">=3.12,<4"
|
|
30
30
|
dependencies = [
|
|
31
31
|
"jinja2 >= 3.0",
|
|
32
|
-
"markupsafe >= 2.0",
|
|
33
|
-
"ty>=0.0.1a15",
|
|
34
32
|
]
|
|
35
33
|
|
|
36
34
|
[project.urls]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
documentation = "https://jx.scaletti.dev/"
|
|
35
|
+
Code = "https://github.com/jpsca/jx"
|
|
36
|
+
Documentation = "https://jx.scaletti.dev/"
|
|
40
37
|
|
|
41
38
|
[dependency-groups]
|
|
42
39
|
dev = [
|
|
43
|
-
"ipdb
|
|
40
|
+
"ipdb",
|
|
44
41
|
"pre-commit",
|
|
45
|
-
"ruff >= 0.2.0",
|
|
46
42
|
"tox-uv",
|
|
47
|
-
"ty
|
|
43
|
+
"ty",
|
|
44
|
+
]
|
|
45
|
+
docs = [
|
|
46
|
+
"writeadoc>=0.7.0",
|
|
48
47
|
]
|
|
49
48
|
test = [
|
|
50
49
|
"pytest >= 7.2",
|
|
@@ -56,6 +55,13 @@ test = [
|
|
|
56
55
|
[tool.setuptools.packages.find]
|
|
57
56
|
where = ["src"]
|
|
58
57
|
|
|
58
|
+
|
|
59
|
+
[tool.ty.src]
|
|
60
|
+
exclude = [
|
|
61
|
+
"docs",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
59
65
|
[tool.coverage.run]
|
|
60
66
|
branch = true
|
|
61
67
|
|
|
@@ -82,7 +88,6 @@ addopts = "--doctest-modules"
|
|
|
82
88
|
legacy_tox_ini = """
|
|
83
89
|
[tox]
|
|
84
90
|
env_list =
|
|
85
|
-
3.11
|
|
86
91
|
3.12
|
|
87
92
|
3.13
|
|
88
93
|
3.14
|
|
@@ -100,7 +105,7 @@ commands =
|
|
|
100
105
|
[tool.ruff]
|
|
101
106
|
line-length = 90
|
|
102
107
|
indent-width = 4
|
|
103
|
-
target-version = "
|
|
108
|
+
target-version = "py312"
|
|
104
109
|
exclude = [
|
|
105
110
|
".*",
|
|
106
111
|
"_build",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import typing as t
|
|
6
6
|
from collections import UserString
|
|
7
7
|
from functools import cached_property
|
|
@@ -14,10 +14,6 @@ CLASS_ALT_KEY = "classes"
|
|
|
14
14
|
CLASS_KEYS = (CLASS_KEY, CLASS_ALT_KEY)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def split(ssl: str) -> list[str]:
|
|
18
|
-
return re.split(r"\s+", ssl.strip())
|
|
19
|
-
|
|
20
|
-
|
|
21
17
|
def quote(text: str) -> str:
|
|
22
18
|
if '"' in text:
|
|
23
19
|
if "'" in text:
|
|
@@ -45,6 +41,10 @@ class LazyString(UserString):
|
|
|
45
41
|
|
|
46
42
|
|
|
47
43
|
class Attrs:
|
|
44
|
+
__classes: tuple[str, ...]
|
|
45
|
+
__attributes: dict[str, str | LazyString]
|
|
46
|
+
__properties: set[str]
|
|
47
|
+
|
|
48
48
|
def __init__(self, attrs: "dict[str, t.Any| LazyString]") -> None:
|
|
49
49
|
"""
|
|
50
50
|
Contains all the HTML attributes/properties (a property is an
|
|
@@ -62,11 +62,15 @@ class Attrs:
|
|
|
62
62
|
attributes: "dict[str, str | LazyString]" = {}
|
|
63
63
|
properties: set[str] = set()
|
|
64
64
|
|
|
65
|
-
class_names =
|
|
65
|
+
class_names = (" ".join([
|
|
66
66
|
str(attrs.pop(CLASS_KEY, "")),
|
|
67
67
|
str(attrs.get(CLASS_ALT_KEY, "")),
|
|
68
|
-
]))
|
|
69
|
-
|
|
68
|
+
])).strip().split()
|
|
69
|
+
classes = []
|
|
70
|
+
for name in class_names:
|
|
71
|
+
if name and name not in classes:
|
|
72
|
+
classes.append(name)
|
|
73
|
+
self.__classes = tuple(classes)
|
|
70
74
|
|
|
71
75
|
for name, value in attrs.items():
|
|
72
76
|
if name.startswith("_"):
|
|
@@ -83,7 +87,7 @@ class Attrs:
|
|
|
83
87
|
@property
|
|
84
88
|
def classes(self) -> str:
|
|
85
89
|
"""
|
|
86
|
-
All the HTML classes
|
|
90
|
+
All the HTML classes separated by a space.
|
|
87
91
|
|
|
88
92
|
Example:
|
|
89
93
|
|
|
@@ -91,11 +95,11 @@ class Attrs:
|
|
|
91
95
|
attrs = Attrs({"class": "italic bold bg-blue wide abcde"})
|
|
92
96
|
attrs.set(class="bold text-white")
|
|
93
97
|
print(attrs.classes)
|
|
94
|
-
|
|
98
|
+
italic bold bg-blue wide abcde text-white
|
|
95
99
|
```
|
|
96
100
|
|
|
97
101
|
"""
|
|
98
|
-
return " ".join(
|
|
102
|
+
return " ".join(self.__classes)
|
|
99
103
|
|
|
100
104
|
@property
|
|
101
105
|
def as_dict(self) -> dict[str, t.Any]:
|
|
@@ -116,7 +120,7 @@ class Attrs:
|
|
|
116
120
|
attrs.as_dict
|
|
117
121
|
{
|
|
118
122
|
"aria_label": "hello",
|
|
119
|
-
"class": "ipsum
|
|
123
|
+
"class": "lorem ipsum",
|
|
120
124
|
"id": "world",
|
|
121
125
|
"data_test": True,
|
|
122
126
|
"hidden": True
|
|
@@ -155,8 +159,6 @@ class Attrs:
|
|
|
155
159
|
- The underscores in the names will be translated automatically to dashes,
|
|
156
160
|
so `aria_selected` becomes the attribute `aria-selected`.
|
|
157
161
|
|
|
158
|
-
TODO: vue-style
|
|
159
|
-
|
|
160
162
|
Example:
|
|
161
163
|
|
|
162
164
|
```python
|
|
@@ -172,7 +174,7 @@ class Attrs:
|
|
|
172
174
|
attrs = Attrs({"class": "b c a"})
|
|
173
175
|
attrs.set(class="c b f d e")
|
|
174
176
|
attrs.as_dict
|
|
175
|
-
{"class": "
|
|
177
|
+
{"class": "b c a f d e"}
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
"""
|
|
@@ -212,7 +214,9 @@ class Attrs:
|
|
|
212
214
|
continue
|
|
213
215
|
|
|
214
216
|
if name in CLASS_KEYS:
|
|
215
|
-
self.
|
|
217
|
+
if not self.__classes:
|
|
218
|
+
self.add_class(value)
|
|
219
|
+
continue
|
|
216
220
|
|
|
217
221
|
name = name.replace("_", "-")
|
|
218
222
|
if name not in self.__attributes:
|
|
@@ -220,10 +224,10 @@ class Attrs:
|
|
|
220
224
|
|
|
221
225
|
def add_class(self, *values: str) -> None:
|
|
222
226
|
"""
|
|
223
|
-
Adds one or more classes to the list of classes,
|
|
227
|
+
Adds one or more classes to the end of the list of classes,
|
|
228
|
+
if not already present.
|
|
224
229
|
|
|
225
230
|
Arguments:
|
|
226
|
-
|
|
227
231
|
values:
|
|
228
232
|
One or more class names to add, separated by spaces.
|
|
229
233
|
|
|
@@ -231,15 +235,44 @@ class Attrs:
|
|
|
231
235
|
|
|
232
236
|
```python
|
|
233
237
|
attrs = Attrs({"class": "a b c"})
|
|
234
|
-
attrs.add_class("c
|
|
238
|
+
attrs.add_class("c d")
|
|
235
239
|
attrs.as_dict
|
|
236
240
|
{"class": "a b c d"}
|
|
237
241
|
```
|
|
238
242
|
|
|
239
243
|
"""
|
|
240
244
|
for names in values:
|
|
241
|
-
for name in split(
|
|
242
|
-
self.__classes
|
|
245
|
+
for name in names.strip().split():
|
|
246
|
+
if name not in self.__classes:
|
|
247
|
+
self.__classes += (name,)
|
|
248
|
+
|
|
249
|
+
def prepend_class(self, *values: str) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Adds one or more classes to the beginning of the list of classes,
|
|
252
|
+
if not already present.
|
|
253
|
+
|
|
254
|
+
Arguments:
|
|
255
|
+
values:
|
|
256
|
+
One or more class names to add, separated by spaces.
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
attrs = Attrs({"class": "a b c"})
|
|
262
|
+
attrs.add_class("c d |")
|
|
263
|
+
attrs.as_dict
|
|
264
|
+
{"class": "d | a b c"}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
new_classes = [
|
|
269
|
+
name
|
|
270
|
+
for names in values
|
|
271
|
+
for name in names.strip().split()
|
|
272
|
+
if name not in self.__classes
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
self.__classes = tuple(new_classes) + self.__classes
|
|
243
276
|
|
|
244
277
|
def remove_class(self, *names: str) -> None:
|
|
245
278
|
"""
|
|
@@ -255,8 +288,7 @@ class Attrs:
|
|
|
255
288
|
```
|
|
256
289
|
|
|
257
290
|
"""
|
|
258
|
-
for
|
|
259
|
-
self.__classes.remove(name)
|
|
291
|
+
self.__classes = tuple(c for c in self.__classes if c not in names)
|
|
260
292
|
|
|
261
293
|
def get(self, name: str, default: t.Any = None) -> t.Any:
|
|
262
294
|
"""
|
|
@@ -358,7 +390,7 @@ class Attrs:
|
|
|
358
390
|
Removes an attribute or property.
|
|
359
391
|
"""
|
|
360
392
|
if name in CLASS_KEYS:
|
|
361
|
-
self.__classes =
|
|
393
|
+
self.__classes = ()
|
|
362
394
|
if name in self.__attributes:
|
|
363
395
|
del self.__attributes[name]
|
|
364
396
|
if name in self.__properties:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import typing as t
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from pathlib import Path
|
|
@@ -23,7 +24,7 @@ class CData:
|
|
|
23
24
|
mtime: float
|
|
24
25
|
code: CodeType | None = None
|
|
25
26
|
required: tuple[str, ...] = ()
|
|
26
|
-
optional: dict[str, t.Any] = field(default_factory=dict)
|
|
27
|
+
optional: dict[str, t.Any] = field(default_factory=dict) # { attr: default_value }
|
|
27
28
|
imports: dict[str, str] = field(default_factory=dict) # { name: relpath }
|
|
28
29
|
css: tuple[str, ...] = ()
|
|
29
30
|
js: tuple[str, ...] = ()
|
|
@@ -31,8 +32,9 @@ class CData:
|
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class Catalog:
|
|
35
|
+
|
|
34
36
|
# IDEA: This dict could be replaced by a dict-like object
|
|
35
|
-
# that
|
|
37
|
+
# that uses a LRU cache (to limit the memory used)
|
|
36
38
|
# or even a shared Redis/Memcache cache.
|
|
37
39
|
components: dict[str, CData]
|
|
38
40
|
|
|
@@ -40,11 +42,11 @@ class Catalog:
|
|
|
40
42
|
self,
|
|
41
43
|
folder: str | Path | None = None,
|
|
42
44
|
*,
|
|
43
|
-
auto_reload: bool = True,
|
|
44
45
|
jinja_env: jinja2.Environment | None = None,
|
|
46
|
+
extensions: list | None = None,
|
|
45
47
|
filters: dict[str, t.Any] | None = None,
|
|
46
48
|
tests: dict[str, t.Any] | None = None,
|
|
47
|
-
|
|
49
|
+
auto_reload: bool = True,
|
|
48
50
|
**globals: t.Any,
|
|
49
51
|
) -> None:
|
|
50
52
|
"""
|
|
@@ -54,19 +56,19 @@ class Catalog:
|
|
|
54
56
|
folder:
|
|
55
57
|
Optional folder path to scan for components. It's a shortcut to
|
|
56
58
|
calling `add_folder` when only one is used.
|
|
57
|
-
auto_reload:
|
|
58
|
-
Whether to check the last-modified time of the components files and
|
|
59
|
-
automatically re-process them if they change. The performance impact of
|
|
60
|
-
leaving it on is minimal, but *might* be noticeable when rendering a page
|
|
61
|
-
that uses a large number of different components.
|
|
62
59
|
jinja_env:
|
|
63
60
|
Optional Jinja2 environment to use for rendering.
|
|
64
|
-
filters:
|
|
65
|
-
Optional extra Jinja2 filters to add to the environment.
|
|
66
61
|
extensions:
|
|
67
62
|
Optional extra Jinja2 extensions to add to the environment.
|
|
63
|
+
filters:
|
|
64
|
+
Optional extra Jinja2 filters to add to the environment.
|
|
68
65
|
tests:
|
|
69
66
|
Optional extra Jinja2 tests to add to the environment.
|
|
67
|
+
auto_reload:
|
|
68
|
+
Whether to check the last-modified time of the components files and
|
|
69
|
+
automatically re-process them if they change. The performance impact of
|
|
70
|
+
leaving it on is minimal, but *might* be noticeable when rendering a
|
|
71
|
+
component that uses a large number of child components.
|
|
70
72
|
**globals:
|
|
71
73
|
Variables to make available to all components by default.
|
|
72
74
|
|
|
@@ -84,11 +86,7 @@ class Catalog:
|
|
|
84
86
|
self.add_folder(folder)
|
|
85
87
|
|
|
86
88
|
def add_folder(
|
|
87
|
-
self,
|
|
88
|
-
path: str | Path,
|
|
89
|
-
*,
|
|
90
|
-
prefix: str = "",
|
|
91
|
-
preload: bool = True
|
|
89
|
+
self, path: str | Path, *, prefix: str = "", preload: bool = True
|
|
92
90
|
) -> None:
|
|
93
91
|
"""
|
|
94
92
|
Add a folder path from which to search for components, optionally under a prefix.
|
|
@@ -141,9 +139,7 @@ class Catalog:
|
|
|
141
139
|
logger.debug(f"Component already exists: {relpath}")
|
|
142
140
|
continue
|
|
143
141
|
cdata = CData(
|
|
144
|
-
base_path=base_path,
|
|
145
|
-
path=filepath,
|
|
146
|
-
mtime=filepath.stat().st_mtime
|
|
142
|
+
base_path=base_path, path=filepath, mtime=filepath.stat().st_mtime
|
|
147
143
|
)
|
|
148
144
|
self.components[relpath] = cdata
|
|
149
145
|
|
|
@@ -151,7 +147,9 @@ class Catalog:
|
|
|
151
147
|
for relpath in self.components:
|
|
152
148
|
self.components[relpath] = self.get_component_data(relpath)
|
|
153
149
|
|
|
154
|
-
def render(
|
|
150
|
+
def render(
|
|
151
|
+
self, relpath: str, globals: dict[str, t.Any] | None = None, **kwargs
|
|
152
|
+
) -> str:
|
|
155
153
|
"""
|
|
156
154
|
Render a component with the given relative path and context.
|
|
157
155
|
|
|
@@ -173,6 +171,64 @@ class Catalog:
|
|
|
173
171
|
relpath = relpath.replace("\\", "/").strip("/")
|
|
174
172
|
co = self.get_component(relpath)
|
|
175
173
|
|
|
174
|
+
globals = globals or {}
|
|
175
|
+
globals.update(
|
|
176
|
+
{
|
|
177
|
+
"assets": {
|
|
178
|
+
"css": co.collect_css,
|
|
179
|
+
"js": co.collect_js,
|
|
180
|
+
"render_css": co.render_css,
|
|
181
|
+
"render_js": co.render_js,
|
|
182
|
+
"render": co.render_assets,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
co.globals = globals
|
|
187
|
+
|
|
188
|
+
return co.render(**kwargs)
|
|
189
|
+
|
|
190
|
+
def render_string(
|
|
191
|
+
self, source: str, globals: dict[str, t.Any] | None = None, **kwargs
|
|
192
|
+
) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Render a component from a string source.
|
|
195
|
+
Works like `render`, but the component is not cached and cannot do relative imports.
|
|
196
|
+
|
|
197
|
+
Arguments:
|
|
198
|
+
source:
|
|
199
|
+
The Jinja2 source code of the component to render.
|
|
200
|
+
globals:
|
|
201
|
+
Optional global variables to make available to the component and all its
|
|
202
|
+
imported components.
|
|
203
|
+
**kwargs:
|
|
204
|
+
Keyword arguments to pass to the component.
|
|
205
|
+
They will be available in the component's context but not to its imported components.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The rendered component as a string.
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
meta = extract_metadata(source, base_path=Path(), fullpath=Path())
|
|
212
|
+
name = "<string>"
|
|
213
|
+
|
|
214
|
+
parser = JxParser(name=name, source=source, components=list(meta.imports.keys()))
|
|
215
|
+
parsed_source, slots = parser.parse()
|
|
216
|
+
|
|
217
|
+
code = self.jinja_env.compile(source=parsed_source, name=name, filename=name)
|
|
218
|
+
tmpl = jinja2.Template.from_code(self.jinja_env, code, self.jinja_env.globals)
|
|
219
|
+
|
|
220
|
+
co = Component(
|
|
221
|
+
relpath=name,
|
|
222
|
+
tmpl=tmpl,
|
|
223
|
+
get_component=self.get_component,
|
|
224
|
+
required=meta.required,
|
|
225
|
+
optional=meta.optional,
|
|
226
|
+
imports=meta.imports,
|
|
227
|
+
css=meta.css,
|
|
228
|
+
js=meta.js,
|
|
229
|
+
slots=slots,
|
|
230
|
+
)
|
|
231
|
+
|
|
176
232
|
globals = globals or {}
|
|
177
233
|
globals.update({
|
|
178
234
|
"assets": {
|
|
@@ -181,7 +237,7 @@ class Catalog:
|
|
|
181
237
|
"render_css": co.render_css,
|
|
182
238
|
"render_js": co.render_js,
|
|
183
239
|
"render": co.render_assets,
|
|
184
|
-
}
|
|
240
|
+
}
|
|
185
241
|
})
|
|
186
242
|
co.globals = globals
|
|
187
243
|
|
|
@@ -189,8 +245,8 @@ class Catalog:
|
|
|
189
245
|
|
|
190
246
|
def get_component_data(self, relpath: str) -> CData:
|
|
191
247
|
"""
|
|
192
|
-
Get the component data from the cache
|
|
193
|
-
|
|
248
|
+
Get the component data from the cache.
|
|
249
|
+
If the file has been updated, the component is re-processed.
|
|
194
250
|
|
|
195
251
|
Arguments:
|
|
196
252
|
relpath:
|
|
@@ -214,15 +270,11 @@ class Catalog:
|
|
|
214
270
|
meta = extract_metadata(source, base_path=cdata.base_path, fullpath=cdata.path)
|
|
215
271
|
|
|
216
272
|
parser = JxParser(
|
|
217
|
-
name=relpath,
|
|
218
|
-
source=source,
|
|
219
|
-
components=list(meta.imports.keys())
|
|
273
|
+
name=relpath, source=source, components=list(meta.imports.keys())
|
|
220
274
|
)
|
|
221
275
|
parsed_source, slots = parser.parse()
|
|
222
276
|
code = self.jinja_env.compile(
|
|
223
|
-
source=parsed_source,
|
|
224
|
-
name=relpath,
|
|
225
|
-
filename=cdata.path.as_posix()
|
|
277
|
+
source=parsed_source, name=relpath, filename=cdata.path.as_posix()
|
|
226
278
|
)
|
|
227
279
|
|
|
228
280
|
cdata.code = code
|
|
@@ -247,9 +299,7 @@ class Catalog:
|
|
|
247
299
|
cdata = self.get_component_data(relpath)
|
|
248
300
|
assert cdata.code is not None
|
|
249
301
|
tmpl = jinja2.Template.from_code(
|
|
250
|
-
self.jinja_env,
|
|
251
|
-
cdata.code,
|
|
252
|
-
self.jinja_env.globals
|
|
302
|
+
self.jinja_env, cdata.code, self.jinja_env.globals
|
|
253
303
|
)
|
|
254
304
|
|
|
255
305
|
co = Component(
|
|
@@ -279,9 +329,6 @@ class Catalog:
|
|
|
279
329
|
"""
|
|
280
330
|
Create a new Jinja2 environment with the specified settings.
|
|
281
331
|
|
|
282
|
-
If an existing environment is provided, an "overlay" of it will
|
|
283
|
-
be created and used.
|
|
284
|
-
|
|
285
332
|
Arguments:
|
|
286
333
|
jinja_env:
|
|
287
334
|
Optional Jinja2 environment to use as a base.
|
|
@@ -297,16 +344,21 @@ class Catalog:
|
|
|
297
344
|
"""
|
|
298
345
|
jinja_env = jinja_env or getattr(self, "jinja_env", None)
|
|
299
346
|
if jinja_env:
|
|
300
|
-
|
|
347
|
+
# It could be `jinja_env.overlay()` instead, but that might
|
|
348
|
+
# might lead to confusion if the user expects changes
|
|
349
|
+
# to the original environment to be reflected here.
|
|
350
|
+
env = jinja_env
|
|
301
351
|
else:
|
|
302
352
|
env = jinja2.Environment()
|
|
303
353
|
|
|
304
354
|
globals = globals or {}
|
|
305
|
-
globals.update(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
355
|
+
globals.update(
|
|
356
|
+
{
|
|
357
|
+
# A unique ID generator for HTML elements, see `utils.get_random_id`
|
|
358
|
+
# docstring for more information.
|
|
359
|
+
"_get_random_id": utils.get_random_id,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
310
362
|
env.globals.update(globals)
|
|
311
363
|
|
|
312
364
|
filters = filters or {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import typing as t
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
|
|
@@ -12,9 +12,6 @@ from .attrs import Attrs
|
|
|
12
12
|
from .exceptions import MissingRequiredArgument
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
rx_external_url = re.compile(r"^[a-z]+://", re.IGNORECASE)
|
|
16
|
-
|
|
17
|
-
|
|
18
15
|
class Component:
|
|
19
16
|
__slots__ = (
|
|
20
17
|
"relpath",
|
|
@@ -85,7 +82,7 @@ class Component:
|
|
|
85
82
|
content: str | None = None,
|
|
86
83
|
attrs: Attrs | dict[str, t.Any] | None = None,
|
|
87
84
|
caller: Callable[[str], str] | None = None,
|
|
88
|
-
**params: t.Any
|
|
85
|
+
**params: t.Any,
|
|
89
86
|
) -> Markup:
|
|
90
87
|
content = content if content is not None else caller("") if caller else ""
|
|
91
88
|
attrs = attrs.as_dict if isinstance(attrs, Attrs) else attrs or {}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class JxException(Exception):
|
|
6
7
|
"""Base class for all Jx exceptions."""
|
|
7
8
|
|
|
@@ -18,6 +19,7 @@ class ImportError(JxException):
|
|
|
18
19
|
Raised when an import fails.
|
|
19
20
|
This is usually caused by a missing or inaccessible component.
|
|
20
21
|
"""
|
|
22
|
+
|
|
21
23
|
def __init__(self, relpath: str, **kw) -> None:
|
|
22
24
|
msg = f"Component not found: {relpath}"
|
|
23
25
|
super().__init__(msg, **kw)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import ast
|
|
5
6
|
import re
|
|
6
7
|
import typing as t
|
|
@@ -56,9 +57,9 @@ def extract_metadata(source: str, base_path: Path, fullpath: Path) -> Meta:
|
|
|
56
57
|
source:
|
|
57
58
|
The template source code.
|
|
58
59
|
base_path:
|
|
59
|
-
Absolute base path for all the template files
|
|
60
|
+
Absolute base path for all the template files, for relative imports.
|
|
60
61
|
fullpath:
|
|
61
|
-
The absolute full path of the current template.
|
|
62
|
+
The absolute full path of the current template, for relative imports.
|
|
62
63
|
|
|
63
64
|
Returns:
|
|
64
65
|
A `Meta` object containing the extracted metadata.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import re
|
|
@@ -24,7 +24,7 @@ RX_TAG_NAME = re.compile(rf"<(?P<tag>{re_tag_name})(\s|\n|/|>)")
|
|
|
24
24
|
re_attr_name = r""
|
|
25
25
|
re_equal = r""
|
|
26
26
|
re_attr = r"""
|
|
27
|
-
(?P<name>[:a-zA-Z@$_][a-zA-Z@:$_0-9
|
|
27
|
+
(?P<name>[:a-zA-Z@$_][a-zA-Z@:$_0-9-\.]*)
|
|
28
28
|
(?:
|
|
29
29
|
\s*=\s*
|
|
30
30
|
(?P<value>".*?"|'.*?'|\{\{.*?\}\})
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Replace your HTML templates with Python server-Side components
|
|
5
5
|
Author-email: Juan Pablo Scaletti <juanpablo@jpscaletti.com>
|
|
6
|
-
Project-URL:
|
|
7
|
-
Project-URL:
|
|
8
|
-
Project-URL: documentation, https://jx.scaletti.dev/
|
|
6
|
+
Project-URL: Code, https://github.com/jpsca/jx
|
|
7
|
+
Project-URL: Documentation, https://jx.scaletti.dev/
|
|
9
8
|
Classifier: Development Status :: 4 - Beta
|
|
10
9
|
Classifier: Environment :: Web Environment
|
|
11
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -19,25 +18,22 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
19
18
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
19
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
21
20
|
Classifier: Typing :: Typed
|
|
22
|
-
Requires-Python: <4,>=3.
|
|
21
|
+
Requires-Python: <4,>=3.12
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
License-File: LICENSE
|
|
25
24
|
Requires-Dist: jinja2>=3.0
|
|
26
|
-
Requires-Dist: markupsafe>=2.0
|
|
27
|
-
Requires-Dist: ty>=0.0.1a15
|
|
28
25
|
Dynamic: license-file
|
|
29
26
|
|
|
30
|
-
<
|
|
31
|
-
<img src="https://
|
|
32
|
-
</
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<p>
|
|
37
|
-
<img alt="python: 3.11, 3.12, 3.13, 3.14" src="https://github.com/jpsca/jx/raw/main/docs/python.svg">
|
|
38
|
-
<img alt="license: MIT" src="https://github.com/jpsca/jx/raw/main/docs/license.svg">
|
|
27
|
+
<div align="center">
|
|
28
|
+
<h1><img alt="Jx" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/assets/images/logo-jx.png" height="100" align="top"></h1>
|
|
29
|
+
</div>
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img alt="python: 3.12, 3.13, 3.14" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/python.svg">
|
|
32
|
+
<img alt="license: MIT" src="https://raw.githubusercontent.com/jpsca/jx/main/docs/license.svg">
|
|
39
33
|
</p>
|
|
40
34
|
|
|
35
|
+
### Python server-side components
|
|
36
|
+
|
|
41
37
|
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
42
38
|
|
|
43
39
|
<!-- Documentation: https://jx.scaletti.dev/ -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jinja2>=3.0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from jx.attrs import Attrs
|
|
@@ -19,8 +20,8 @@ def test_parse_initial_attrs():
|
|
|
19
20
|
"_content": "content",
|
|
20
21
|
}
|
|
21
22
|
)
|
|
22
|
-
assert attrs.classes == "a1 b2
|
|
23
|
-
assert attrs.get("class") == "a1 b2
|
|
23
|
+
assert attrs.classes == "z4 c3 a1 b2"
|
|
24
|
+
assert attrs.get("class") == "z4 c3 a1 b2"
|
|
24
25
|
assert attrs.get("data-position") == "top"
|
|
25
26
|
assert attrs.get("data_position") == "top"
|
|
26
27
|
assert attrs.get("title") == "hi"
|
|
@@ -37,6 +38,7 @@ def test_parse_initial_attrs():
|
|
|
37
38
|
assert attrs.get("data-position") is None
|
|
38
39
|
assert attrs.get("data_position") is None
|
|
39
40
|
|
|
41
|
+
|
|
40
42
|
def test_getattr():
|
|
41
43
|
attrs = Attrs(
|
|
42
44
|
{
|
|
@@ -45,7 +47,7 @@ def test_getattr():
|
|
|
45
47
|
"open": True,
|
|
46
48
|
}
|
|
47
49
|
)
|
|
48
|
-
assert attrs["class"] == "a1 b2
|
|
50
|
+
assert attrs["class"] == "z4 c3 a1 b2"
|
|
49
51
|
assert attrs["title"] == "hi"
|
|
50
52
|
assert attrs["open"] is True
|
|
51
53
|
assert attrs["lorem"] is None
|
|
@@ -59,7 +61,7 @@ def test_deltattr():
|
|
|
59
61
|
"open": True,
|
|
60
62
|
}
|
|
61
63
|
)
|
|
62
|
-
assert attrs["class"] == "a1 b2
|
|
64
|
+
assert attrs["class"] == "z4 c3 a1 b2"
|
|
63
65
|
del attrs["title"]
|
|
64
66
|
assert attrs["title"] is None
|
|
65
67
|
|
|
@@ -74,7 +76,7 @@ def test_render():
|
|
|
74
76
|
"disabled": False,
|
|
75
77
|
}
|
|
76
78
|
)
|
|
77
|
-
assert 'class="a1 b2
|
|
79
|
+
assert 'class="z4 c3 a1 b2" data-position="top" title="hi" open' == attrs.render()
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
def test_set():
|
|
@@ -90,15 +92,18 @@ def test_set():
|
|
|
90
92
|
def test_class_management():
|
|
91
93
|
attrs = Attrs(
|
|
92
94
|
{
|
|
93
|
-
"class": "z4 c3
|
|
95
|
+
"class": "z4 c3 a1 z4 b2",
|
|
94
96
|
}
|
|
95
97
|
)
|
|
96
98
|
attrs.set(classes="lorem bipsum lorem a1")
|
|
97
99
|
|
|
98
|
-
assert attrs.classes == "a1 b2
|
|
100
|
+
assert attrs.classes == "z4 c3 a1 b2 lorem bipsum"
|
|
99
101
|
|
|
100
|
-
attrs.remove_class("
|
|
101
|
-
assert attrs.classes == "a1 b2
|
|
102
|
+
attrs.remove_class("lorem")
|
|
103
|
+
assert attrs.classes == "z4 c3 a1 b2 bipsum"
|
|
104
|
+
|
|
105
|
+
attrs.prepend_class("button |", "wat")
|
|
106
|
+
assert attrs.classes == "button | wat z4 c3 a1 b2 bipsum"
|
|
102
107
|
|
|
103
108
|
attrs.set(classes=None)
|
|
104
109
|
attrs.set(classes="meh")
|
|
@@ -121,11 +126,11 @@ def test_setdefault():
|
|
|
121
126
|
|
|
122
127
|
|
|
123
128
|
def test_setdefault_classes():
|
|
124
|
-
attrs = Attrs(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
+
attrs = Attrs({"class": "a"})
|
|
130
|
+
attrs.setdefault(classes="a b c")
|
|
131
|
+
assert 'class="a"' == attrs.render()
|
|
132
|
+
|
|
133
|
+
attrs = Attrs({})
|
|
129
134
|
attrs.setdefault(classes="a b c")
|
|
130
135
|
assert 'class="a b c"' == attrs.render()
|
|
131
136
|
|
|
@@ -141,7 +146,7 @@ def test_as_dict():
|
|
|
141
146
|
}
|
|
142
147
|
)
|
|
143
148
|
assert attrs.as_dict == {
|
|
144
|
-
"class": "a1 b2
|
|
149
|
+
"class": "z4 c3 a1 b2",
|
|
145
150
|
"data-position": "top",
|
|
146
151
|
"title": "hi",
|
|
147
152
|
"open": True,
|
|
@@ -165,7 +170,7 @@ def test_as_dict_no_classes():
|
|
|
165
170
|
|
|
166
171
|
def test_render_attrs_lik_set():
|
|
167
172
|
attrs = Attrs({"class": "lorem"})
|
|
168
|
-
expected = 'class="ipsum
|
|
173
|
+
expected = 'class="lorem ipsum" data-position="top" title="hi" open'
|
|
169
174
|
result = attrs.render(
|
|
170
175
|
title="hi",
|
|
171
176
|
data_position="top",
|
|
@@ -178,7 +183,7 @@ def test_render_attrs_lik_set():
|
|
|
178
183
|
|
|
179
184
|
def test_do_not_escape_tailwind_syntax():
|
|
180
185
|
attrs = Attrs({"class": "lorem [&_a]:flex"})
|
|
181
|
-
expected = 'class="[&_a]:flex ipsum
|
|
186
|
+
expected = 'class="lorem [&_a]:flex ipsum" title="Hi&Stuff"'
|
|
182
187
|
result = attrs.render(
|
|
183
188
|
**{
|
|
184
189
|
"title": "Hi&Stuff",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import jinja2
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -27,6 +28,27 @@ def test_add_folder(folder):
|
|
|
27
28
|
assert catalog.components["b.jinja"].code is not None
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def test_add_folder_no_preload(folder):
|
|
32
|
+
(folder / "a.jinja").write_text("AAAAA")
|
|
33
|
+
(folder / "b.jinja").write_text("BBBBB")
|
|
34
|
+
|
|
35
|
+
catalog = Catalog()
|
|
36
|
+
catalog.add_folder(folder, preload=False)
|
|
37
|
+
|
|
38
|
+
assert "a.jinja" in catalog.components
|
|
39
|
+
assert "b.jinja" in catalog.components
|
|
40
|
+
|
|
41
|
+
assert catalog.components["a.jinja"].base_path == folder
|
|
42
|
+
assert catalog.components["a.jinja"].path == folder / "a.jinja"
|
|
43
|
+
assert catalog.components["a.jinja"].mtime > 0
|
|
44
|
+
assert catalog.components["a.jinja"].code is None
|
|
45
|
+
|
|
46
|
+
assert catalog.components["b.jinja"].base_path == folder
|
|
47
|
+
assert catalog.components["b.jinja"].path == folder / "b.jinja"
|
|
48
|
+
assert catalog.components["b.jinja"].mtime > 0
|
|
49
|
+
assert catalog.components["b.jinja"].code is None
|
|
50
|
+
|
|
51
|
+
|
|
30
52
|
def test_add_folder_nested(tmp_path):
|
|
31
53
|
folder = tmp_path / "views"
|
|
32
54
|
nested = folder / "a" / "b" / "c"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
2
|
+
Jx | Copyright (c) Juan-Pablo Scaletti
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from jx import Catalog, MissingRequiredArgument, TemplateSyntaxError
|
|
@@ -17,6 +18,17 @@ def test_render_simple(folder):
|
|
|
17
18
|
assert html.strip() == '<button id="btn1">Submit</button>'
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def test_render_simple_from_string():
|
|
22
|
+
source = """
|
|
23
|
+
{# def bid, text="Click me!" #}
|
|
24
|
+
<button id="{{ bid }}">{{ text }}</button>
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
cat = Catalog()
|
|
28
|
+
html = cat.render_string(source, bid="btn1", text="Submit")
|
|
29
|
+
assert html.strip() == '<button id="btn1">Submit</button>'
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
def test_render_content(folder):
|
|
21
33
|
(folder / "child.jinja").write_text("""
|
|
22
34
|
<span>{{ content }}</span>
|
|
@@ -32,6 +44,19 @@ def test_render_content(folder):
|
|
|
32
44
|
assert html.strip() == "<div><span>Hello</span></div>"
|
|
33
45
|
|
|
34
46
|
|
|
47
|
+
def test_render_content_from_string(folder):
|
|
48
|
+
(folder / "child.jinja").write_text("""
|
|
49
|
+
<span>{{ content }}</span>
|
|
50
|
+
""")
|
|
51
|
+
source = """
|
|
52
|
+
{# import "child.jinja" as Child #}
|
|
53
|
+
<div><Child>Hello</Child></div>
|
|
54
|
+
"""
|
|
55
|
+
cat = Catalog(folder)
|
|
56
|
+
html = cat.render_string(source)
|
|
57
|
+
assert html.strip() == "<div><span>Hello</span></div>"
|
|
58
|
+
|
|
59
|
+
|
|
35
60
|
def test_render_custom_content(folder):
|
|
36
61
|
(folder / "child.jinja").write_text("""
|
|
37
62
|
<span>{{ content }}</span>
|
|
@@ -142,6 +167,21 @@ def test_render_globals(folder):
|
|
|
142
167
|
assert cat.render("page.jinja") == '<div class="ipsum"><p>ipsum</p></div>'
|
|
143
168
|
|
|
144
169
|
|
|
170
|
+
def test_render_globals_from_string(folder):
|
|
171
|
+
(folder / "child.jinja").write_text("""<p>{{ lorem }}</p>""")
|
|
172
|
+
|
|
173
|
+
(folder / "layout.jinja").write_text("""<div class="{{ lorem }}">{{ content }}</div>""")
|
|
174
|
+
|
|
175
|
+
source = """
|
|
176
|
+
{# import "layout.jinja" as Layout #}
|
|
177
|
+
{# import "child.jinja" as Child #}
|
|
178
|
+
<Layout><Child /></Layout>
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
cat = Catalog(folder, lorem="ipsum")
|
|
182
|
+
assert cat.render_string(source) == '<div class="ipsum"><p>ipsum</p></div>'
|
|
183
|
+
|
|
184
|
+
|
|
145
185
|
def test_collect_assets(folder):
|
|
146
186
|
(folder / "child.jinja").write_text("""
|
|
147
187
|
{# css "child.css", "/static/common/parent.css" #}
|
|
@@ -433,3 +473,30 @@ AFTER
|
|
|
433
473
|
<link rel="stylesheet" href="before.css">
|
|
434
474
|
BEFORE
|
|
435
475
|
""".strip()
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_alpine_sintax(folder):
|
|
479
|
+
(folder / "greeting.jinja").write_text("""
|
|
480
|
+
{#def message #}
|
|
481
|
+
<button @click.prevent="alert('{{ message }}')">Say Hi</button>""")
|
|
482
|
+
cat = Catalog(folder, auto_reload=False)
|
|
483
|
+
|
|
484
|
+
html = cat.render("greeting.jinja", message="Hello world!")
|
|
485
|
+
print(html)
|
|
486
|
+
assert html == """<button @click.prevent="alert('Hello world!')">Say Hi</button>"""
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_alpine_sintax_in_component(folder):
|
|
490
|
+
(folder / "button.jinja").write_text(
|
|
491
|
+
"""<button {{ attrs.render() }}>{{ content }}</button>"""
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
(folder / "greeting.jinja").write_text("""
|
|
495
|
+
{# import "button.jinja" as Button #}
|
|
496
|
+
<Button @click.prevent="alert('Hello world!')">Say Hi</Button>
|
|
497
|
+
""")
|
|
498
|
+
cat = Catalog(folder, auto_reload=False)
|
|
499
|
+
|
|
500
|
+
html = cat.render("greeting.jinja")
|
|
501
|
+
print(html)
|
|
502
|
+
assert html == """<button @click.prevent="alert('Hello world!')">Say Hi</button>"""
|
{jx-0.2.0 → jx-0.4.0}/LICENSE
RENAMED
|
File without changes
|
{jx-0.2.0 → jx-0.4.0}/setup.cfg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|