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/parser.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
- Jx | Copyright (c) Juan-Pablo Scaletti <juanpablo@jpscaletti.com>
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__(self, *, name: str, source: str, components: list[str]) :
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(self, source: str, match: re.Match, *, validate_tags: bool = True) -> str:
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(f"[{self.name}:{lineno}] Unknown component `{tag}`\n{line}")
192
+ raise TemplateSyntaxError(
193
+ f"[{self.name}:{lineno}] Unknown component `{tag}`\n{line}"
194
+ )
96
195
 
97
- attrs, end = self._parse_opening_tag(source, lineno=lineno, start=curr - 1)
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(f"[{self.name}:{lineno}] Syntax error: `{tag}`\n{line}")
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(f"[{self.name}:{lineno}] Unclosed component `{tag}`\n{line}")
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
- attrs_list = self._parse_attrs(attrs)
116
- repl = self._build_call(tag, attrs_list, content)
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 _parse_opening_tag(self, source: str, *, lineno: int, start: int) -> tuple[str, int]:
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 # dentro de '…' / "…"
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(f"[{self.name}:{lineno}] Unmatched braces")
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(f"[{self.name}:{lineno}] Unmatched braces")
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, attrs: str) -> list[tuple[str, str]]:
168
- attrs = attrs.replace("\n", " ").strip()
169
- if not attrs:
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 attrs_list:
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}"=True')
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}"={value}')
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([a.replace("=", ":", 1) for a in attrs]) + "}"
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 <juanpablo@jpscaletti.com>
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,,
@@ -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