jx 0.1.0__tar.gz → 0.2.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.1.0/src/jx.egg-info → jx-0.2.0}/PKG-INFO +37 -1
- jx-0.2.0/README.md +49 -0
- {jx-0.1.0 → jx-0.2.0}/pyproject.toml +2 -1
- {jx-0.1.0 → jx-0.2.0}/src/jx/attrs.py +40 -27
- {jx-0.1.0 → jx-0.2.0}/src/jx/catalog.py +170 -68
- {jx-0.1.0 → jx-0.2.0}/src/jx/component.py +71 -27
- {jx-0.1.0 → jx-0.2.0}/src/jx/meta.py +3 -2
- jx-0.2.0/src/jx/parser.py +411 -0
- jx-0.2.0/src/jx/utils.py +25 -0
- {jx-0.1.0 → jx-0.2.0/src/jx.egg-info}/PKG-INFO +37 -1
- {jx-0.1.0 → jx-0.2.0}/src/jx.egg-info/SOURCES.txt +1 -0
- {jx-0.1.0 → jx-0.2.0}/src/jx.egg-info/requires.txt +1 -0
- jx-0.2.0/tests/test_parser.py +312 -0
- jx-0.2.0/tests/test_slots.py +205 -0
- jx-0.1.0/README.md +0 -14
- jx-0.1.0/src/jx/parser.py +0 -215
- jx-0.1.0/src/jx/utils.py +0 -13
- jx-0.1.0/tests/test_parser.py +0 -171
- {jx-0.1.0 → jx-0.2.0}/LICENSE +0 -0
- {jx-0.1.0 → jx-0.2.0}/setup.cfg +0 -0
- {jx-0.1.0 → jx-0.2.0}/src/jx/__init__.py +0 -0
- {jx-0.1.0 → jx-0.2.0}/src/jx/exceptions.py +0 -0
- {jx-0.1.0 → jx-0.2.0}/src/jx.egg-info/dependency_links.txt +0 -0
- {jx-0.1.0 → jx-0.2.0}/src/jx.egg-info/top_level.txt +0 -0
- {jx-0.1.0 → jx-0.2.0}/tests/test_attrs.py +0 -0
- {jx-0.1.0 → jx-0.2.0}/tests/test_catalog.py +0 -0
- {jx-0.1.0 → jx-0.2.0}/tests/test_component.py +0 -0
- {jx-0.1.0 → jx-0.2.0}/tests/test_meta.py +0 -0
- {jx-0.1.0 → jx-0.2.0}/tests/test_thread_safety.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Replace your HTML templates with Python server-Side components
|
|
5
5
|
Author-email: Juan Pablo Scaletti <juanpablo@jpscaletti.com>
|
|
6
6
|
Project-URL: homepage, https://jx.scaletti.dev/
|
|
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: jinja2>=3.0
|
|
26
26
|
Requires-Dist: markupsafe>=2.0
|
|
27
|
+
Requires-Dist: ty>=0.0.1a15
|
|
27
28
|
Dynamic: license-file
|
|
28
29
|
|
|
29
30
|
<h1>
|
|
@@ -40,3 +41,38 @@ Super components powers for your Jinja templates.
|
|
|
40
41
|
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
41
42
|
|
|
42
43
|
<!-- Documentation: https://jx.scaletti.dev/ -->
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
Jx is a Python library for creating reusable template components with Jinja2. It works by pre-parsing the template source and replacing TitleCased HTML tags with Jinja calls that render the component.
|
|
48
|
+
|
|
49
|
+
### Component Definition
|
|
50
|
+
|
|
51
|
+
Components are defined as regular Jinja2 templates (.jinja files) with special metadata comments:
|
|
52
|
+
|
|
53
|
+
- `{# def parameter1 parameter2=default_value #}` - Defines required and optional parameters
|
|
54
|
+
- `{# import "path/to/component.jinja" as ComponentName #}` - Imports other components
|
|
55
|
+
- `{# css "/path/to/style.css" #}` - Includes CSS files
|
|
56
|
+
- `{# js "/path/to/script.js" #}` - Includes JavaScript files
|
|
57
|
+
|
|
58
|
+
Example component:
|
|
59
|
+
|
|
60
|
+
```jinja
|
|
61
|
+
{# def message #}
|
|
62
|
+
{# import "button.jinja" as Button #}
|
|
63
|
+
|
|
64
|
+
<div class="greeting">{{ message }}</div>
|
|
65
|
+
<Button text="OK" />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Usage Example
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from jx import Catalog
|
|
72
|
+
|
|
73
|
+
# Create a catalog and add a components folder
|
|
74
|
+
catalog = Catalog("templates/components")
|
|
75
|
+
|
|
76
|
+
# Render a component with parameters
|
|
77
|
+
html = catalog.render("card.jinja", title="Hello", content="This is a card")
|
|
78
|
+
```
|
jx-0.2.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<h1>
|
|
2
|
+
<img src="https://github.com/jpsca/jx/raw/main/docs/logo-jx.png" height="100" align="top">
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
Super components powers for your Jinja templates.
|
|
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">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
13
|
+
|
|
14
|
+
<!-- Documentation: https://jx.scaletti.dev/ -->
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
Jx is a Python library for creating reusable template components with Jinja2. It works by pre-parsing the template source and replacing TitleCased HTML tags with Jinja calls that render the component.
|
|
19
|
+
|
|
20
|
+
### Component Definition
|
|
21
|
+
|
|
22
|
+
Components are defined as regular Jinja2 templates (.jinja files) with special metadata comments:
|
|
23
|
+
|
|
24
|
+
- `{# def parameter1 parameter2=default_value #}` - Defines required and optional parameters
|
|
25
|
+
- `{# import "path/to/component.jinja" as ComponentName #}` - Imports other components
|
|
26
|
+
- `{# css "/path/to/style.css" #}` - Includes CSS files
|
|
27
|
+
- `{# js "/path/to/script.js" #}` - Includes JavaScript files
|
|
28
|
+
|
|
29
|
+
Example component:
|
|
30
|
+
|
|
31
|
+
```jinja
|
|
32
|
+
{# def message #}
|
|
33
|
+
{# import "button.jinja" as Button #}
|
|
34
|
+
|
|
35
|
+
<div class="greeting">{{ message }}</div>
|
|
36
|
+
<Button text="OK" />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Usage Example
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from jx import Catalog
|
|
43
|
+
|
|
44
|
+
# Create a catalog and add a components folder
|
|
45
|
+
catalog = Catalog("templates/components")
|
|
46
|
+
|
|
47
|
+
# Render a component with parameters
|
|
48
|
+
html = catalog.render("card.jinja", title="Hello", content="This is a card")
|
|
49
|
+
```
|
|
@@ -4,7 +4,7 @@ requires = ["setuptools"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.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"},
|
|
@@ -30,6 +30,7 @@ requires-python = ">=3.11,<4"
|
|
|
30
30
|
dependencies = [
|
|
31
31
|
"jinja2 >= 3.0",
|
|
32
32
|
"markupsafe >= 2.0",
|
|
33
|
+
"ty>=0.0.1a15",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.urls]
|
|
@@ -30,14 +30,13 @@ def quote(text: str) -> str:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class LazyString(UserString):
|
|
33
|
-
"""
|
|
34
|
-
Behave like regular strings, but the actual casting of the initial value
|
|
35
|
-
is deferred until the value is actually required.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
33
|
__slots__ = ("_seq",)
|
|
39
34
|
|
|
40
35
|
def __init__(self, seq):
|
|
36
|
+
"""
|
|
37
|
+
Behave like regular strings, but the actual casting of the initial value
|
|
38
|
+
is deferred until the value is actually required.
|
|
39
|
+
"""
|
|
41
40
|
self._seq = seq
|
|
42
41
|
|
|
43
42
|
@cached_property
|
|
@@ -46,21 +45,20 @@ class LazyString(UserString):
|
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
class Attrs:
|
|
49
|
-
""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
For HTML classes you can use the name "classes" (instead of "class")
|
|
55
|
-
if you need to.
|
|
48
|
+
def __init__(self, attrs: "dict[str, t.Any| LazyString]") -> None:
|
|
49
|
+
"""
|
|
50
|
+
Contains all the HTML attributes/properties (a property is an
|
|
51
|
+
attribute without a value) passed to a component but that weren't
|
|
52
|
+
in the declared attributes list.
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`attrs.render()` is invoked.
|
|
54
|
+
For HTML classes you can use the name "classes" (instead of "class")
|
|
55
|
+
if you need to.
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+
**NOTE**: The string values passed to this class, are not cast to `str` until
|
|
58
|
+
the string representation is actually needed, for example when
|
|
59
|
+
`attrs.render()` is invoked.
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
"""
|
|
64
62
|
attributes: "dict[str, str | LazyString]" = {}
|
|
65
63
|
properties: set[str] = set()
|
|
66
64
|
|
|
@@ -90,7 +88,7 @@ class Attrs:
|
|
|
90
88
|
Example:
|
|
91
89
|
|
|
92
90
|
```python
|
|
93
|
-
attrs =
|
|
91
|
+
attrs = Attrs({"class": "italic bold bg-blue wide abcde"})
|
|
94
92
|
attrs.set(class="bold text-white")
|
|
95
93
|
print(attrs.classes)
|
|
96
94
|
abcde bg-blue bold italic text-white wide
|
|
@@ -108,7 +106,7 @@ class Attrs:
|
|
|
108
106
|
Example:
|
|
109
107
|
|
|
110
108
|
```python
|
|
111
|
-
attrs =
|
|
109
|
+
attrs = Attrs({
|
|
112
110
|
"class": "lorem ipsum",
|
|
113
111
|
"data_test": True,
|
|
114
112
|
"hidden": True,
|
|
@@ -157,10 +155,12 @@ class Attrs:
|
|
|
157
155
|
- The underscores in the names will be translated automatically to dashes,
|
|
158
156
|
so `aria_selected` becomes the attribute `aria-selected`.
|
|
159
157
|
|
|
158
|
+
TODO: vue-style
|
|
159
|
+
|
|
160
160
|
Example:
|
|
161
161
|
|
|
162
162
|
```python
|
|
163
|
-
attrs =
|
|
163
|
+
attrs = Attrs({"secret": "qwertyuiop"})
|
|
164
164
|
attrs.set(secret=False)
|
|
165
165
|
attrs.as_dict
|
|
166
166
|
{}
|
|
@@ -169,7 +169,7 @@ class Attrs:
|
|
|
169
169
|
attrs.as_dict
|
|
170
170
|
{"count":42, "lorem":"ipsum", "data_good": True}
|
|
171
171
|
|
|
172
|
-
attrs =
|
|
172
|
+
attrs = Attrs({"class": "b c a"})
|
|
173
173
|
attrs.set(class="c b f d e")
|
|
174
174
|
attrs.as_dict
|
|
175
175
|
{"class": "a b c d e f"}
|
|
@@ -199,7 +199,7 @@ class Attrs:
|
|
|
199
199
|
Example:
|
|
200
200
|
|
|
201
201
|
```python
|
|
202
|
-
attrs =
|
|
202
|
+
attrs = Attrs({"lorem": "ipsum"})
|
|
203
203
|
attrs.setdefault(tabindex=0, lorem="meh")
|
|
204
204
|
attrs.as_dict
|
|
205
205
|
# "tabindex" changed but "lorem" didn't
|
|
@@ -222,10 +222,15 @@ class Attrs:
|
|
|
222
222
|
"""
|
|
223
223
|
Adds one or more classes to the list of classes, if not already present.
|
|
224
224
|
|
|
225
|
+
Arguments:
|
|
226
|
+
|
|
227
|
+
values:
|
|
228
|
+
One or more class names to add, separated by spaces.
|
|
229
|
+
|
|
225
230
|
Example:
|
|
226
231
|
|
|
227
232
|
```python
|
|
228
|
-
attrs =
|
|
233
|
+
attrs = Attrs({"class": "a b c"})
|
|
229
234
|
attrs.add_class("c", "d")
|
|
230
235
|
attrs.as_dict
|
|
231
236
|
{"class": "a b c d"}
|
|
@@ -243,7 +248,7 @@ class Attrs:
|
|
|
243
248
|
Example:
|
|
244
249
|
|
|
245
250
|
```python
|
|
246
|
-
attrs =
|
|
251
|
+
attrs = Attrs({"class": "a b c"})
|
|
247
252
|
attrs.remove_class("c", "d")
|
|
248
253
|
attrs.as_dict
|
|
249
254
|
{"class": "a b"}
|
|
@@ -258,10 +263,18 @@ class Attrs:
|
|
|
258
263
|
Returns the value of the attribute or property,
|
|
259
264
|
or the default value if it doesn't exists.
|
|
260
265
|
|
|
266
|
+
Arguments:
|
|
267
|
+
|
|
268
|
+
name:
|
|
269
|
+
The name of the attribute or property to get.
|
|
270
|
+
|
|
271
|
+
default:
|
|
272
|
+
The default value to return if the attribute or property doesn't exist.
|
|
273
|
+
|
|
261
274
|
Example:
|
|
262
275
|
|
|
263
276
|
```python
|
|
264
|
-
attrs =
|
|
277
|
+
attrs = Attrs({"lorem": "ipsum", "hidden": True})
|
|
265
278
|
|
|
266
279
|
attrs.get("lorem", defaut="bar")
|
|
267
280
|
'ipsum'
|
|
@@ -291,7 +304,7 @@ class Attrs:
|
|
|
291
304
|
Renders the attributes and properties as a string.
|
|
292
305
|
|
|
293
306
|
Any arguments you use with this function are merged with the existing
|
|
294
|
-
attibutes/properties by the same rules as the `
|
|
307
|
+
attibutes/properties by the same rules as the `Attrs.set()` function:
|
|
295
308
|
|
|
296
309
|
- Pass a name and a value to set an attribute (e.g. `type="text"`)
|
|
297
310
|
- Use `True` as a value to set a property (e.g. `disabled`)
|
|
@@ -308,7 +321,7 @@ class Attrs:
|
|
|
308
321
|
Example:
|
|
309
322
|
|
|
310
323
|
```python
|
|
311
|
-
attrs =
|
|
324
|
+
attrs = Attrs({"class": "ipsum", "data_good": True, "width": 42})
|
|
312
325
|
|
|
313
326
|
attrs.render()
|
|
314
327
|
'class="ipsum" width="42" data-good'
|
|
@@ -24,32 +24,55 @@ class CData:
|
|
|
24
24
|
code: CodeType | None = None
|
|
25
25
|
required: tuple[str, ...] = ()
|
|
26
26
|
optional: dict[str, t.Any] = field(default_factory=dict) # { attr: default_value }
|
|
27
|
-
imports: dict[str, str] = field(default_factory=dict) # {
|
|
27
|
+
imports: dict[str, str] = field(default_factory=dict) # { name: relpath }
|
|
28
28
|
css: tuple[str, ...] = ()
|
|
29
29
|
js: tuple[str, ...] = ()
|
|
30
|
+
slots: tuple[str, ...] = ()
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class Catalog:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Arguments:
|
|
37
|
-
"""
|
|
34
|
+
# IDEA: This dict could be replaced by a dict-like object
|
|
35
|
+
# that usesa LRU cache (to limit the memory used)
|
|
36
|
+
# or even a shared Redis/Memcache cache.
|
|
38
37
|
components: dict[str, CData]
|
|
39
38
|
|
|
40
39
|
def __init__(
|
|
41
40
|
self,
|
|
42
41
|
folder: str | Path | None = None,
|
|
43
42
|
*,
|
|
43
|
+
auto_reload: bool = True,
|
|
44
44
|
jinja_env: jinja2.Environment | None = None,
|
|
45
45
|
filters: dict[str, t.Any] | None = None,
|
|
46
46
|
tests: dict[str, t.Any] | None = None,
|
|
47
47
|
extensions: list | None = None,
|
|
48
|
-
auto_reload: bool = True,
|
|
49
48
|
**globals: t.Any,
|
|
50
49
|
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Manager of the components and their global settings.
|
|
52
|
+
|
|
53
|
+
Arguments:
|
|
54
|
+
folder:
|
|
55
|
+
Optional folder path to scan for components. It's a shortcut to
|
|
56
|
+
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
|
+
jinja_env:
|
|
63
|
+
Optional Jinja2 environment to use for rendering.
|
|
64
|
+
filters:
|
|
65
|
+
Optional extra Jinja2 filters to add to the environment.
|
|
66
|
+
extensions:
|
|
67
|
+
Optional extra Jinja2 extensions to add to the environment.
|
|
68
|
+
tests:
|
|
69
|
+
Optional extra Jinja2 tests to add to the environment.
|
|
70
|
+
**globals:
|
|
71
|
+
Variables to make available to all components by default.
|
|
72
|
+
|
|
73
|
+
"""
|
|
51
74
|
self.components = {}
|
|
52
|
-
self.jinja_env = self.
|
|
75
|
+
self.jinja_env = self._make_jinja_env(
|
|
53
76
|
jinja_env=jinja_env,
|
|
54
77
|
globals=globals,
|
|
55
78
|
filters=filters,
|
|
@@ -60,64 +83,22 @@ class Catalog:
|
|
|
60
83
|
if folder:
|
|
61
84
|
self.add_folder(folder)
|
|
62
85
|
|
|
63
|
-
def make_jinja_env(
|
|
64
|
-
self,
|
|
65
|
-
*,
|
|
66
|
-
jinja_env: jinja2.Environment | None = None,
|
|
67
|
-
globals: dict[str, t.Any] | None = None,
|
|
68
|
-
filters: dict[str, t.Any] | None = None,
|
|
69
|
-
tests: dict[str, t.Any] | None = None,
|
|
70
|
-
extensions: list | None = None,
|
|
71
|
-
) -> jinja2.Environment:
|
|
72
|
-
env = jinja2.Environment()
|
|
73
|
-
jinja_env = jinja_env or getattr(self, "jinja_env", None)
|
|
74
|
-
if jinja_env:
|
|
75
|
-
env = jinja_env.overlay()
|
|
76
|
-
|
|
77
|
-
globals = globals or {}
|
|
78
|
-
globals.update({
|
|
79
|
-
"_get_random_id": utils.get_random_id,
|
|
80
|
-
})
|
|
81
|
-
env.globals.update(globals)
|
|
82
|
-
|
|
83
|
-
filters = filters or {}
|
|
84
|
-
env.filters.update(filters)
|
|
85
|
-
|
|
86
|
-
tests = tests or {}
|
|
87
|
-
env.tests.update(tests)
|
|
88
|
-
|
|
89
|
-
extensions = extensions or []
|
|
90
|
-
extensions.extend(["jinja2.ext.do"])
|
|
91
|
-
for ext in extensions:
|
|
92
|
-
env.add_extension(ext)
|
|
93
|
-
|
|
94
|
-
env.autoescape = True
|
|
95
|
-
env.undefined = jinja2.StrictUndefined
|
|
96
|
-
|
|
97
|
-
return env
|
|
98
|
-
|
|
99
86
|
def add_folder(
|
|
100
87
|
self,
|
|
101
88
|
path: str | Path,
|
|
102
89
|
*,
|
|
103
90
|
prefix: str = "",
|
|
91
|
+
preload: bool = True
|
|
104
92
|
) -> None:
|
|
105
93
|
"""
|
|
106
94
|
Add a folder path from which to search for components, optionally under a prefix.
|
|
107
95
|
|
|
108
|
-
Arguments:
|
|
109
|
-
|
|
110
|
-
path:
|
|
111
|
-
Absolute path of the folder with component files.
|
|
112
|
-
|
|
113
|
-
prefix:
|
|
114
|
-
Optional path prefix that all the components in the folder
|
|
115
|
-
will have. The default is empty.
|
|
116
|
-
|
|
117
96
|
Components without a prefix can be imported as a path relative to the folder,
|
|
118
97
|
e.g.: `sub/folder/component.jinja` or with a path relative to the component
|
|
119
98
|
where it is used: `./folder/component.jinja`.
|
|
120
99
|
|
|
100
|
+
Relative imports cannot go outside the folder.
|
|
101
|
+
|
|
121
102
|
Components added with a prefix must be imported using the prefix followed
|
|
122
103
|
by a colon: `prefix:sub/folder/component.jinja`. If the importing is
|
|
123
104
|
done from within a component with the prefix itself, a relative
|
|
@@ -128,6 +109,23 @@ class Catalog:
|
|
|
128
109
|
with a component with the same subpath/filename, the one in the folder
|
|
129
110
|
added **first** will be used and the other ignored.
|
|
130
111
|
|
|
112
|
+
WARNING: You cannot move or delete components files from the folder after
|
|
113
|
+
calling this method, but you can call it again to add new components added
|
|
114
|
+
to the folder. This is unrelated to the value of `preload`.
|
|
115
|
+
|
|
116
|
+
Arguments:
|
|
117
|
+
path:
|
|
118
|
+
Absolute path of the folder with component files.
|
|
119
|
+
prefix:
|
|
120
|
+
Optional path prefix that all the components in the folder
|
|
121
|
+
will have. The default is empty.
|
|
122
|
+
preload:
|
|
123
|
+
Whether to preload the data of components in the folder.
|
|
124
|
+
If set to `True` (the default), the component data will be loaded into
|
|
125
|
+
memory when the folder is added, instead of just before rendering it.
|
|
126
|
+
This makes the first render faster at the expense of a few
|
|
127
|
+
microseconds upfront.
|
|
128
|
+
|
|
131
129
|
"""
|
|
132
130
|
base_path = Path(path).resolve()
|
|
133
131
|
prefix = prefix.replace("\\", "/").strip("./@ ")
|
|
@@ -149,10 +147,57 @@ class Catalog:
|
|
|
149
147
|
)
|
|
150
148
|
self.components[relpath] = cdata
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
if preload:
|
|
151
|
+
for relpath in self.components:
|
|
152
|
+
self.components[relpath] = self.get_component_data(relpath)
|
|
153
|
+
|
|
154
|
+
def render(self, relpath: str, globals: dict[str, t.Any] | None = None, **kwargs) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Render a component with the given relative path and context.
|
|
157
|
+
|
|
158
|
+
Arguments:
|
|
159
|
+
relpath:
|
|
160
|
+
The path of the component to render, including the extension,relative to its view folder.
|
|
161
|
+
e.g.: "sub/component.jinja". Always use the forward slash (/) as the path separator.
|
|
162
|
+
globals:
|
|
163
|
+
Optional global variables to make available to the component and all its
|
|
164
|
+
imported components.
|
|
165
|
+
**kwargs:
|
|
166
|
+
Keyword arguments to pass to the component.
|
|
167
|
+
They will be available in the component's context but not to its imported components.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
The rendered component as a string.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
relpath = relpath.replace("\\", "/").strip("/")
|
|
174
|
+
co = self.get_component(relpath)
|
|
175
|
+
|
|
176
|
+
globals = globals or {}
|
|
177
|
+
globals.update({
|
|
178
|
+
"assets": {
|
|
179
|
+
"css": co.collect_css,
|
|
180
|
+
"js": co.collect_js,
|
|
181
|
+
"render_css": co.render_css,
|
|
182
|
+
"render_js": co.render_js,
|
|
183
|
+
"render": co.render_assets,
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
co.globals = globals
|
|
187
|
+
|
|
188
|
+
return co.render(**kwargs)
|
|
154
189
|
|
|
155
190
|
def get_component_data(self, relpath: str) -> CData:
|
|
191
|
+
"""
|
|
192
|
+
Get the component data from the cache, or load it from the file system
|
|
193
|
+
if needed.
|
|
194
|
+
|
|
195
|
+
Arguments:
|
|
196
|
+
relpath:
|
|
197
|
+
The path of the component to render, including the extension,relative to its view folder.
|
|
198
|
+
e.g.: "sub/component.jinja". Always use the forward slash (/) as the path separator.
|
|
199
|
+
|
|
200
|
+
"""
|
|
156
201
|
cdata = self.components.get(relpath)
|
|
157
202
|
if not cdata:
|
|
158
203
|
raise ImportError(relpath)
|
|
@@ -173,7 +218,7 @@ class Catalog:
|
|
|
173
218
|
source=source,
|
|
174
219
|
components=list(meta.imports.keys())
|
|
175
220
|
)
|
|
176
|
-
parsed_source = parser.parse()
|
|
221
|
+
parsed_source, slots = parser.parse()
|
|
177
222
|
code = self.jinja_env.compile(
|
|
178
223
|
source=parsed_source,
|
|
179
224
|
name=relpath,
|
|
@@ -186,9 +231,19 @@ class Catalog:
|
|
|
186
231
|
cdata.imports = meta.imports
|
|
187
232
|
cdata.css = meta.css
|
|
188
233
|
cdata.js = meta.js
|
|
234
|
+
cdata.slots = slots
|
|
189
235
|
return cdata
|
|
190
236
|
|
|
191
237
|
def get_component(self, relpath: str) -> Component:
|
|
238
|
+
"""
|
|
239
|
+
Instantiate and return a component object by its relative path.
|
|
240
|
+
|
|
241
|
+
Arguments:
|
|
242
|
+
relpath:
|
|
243
|
+
The path of the component to render, including the extension,relative to its view folder.
|
|
244
|
+
e.g.: "sub/component.jinja". Always use the forward slash (/) as the path separator.
|
|
245
|
+
|
|
246
|
+
"""
|
|
192
247
|
cdata = self.get_component_data(relpath)
|
|
193
248
|
assert cdata.code is not None
|
|
194
249
|
tmpl = jinja2.Template.from_code(
|
|
@@ -203,25 +258,72 @@ class Catalog:
|
|
|
203
258
|
get_component=self.get_component,
|
|
204
259
|
required=cdata.required,
|
|
205
260
|
optional=cdata.optional,
|
|
261
|
+
imports=cdata.imports,
|
|
206
262
|
css=cdata.css,
|
|
207
263
|
js=cdata.js,
|
|
208
|
-
|
|
264
|
+
slots=cdata.slots,
|
|
209
265
|
)
|
|
210
266
|
return co
|
|
211
267
|
|
|
212
|
-
|
|
213
|
-
|
|
268
|
+
# Private
|
|
269
|
+
|
|
270
|
+
def _make_jinja_env(
|
|
271
|
+
self,
|
|
272
|
+
*,
|
|
273
|
+
jinja_env: jinja2.Environment | None = None,
|
|
274
|
+
globals: dict[str, t.Any] | None = None,
|
|
275
|
+
filters: dict[str, t.Any] | None = None,
|
|
276
|
+
tests: dict[str, t.Any] | None = None,
|
|
277
|
+
extensions: list | None = None,
|
|
278
|
+
) -> jinja2.Environment:
|
|
279
|
+
"""
|
|
280
|
+
Create a new Jinja2 environment with the specified settings.
|
|
281
|
+
|
|
282
|
+
If an existing environment is provided, an "overlay" of it will
|
|
283
|
+
be created and used.
|
|
284
|
+
|
|
285
|
+
Arguments:
|
|
286
|
+
jinja_env:
|
|
287
|
+
Optional Jinja2 environment to use as a base.
|
|
288
|
+
globals:
|
|
289
|
+
Optional global variables to add to the environment.
|
|
290
|
+
filters:
|
|
291
|
+
Optional extra Jinja2 filters to add to the environment.
|
|
292
|
+
extensions:
|
|
293
|
+
Optional extra Jinja2 extensions to add to the environment.
|
|
294
|
+
tests:
|
|
295
|
+
Optional extra Jinja2 tests to add to the environment.
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
jinja_env = jinja_env or getattr(self, "jinja_env", None)
|
|
299
|
+
if jinja_env:
|
|
300
|
+
env = jinja_env.overlay()
|
|
301
|
+
else:
|
|
302
|
+
env = jinja2.Environment()
|
|
214
303
|
|
|
215
304
|
globals = globals or {}
|
|
216
305
|
globals.update({
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"render_css": co.render_css,
|
|
221
|
-
"render_js": co.render_js,
|
|
222
|
-
"render": co.render_assets,
|
|
223
|
-
},
|
|
306
|
+
# A unique ID generator for HTML elements, see `utils.get_random_id`
|
|
307
|
+
# docstring for more information.
|
|
308
|
+
"_get_random_id": utils.get_random_id,
|
|
224
309
|
})
|
|
225
|
-
|
|
310
|
+
env.globals.update(globals)
|
|
226
311
|
|
|
227
|
-
|
|
312
|
+
filters = filters or {}
|
|
313
|
+
env.filters.update(filters)
|
|
314
|
+
|
|
315
|
+
tests = tests or {}
|
|
316
|
+
env.tests.update(tests)
|
|
317
|
+
|
|
318
|
+
extensions = extensions or []
|
|
319
|
+
# The "jinja2.ext.do" extension allows the use of the "do" statement in templates,
|
|
320
|
+
# that execute statements without outputting a value.
|
|
321
|
+
# Is specially useful for manipulating the `attrs` object.
|
|
322
|
+
extensions.extend(["jinja2.ext.do"])
|
|
323
|
+
for ext in extensions:
|
|
324
|
+
env.add_extension(ext)
|
|
325
|
+
|
|
326
|
+
env.autoescape = True
|
|
327
|
+
env.undefined = jinja2.StrictUndefined
|
|
328
|
+
|
|
329
|
+
return env
|