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/parser.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 re
|
|
5
6
|
import typing as t
|
|
6
7
|
from uuid import uuid4
|
|
@@ -11,7 +12,7 @@ from .exceptions import TemplateSyntaxError
|
|
|
11
12
|
from .utils import logger
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
BLOCK_CALL = '{% call _get("[TAG]").render([ATTRS]) -%}[CONTENT]{%- endcall %}'
|
|
15
|
+
BLOCK_CALL = '{% call(_slot="") _get("[TAG]").render([ATTRS]) -%}[CONTENT]{%- endcall %}'
|
|
15
16
|
INLINE_CALL = '{{ _get("[TAG]").render([ATTRS]) }}'
|
|
16
17
|
|
|
17
18
|
re_raw = r"\{%-?\s*raw\s*-?%\}.+?\{%-?\s*endraw\s*-?%\}"
|
|
@@ -32,6 +33,17 @@ re_attr = r"""
|
|
|
32
33
|
"""
|
|
33
34
|
RX_ATTR = re.compile(re_attr, re.VERBOSE | re.DOTALL)
|
|
34
35
|
|
|
36
|
+
RE_LSTRIP = r"\s*(?P<lstrip>-?)%}"
|
|
37
|
+
RE_RSTRIP = r"{%(?P<rstrip>-?)\s*"
|
|
38
|
+
|
|
39
|
+
RE_SLOT_OPEN = r"{%-?\s*slot\s+(?P<name>[0-9A-Za-z_.:$-]+)" + RE_LSTRIP
|
|
40
|
+
RE_SLOT_CLOSE = RE_RSTRIP + r"endslot\s*-?%}"
|
|
41
|
+
RX_SLOT = re.compile(rf"{RE_SLOT_OPEN}(?P<default>.*?)({RE_SLOT_CLOSE})", re.DOTALL)
|
|
42
|
+
|
|
43
|
+
RE_FILL_OPEN = r"{%-?\s*fill\s+(?P<name>[0-9A-Za-z_.:$-]+)" + RE_LSTRIP
|
|
44
|
+
RE_FILL_CLOSE = RE_RSTRIP + r"endfill\s*-?%}"
|
|
45
|
+
RX_FILL = re.compile(rf"{RE_FILL_OPEN}(?P<body>.*?)({RE_FILL_CLOSE})", re.DOTALL)
|
|
46
|
+
|
|
35
47
|
|
|
36
48
|
def escape(s: t.Any, /) -> Markup:
|
|
37
49
|
return Markup(
|
|
@@ -45,20 +57,66 @@ def escape(s: t.Any, /) -> Markup:
|
|
|
45
57
|
|
|
46
58
|
|
|
47
59
|
class JxParser:
|
|
48
|
-
def __init__(
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
name: str,
|
|
64
|
+
source: str,
|
|
65
|
+
components: list[str],
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
A parser that transforms a template's source code by replacing
|
|
69
|
+
TitledCased HTML tags with their corresponding component calls.
|
|
70
|
+
|
|
71
|
+
Only the names defined in the `components` list are allowed.
|
|
72
|
+
|
|
73
|
+
Arguments:
|
|
74
|
+
name:
|
|
75
|
+
The name of the template for error reporting.
|
|
76
|
+
source:
|
|
77
|
+
The source code of the template.
|
|
78
|
+
components:
|
|
79
|
+
A list of allowed component names.
|
|
80
|
+
|
|
81
|
+
"""
|
|
49
82
|
self.name = name
|
|
50
83
|
self.source = source
|
|
51
84
|
self.components = components
|
|
52
85
|
|
|
53
|
-
def parse(self, *, validate_tags: bool = True) -> str:
|
|
86
|
+
def parse(self, *, validate_tags: bool = True) -> tuple[str, tuple[str, ...]]:
|
|
87
|
+
"""
|
|
88
|
+
Parses the template source code.
|
|
89
|
+
|
|
90
|
+
Arguments:
|
|
91
|
+
validate_tags:
|
|
92
|
+
Whether to raise an error for unknown TitleCased tags.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
- The transformed template source code
|
|
96
|
+
- The list of slot names.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
TemplateSyntaxError:
|
|
100
|
+
If the template contains unknown components or syntax errors.
|
|
101
|
+
|
|
102
|
+
"""
|
|
54
103
|
raw_blocks = {}
|
|
55
104
|
source = self.source
|
|
56
105
|
source, raw_blocks = self.replace_raw_blocks(source)
|
|
57
106
|
source = self.process_tags(source, validate_tags=validate_tags)
|
|
107
|
+
source, slots = self.process_slots(source)
|
|
58
108
|
source = self.restore_raw_blocks(source, raw_blocks)
|
|
59
|
-
return source
|
|
109
|
+
return source, slots
|
|
60
110
|
|
|
61
111
|
def replace_raw_blocks(self, source: str) -> tuple[str, dict[str, str]]:
|
|
112
|
+
"""
|
|
113
|
+
Replace the `{% raw %}` blocks with temporary placeholders.
|
|
114
|
+
|
|
115
|
+
Arguments:
|
|
116
|
+
source:
|
|
117
|
+
The template source code.
|
|
118
|
+
|
|
119
|
+
"""
|
|
62
120
|
raw_blocks = {}
|
|
63
121
|
while True:
|
|
64
122
|
match = RX_RAW.search(source)
|
|
@@ -73,11 +131,32 @@ class JxParser:
|
|
|
73
131
|
return source, raw_blocks
|
|
74
132
|
|
|
75
133
|
def restore_raw_blocks(self, source: str, raw_blocks: dict[str, str]) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Restores the original `{% raw %}` blocks from the temporary placeholders.
|
|
136
|
+
|
|
137
|
+
Arguments:
|
|
138
|
+
source:
|
|
139
|
+
The template source code.
|
|
140
|
+
raw_blocks:
|
|
141
|
+
A dictionary mapping placeholder keys to their original raw block content.
|
|
142
|
+
|
|
143
|
+
"""
|
|
76
144
|
for uid, code in raw_blocks.items():
|
|
77
145
|
source = source.replace(uid, code)
|
|
78
146
|
return source
|
|
79
147
|
|
|
80
148
|
def process_tags(self, source: str, *, validate_tags: bool = True) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Search for TitledCased HTML tags in the template source code and replace
|
|
151
|
+
them with their corresponding component calls.
|
|
152
|
+
|
|
153
|
+
Arguments:
|
|
154
|
+
source:
|
|
155
|
+
The template source code.
|
|
156
|
+
validate_tags:
|
|
157
|
+
Whether to raise an error for unknown TitleCased tags.
|
|
158
|
+
|
|
159
|
+
"""
|
|
81
160
|
while True:
|
|
82
161
|
match = RX_TAG_NAME.search(source)
|
|
83
162
|
if not match:
|
|
@@ -85,21 +164,43 @@ class JxParser:
|
|
|
85
164
|
source = self.replace_tag(source, match, validate_tags=validate_tags)
|
|
86
165
|
return source
|
|
87
166
|
|
|
88
|
-
def replace_tag(
|
|
167
|
+
def replace_tag(
|
|
168
|
+
self,
|
|
169
|
+
source: str,
|
|
170
|
+
match: re.Match,
|
|
171
|
+
*,
|
|
172
|
+
validate_tags: bool = True,
|
|
173
|
+
) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Replaces a single TitledCased HTML tag with its corresponding component call.
|
|
176
|
+
|
|
177
|
+
Arguments:
|
|
178
|
+
source:
|
|
179
|
+
The template source code.
|
|
180
|
+
match:
|
|
181
|
+
The regex match object for the tag.
|
|
182
|
+
validate_tags:
|
|
183
|
+
Whether to raise an error for unknown TitleCased tags.
|
|
184
|
+
|
|
185
|
+
"""
|
|
89
186
|
start, curr = match.span(0)
|
|
90
187
|
lineno = source[:start].count("\n") + 1
|
|
91
188
|
|
|
92
189
|
tag = match.group("tag")
|
|
93
190
|
if validate_tags and tag not in self.components:
|
|
94
191
|
line = self.source.split("\n")[lineno - 1]
|
|
95
|
-
raise TemplateSyntaxError(
|
|
192
|
+
raise TemplateSyntaxError(
|
|
193
|
+
f"[{self.name}:{lineno}] Unknown component `{tag}`\n{line}"
|
|
194
|
+
)
|
|
96
195
|
|
|
97
|
-
|
|
196
|
+
raw_attrs, end = self._parse_opening_tag(source, lineno=lineno, start=curr - 1)
|
|
98
197
|
if end == -1:
|
|
99
198
|
line = self.source.split("\n")[lineno - 1]
|
|
100
|
-
raise TemplateSyntaxError(
|
|
199
|
+
raise TemplateSyntaxError(
|
|
200
|
+
f"[{self.name}:{lineno}] Syntax error: `{tag}`\n{line}"
|
|
201
|
+
)
|
|
101
202
|
|
|
102
|
-
inline = source[end - 2:end] == "/>"
|
|
203
|
+
inline = source[end - 2 : end] == "/>"
|
|
103
204
|
if inline:
|
|
104
205
|
content = ""
|
|
105
206
|
else:
|
|
@@ -107,39 +208,135 @@ class JxParser:
|
|
|
107
208
|
index = source.find(close_tag, end, None)
|
|
108
209
|
if index == -1:
|
|
109
210
|
line = self.source.split("\n")[lineno - 1]
|
|
110
|
-
raise TemplateSyntaxError(
|
|
211
|
+
raise TemplateSyntaxError(
|
|
212
|
+
f"[{self.name}:{lineno}] Unclosed component `{tag}`\n{line}"
|
|
213
|
+
)
|
|
111
214
|
|
|
112
215
|
content = source[end:index]
|
|
113
216
|
end = index + len(close_tag)
|
|
114
217
|
|
|
115
|
-
|
|
116
|
-
|
|
218
|
+
if content:
|
|
219
|
+
content = self.process_fills(content)
|
|
117
220
|
|
|
221
|
+
attrs = self._parse_attrs(raw_attrs)
|
|
222
|
+
repl = self._build_call(tag, attrs, content)
|
|
118
223
|
return f"{source[:start]}{repl}{source[end:]}"
|
|
119
224
|
|
|
120
|
-
def
|
|
225
|
+
def process_slots(self, source: str) -> tuple[str, tuple[str, ...]]:
|
|
226
|
+
"""
|
|
227
|
+
Extracts slot content from the template source code.
|
|
228
|
+
|
|
229
|
+
Arguments:
|
|
230
|
+
source:
|
|
231
|
+
The template source code
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
- The transformed template source code
|
|
235
|
+
- The list of slot names.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
slots = {}
|
|
239
|
+
while True:
|
|
240
|
+
match = RX_SLOT.search(source)
|
|
241
|
+
if not match:
|
|
242
|
+
break
|
|
243
|
+
start, end = match.span(0)
|
|
244
|
+
slot_name = match.group("name")
|
|
245
|
+
slot_default = match.group("default") or ""
|
|
246
|
+
lstrip = match.group("lstrip") == "-"
|
|
247
|
+
rstrip = match.group("rstrip") == "-"
|
|
248
|
+
if lstrip:
|
|
249
|
+
slot_default = slot_default.lstrip()
|
|
250
|
+
if rstrip:
|
|
251
|
+
slot_default = slot_default.rstrip()
|
|
252
|
+
|
|
253
|
+
slot_expr = "".join([
|
|
254
|
+
"{% if _slots.get('", slot_name,
|
|
255
|
+
"') %}{{ _slots['", slot_name,
|
|
256
|
+
"'] }}{% else %}", slot_default,
|
|
257
|
+
"{% endif %}"
|
|
258
|
+
])
|
|
259
|
+
source = f"{source[:start]}{slot_expr}{source[end:]}"
|
|
260
|
+
slots[slot_name] = 1
|
|
261
|
+
|
|
262
|
+
return source, tuple(slots.keys())
|
|
263
|
+
|
|
264
|
+
def process_fills(self, source: str) -> str:
|
|
265
|
+
"""
|
|
266
|
+
Processes `{% fill slot_name %}...{% endfill %}` blocks in the template source code.
|
|
267
|
+
|
|
268
|
+
Arguments:
|
|
269
|
+
source:
|
|
270
|
+
The template source code.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
The modified source code prepended by fill contents as `if` statements.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
fills = {}
|
|
277
|
+
|
|
278
|
+
while True:
|
|
279
|
+
match = RX_FILL.search(source)
|
|
280
|
+
if not match:
|
|
281
|
+
break
|
|
282
|
+
start, end = match.span(0)
|
|
283
|
+
fill_name = match.group("name")
|
|
284
|
+
fill_body = match.group("body") or ""
|
|
285
|
+
lstrip = match.group("lstrip") == "-"
|
|
286
|
+
rstrip = match.group("rstrip") == "-"
|
|
287
|
+
if lstrip:
|
|
288
|
+
fill_body = fill_body.lstrip()
|
|
289
|
+
if rstrip:
|
|
290
|
+
fill_body = fill_body.rstrip()
|
|
291
|
+
fills[fill_name] = fill_body
|
|
292
|
+
source = f"{source[:start]}{source[end:]}"
|
|
293
|
+
|
|
294
|
+
if not fills:
|
|
295
|
+
return source
|
|
296
|
+
|
|
297
|
+
ifs = []
|
|
298
|
+
for fill_name, fill_body in fills.items():
|
|
299
|
+
ifs.append(f"{{% elif _slot == '{fill_name}' %}}{fill_body}")
|
|
300
|
+
# Replace the first occurrence of "elif" with "if"
|
|
301
|
+
str_ifs = f"\n{{% {''.join(ifs)[5:]}"
|
|
302
|
+
|
|
303
|
+
return f"{str_ifs}{{% else -%}}\n{source.strip()}\n{{%- endif %}}\n"
|
|
304
|
+
|
|
305
|
+
# Private
|
|
306
|
+
|
|
307
|
+
def _parse_opening_tag(
|
|
308
|
+
self, source: str, *, lineno: int, start: int
|
|
309
|
+
) -> tuple[str, int]:
|
|
310
|
+
"""
|
|
311
|
+
Parses the opening tag and returns the raw attributes and the position
|
|
312
|
+
where the opening tag ends.
|
|
313
|
+
"""
|
|
121
314
|
eof = len(source)
|
|
122
|
-
in_single_quotes = in_double_quotes = in_braces = False
|
|
315
|
+
in_single_quotes = in_double_quotes = in_braces = False
|
|
123
316
|
i = start
|
|
124
317
|
end = -1
|
|
125
318
|
|
|
126
319
|
while i < eof:
|
|
127
320
|
ch = source[i]
|
|
128
|
-
ch2 = source[i:i + 2]
|
|
321
|
+
ch2 = source[i : i + 2]
|
|
129
322
|
# print(ch, ch2, in_single_quotes, in_double_quotes, in_braces)
|
|
130
323
|
|
|
131
324
|
# Detects {{ … }} only when NOT inside quotes
|
|
132
325
|
if not in_single_quotes and not in_double_quotes:
|
|
133
326
|
if ch2 == "{{":
|
|
134
327
|
if in_braces:
|
|
135
|
-
raise TemplateSyntaxError(
|
|
328
|
+
raise TemplateSyntaxError(
|
|
329
|
+
f"[{self.name}:{lineno}] Unmatched braces"
|
|
330
|
+
)
|
|
136
331
|
in_braces = True
|
|
137
332
|
i += 2
|
|
138
333
|
continue
|
|
139
334
|
|
|
140
335
|
if ch2 == "}}":
|
|
141
336
|
if not in_braces:
|
|
142
|
-
raise TemplateSyntaxError(
|
|
337
|
+
raise TemplateSyntaxError(
|
|
338
|
+
f"[{self.name}:{lineno}] Unmatched braces"
|
|
339
|
+
)
|
|
143
340
|
in_braces = False
|
|
144
341
|
i += 2
|
|
145
342
|
continue
|
|
@@ -164,33 +361,25 @@ class JxParser:
|
|
|
164
361
|
attrs = source[start:end].strip().removesuffix("/>").removesuffix(">")
|
|
165
362
|
return attrs, end
|
|
166
363
|
|
|
167
|
-
def _parse_attrs(self,
|
|
168
|
-
|
|
169
|
-
|
|
364
|
+
def _parse_attrs(self, raw_attrs: str) -> list[str]:
|
|
365
|
+
"""
|
|
366
|
+
Parses the HTML attributes string and returns a list of '"key":value'
|
|
367
|
+
strings to be used in a components call.
|
|
368
|
+
"""
|
|
369
|
+
raw_attrs = raw_attrs.replace("\n", " ").strip()
|
|
370
|
+
if not raw_attrs:
|
|
170
371
|
return []
|
|
171
|
-
return RX_ATTR.findall(attrs)
|
|
172
372
|
|
|
173
|
-
def _build_call(
|
|
174
|
-
self,
|
|
175
|
-
tag: str,
|
|
176
|
-
attrs_list: list[tuple[str, str]],
|
|
177
|
-
content: str = "",
|
|
178
|
-
) -> str:
|
|
179
|
-
logger.debug(f"{tag} {attrs_list} {'inline' if not content else ''}")
|
|
180
373
|
attrs = []
|
|
181
|
-
for name, value in
|
|
374
|
+
for name, value in RX_ATTR.findall(raw_attrs):
|
|
182
375
|
name = name.strip().replace("-", "_")
|
|
183
376
|
value = value.strip()
|
|
184
377
|
|
|
185
378
|
if not value:
|
|
186
|
-
attrs.append(f'"{name}"
|
|
379
|
+
attrs.append(f'"{name}":True')
|
|
187
380
|
else:
|
|
188
|
-
# vue-like syntax
|
|
189
|
-
# if (
|
|
190
|
-
# name[0] == ":"
|
|
191
|
-
# and value[0] in ("\"'")
|
|
192
|
-
# and value[-1] in ("\"'")
|
|
193
|
-
# ):
|
|
381
|
+
# vue-like syntax could be possible
|
|
382
|
+
# if (name[0] == ":" and value[0] in ("\"'") and value[-1] in ("\"'")):
|
|
194
383
|
# value = value[1:-1].strip()
|
|
195
384
|
# name = name.lstrip(":")
|
|
196
385
|
|
|
@@ -198,16 +387,23 @@ class JxParser:
|
|
|
198
387
|
if value[:2] == "{{" and value[-2:] == "}}":
|
|
199
388
|
value = value[2:-2].strip()
|
|
200
389
|
|
|
201
|
-
attrs.append(f'"{name}"
|
|
390
|
+
attrs.append(f'"{name}":{value}')
|
|
391
|
+
|
|
392
|
+
return attrs
|
|
393
|
+
|
|
394
|
+
def _build_call(self, tag: str, attrs: list[str], content: str = "") -> str:
|
|
395
|
+
"""
|
|
396
|
+
Builds a component call string.
|
|
397
|
+
"""
|
|
398
|
+
logger.debug(f"{tag} {attrs} {'inline' if not content else ''}")
|
|
202
399
|
|
|
203
400
|
str_attrs = ""
|
|
204
401
|
if attrs:
|
|
205
|
-
str_attrs = "**{" + ", ".join(
|
|
402
|
+
str_attrs = "**{" + ", ".join(attrs) + "}"
|
|
206
403
|
|
|
207
404
|
if content:
|
|
208
405
|
return (
|
|
209
|
-
BLOCK_CALL
|
|
210
|
-
.replace("[TAG]", tag)
|
|
406
|
+
BLOCK_CALL.replace("[TAG]", tag)
|
|
211
407
|
.replace("[ATTRS]", str_attrs)
|
|
212
408
|
.replace("[CONTENT]", content)
|
|
213
409
|
)
|
jx/utils.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 logging
|
|
5
6
|
import uuid
|
|
6
7
|
|
|
@@ -9,5 +10,17 @@ logger = logging.getLogger("jx")
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def get_random_id(prefix: str = "id") -> str:
|
|
13
|
+
"""
|
|
14
|
+
Returns an unique string suitable to be used for HTML element IDs.
|
|
15
|
+
|
|
16
|
+
HTML form elements, popovers, and other components require unique IDs
|
|
17
|
+
to function correctly. When you are writing custom components, this function
|
|
18
|
+
can be used to generate default IDs for such elements, so you don't have to
|
|
19
|
+
make it a required argument.
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
prefix: The prefix to use for the ID. Defaults to "id".
|
|
23
|
+
|
|
24
|
+
"""
|
|
12
25
|
return f"{prefix}-{str(uuid.uuid4().hex)}"
|
|
13
26
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jx
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Replace your HTML templates with Python server-Side components
|
|
5
|
+
Author-email: Juan Pablo Scaletti <juanpablo@jpscaletti.com>
|
|
6
|
+
Project-URL: Homepage, https://jx.scaletti.dev/
|
|
7
|
+
Project-URL: GitHub, https://github.com/jpsca/jx
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: <4,>=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: jinja2>=3.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<h1>
|
|
28
|
+
<img src="./docs/logo-jx.png" height="100" align="top">
|
|
29
|
+
</h1>
|
|
30
|
+
|
|
31
|
+
Super components powers for your Jinja templates.
|
|
32
|
+
|
|
33
|
+
<p>
|
|
34
|
+
<img alt="python: 3.11, 3.12, 3.13, 3.14" src="./docs/python.svg">
|
|
35
|
+
<img alt="license: MIT" src="./docs/license.svg">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
39
|
+
|
|
40
|
+
<!-- Documentation: https://jx.scaletti.dev/ -->
|
|
41
|
+
|
|
42
|
+
## How It Works
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
### Component Definition
|
|
47
|
+
|
|
48
|
+
Components are defined as regular Jinja2 templates (.jinja files) with special metadata comments:
|
|
49
|
+
|
|
50
|
+
- `{# def parameter1 parameter2=default_value #}` - Defines required and optional parameters
|
|
51
|
+
- `{# import "path/to/component.jinja" as ComponentName #}` - Imports other components
|
|
52
|
+
- `{# css "/path/to/style.css" #}` - Includes CSS files
|
|
53
|
+
- `{# js "/path/to/script.js" #}` - Includes JavaScript files
|
|
54
|
+
|
|
55
|
+
Example component:
|
|
56
|
+
|
|
57
|
+
```jinja
|
|
58
|
+
{# def message #}
|
|
59
|
+
{# import "button.jinja" as Button #}
|
|
60
|
+
|
|
61
|
+
<div class="greeting">{{ message }}</div>
|
|
62
|
+
<Button text="OK" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Usage Example
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from jx import Catalog
|
|
69
|
+
|
|
70
|
+
# Create a catalog and add a components folder
|
|
71
|
+
catalog = Catalog("templates/components")
|
|
72
|
+
|
|
73
|
+
# Render a component with parameters
|
|
74
|
+
html = catalog.render("card.jinja", title="Hello", content="This is a card")
|
|
75
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
jx/__init__.py,sha256=f05q3I3xvO72xqVOhtWI_KMSVa0vACbGsK2eMrhzO1g,126
|
|
2
|
+
jx/attrs.py,sha256=3cSSJb-idpD2CdhA_BZLX-OId1t8c2nzJp0jSJpTjvs,11344
|
|
3
|
+
jx/catalog.py,sha256=6NxK88krwH0Aw86SeRSW4Ws7pVENNu9dCtrAFYVKg5o,13512
|
|
4
|
+
jx/component.py,sha256=04Ic7wGm65uUPAiVGhz8YbD2orBbasKd1dFo4FtbIv4,7004
|
|
5
|
+
jx/exceptions.py,sha256=eQVCjt49FOgOjdGPPAhU4eIgBvumQ6BXmTh_siYpyIg,1493
|
|
6
|
+
jx/meta.py,sha256=nKSeZzgdtJEjPoc7PaYGhBkBbwFRC97XZc_GUMBndPI,5167
|
|
7
|
+
jx/parser.py,sha256=fLDIBqt1hAzCOkRMZoOsAJOswtrw7PE3EV9ztw1K358,12841
|
|
8
|
+
jx/utils.py,sha256=BTxDPhiWuKHRcVCND07OGkYLhzPp9G6iDgDC7H2o4Lo,643
|
|
9
|
+
jx-0.3.0.dist-info/licenses/LICENSE,sha256=RHwNifuIFfQM9QUhA2FQfnqlBcnhBHlJEVp8QcRllew,1076
|
|
10
|
+
jx-0.3.0.dist-info/METADATA,sha256=Dt5KJ7rJoGa7UXVRDq1ZEf_1LZQQO_BIbuDHoeIYAGo,2540
|
|
11
|
+
jx-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
jx-0.3.0.dist-info/top_level.txt,sha256=P61YQxqfmzVpxTMe3C48gt0vc6fnHLF8Ml0JXC-QuEI,3
|
|
13
|
+
jx-0.3.0.dist-info/RECORD,,
|
jx-0.1.0.dist-info/METADATA
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: jx
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Replace your HTML templates with Python server-Side components
|
|
5
|
-
Author-email: Juan Pablo Scaletti <juanpablo@jpscaletti.com>
|
|
6
|
-
Project-URL: homepage, https://jx.scaletti.dev/
|
|
7
|
-
Project-URL: repository, https://github.com/jpsca/jx
|
|
8
|
-
Project-URL: documentation, https://jx.scaletti.dev/
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
|
-
Classifier: Environment :: Web Environment
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
17
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
-
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
|
-
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
21
|
-
Classifier: Typing :: Typed
|
|
22
|
-
Requires-Python: <4,>=3.11
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
License-File: LICENSE
|
|
25
|
-
Requires-Dist: jinja2>=3.0
|
|
26
|
-
Requires-Dist: markupsafe>=2.0
|
|
27
|
-
Dynamic: license-file
|
|
28
|
-
|
|
29
|
-
<h1>
|
|
30
|
-
<img src="https://github.com/jpsca/jx/raw/main/docs/logo-jx.png" height="100" align="top">
|
|
31
|
-
</h1>
|
|
32
|
-
|
|
33
|
-
Super components powers for your Jinja templates.
|
|
34
|
-
|
|
35
|
-
<p>
|
|
36
|
-
<img alt="python: 3.11, 3.12, 3.13, 3.14" src="https://github.com/jpsca/jx/raw/main/docs/python.svg">
|
|
37
|
-
<img alt="license: MIT" src="https://github.com/jpsca/jx/raw/main/docs/license.svg">
|
|
38
|
-
</p>
|
|
39
|
-
|
|
40
|
-
From chaos to clarity: The power of components in your server-side-rendered Python web app.
|
|
41
|
-
|
|
42
|
-
<!-- Documentation: https://jx.scaletti.dev/ -->
|
jx-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
jx/__init__.py,sha256=MZ7KbNZkQx2D5CJpccSKuyJi179iAff3a9jG7DwEBAE,152
|
|
2
|
-
jx/attrs.py,sha256=zjcZhaVxpgqE0nAE-kDDqKLG3J_AuZg_q4pk6iHy9FE,10054
|
|
3
|
-
jx/catalog.py,sha256=0YDOz3QJnQC2MMCKfL_Zbkrff7CQaK4M-DCMh2lA6Cs,7037
|
|
4
|
-
jx/component.py,sha256=Iv4ixTbof02N3cAXW6QcluoT30vFe3aHaRRWEJX1znw,5845
|
|
5
|
-
jx/exceptions.py,sha256=rDWxkgOqeMPptmQ21joB2ujfpIU5EwMKtAL-ImB9nuA,1518
|
|
6
|
-
jx/meta.py,sha256=8PWv09laXiIb-6rwcJ0KFT0rmuIpOfR59wiH_GZ1hZw,5131
|
|
7
|
-
jx/parser.py,sha256=BlOcLH83A0xxbzQYijzbGukVpotDUg3uj32qTTLYxHc,7103
|
|
8
|
-
jx/utils.py,sha256=UDuyu3pIXirct71XbTClhMFeMaHr2ujmcQ6s2tKGBrY,232
|
|
9
|
-
jx-0.1.0.dist-info/licenses/LICENSE,sha256=RHwNifuIFfQM9QUhA2FQfnqlBcnhBHlJEVp8QcRllew,1076
|
|
10
|
-
jx-0.1.0.dist-info/METADATA,sha256=DuJ03AieE3hsJMKN8SLtlIAeXqre4oypWAIvCA5CVa0,1674
|
|
11
|
-
jx-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
jx-0.1.0.dist-info/top_level.txt,sha256=P61YQxqfmzVpxTMe3C48gt0vc6fnHLF8Ml0JXC-QuEI,3
|
|
13
|
-
jx-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|