htmy 0.7.1__py3-none-any.whl → 0.9.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.
htmy/__init__.py CHANGED
@@ -30,12 +30,17 @@ from .typing import HTMYComponentType as HTMYComponentType
30
30
  from .typing import MutableContext as MutableContext
31
31
  from .typing import Properties as Properties
32
32
  from .typing import PropertyValue as PropertyValue
33
+ from .typing import RendererType as RendererType
34
+ from .typing import StrictComponentType as StrictComponentType
33
35
  from .typing import SyncComponent as SyncComponent
34
36
  from .typing import SyncContextProvider as SyncContextProvider
35
37
  from .utils import as_component_sequence as as_component_sequence
36
38
  from .utils import as_component_type as as_component_type
37
39
  from .utils import is_component_sequence as is_component_sequence
40
+ from .utils import join
38
41
  from .utils import join_components as join_components
39
42
 
43
+ join_classes = join
44
+
40
45
  HTMY = Renderer
41
46
  """Deprecated alias for `Renderer`."""
htmy/core.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import abc
4
4
  import enum
5
+ import json
5
6
  from collections.abc import Callable, Container
6
7
  from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
7
8
  from xml.sax.saxutils import escape as xml_escape
@@ -251,6 +252,9 @@ class Formatter(ContextAware):
251
252
  """
252
253
  The default, context-aware property name and value formatter.
253
254
 
255
+ The formatter supports both primitive and (many) complex values, such as lists,
256
+ dictionaries, tuples, and sets. Complex values are JSON-serialized by default.
257
+
254
258
  Important: the default implementation looks up the formatter for a given value by checking
255
259
  its type, but it doesn't do this check with the base classes of the encountered type. For
256
260
  example the formatter will know how to format `datetime` object, but it won't know how to
@@ -258,7 +262,7 @@ class Formatter(ContextAware):
258
262
 
259
263
  One reason for this is efficiency: always checking the base classes of every single value is a
260
264
  lot of unnecessary calculation. The other reason is customizability: this way you could use
261
- subclassing for fomatter selection, e.g. with `LocaleDatetime(datetime)`-like classes.
265
+ subclassing for formatter selection, e.g. with `LocaleDatetime(datetime)`-like classes.
262
266
 
263
267
  Property name and value formatters may raise a `SkipProperty` error if a property should be skipped.
264
268
  """
@@ -337,6 +341,10 @@ class Formatter(ContextAware):
337
341
  bool: lambda v: "true" if v else "false",
338
342
  date: lambda d: cast(date, d).isoformat(),
339
343
  datetime: lambda d: cast(datetime, d).isoformat(),
344
+ dict: lambda v: json.dumps(v),
345
+ list: lambda v: json.dumps(v),
346
+ tuple: lambda v: json.dumps(v),
347
+ set: lambda v: json.dumps(tuple(v)),
340
348
  XBool: lambda v: cast(XBool, v).format(),
341
349
  type(None): SkipProperty.format_property,
342
350
  }
htmy/etree.py CHANGED
@@ -1,23 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
- import xml.etree.ElementTree as ET
4
- from collections.abc import Callable, Generator
5
3
  from typing import TYPE_CHECKING, ClassVar
6
4
  from xml.sax.saxutils import unescape
7
5
 
8
- if TYPE_CHECKING:
9
- from collections.abc import Mapping
10
- from xml.etree.ElementTree import Element
11
-
12
- from htmy.typing import ComponentType, Properties
13
-
6
+ try:
7
+ from lxml.etree import _Element as Element
8
+ from lxml.etree import tostring as etree_to_string
9
+ from lxml.html import fragment_fromstring as etree_from_string
10
+ except ImportError:
11
+ from xml.etree.ElementTree import Element # type: ignore[assignment]
12
+ from xml.etree.ElementTree import fromstring as etree_from_string # type: ignore[assignment]
13
+ from xml.etree.ElementTree import tostring as etree_to_string # type: ignore[no-redef]
14
14
 
15
+ from htmy import ComponentType, Properties
15
16
  from htmy.core import Fragment, SafeStr, WildcardTag
16
17
 
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable, Generator, Mapping
20
+
17
21
 
18
22
  class ETreeConverter:
19
23
  """
20
24
  Utility for converting XML strings to custom components.
25
+
26
+ By default the converter uses the standard library's `xml.etree.ElementTree`
27
+ module for string to element tree, and element tree to string conversion,
28
+ but if `lxml` is installed, it will be used instead.
29
+
30
+ Installing `lxml` is recommended for better performance and additional features,
31
+ like performance and support for broken HTML fragments. **Important:** `lxml` is
32
+ far more lenient and flexible than the standard library, so having it installed is
33
+ not only a performance boost, but it may also slightly change the element conversion
34
+ behavior in certain edge-cases!
21
35
  """
22
36
 
23
37
  __slots__ = ("_rules",)
@@ -43,15 +57,15 @@ class ETreeConverter:
43
57
  return SafeStr(element)
44
58
 
45
59
  element = f"<{self._htmy_fragment}>{element}</{self._htmy_fragment}>"
46
- return self.convert_element(ET.fromstring(element)) # noqa: S314 # Only use from XML strings from a trusted source.
60
+ return self.convert_element(etree_from_string(element)) # noqa: S314 # Only use XML strings from a trusted source.
47
61
 
48
62
  def convert_element(self, element: Element) -> ComponentType:
49
63
  """Converts the given `Element` to a component."""
50
64
  rules = self._rules
51
65
  if len(rules) == 0:
52
- return SafeStr(ET.tostring(element))
66
+ return SafeStr(etree_to_string(element, encoding="unicode"))
53
67
 
54
- tag: str = element.tag
68
+ tag: str = element.tag # type: ignore[assignment]
55
69
  component = Fragment if tag == self._htmy_fragment else rules.get(tag)
56
70
  children = self._convert_children(element)
57
71
  properties = self._convert_properties(element)
htmy/html.py CHANGED
@@ -285,7 +285,7 @@ class hr(TagWithProps):
285
285
  __slots__ = ()
286
286
 
287
287
 
288
- class iframe(TagWithProps):
288
+ class iframe(Tag):
289
289
  """
290
290
  `<iframe>` element.
291
291
 
@@ -709,6 +709,18 @@ class i(Tag):
709
709
  tag_config = _DefaultTagConfig.inline_children
710
710
 
711
711
 
712
+ class kbd(Tag):
713
+ """
714
+ `<kbd>` element.
715
+
716
+ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd.
717
+ """
718
+
719
+ __slots__ = ()
720
+
721
+ tag_config = _DefaultTagConfig.inline_children
722
+
723
+
712
724
  class picture(Tag):
713
725
  """
714
726
  `<picture>` element.
htmy/i18n.py CHANGED
@@ -112,7 +112,7 @@ class I18n(ContextAware):
112
112
  return result
113
113
 
114
114
 
115
- @alru_cache(8)
115
+ @alru_cache()
116
116
  async def load_translation_resource(path: Path) -> TranslationResource:
117
117
  """
118
118
  Loads the translation resource from the given path.
htmy/io.py CHANGED
@@ -1 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from anyio import open_file as open_file
6
+
7
+ if TYPE_CHECKING:
8
+ from pathlib import Path
9
+
10
+
11
+ async def load_text_file(path: str | Path) -> str:
12
+ """Loads the text content from the given path."""
13
+ async with await open_file(path, "r") as f:
14
+ return await f.read()
htmy/md/core.py CHANGED
@@ -22,7 +22,7 @@ class MarkdownParser(ContextAware):
22
22
  Context-aware markdown parser.
23
23
 
24
24
  By default, this class uses the `markdown` library with a sensible set of
25
- [extensions](https://python-markdown.github.io/extensions/) including code highlighing.
25
+ [extensions](https://python-markdown.github.io/extensions/) including code highlighting.
26
26
  """
27
27
 
28
28
  __slots__ = ("_md",)
@@ -85,10 +85,14 @@ class MD(Snippet):
85
85
  It supports all the processing utilities of `Snippet`, including `text_resolver` and
86
86
  `text_processor` for formatting, token replacement, and slot conversion to components.
87
87
 
88
- One note regaring slot convesion (`text_resolver`): it is executed before markdown parsing,
88
+ One note regarding slot conversion (`text_resolver`): it is executed before markdown parsing,
89
89
  and all string segments of the resulting component sequence are parsed individually by the
90
90
  markdown parser. As a consequence, you should only use slots in places where the preceding
91
91
  and following texts individually result in valid markdown.
92
+
93
+ **Warning:** The component treats its input as trusted. If any part of the input comes from
94
+ untrusted sources, ensure it is safely escaped (using for example `htmy.xml_format_string`)!
95
+ Passing untrusted input to this component leads to XSS vulnerabilities.
92
96
  """
93
97
 
94
98
  __slots__ = (
htmy/renderer/baseline.py CHANGED
@@ -44,6 +44,8 @@ class Renderer:
44
44
  """
45
45
  Renders the given component.
46
46
 
47
+ Implements `htmy.typing.RendererType`.
48
+
47
49
  Arguments:
48
50
  component: The component to render.
49
51
  context: An optional rendering context.
@@ -71,16 +73,18 @@ class Renderer:
71
73
  """
72
74
  if isinstance(component, str):
73
75
  return self._string_formatter(component)
76
+ elif component is None:
77
+ return ""
74
78
  elif isinstance(component, Iterable):
75
79
  rendered_children = await asyncio.gather(
76
- *(self._render_one(comp, context) for comp in component)
80
+ *(self._render_one(comp, context) for comp in component if comp is not None)
77
81
  )
78
82
 
79
- return "".join(rendered_children)
83
+ return "".join(child for child in rendered_children if child is not None)
80
84
  else:
81
- return await self._render_one(component, context)
85
+ return await self._render_one(component, context) or ""
82
86
 
83
- async def _render_one(self, component: ComponentType, context: Context) -> str:
87
+ async def _render_one(self, component: ComponentType, context: Context) -> str | None:
84
88
  """
85
89
  Renders a single component.
86
90
 
@@ -93,6 +97,8 @@ class Renderer:
93
97
  """
94
98
  if isinstance(component, str):
95
99
  return self._string_formatter(component)
100
+ elif component is None:
101
+ return None
96
102
  else:
97
103
  child_context: Context = context
98
104
  if hasattr(component, "htmy_context"): # isinstance() is too expensive.
htmy/renderer/default.py CHANGED
@@ -181,7 +181,9 @@ class _ComponentRenderer:
181
181
  `node.component` must be an `HTMYComponentType` (single component and not `str`).
182
182
  """
183
183
  component = node.component
184
- if asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
184
+ if component is None:
185
+ pass # Just skip the node
186
+ elif asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
185
187
  self._async_todos.append((node, child_context))
186
188
  elif isinstance(component, ErrorBoundary):
187
189
  self._error_boundary_todos.append((node, child_context))
@@ -199,13 +201,16 @@ class _ComponentRenderer:
199
201
  while sync_todos:
200
202
  node, child_context = sync_todos.pop()
201
203
  component = node.component
204
+ if component is None:
205
+ continue
206
+
202
207
  if hasattr(component, "htmy_context"): # isinstance() is too expensive.
203
208
  child_context = await self._extend_context(component, child_context) # type: ignore[arg-type]
204
209
 
205
- if asyncio.iscoroutinefunction(node.component.htmy): # type: ignore[union-attr]
210
+ if asyncio.iscoroutinefunction(component.htmy): # type: ignore[union-attr]
206
211
  async_todos.append((node, child_context))
207
212
  else:
208
- result: Component = node.component.htmy(child_context) # type: ignore[assignment,union-attr]
213
+ result: Component = component.htmy(child_context) # type: ignore[assignment,union-attr]
209
214
  process_node_result(node, result, child_context)
210
215
 
211
216
  if async_todos:
@@ -218,7 +223,7 @@ class _ComponentRenderer:
218
223
  *(self._process_error_boundary(n, ctx) for n, ctx in self._error_boundary_todos)
219
224
  )
220
225
 
221
- return "".join(node.component for node in self._root.iter_nodes()) # type: ignore[misc]
226
+ return "".join(node.component for node in self._root.iter_nodes() if node.component is not None) # type: ignore[misc]
222
227
 
223
228
 
224
229
  async def _render_component(
@@ -270,6 +275,8 @@ class Renderer:
270
275
  """
271
276
  Renders the given component.
272
277
 
278
+ Implements `htmy.typing.RendererType`.
279
+
273
280
  Arguments:
274
281
  component: The component to render.
275
282
  context: An optional rendering context.
htmy/snippet.py CHANGED
@@ -1,9 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
  from collections.abc import Awaitable, Iterator, Mapping
3
- from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from async_lru import alru_cache
4
8
 
5
9
  from .core import SafeStr, Text
6
- from .io import open_file
10
+ from .io import load_text_file
7
11
  from .typing import (
8
12
  Component,
9
13
  ComponentType,
@@ -13,6 +17,9 @@ from .typing import (
13
17
  )
14
18
  from .utils import as_component_sequence, as_component_type, is_component_sequence
15
19
 
20
+ if TYPE_CHECKING:
21
+ from pathlib import Path
22
+
16
23
  # -- Components and utilities
17
24
 
18
25
 
@@ -151,6 +158,10 @@ class Snippet:
151
158
  """
152
159
  Component that renders text, which may be asynchronously loaded from a file.
153
160
 
161
+ **Warning:** The component treats its input as trusted. If any part of the input comes from
162
+ untrusted sources, ensure it is safely escaped (using for example `htmy.xml_format_string`)!
163
+ Passing untrusted input to this component leads to XSS vulnerabilities.
164
+
154
165
  The entire snippet processing pipeline consists of the following steps:
155
166
 
156
167
  1. The text content is loaded from a file or passed directly as a `Text` instance.
@@ -248,8 +259,7 @@ class Snippet:
248
259
  if isinstance(path_or_text, Text):
249
260
  return path_or_text
250
261
  else:
251
- async with await open_file(path_or_text, "r") as f:
252
- return await f.read()
262
+ return await Snippet._load_text_file(path_or_text)
253
263
 
254
264
  def _render_text(self, text: str, context: Context) -> Component:
255
265
  """
@@ -257,3 +267,9 @@ class Snippet:
257
267
  and returns the corresponding component.
258
268
  """
259
269
  return SafeStr(text)
270
+
271
+ @staticmethod
272
+ @alru_cache()
273
+ async def _load_text_file(path: str | Path) -> str:
274
+ """Async text loader with an LRU cache."""
275
+ return await load_text_file(path)
htmy/typing.py CHANGED
@@ -55,7 +55,10 @@ class AsyncComponent(Protocol):
55
55
  HTMYComponentType: TypeAlias = SyncComponent | AsyncComponent
56
56
  """Sync or async `htmy` component type."""
57
57
 
58
- ComponentType: TypeAlias = HTMYComponentType | str
58
+ StrictComponentType: TypeAlias = HTMYComponentType | str
59
+ """Type definition for a single component that's not `None`."""
60
+
61
+ ComponentType: TypeAlias = StrictComponentType | None
59
62
  """Type definition for a single component."""
60
63
 
61
64
  # Omit strings from this type to simplify checks.
@@ -65,6 +68,27 @@ ComponentSequence: TypeAlias = list[ComponentType] | tuple[ComponentType, ...]
65
68
  Component: TypeAlias = ComponentType | ComponentSequence
66
69
  """Component type: a single component or a sequence of components."""
67
70
 
71
+
72
+ # -- Renderer
73
+
74
+
75
+ class RendererType(Protocol):
76
+ """Protocol definition for `htmy` renderers."""
77
+
78
+ async def render(self, component: Component, context: Context | None = None) -> str:
79
+ """
80
+ Renders the given component.
81
+
82
+ Arguments:
83
+ component: The component to render.
84
+ context: An optional rendering context.
85
+
86
+ Returns:
87
+ The rendered string.
88
+ """
89
+ ...
90
+
91
+
68
92
  # -- Context providers
69
93
 
70
94
 
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: htmy
3
- Version: 0.7.1
4
- Summary: Async, pure-Python rendering engine.
3
+ Version: 0.9.0
4
+ Summary: Async, pure-Python server-side rendering engine.
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: Peter Volf
7
8
  Author-email: do.volfp@gmail.com
8
9
  Requires-Python: >=3.10,<4.0
@@ -12,9 +13,12 @@ Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Provides-Extra: lxml
15
18
  Requires-Dist: anyio (>=4.6.2.post1,<5.0.0)
16
19
  Requires-Dist: async-lru (>=2.0.4,<3.0.0)
17
- Requires-Dist: markdown (>=3.7,<4.0)
20
+ Requires-Dist: lxml (>=6.0.0) ; extra == "lxml"
21
+ Requires-Dist: markdown (>=3.8,<4.0)
18
22
  Description-Content-Type: text/markdown
19
23
 
20
24
  ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
@@ -28,13 +32,13 @@ Description-Content-Type: text/markdown
28
32
 
29
33
  # `htmy`
30
34
 
31
- **Async**, **pure-Python** rendering engine.
35
+ **Async**, **pure-Python** server-side rendering engine.
32
36
 
33
37
  Unleash your creativity with the full power and Python, without the hassle of learning a new templating language or dealing with its limitations!
34
38
 
35
39
  ## Key features
36
40
 
37
- - **Async**-first, to let you make the best use of [modern async tools](https://github.com/timofurrer/awesome-asyncio).
41
+ - **Async**-first, to let you make the best use of modern async tools.
38
42
  - **Powerful**, React-like **context support**, so you can avoid prop-drilling.
39
43
  - Sync and async **function components** with **decorator syntax**.
40
44
  - All baseline **HTML** tags built-in.
@@ -45,8 +49,23 @@ Unleash your creativity with the full power and Python, without the hassle of le
45
49
  - **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
46
50
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
47
51
  - Automatic and customizable **property-name conversion** from snake case to kebab case.
52
+ - **Compatible** with any other templating library through wrappers.
48
53
  - **Fully-typed**.
49
54
 
55
+ ## Testimonials
56
+
57
+ "Thank you for your work on `fasthx`, as well as `htmy`! I've never had an easier time developing with another stack." ([ref](https://github.com/volfpeter/fasthx/discussions/77))
58
+
59
+ "One of the main parts of the `FastAPI` -> `fasthx` -> `htmy` integration I'm falling in love with is its explicitness, and not too much magic happening." ([ref](https://github.com/volfpeter/fasthx/issues/54))
60
+
61
+ "Thank you for your work on `htmy` and `fasthx`, both have been very pleasant to use, and the APIs are both intuitive and simple. Great work." ([ref](https://github.com/volfpeter/fasthx/issues/54))
62
+
63
+ "I love that the language-embedded HTML generation library approach is becoming more popular." ([ref](https://www.reddit.com/r/programming/comments/1h1a0dx/comment/lzd3phw))
64
+
65
+ "Neat approach and it naturally solves the partial templates problem 👍" ([ref](https://www.reddit.com/r/Python/comments/1gp3mww/comment/lwqj4fc))
66
+
67
+ "Great API design!" ([ref](https://www.reddit.com/r/Python/comments/1gp3mww/comment/lwpdyq9))
68
+
50
69
  ## Support
51
70
 
52
71
  Consider supporting the development and maintenance of the project through [sponsoring](https://buymeacoffee.com/volfpeter), or reach out for [consulting](https://www.volfp.com/contact?subject=Consulting%20-%20HTMY) so you can get the most out of the library.
@@ -59,6 +78,10 @@ The package is available on PyPI and can be installed with:
59
78
  $ pip install htmy
60
79
  ```
61
80
 
81
+ The package has the following optional dependencies:
82
+
83
+ - `lxml` *(recommended)*: When installed, it is prioritized over `xml.etree.ElementTree` and provides more secure, faster, and more flexible HTML and XML processing. It is used, for example, for Markdown processing. Install with: `pip install "htmy[lxml]"`.
84
+
62
85
  ## Concepts
63
86
 
64
87
  The entire library -- from the rendering engine itself to the built-in components -- is built around a few simple protocols and a handful of simple utility classes. This means that you can easily customize, extend, or replace basically everything in the library. Yes, even the rendering engine. The remaining parts will keep working as expected.
@@ -285,6 +308,7 @@ These are default tag attribute formatting rules:
285
308
  - `bool` attribute values are converted to strings (`"true"` and `"false"`).
286
309
  - `XBool.true` attributes values are converted to an empty string, and `XBool.false` values are skipped (only the attribute name is rendered).
287
310
  - `date` and `datetime` attribute values are converted to ISO strings.
311
+ - Complex values such as lists, dictionaries, tuples, and sets are JSON serialized.
288
312
 
289
313
  ### Error boundary
290
314
 
@@ -302,11 +326,39 @@ If a component executes a potentially "long-running" synchronous call, it is str
302
326
 
303
327
  In all other cases, it's best to use sync components.
304
328
 
329
+ ## XSS prevention
330
+
331
+ `htmy` does XML/HTML escaping by default. This means user input is normally sanitized and rendered safely.
332
+
333
+ There are a couple of notable exceptions to this, where components by design allow XML/HTML inputs and assume they are safe:
334
+
335
+ - `Snippet`: The primary use-case is to efficiently render XML/HTML templates, filling in placeholders with dynamic content. In this case you must ensure that the input template itself is safe!
336
+ - `MD`: This component builds on `Snippet` to support markdown inputs and performs automatic markdown to HTML conversion. You must ensure the input text is safe!
337
+
338
+ ## AI assistance
339
+
340
+ The library is registered at [Context7](https://context7.com/volfpeter).
341
+
342
+ To get good AI assistance, all you need to do is register the Context7 MCP server in your coding tool and tell the agent to use it.
343
+
344
+ Because of the similarity with native HTML, JSX, and React, you can expect good results, both for vibe coding or inline completion.
345
+
346
+ ## Compatibility and performance
347
+
348
+ By design, `htmy` is compatible with any other Python templating library, for example Jinja, through wrappers. A wrapper is simply a custom `htmy` component that internally offloads rendering to another templating framework. This makes it possible to easily combine `htmy` with other libraries, to gradually adopt it, and even to enjoy the benefits of multiple frameworks.
349
+
350
+ Performance strongly depends on how you use `htmy`. The `Snippet` component for example makes it possible to reach almost Python string formatting performance, while rendering large, deep component trees is noticeably slower than Jinja for example. Wrapping another templating library for certain use-cases, or pre-rendering components and later using `Snippet` to fill in the dynamic content can be beneficial for performance.
351
+
305
352
  ## Framework integrations
306
353
 
307
354
  FastAPI:
308
355
 
309
- - [FastHX](https://github.com/volfpeter/fasthx)
356
+ - [holm](https://github.com/volfpeter/holm): Web development framework that brings the Next.js developer experience to Python, built on FastAPI, htmy, and FastHX.
357
+ - [FastHX](https://github.com/volfpeter/fasthx): Declarative server-side rendering utility for FastAPI with built-in HTMX support.
358
+
359
+ ## External examples
360
+
361
+ - [lipsum-chat](https://github.com/volfpeter/lipsum-chat): A simple chat application using `FastAPI`, `htmx`, and `fasthx`.
310
362
 
311
363
  ## Why
312
364
 
@@ -0,0 +1,21 @@
1
+ htmy/__init__.py,sha256=YPl0qVUFQpkISXYkXTCpnWnqnHUdfATmHPu9L3pW5FA,2037
2
+ htmy/core.py,sha256=mvNbxTRS8HNnjMSwe4NEJdL1KF19pJImF4Ciip6837I,15484
3
+ htmy/etree.py,sha256=3znZCYQ5xbNqyXYSYFkUh6M22QLWMYWhTNjUcgpjCLc,4051
4
+ htmy/function_component.py,sha256=iSp5cGrErmIsc-VfNq053_J2m-Nuu_k2xK9UxvEnlw8,12431
5
+ htmy/html.py,sha256=Pw9KCSn0X01D_fwIpcckI9nsQWNJMiYbcqQH0q2ezuM,21276
6
+ htmy/i18n.py,sha256=Kobvm9mFoNcJas52KQbheiRIzJF1Ad1azOhtfm_k0BE,5123
7
+ htmy/io.py,sha256=oEXXVnpdbjs2NzAGi36Pept-pyvXshEGHrbBFzcHYio,344
8
+ htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
9
+ htmy/md/core.py,sha256=jkgI78otXg7VmjmgeCpyjsNi0E-CqUnKYNVWVt3Sbcs,4729
10
+ htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
11
+ htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
13
+ htmy/renderer/baseline.py,sha256=WrKO5cQpvF5AFUuFoEoAS8_lUqTg51m8hkolq9-Uetw,4519
14
+ htmy/renderer/default.py,sha256=n1nAbU4cCtA26rng5D2T4oIjHVSPjRK5GCphJ8NFPmA,11076
15
+ htmy/snippet.py,sha256=Lo99zoIzU8n-qO2-10d8tX_J8SCEmEBpQWdYR0AVsZM,10808
16
+ htmy/typing.py,sha256=qgOj8nZmemzSEQzNRib9tT3l7LETvajOyLkxveA6nss,3671
17
+ htmy/utils.py,sha256=Kp0j9G8CBeRiyFGmz-CoDiLtXHfpvHzlTVsWeDhIebM,1935
18
+ htmy-0.9.0.dist-info/METADATA,sha256=aX7JtrXXJTD4bDExa3W08orerBX3J98hI7k3l0nJiMs,22267
19
+ htmy-0.9.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
20
+ htmy-0.9.0.dist-info/licenses/LICENSE,sha256=ulLk8GOf1aK1cTSWjx5Hw1C4r_FtcgL4NSXs48zam5M,1067
21
+ htmy-0.9.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Peter Volf
3
+ Copyright (c) 2025 Peter Volf
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,21 +0,0 @@
1
- htmy/__init__.py,sha256=Us5P9Y6ZSp38poIz88bsAh2Hxuze5jE3V_uMtMyuH-E,1880
2
- htmy/core.py,sha256=OoL11j2V-CfePC0dbkC2A5GbdK942b5Huszw3rLo7fc,15124
3
- htmy/etree.py,sha256=yKxom__AdsJY-Q1kbU0sdTMr0ZF5dMSVBKxayntNFyQ,3062
4
- htmy/function_component.py,sha256=iSp5cGrErmIsc-VfNq053_J2m-Nuu_k2xK9UxvEnlw8,12431
5
- htmy/html.py,sha256=7UohfPRtl-3IoSbOiDxazsSHQpCZ0tyRdNayQISPM8A,21086
6
- htmy/i18n.py,sha256=brNazQjObBFfbnViZCpcnxa0qgxQbJfX7xJAH-MqTW8,5124
7
- htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
8
- htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
9
- htmy/md/core.py,sha256=Xu-8xGAOGqSYLGPOib0Wn-blmyQBHl3MrAOza_w__Y8,4456
10
- htmy/md/typing.py,sha256=LF-AEvo7FCW2KumyR5l55rsXizV2E4AHVLKFf6lApgM,762
11
- htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- htmy/renderer/__init__.py,sha256=xnP_aaoK-pTok-69wi8O_xlsgjoKTzWd2lIIeHGcuaY,226
13
- htmy/renderer/baseline.py,sha256=hHb7CoQhFFdD7Sdw0ltR1-XLGwE9pqmfL5yKFeF2rCg,4288
14
- htmy/renderer/default.py,sha256=G6K5YptvH9QvEvMQZdLPtgUblO_zTv4Eo6TETHZDlX8,10869
15
- htmy/snippet.py,sha256=dkHEOuULGsgawIMnSz99hghvNu8pLVGAQMQSlrn9ibY,10260
16
- htmy/typing.py,sha256=0spTpz_JWql2yy_lSlRx0uqgXar7fxwyBqWeIzltvKU,3111
17
- htmy/utils.py,sha256=Kp0j9G8CBeRiyFGmz-CoDiLtXHfpvHzlTVsWeDhIebM,1935
18
- htmy-0.7.1.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
19
- htmy-0.7.1.dist-info/METADATA,sha256=-1VV-vjHWSTWiqNq1bi_E8sKy3PGSLMmauuuKscc70A,18459
20
- htmy-0.7.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
21
- htmy-0.7.1.dist-info/RECORD,,