jx 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +2 -1
- jx/attrs.py +93 -48
- jx/catalog.py +241 -87
- jx/component.py +74 -33
- jx/exceptions.py +3 -1
- jx/meta.py +7 -5
- jx/parser.py +237 -41
- jx/utils.py +14 -1
- jx-0.3.0.dist-info/METADATA +75 -0
- jx-0.3.0.dist-info/RECORD +13 -0
- jx-0.1.0.dist-info/METADATA +0 -42
- jx-0.1.0.dist-info/RECORD +0 -13
- {jx-0.1.0.dist-info → jx-0.3.0.dist-info}/WHEEL +0 -0
- {jx-0.1.0.dist-info → jx-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {jx-0.1.0.dist-info → jx-0.3.0.dist-info}/top_level.txt +0 -0
jx/catalog.py
CHANGED
|
@@ -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,18 +24,18 @@ 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
|
-
imports: dict[str, str] = field(default_factory=dict) # {
|
|
27
|
+
optional: dict[str, t.Any] = field(default_factory=dict) # { attr: default_value }
|
|
28
|
+
imports: dict[str, str] = field(default_factory=dict) # { name: relpath }
|
|
28
29
|
css: tuple[str, ...] = ()
|
|
29
30
|
js: tuple[str, ...] = ()
|
|
31
|
+
slots: tuple[str, ...] = ()
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class Catalog:
|
|
33
|
-
"""
|
|
34
|
-
The object that manages the components and their global settings.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# IDEA: This dict could be replaced by a dict-like object
|
|
37
|
+
# that uses a LRU cache (to limit the memory used)
|
|
38
|
+
# or even a shared Redis/Memcache cache.
|
|
38
39
|
components: dict[str, CData]
|
|
39
40
|
|
|
40
41
|
def __init__(
|
|
@@ -42,14 +43,38 @@ class Catalog:
|
|
|
42
43
|
folder: str | Path | None = None,
|
|
43
44
|
*,
|
|
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
|
-
extensions: list | None = None,
|
|
48
49
|
auto_reload: bool = True,
|
|
49
50
|
**globals: t.Any,
|
|
50
51
|
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Manager of the components and their global settings.
|
|
54
|
+
|
|
55
|
+
Arguments:
|
|
56
|
+
folder:
|
|
57
|
+
Optional folder path to scan for components. It's a shortcut to
|
|
58
|
+
calling `add_folder` when only one is used.
|
|
59
|
+
jinja_env:
|
|
60
|
+
Optional Jinja2 environment to use for rendering.
|
|
61
|
+
extensions:
|
|
62
|
+
Optional extra Jinja2 extensions to add to the environment.
|
|
63
|
+
filters:
|
|
64
|
+
Optional extra Jinja2 filters to add to the environment.
|
|
65
|
+
tests:
|
|
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.
|
|
72
|
+
**globals:
|
|
73
|
+
Variables to make available to all components by default.
|
|
74
|
+
|
|
75
|
+
"""
|
|
51
76
|
self.components = {}
|
|
52
|
-
self.jinja_env = self.
|
|
77
|
+
self.jinja_env = self._make_jinja_env(
|
|
53
78
|
jinja_env=jinja_env,
|
|
54
79
|
globals=globals,
|
|
55
80
|
filters=filters,
|
|
@@ -60,64 +85,18 @@ class Catalog:
|
|
|
60
85
|
if folder:
|
|
61
86
|
self.add_folder(folder)
|
|
62
87
|
|
|
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
88
|
def add_folder(
|
|
100
|
-
self,
|
|
101
|
-
path: str | Path,
|
|
102
|
-
*,
|
|
103
|
-
prefix: str = "",
|
|
89
|
+
self, path: str | Path, *, prefix: str = "", preload: bool = True
|
|
104
90
|
) -> None:
|
|
105
91
|
"""
|
|
106
92
|
Add a folder path from which to search for components, optionally under a prefix.
|
|
107
93
|
|
|
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
94
|
Components without a prefix can be imported as a path relative to the folder,
|
|
118
95
|
e.g.: `sub/folder/component.jinja` or with a path relative to the component
|
|
119
96
|
where it is used: `./folder/component.jinja`.
|
|
120
97
|
|
|
98
|
+
Relative imports cannot go outside the folder.
|
|
99
|
+
|
|
121
100
|
Components added with a prefix must be imported using the prefix followed
|
|
122
101
|
by a colon: `prefix:sub/folder/component.jinja`. If the importing is
|
|
123
102
|
done from within a component with the prefix itself, a relative
|
|
@@ -128,6 +107,23 @@ class Catalog:
|
|
|
128
107
|
with a component with the same subpath/filename, the one in the folder
|
|
129
108
|
added **first** will be used and the other ignored.
|
|
130
109
|
|
|
110
|
+
WARNING: You cannot move or delete components files from the folder after
|
|
111
|
+
calling this method, but you can call it again to add new components added
|
|
112
|
+
to the folder. This is unrelated to the value of `preload`.
|
|
113
|
+
|
|
114
|
+
Arguments:
|
|
115
|
+
path:
|
|
116
|
+
Absolute path of the folder with component files.
|
|
117
|
+
prefix:
|
|
118
|
+
Optional path prefix that all the components in the folder
|
|
119
|
+
will have. The default is empty.
|
|
120
|
+
preload:
|
|
121
|
+
Whether to preload the data of components in the folder.
|
|
122
|
+
If set to `True` (the default), the component data will be loaded into
|
|
123
|
+
memory when the folder is added, instead of just before rendering it.
|
|
124
|
+
This makes the first render faster at the expense of a few
|
|
125
|
+
microseconds upfront.
|
|
126
|
+
|
|
131
127
|
"""
|
|
132
128
|
base_path = Path(path).resolve()
|
|
133
129
|
prefix = prefix.replace("\\", "/").strip("./@ ")
|
|
@@ -143,16 +139,121 @@ class Catalog:
|
|
|
143
139
|
logger.debug(f"Component already exists: {relpath}")
|
|
144
140
|
continue
|
|
145
141
|
cdata = CData(
|
|
146
|
-
base_path=base_path,
|
|
147
|
-
path=filepath,
|
|
148
|
-
mtime=filepath.stat().st_mtime
|
|
142
|
+
base_path=base_path, path=filepath, mtime=filepath.stat().st_mtime
|
|
149
143
|
)
|
|
150
144
|
self.components[relpath] = cdata
|
|
151
145
|
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
if preload:
|
|
147
|
+
for relpath in self.components:
|
|
148
|
+
self.components[relpath] = self.get_component_data(relpath)
|
|
149
|
+
|
|
150
|
+
def render(
|
|
151
|
+
self, relpath: str, globals: dict[str, t.Any] | None = None, **kwargs
|
|
152
|
+
) -> str:
|
|
153
|
+
"""
|
|
154
|
+
Render a component with the given relative path and context.
|
|
155
|
+
|
|
156
|
+
Arguments:
|
|
157
|
+
relpath:
|
|
158
|
+
The path of the component to render, including the extension,relative to its view folder.
|
|
159
|
+
e.g.: "sub/component.jinja". Always use the forward slash (/) as the path separator.
|
|
160
|
+
globals:
|
|
161
|
+
Optional global variables to make available to the component and all its
|
|
162
|
+
imported components.
|
|
163
|
+
**kwargs:
|
|
164
|
+
Keyword arguments to pass to the component.
|
|
165
|
+
They will be available in the component's context but not to its imported components.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The rendered component as a string.
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
relpath = relpath.replace("\\", "/").strip("/")
|
|
172
|
+
co = self.get_component(relpath)
|
|
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
|
+
|
|
232
|
+
globals = globals or {}
|
|
233
|
+
globals.update({
|
|
234
|
+
"assets": {
|
|
235
|
+
"css": co.collect_css,
|
|
236
|
+
"js": co.collect_js,
|
|
237
|
+
"render_css": co.render_css,
|
|
238
|
+
"render_js": co.render_js,
|
|
239
|
+
"render": co.render_assets,
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
co.globals = globals
|
|
243
|
+
|
|
244
|
+
return co.render(**kwargs)
|
|
154
245
|
|
|
155
246
|
def get_component_data(self, relpath: str) -> CData:
|
|
247
|
+
"""
|
|
248
|
+
Get the component data from the cache.
|
|
249
|
+
If the file has been updated, the component is re-processed.
|
|
250
|
+
|
|
251
|
+
Arguments:
|
|
252
|
+
relpath:
|
|
253
|
+
The path of the component to render, including the extension,relative to its view folder.
|
|
254
|
+
e.g.: "sub/component.jinja". Always use the forward slash (/) as the path separator.
|
|
255
|
+
|
|
256
|
+
"""
|
|
156
257
|
cdata = self.components.get(relpath)
|
|
157
258
|
if not cdata:
|
|
158
259
|
raise ImportError(relpath)
|
|
@@ -169,15 +270,11 @@ class Catalog:
|
|
|
169
270
|
meta = extract_metadata(source, base_path=cdata.base_path, fullpath=cdata.path)
|
|
170
271
|
|
|
171
272
|
parser = JxParser(
|
|
172
|
-
name=relpath,
|
|
173
|
-
source=source,
|
|
174
|
-
components=list(meta.imports.keys())
|
|
273
|
+
name=relpath, source=source, components=list(meta.imports.keys())
|
|
175
274
|
)
|
|
176
|
-
parsed_source = parser.parse()
|
|
275
|
+
parsed_source, slots = parser.parse()
|
|
177
276
|
code = self.jinja_env.compile(
|
|
178
|
-
source=parsed_source,
|
|
179
|
-
name=relpath,
|
|
180
|
-
filename=cdata.path.as_posix()
|
|
277
|
+
source=parsed_source, name=relpath, filename=cdata.path.as_posix()
|
|
181
278
|
)
|
|
182
279
|
|
|
183
280
|
cdata.code = code
|
|
@@ -186,15 +283,23 @@ class Catalog:
|
|
|
186
283
|
cdata.imports = meta.imports
|
|
187
284
|
cdata.css = meta.css
|
|
188
285
|
cdata.js = meta.js
|
|
286
|
+
cdata.slots = slots
|
|
189
287
|
return cdata
|
|
190
288
|
|
|
191
289
|
def get_component(self, relpath: str) -> Component:
|
|
290
|
+
"""
|
|
291
|
+
Instantiate and return a component object by its relative path.
|
|
292
|
+
|
|
293
|
+
Arguments:
|
|
294
|
+
relpath:
|
|
295
|
+
The path of the component to render, including the extension,relative to its view folder.
|
|
296
|
+
e.g.: "sub/component.jinja". Always use the forward slash (/) as the path separator.
|
|
297
|
+
|
|
298
|
+
"""
|
|
192
299
|
cdata = self.get_component_data(relpath)
|
|
193
300
|
assert cdata.code is not None
|
|
194
301
|
tmpl = jinja2.Template.from_code(
|
|
195
|
-
self.jinja_env,
|
|
196
|
-
cdata.code,
|
|
197
|
-
self.jinja_env.globals
|
|
302
|
+
self.jinja_env, cdata.code, self.jinja_env.globals
|
|
198
303
|
)
|
|
199
304
|
|
|
200
305
|
co = Component(
|
|
@@ -203,25 +308,74 @@ class Catalog:
|
|
|
203
308
|
get_component=self.get_component,
|
|
204
309
|
required=cdata.required,
|
|
205
310
|
optional=cdata.optional,
|
|
311
|
+
imports=cdata.imports,
|
|
206
312
|
css=cdata.css,
|
|
207
313
|
js=cdata.js,
|
|
208
|
-
|
|
314
|
+
slots=cdata.slots,
|
|
209
315
|
)
|
|
210
316
|
return co
|
|
211
317
|
|
|
212
|
-
|
|
213
|
-
|
|
318
|
+
# Private
|
|
319
|
+
|
|
320
|
+
def _make_jinja_env(
|
|
321
|
+
self,
|
|
322
|
+
*,
|
|
323
|
+
jinja_env: jinja2.Environment | None = None,
|
|
324
|
+
globals: dict[str, t.Any] | None = None,
|
|
325
|
+
filters: dict[str, t.Any] | None = None,
|
|
326
|
+
tests: dict[str, t.Any] | None = None,
|
|
327
|
+
extensions: list | None = None,
|
|
328
|
+
) -> jinja2.Environment:
|
|
329
|
+
"""
|
|
330
|
+
Create a new Jinja2 environment with the specified settings.
|
|
331
|
+
|
|
332
|
+
Arguments:
|
|
333
|
+
jinja_env:
|
|
334
|
+
Optional Jinja2 environment to use as a base.
|
|
335
|
+
globals:
|
|
336
|
+
Optional global variables to add to the environment.
|
|
337
|
+
filters:
|
|
338
|
+
Optional extra Jinja2 filters to add to the environment.
|
|
339
|
+
extensions:
|
|
340
|
+
Optional extra Jinja2 extensions to add to the environment.
|
|
341
|
+
tests:
|
|
342
|
+
Optional extra Jinja2 tests to add to the environment.
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
jinja_env = jinja_env or getattr(self, "jinja_env", None)
|
|
346
|
+
if jinja_env:
|
|
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
|
|
351
|
+
else:
|
|
352
|
+
env = jinja2.Environment()
|
|
214
353
|
|
|
215
354
|
globals = globals or {}
|
|
216
|
-
globals.update(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
})
|
|
225
|
-
co.globals = globals
|
|
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
|
+
)
|
|
362
|
+
env.globals.update(globals)
|
|
226
363
|
|
|
227
|
-
|
|
364
|
+
filters = filters or {}
|
|
365
|
+
env.filters.update(filters)
|
|
366
|
+
|
|
367
|
+
tests = tests or {}
|
|
368
|
+
env.tests.update(tests)
|
|
369
|
+
|
|
370
|
+
extensions = extensions or []
|
|
371
|
+
# The "jinja2.ext.do" extension allows the use of the "do" statement in templates,
|
|
372
|
+
# that execute statements without outputting a value.
|
|
373
|
+
# Is specially useful for manipulating the `attrs` object.
|
|
374
|
+
extensions.extend(["jinja2.ext.do"])
|
|
375
|
+
for ext in extensions:
|
|
376
|
+
env.add_extension(ext)
|
|
377
|
+
|
|
378
|
+
env.autoescape = True
|
|
379
|
+
env.undefined = jinja2.StrictUndefined
|
|
380
|
+
|
|
381
|
+
return env
|
jx/component.py
CHANGED
|
@@ -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",
|
|
@@ -22,9 +19,10 @@ class Component:
|
|
|
22
19
|
"get_component",
|
|
23
20
|
"required",
|
|
24
21
|
"optional",
|
|
22
|
+
"imports",
|
|
25
23
|
"css",
|
|
26
24
|
"js",
|
|
27
|
-
"
|
|
25
|
+
"slots",
|
|
28
26
|
"globals",
|
|
29
27
|
)
|
|
30
28
|
|
|
@@ -36,19 +34,45 @@ class Component:
|
|
|
36
34
|
get_component: Callable[[str], "Component"],
|
|
37
35
|
required: tuple[str, ...] = (),
|
|
38
36
|
optional: dict[str, t.Any] | None = None,
|
|
37
|
+
imports: dict[str, str] | None = None,
|
|
39
38
|
css: tuple[str, ...] = (),
|
|
40
39
|
js: tuple[str, ...] = (),
|
|
41
|
-
|
|
40
|
+
slots: tuple[str, ...] = (),
|
|
42
41
|
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Internal object that represents a Jx component.
|
|
44
|
+
|
|
45
|
+
Arguments:
|
|
46
|
+
relpath:
|
|
47
|
+
The "name" of the component.
|
|
48
|
+
tmpl:
|
|
49
|
+
The jinja2.Template for the component.
|
|
50
|
+
get_component:
|
|
51
|
+
A callable that retrieves a component by its name/relpath.
|
|
52
|
+
required:
|
|
53
|
+
A tuple of required attribute names.
|
|
54
|
+
optional:
|
|
55
|
+
A dictionary of optional attributes and their default values.
|
|
56
|
+
imports:
|
|
57
|
+
A dictionary of imported component names as "name": "relpath" pairs.
|
|
58
|
+
css:
|
|
59
|
+
A tuple of CSS file URLs.
|
|
60
|
+
js:
|
|
61
|
+
A tuple of JS file URLs.
|
|
62
|
+
slots:
|
|
63
|
+
A tuple of slot names.
|
|
64
|
+
|
|
65
|
+
"""
|
|
43
66
|
self.relpath = relpath
|
|
44
67
|
self.tmpl = tmpl
|
|
45
68
|
self.get_component = get_component
|
|
46
69
|
|
|
47
70
|
self.required = required
|
|
48
71
|
self.optional = optional or {}
|
|
72
|
+
self.imports = imports or {}
|
|
49
73
|
self.css = css
|
|
50
74
|
self.js = js
|
|
51
|
-
self.
|
|
75
|
+
self.slots = slots
|
|
52
76
|
|
|
53
77
|
self.globals: dict[str, t.Any] = {}
|
|
54
78
|
|
|
@@ -57,10 +81,10 @@ class Component:
|
|
|
57
81
|
*,
|
|
58
82
|
content: str | None = None,
|
|
59
83
|
attrs: Attrs | dict[str, t.Any] | None = None,
|
|
60
|
-
caller: Callable[[], str] | None = None,
|
|
61
|
-
**params: t.Any
|
|
84
|
+
caller: Callable[[str], str] | None = None,
|
|
85
|
+
**params: t.Any,
|
|
62
86
|
) -> Markup:
|
|
63
|
-
content = content if content is not None else caller() if caller else ""
|
|
87
|
+
content = content if content is not None else caller("") if caller else ""
|
|
64
88
|
attrs = attrs.as_dict if isinstance(attrs, Attrs) else attrs or {}
|
|
65
89
|
params = {**attrs, **params}
|
|
66
90
|
props, attrs = self.filter_attrs(params)
|
|
@@ -69,6 +93,14 @@ class Component:
|
|
|
69
93
|
globals.setdefault("attrs", Attrs(attrs))
|
|
70
94
|
globals.setdefault("content", content)
|
|
71
95
|
|
|
96
|
+
slots = {}
|
|
97
|
+
if caller:
|
|
98
|
+
for name in self.slots:
|
|
99
|
+
body = caller(name)
|
|
100
|
+
if body != content:
|
|
101
|
+
slots[name] = body
|
|
102
|
+
props["_slots"] = slots
|
|
103
|
+
|
|
72
104
|
html = self.tmpl.render({**props, **globals}).lstrip()
|
|
73
105
|
return Markup(html)
|
|
74
106
|
|
|
@@ -93,41 +125,41 @@ class Component:
|
|
|
93
125
|
child.globals = self.globals
|
|
94
126
|
return child
|
|
95
127
|
|
|
96
|
-
def collect_css(self,
|
|
128
|
+
def collect_css(self, _visited: set[str] | None = None) -> list[str]:
|
|
97
129
|
"""
|
|
98
130
|
Returns a list of CSS files for the component and its children.
|
|
99
131
|
"""
|
|
100
132
|
urls = dict.fromkeys(self.css, 1)
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
_visited = _visited or set()
|
|
134
|
+
_visited.add(self.relpath)
|
|
103
135
|
|
|
104
136
|
for name, relpath in self.imports.items():
|
|
105
|
-
if relpath in
|
|
137
|
+
if relpath in _visited:
|
|
106
138
|
continue
|
|
107
139
|
co = self.get_child(name)
|
|
108
|
-
for file in co.collect_css(
|
|
140
|
+
for file in co.collect_css(_visited=_visited):
|
|
109
141
|
if file not in urls:
|
|
110
142
|
urls[file] = 1
|
|
111
|
-
|
|
143
|
+
_visited.add(relpath)
|
|
112
144
|
|
|
113
145
|
return list(urls.keys())
|
|
114
146
|
|
|
115
|
-
def collect_js(self,
|
|
147
|
+
def collect_js(self, _visited: set[str] | None = None) -> list[str]:
|
|
116
148
|
"""
|
|
117
149
|
Returns a list of JS files for the component and its children.
|
|
118
150
|
"""
|
|
119
151
|
urls = dict.fromkeys(self.js, 1)
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
_visited = _visited or set()
|
|
153
|
+
_visited.add(self.relpath)
|
|
122
154
|
|
|
123
155
|
for name, relpath in self.imports.items():
|
|
124
|
-
if relpath in
|
|
156
|
+
if relpath in _visited:
|
|
125
157
|
continue
|
|
126
158
|
co = self.get_child(name)
|
|
127
|
-
for file in co.collect_js(
|
|
159
|
+
for file in co.collect_js(_visited=_visited):
|
|
128
160
|
if file not in urls:
|
|
129
161
|
urls[file] = 1
|
|
130
|
-
|
|
162
|
+
_visited.add(relpath)
|
|
131
163
|
|
|
132
164
|
return list(urls.keys())
|
|
133
165
|
|
|
@@ -135,10 +167,6 @@ class Component:
|
|
|
135
167
|
"""
|
|
136
168
|
Uses the `collect_css()` list to generate an HTML fragment
|
|
137
169
|
with `<link rel="stylesheet" href="{url}">` tags.
|
|
138
|
-
|
|
139
|
-
Unless it's an external URL (e.g.: beginning with "http://" or "https://")
|
|
140
|
-
or a root-relative URL (e.g.: starting with "/"),
|
|
141
|
-
the URL is prefixed by `base_url`.
|
|
142
170
|
"""
|
|
143
171
|
html = []
|
|
144
172
|
for url in self.collect_css():
|
|
@@ -151,9 +179,15 @@ class Component:
|
|
|
151
179
|
Uses the `collected_js()` list to generate an HTML fragment
|
|
152
180
|
with `<script type="module" src="{url}"></script>` tags.
|
|
153
181
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
182
|
+
Arguments:
|
|
183
|
+
module:
|
|
184
|
+
Whether to render the script tags as modules, e.g.:
|
|
185
|
+
`<script type="module" src="..."></script>`
|
|
186
|
+
defer:
|
|
187
|
+
Whether to add the `defer` attribute to the script tags,
|
|
188
|
+
if `module` is `False` (all module scripts are also deferred), e.g.:
|
|
189
|
+
`<script src="..." defer></script>`
|
|
190
|
+
|
|
157
191
|
"""
|
|
158
192
|
html = []
|
|
159
193
|
for url in self.collect_js():
|
|
@@ -172,9 +206,16 @@ class Component:
|
|
|
172
206
|
Calls `render_css()` and `render_js()` to generate
|
|
173
207
|
an HTML fragment with `<link rel="stylesheet" href="{url}">`
|
|
174
208
|
and `<script type="module" src="{url}"></script>` tags.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
|
|
210
|
+
Arguments:
|
|
211
|
+
module:
|
|
212
|
+
Whether to render the script tags as modules, e.g.:
|
|
213
|
+
`<script type="module" src="..."></script>`
|
|
214
|
+
defer:
|
|
215
|
+
Whether to add the `defer` attribute to the script tags,
|
|
216
|
+
if `module` is `False` (all module scripts are also deferred), e.g.:
|
|
217
|
+
`<script src="..." defer></script>`
|
|
218
|
+
|
|
178
219
|
"""
|
|
179
220
|
html_css = self.render_css()
|
|
180
221
|
html_js = self.render_js()
|
jx/exceptions.py
CHANGED
|
@@ -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)
|
jx/meta.py
CHANGED
|
@@ -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,12 +57,13 @@ 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
|
-
A Meta object containing the extracted metadata.
|
|
65
|
+
A `Meta` object containing the extracted metadata.
|
|
66
|
+
|
|
65
67
|
"""
|
|
66
68
|
meta = Meta()
|
|
67
69
|
|
|
@@ -138,7 +140,7 @@ def parse_args_expr(expr: str) -> tuple[tuple[str, ...], dict[str, t.Any]]:
|
|
|
138
140
|
return tuple(required), optional
|
|
139
141
|
|
|
140
142
|
|
|
141
|
-
def eval_expression(input_string):
|
|
143
|
+
def eval_expression(input_string: str) -> t.Any:
|
|
142
144
|
code = compile(input_string, "<string>", "eval")
|
|
143
145
|
for name in code.co_names:
|
|
144
146
|
if name not in ALLOWED_NAMES_IN_EXPRESSION_VALUES:
|