htmy 0.1.0__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of htmy might be problematic. Click here for more details.
- htmy/__init__.py +3 -1
- htmy/core.py +128 -23
- htmy/etree.py +90 -0
- htmy/html.py +18 -18
- htmy/io.py +1 -0
- htmy/md/__init__.py +6 -0
- htmy/md/core.py +111 -0
- htmy/md/typing.py +20 -0
- htmy/typing.py +12 -4
- {htmy-0.1.0.dist-info → htmy-0.2.0.dist-info}/METADATA +16 -6
- htmy-0.2.0.dist-info/RECORD +16 -0
- {htmy-0.1.0.dist-info → htmy-0.2.0.dist-info}/WHEEL +1 -1
- htmy-0.1.0.dist-info/RECORD +0 -11
- {htmy-0.1.0.dist-info → htmy-0.2.0.dist-info}/LICENSE +0 -0
htmy/__init__.py
CHANGED
|
@@ -5,10 +5,12 @@ from .core import Formatter as Formatter
|
|
|
5
5
|
from .core import Fragment as Fragment
|
|
6
6
|
from .core import SafeStr as SafeStr
|
|
7
7
|
from .core import SkipProperty as SkipProperty
|
|
8
|
-
from .core import
|
|
8
|
+
from .core import Snippet as Snippet
|
|
9
9
|
from .core import Tag as Tag
|
|
10
10
|
from .core import TagConfig as TagConfig
|
|
11
11
|
from .core import TagWithProps as TagWithProps
|
|
12
|
+
from .core import Text as Text
|
|
13
|
+
from .core import WildcardTag as WildcardTag
|
|
12
14
|
from .core import WithContext as WithContext
|
|
13
15
|
from .core import XBool as XBool
|
|
14
16
|
from .core import component as component
|
htmy/core.py
CHANGED
|
@@ -4,9 +4,11 @@ import abc
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import enum
|
|
6
6
|
from collections.abc import Callable, Container
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import Any, ClassVar, Generic, Self, TypedDict, cast, overload
|
|
8
9
|
from xml.sax.saxutils import escape as xml_escape
|
|
9
10
|
|
|
11
|
+
from .io import open_file
|
|
10
12
|
from .typing import (
|
|
11
13
|
AsyncFunctionComponent,
|
|
12
14
|
Component,
|
|
@@ -40,7 +42,8 @@ class Fragment:
|
|
|
40
42
|
self._children = children
|
|
41
43
|
|
|
42
44
|
def htmy(self, context: Context) -> Component:
|
|
43
|
-
|
|
45
|
+
"""Renders the component."""
|
|
46
|
+
return self._children
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
class ErrorBoundary(Fragment):
|
|
@@ -112,9 +115,50 @@ class WithContext(Fragment):
|
|
|
112
115
|
self._context = context
|
|
113
116
|
|
|
114
117
|
def htmy_context(self) -> Context:
|
|
118
|
+
"""Returns an HTMY context for child rendering."""
|
|
115
119
|
return self._context
|
|
116
120
|
|
|
117
121
|
|
|
122
|
+
class Snippet:
|
|
123
|
+
"""
|
|
124
|
+
Base component that can load its content from a file.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
__slots__ = ("_path_or_text",)
|
|
128
|
+
|
|
129
|
+
def __init__(self, path_or_text: Text | str | Path) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Initialization.
|
|
132
|
+
|
|
133
|
+
Arguments:
|
|
134
|
+
path_or_text: The path from where the content should be loaded or a `Text`
|
|
135
|
+
instance if this value should be rendered directly.
|
|
136
|
+
"""
|
|
137
|
+
self._path_or_text = path_or_text
|
|
138
|
+
|
|
139
|
+
async def htmy(self, context: Context) -> Component:
|
|
140
|
+
"""Renders the component."""
|
|
141
|
+
text = await self._get_text_content()
|
|
142
|
+
return self._render_text(text, context)
|
|
143
|
+
|
|
144
|
+
async def _get_text_content(self) -> str:
|
|
145
|
+
"""Returns the plain text content that should be rendered."""
|
|
146
|
+
path_or_text = self._path_or_text
|
|
147
|
+
|
|
148
|
+
if isinstance(path_or_text, Text):
|
|
149
|
+
return path_or_text
|
|
150
|
+
else:
|
|
151
|
+
async with await open_file(path_or_text, "r") as f:
|
|
152
|
+
return await f.read()
|
|
153
|
+
|
|
154
|
+
def _render_text(self, text: str, context: Context) -> Component:
|
|
155
|
+
"""
|
|
156
|
+
Render function that takes the text that must be rendered and the current rendering context,
|
|
157
|
+
and returns the corresponding HTMY component.
|
|
158
|
+
"""
|
|
159
|
+
return SafeStr(text)
|
|
160
|
+
|
|
161
|
+
|
|
118
162
|
# -- Context utilities
|
|
119
163
|
|
|
120
164
|
|
|
@@ -154,7 +198,7 @@ class ContextAware:
|
|
|
154
198
|
```
|
|
155
199
|
"""
|
|
156
200
|
|
|
157
|
-
|
|
201
|
+
__slots__ = ()
|
|
158
202
|
|
|
159
203
|
_base_context_type: ClassVar[type[ContextAware] | None] = None
|
|
160
204
|
|
|
@@ -214,6 +258,7 @@ class SyncFunctionComponentWrapper(Generic[T]):
|
|
|
214
258
|
cls._wrapped_function = func
|
|
215
259
|
|
|
216
260
|
def htmy(self, context: Context) -> Component:
|
|
261
|
+
"""Renders the component."""
|
|
217
262
|
# type(self) is necessary, otherwise the wrapped function would be called
|
|
218
263
|
# with an extra self argument...
|
|
219
264
|
return type(self)._wrapped_function(self._props, context)
|
|
@@ -233,6 +278,7 @@ class AsyncFunctionComponentWrapper(Generic[T]):
|
|
|
233
278
|
cls._wrapped_function = func
|
|
234
279
|
|
|
235
280
|
async def htmy(self, context: Context) -> Component:
|
|
281
|
+
"""Renders the component."""
|
|
236
282
|
# type(self) is necessary, otherwise the wrapped function would be called
|
|
237
283
|
# with an extra self argument...
|
|
238
284
|
return await type(self)._wrapped_function(self._props, context)
|
|
@@ -283,7 +329,13 @@ class SkipProperty(Exception):
|
|
|
283
329
|
...
|
|
284
330
|
|
|
285
331
|
|
|
286
|
-
class
|
|
332
|
+
class Text(str):
|
|
333
|
+
"""Marker class for differentiating text content from other strings."""
|
|
334
|
+
|
|
335
|
+
...
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class SafeStr(Text):
|
|
287
339
|
"""
|
|
288
340
|
String subclass whose instances shouldn't get escaped during rendering.
|
|
289
341
|
|
|
@@ -422,6 +474,8 @@ _default_tag_formatter = Formatter()
|
|
|
422
474
|
|
|
423
475
|
|
|
424
476
|
class TagConfig(TypedDict, total=False):
|
|
477
|
+
"""Tag configuration."""
|
|
478
|
+
|
|
425
479
|
child_separator: ComponentType | None
|
|
426
480
|
|
|
427
481
|
|
|
@@ -437,14 +491,23 @@ class BaseTag(abc.ABC):
|
|
|
437
491
|
resolves the async content and then passes the value to the tag.
|
|
438
492
|
"""
|
|
439
493
|
|
|
440
|
-
__slots__ = ()
|
|
494
|
+
__slots__ = ("_htmy_name",)
|
|
495
|
+
|
|
496
|
+
def __init__(self) -> None:
|
|
497
|
+
self._htmy_name = self._get_htmy_name()
|
|
441
498
|
|
|
442
499
|
@property
|
|
443
|
-
def
|
|
444
|
-
|
|
500
|
+
def htmy_name(self) -> str:
|
|
501
|
+
"""The tag name."""
|
|
502
|
+
return self._htmy_name
|
|
445
503
|
|
|
446
504
|
@abc.abstractmethod
|
|
447
|
-
def htmy(self, context: Context) -> Component:
|
|
505
|
+
def htmy(self, context: Context) -> Component:
|
|
506
|
+
"""Abstract base method for HTMY rendering."""
|
|
507
|
+
...
|
|
508
|
+
|
|
509
|
+
def _get_htmy_name(self) -> str:
|
|
510
|
+
return type(self).__name__
|
|
448
511
|
|
|
449
512
|
|
|
450
513
|
class TagWithProps(BaseTag):
|
|
@@ -453,30 +516,27 @@ class TagWithProps(BaseTag):
|
|
|
453
516
|
__slots__ = ("props",)
|
|
454
517
|
|
|
455
518
|
def __init__(self, **props: PropertyValue) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Initialization.
|
|
521
|
+
|
|
522
|
+
Arguments:
|
|
523
|
+
**props: Tag properties.
|
|
524
|
+
"""
|
|
456
525
|
super().__init__()
|
|
457
526
|
self.props = props
|
|
458
527
|
|
|
459
528
|
def htmy(self, context: Context) -> Component:
|
|
460
|
-
|
|
529
|
+
"""Renders the component."""
|
|
530
|
+
name = self.htmy_name
|
|
461
531
|
props = self._htmy_format_props(context=context)
|
|
462
|
-
return
|
|
532
|
+
return SafeStr(f"<{name} {props}/>")
|
|
463
533
|
|
|
464
534
|
def _htmy_format_props(self, context: Context) -> str:
|
|
535
|
+
"""Formats tag properties."""
|
|
465
536
|
formatter = Formatter.from_context(context, _default_tag_formatter)
|
|
466
537
|
return " ".join(formatter.format(name, value) for name, value in self.props.items())
|
|
467
538
|
|
|
468
539
|
|
|
469
|
-
class StandaloneTag(TagWithProps):
|
|
470
|
-
"""Tag that has properties and no closing elements, e.g. `<img .../>`."""
|
|
471
|
-
|
|
472
|
-
__slots__ = ()
|
|
473
|
-
|
|
474
|
-
def htmy(self, context: Context) -> Component:
|
|
475
|
-
name = self._htmy_name
|
|
476
|
-
props = self._htmy_format_props(context=context)
|
|
477
|
-
return SafeStr(f"<{name} {props}/>")
|
|
478
|
-
|
|
479
|
-
|
|
480
540
|
class Tag(TagWithProps):
|
|
481
541
|
"""Base class for tags with both properties and children."""
|
|
482
542
|
|
|
@@ -485,12 +545,27 @@ class Tag(TagWithProps):
|
|
|
485
545
|
tag_config: TagConfig = {"child_separator": "\n"}
|
|
486
546
|
|
|
487
547
|
def __init__(self, *children: ComponentType, **props: PropertyValue) -> None:
|
|
548
|
+
"""
|
|
549
|
+
Initialization.
|
|
550
|
+
|
|
551
|
+
Arguments:
|
|
552
|
+
*children: Children components.
|
|
553
|
+
**props: Tag properties.
|
|
554
|
+
"""
|
|
555
|
+
super().__init__(**props)
|
|
488
556
|
self.children = children
|
|
489
|
-
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def child_separator(self) -> ComponentType | None:
|
|
560
|
+
"""The child separator to use."""
|
|
561
|
+
return self.tag_config.get("child_separator", None)
|
|
490
562
|
|
|
491
563
|
def htmy(self, context: Context) -> Component:
|
|
492
|
-
|
|
493
|
-
|
|
564
|
+
"""Renders the component."""
|
|
565
|
+
name = self.htmy_name
|
|
566
|
+
props = self._htmy_format_props(context=context)
|
|
567
|
+
opening, closing = SafeStr(f"<{name} {props}>"), SafeStr(f"</{name}>")
|
|
568
|
+
separator = self.child_separator
|
|
494
569
|
return (
|
|
495
570
|
opening,
|
|
496
571
|
*(
|
|
@@ -500,3 +575,33 @@ class Tag(TagWithProps):
|
|
|
500
575
|
),
|
|
501
576
|
closing,
|
|
502
577
|
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class WildcardTag(Tag):
|
|
581
|
+
"""Tag that can have both children and properties, and whose tag name can be set."""
|
|
582
|
+
|
|
583
|
+
__slots__ = ("_child_separator",)
|
|
584
|
+
|
|
585
|
+
def __init__(
|
|
586
|
+
self,
|
|
587
|
+
*children: ComponentType,
|
|
588
|
+
htmy_name: str,
|
|
589
|
+
htmy_child_separator: ComponentType | None = None,
|
|
590
|
+
**props: PropertyValue,
|
|
591
|
+
) -> None:
|
|
592
|
+
"""
|
|
593
|
+
Initialization.
|
|
594
|
+
|
|
595
|
+
Arguments:
|
|
596
|
+
*children: Children components.
|
|
597
|
+
htmy_name: The tag name to use for this tag.
|
|
598
|
+
htmy_child_separator: The child separator to use (if any).
|
|
599
|
+
**props: Tag properties.
|
|
600
|
+
"""
|
|
601
|
+
super().__init__(*children, **props)
|
|
602
|
+
self._htmy_name = htmy_name
|
|
603
|
+
self._child_separator = htmy_child_separator
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def child_separator(self) -> ComponentType | None:
|
|
607
|
+
return self._child_separator
|
htmy/etree.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
from collections.abc import Callable, Generator
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
6
|
+
from xml.sax.saxutils import unescape
|
|
7
|
+
|
|
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
|
+
|
|
14
|
+
|
|
15
|
+
from htmy.core import Fragment, SafeStr, WildcardTag
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ETreeConverter:
|
|
19
|
+
"""
|
|
20
|
+
Utility for converting XML strings to custom HTMY components.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__slots__ = ("_rules",)
|
|
24
|
+
|
|
25
|
+
_htmy_fragment: ClassVar[str] = "htmy_fragment"
|
|
26
|
+
"""
|
|
27
|
+
Placeholder tag name that's used to wrap possibly multi-root XML snippets into a valid
|
|
28
|
+
XML document with a single root that can be processed by standard tools.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, rules: Mapping[str, Callable[..., ComponentType]]) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Initialization.
|
|
34
|
+
|
|
35
|
+
Arguments:
|
|
36
|
+
rules: Tag-name to HTMY component conversion rules.
|
|
37
|
+
"""
|
|
38
|
+
self._rules = rules
|
|
39
|
+
|
|
40
|
+
def convert(self, element: str) -> ComponentType:
|
|
41
|
+
"""Converts the given (possible multi-root) XML string to an HTMY component."""
|
|
42
|
+
if len(self._rules) == 0:
|
|
43
|
+
return SafeStr(element)
|
|
44
|
+
|
|
45
|
+
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.
|
|
47
|
+
|
|
48
|
+
def convert_element(self, element: Element) -> ComponentType:
|
|
49
|
+
"""Converts the given `Element` to an HTMY component."""
|
|
50
|
+
rules = self._rules
|
|
51
|
+
if len(rules) == 0:
|
|
52
|
+
return SafeStr(ET.tostring(element))
|
|
53
|
+
|
|
54
|
+
tag: str = element.tag
|
|
55
|
+
component = Fragment if tag == self._htmy_fragment else rules.get(tag)
|
|
56
|
+
children = self._convert_children(element)
|
|
57
|
+
properties = self._convert_properties(element)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
WildcardTag(*children, htmy_name=tag, **properties)
|
|
61
|
+
if component is None
|
|
62
|
+
else component(
|
|
63
|
+
*children,
|
|
64
|
+
**properties,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _convert_properties(self, element: Element) -> Properties:
|
|
69
|
+
"""
|
|
70
|
+
Converts the attributes of the given `Element` to an HTMY `Properties` mapping.
|
|
71
|
+
|
|
72
|
+
This method should not alter property names in any way.
|
|
73
|
+
"""
|
|
74
|
+
return {key: unescape(value) for key, value in element.items()}
|
|
75
|
+
|
|
76
|
+
def _convert_children(self, element: Element) -> Generator[ComponentType, None, None]:
|
|
77
|
+
"""
|
|
78
|
+
Generator that converts all (text and `Element`) children of the given `Element`
|
|
79
|
+
into an HTMY component."""
|
|
80
|
+
if text := self._process_text(element.text):
|
|
81
|
+
yield text
|
|
82
|
+
|
|
83
|
+
for child in element:
|
|
84
|
+
yield self.convert_element(child)
|
|
85
|
+
if tail := self._process_text(child.tail):
|
|
86
|
+
yield tail
|
|
87
|
+
|
|
88
|
+
def _process_text(self, escaped_text: str | None) -> str | None:
|
|
89
|
+
"""Processes a single XML-escaped text child."""
|
|
90
|
+
return unescape(escaped_text) if escaped_text else None
|
htmy/html.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from .core import SafeStr,
|
|
3
|
+
from .core import SafeStr, Tag, TagConfig, TagWithProps
|
|
4
4
|
from .typing import PropertyValue
|
|
5
5
|
|
|
6
6
|
|
|
@@ -45,7 +45,7 @@ class body(Tag):
|
|
|
45
45
|
__slots__ = ()
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class base(
|
|
48
|
+
class base(TagWithProps):
|
|
49
49
|
"""
|
|
50
50
|
`<base>` element.
|
|
51
51
|
|
|
@@ -70,7 +70,7 @@ class title(Tag):
|
|
|
70
70
|
super().__init__(text, **props)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
class link(
|
|
73
|
+
class link(TagWithProps):
|
|
74
74
|
"""
|
|
75
75
|
`<link>` element.
|
|
76
76
|
|
|
@@ -84,7 +84,7 @@ class link(StandaloneTag):
|
|
|
84
84
|
return cls(rel="stylesheet", type="text/css", href=href)
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
class meta(
|
|
87
|
+
class meta(TagWithProps):
|
|
88
88
|
"""
|
|
89
89
|
`<meta>` element.
|
|
90
90
|
|
|
@@ -215,7 +215,7 @@ class div(Tag):
|
|
|
215
215
|
__slots__ = ()
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
class embed(
|
|
218
|
+
class embed(TagWithProps):
|
|
219
219
|
"""
|
|
220
220
|
`<embed>` element.
|
|
221
221
|
|
|
@@ -275,7 +275,7 @@ class hgroup(Tag):
|
|
|
275
275
|
__slots__ = ()
|
|
276
276
|
|
|
277
277
|
|
|
278
|
-
class hr(
|
|
278
|
+
class hr(TagWithProps):
|
|
279
279
|
"""
|
|
280
280
|
`<hr>` element.
|
|
281
281
|
|
|
@@ -366,6 +366,8 @@ class pre(Tag):
|
|
|
366
366
|
|
|
367
367
|
__slots__ = ()
|
|
368
368
|
|
|
369
|
+
tag_config = _DefaultTagConfig.inline_children
|
|
370
|
+
|
|
369
371
|
|
|
370
372
|
class section(Tag):
|
|
371
373
|
"""
|
|
@@ -457,7 +459,7 @@ class fieldset(Tag):
|
|
|
457
459
|
__slots__ = ()
|
|
458
460
|
|
|
459
461
|
|
|
460
|
-
class input_(
|
|
462
|
+
class input_(TagWithProps):
|
|
461
463
|
"""
|
|
462
464
|
`<input>` element.
|
|
463
465
|
|
|
@@ -466,8 +468,7 @@ class input_(StandaloneTag):
|
|
|
466
468
|
|
|
467
469
|
__slots__ = ()
|
|
468
470
|
|
|
469
|
-
|
|
470
|
-
def _htmy_name(self) -> str:
|
|
471
|
+
def _get_htmy_name(self) -> str:
|
|
471
472
|
return "input"
|
|
472
473
|
|
|
473
474
|
|
|
@@ -611,7 +612,7 @@ class bdo(Tag):
|
|
|
611
612
|
tag_config = _DefaultTagConfig.inline_children
|
|
612
613
|
|
|
613
614
|
|
|
614
|
-
class br(
|
|
615
|
+
class br(TagWithProps):
|
|
615
616
|
"""
|
|
616
617
|
`<br>` element.
|
|
617
618
|
|
|
@@ -668,8 +669,7 @@ class del_(Tag):
|
|
|
668
669
|
|
|
669
670
|
tag_config = _DefaultTagConfig.inline_children
|
|
670
671
|
|
|
671
|
-
|
|
672
|
-
def _htmy_name(self) -> str:
|
|
672
|
+
def _get_htmy_name(self) -> str:
|
|
673
673
|
return "del"
|
|
674
674
|
|
|
675
675
|
|
|
@@ -719,7 +719,7 @@ class picture(Tag):
|
|
|
719
719
|
__slots__ = ()
|
|
720
720
|
|
|
721
721
|
|
|
722
|
-
class img(
|
|
722
|
+
class img(TagWithProps):
|
|
723
723
|
"""
|
|
724
724
|
`<img>` element.
|
|
725
725
|
|
|
@@ -729,7 +729,7 @@ class img(StandaloneTag):
|
|
|
729
729
|
__slots__ = ()
|
|
730
730
|
|
|
731
731
|
|
|
732
|
-
class source(
|
|
732
|
+
class source(TagWithProps):
|
|
733
733
|
"""
|
|
734
734
|
`<source>` element.
|
|
735
735
|
|
|
@@ -1043,7 +1043,7 @@ class td(Tag):
|
|
|
1043
1043
|
tag_config = _DefaultTagConfig.inline_children
|
|
1044
1044
|
|
|
1045
1045
|
|
|
1046
|
-
class colgroup(
|
|
1046
|
+
class colgroup(TagWithProps):
|
|
1047
1047
|
"""
|
|
1048
1048
|
`<colgroup>` element.
|
|
1049
1049
|
|
|
@@ -1053,7 +1053,7 @@ class colgroup(StandaloneTag):
|
|
|
1053
1053
|
__slots__ = ()
|
|
1054
1054
|
|
|
1055
1055
|
|
|
1056
|
-
class col(
|
|
1056
|
+
class col(TagWithProps):
|
|
1057
1057
|
"""
|
|
1058
1058
|
`<col>` element.
|
|
1059
1059
|
|
|
@@ -1179,7 +1179,7 @@ class video(Tag):
|
|
|
1179
1179
|
__slots__ = ()
|
|
1180
1180
|
|
|
1181
1181
|
|
|
1182
|
-
class track(
|
|
1182
|
+
class track(TagWithProps):
|
|
1183
1183
|
"""
|
|
1184
1184
|
`<track>` element.
|
|
1185
1185
|
|
|
@@ -1199,7 +1199,7 @@ class canvas(Tag):
|
|
|
1199
1199
|
__slots__ = ()
|
|
1200
1200
|
|
|
1201
1201
|
|
|
1202
|
-
class area(
|
|
1202
|
+
class area(TagWithProps):
|
|
1203
1203
|
"""
|
|
1204
1204
|
`<area>` element.
|
|
1205
1205
|
|
htmy/io.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from anyio import open_file as open_file
|
htmy/md/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .core import MD as MD
|
|
2
|
+
from .core import MarkdownParser as MarkdownParser
|
|
3
|
+
from .typing import MarkdownMetadataDict as MarkdownMetadataDict
|
|
4
|
+
from .typing import MarkdownParserFunction as MarkdownParserFunction
|
|
5
|
+
from .typing import MarkdownRenderFunction as MarkdownRenderFunction
|
|
6
|
+
from .typing import ParsedMarkdown as ParsedMarkdown
|
htmy/md/core.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
4
|
+
|
|
5
|
+
from markdown import Markdown
|
|
6
|
+
|
|
7
|
+
from htmy.core import ContextAware, SafeStr, Snippet, Text
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from htmy.typing import Component, Context
|
|
14
|
+
|
|
15
|
+
from .typing import MarkdownParserFunction, MarkdownRenderFunction, ParsedMarkdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MarkdownParser(ContextAware):
|
|
19
|
+
"""
|
|
20
|
+
Context-aware markdown parser.
|
|
21
|
+
|
|
22
|
+
By default, this class uses the `markdown` library with a sensible set of
|
|
23
|
+
[extensions](https://python-markdown.github.io/extensions/) including code highlighing.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("_md",)
|
|
27
|
+
|
|
28
|
+
_default: ClassVar[MarkdownParser | None] = None
|
|
29
|
+
"""The default instance or `None` if one hasn't been created already."""
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def default(cls) -> MarkdownParser:
|
|
33
|
+
"""
|
|
34
|
+
Returns the default instance.
|
|
35
|
+
"""
|
|
36
|
+
if cls._default is None:
|
|
37
|
+
cls._default = MarkdownParser()
|
|
38
|
+
|
|
39
|
+
return cls._default
|
|
40
|
+
|
|
41
|
+
def __init__(self, md: MarkdownParserFunction | None = None) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Initialization.
|
|
44
|
+
|
|
45
|
+
Arguments:
|
|
46
|
+
md: The parser function to use.
|
|
47
|
+
"""
|
|
48
|
+
super().__init__()
|
|
49
|
+
self._md = md
|
|
50
|
+
|
|
51
|
+
def parse(self, text: str) -> ParsedMarkdown:
|
|
52
|
+
"""
|
|
53
|
+
Returns the markdown data by parsing the given text.
|
|
54
|
+
"""
|
|
55
|
+
md = self._md
|
|
56
|
+
if md is None:
|
|
57
|
+
md = self._default_md()
|
|
58
|
+
self._md = md
|
|
59
|
+
|
|
60
|
+
return md(text)
|
|
61
|
+
|
|
62
|
+
def _default_md(self) -> MarkdownParserFunction:
|
|
63
|
+
"""
|
|
64
|
+
Function that creates the default markdown parser.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The default parser function.
|
|
68
|
+
"""
|
|
69
|
+
md = Markdown(extensions=("extra", "meta", "codehilite"))
|
|
70
|
+
|
|
71
|
+
def parse(text: str) -> ParsedMarkdown:
|
|
72
|
+
md.reset()
|
|
73
|
+
parsed = md.convert(text)
|
|
74
|
+
return {"content": parsed, "metadata": getattr(md, "Meta", None)}
|
|
75
|
+
|
|
76
|
+
return parse
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MD(Snippet):
|
|
80
|
+
"""Component for reading, customizing, and rendering markdown documents."""
|
|
81
|
+
|
|
82
|
+
__slots__ = (
|
|
83
|
+
"_converter",
|
|
84
|
+
"_renderer",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
path_or_text: Text | str | Path,
|
|
90
|
+
*,
|
|
91
|
+
converter: Callable[[str], Component] | None = None,
|
|
92
|
+
renderer: MarkdownRenderFunction | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Initialization.
|
|
96
|
+
|
|
97
|
+
Arguments:
|
|
98
|
+
path_or_text: The path where the markdown file is located or a markdown `Text`.
|
|
99
|
+
converter: Function that converts an HTML string (the parsed and processed markdown text)
|
|
100
|
+
into an HTMY component.
|
|
101
|
+
renderer: Function that get the parsed and converted content and the metadata (if it exists)
|
|
102
|
+
and turns them into an HTMY component.
|
|
103
|
+
"""
|
|
104
|
+
super().__init__(path_or_text)
|
|
105
|
+
self._converter: Callable[[str], Component] = SafeStr if converter is None else converter
|
|
106
|
+
self._renderer = renderer
|
|
107
|
+
|
|
108
|
+
def _render_text(self, text: str, context: Context) -> Component:
|
|
109
|
+
md = MarkdownParser.from_context(context, MarkdownParser.default()).parse(text)
|
|
110
|
+
result = self._converter(md["content"])
|
|
111
|
+
return result if self._renderer is None else self._renderer(result, md.get("metadata", None))
|
htmy/md/typing.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, NotRequired, TypeAlias, TypedDict
|
|
3
|
+
|
|
4
|
+
from htmy.typing import Component
|
|
5
|
+
|
|
6
|
+
MarkdownMetadataDict: TypeAlias = dict[str, Any]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ParsedMarkdown(TypedDict):
|
|
10
|
+
"""Type definition for parsed markdown data."""
|
|
11
|
+
|
|
12
|
+
content: str
|
|
13
|
+
metadata: NotRequired[MarkdownMetadataDict | None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MarkdownParserFunction: TypeAlias = Callable[[str], ParsedMarkdown]
|
|
17
|
+
"""Callable that converts a markdown string into a `ParsedMarkdown` object."""
|
|
18
|
+
|
|
19
|
+
MarkdownRenderFunction: TypeAlias = Callable[[Component, MarkdownMetadataDict | None], Component]
|
|
20
|
+
"""Renderer function definition for markdown data."""
|
htmy/typing.py
CHANGED
|
@@ -30,14 +30,18 @@ Context: TypeAlias = Mapping[ContextKey, ContextValue]
|
|
|
30
30
|
class SyncComponent(Protocol):
|
|
31
31
|
"""Protocol definition for sync `htmy` components."""
|
|
32
32
|
|
|
33
|
-
def htmy(self, context: Context, /) -> "Component":
|
|
33
|
+
def htmy(self, context: Context, /) -> "Component":
|
|
34
|
+
"""Renders the component."""
|
|
35
|
+
...
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
@runtime_checkable
|
|
37
39
|
class AsyncComponent(Protocol):
|
|
38
40
|
"""Protocol definition for async `htmy` components."""
|
|
39
41
|
|
|
40
|
-
async def htmy(self, context: Context, /) -> "Component":
|
|
42
|
+
async def htmy(self, context: Context, /) -> "Component":
|
|
43
|
+
"""Renders the component."""
|
|
44
|
+
...
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
HTMYComponentType: TypeAlias = SyncComponent | AsyncComponent
|
|
@@ -75,14 +79,18 @@ FunctionComponent: TypeAlias = SyncFunctionComponent[T] | AsyncFunctionComponent
|
|
|
75
79
|
class SyncContextProvider(Protocol):
|
|
76
80
|
"""Protocol definition for sync context providers."""
|
|
77
81
|
|
|
78
|
-
def htmy_context(self) -> Context:
|
|
82
|
+
def htmy_context(self) -> Context:
|
|
83
|
+
"""Returns an HTMY context for child rendering."""
|
|
84
|
+
...
|
|
79
85
|
|
|
80
86
|
|
|
81
87
|
@runtime_checkable
|
|
82
88
|
class AsyncContextProvider(Protocol):
|
|
83
89
|
"""Protocol definition for async context providers."""
|
|
84
90
|
|
|
85
|
-
async def htmy_context(self) -> Context:
|
|
91
|
+
async def htmy_context(self) -> Context:
|
|
92
|
+
"""Returns an HTMY context for child rendering."""
|
|
93
|
+
...
|
|
86
94
|
|
|
87
95
|
|
|
88
96
|
ContextProvider: TypeAlias = SyncContextProvider | AsyncContextProvider
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: htmy
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Async,
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async, pure-Python rendering engine.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Peter Volf
|
|
7
7
|
Author-email: do.volfp@gmail.com
|
|
@@ -10,6 +10,9 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: anyio (>=4.6.2.post1,<5.0.0)
|
|
15
|
+
Requires-Dist: markdown (>=3.7,<4.0)
|
|
13
16
|
Description-Content-Type: text/markdown
|
|
14
17
|
|
|
15
18
|

|
|
@@ -23,7 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
23
26
|
|
|
24
27
|
# `htmy`
|
|
25
28
|
|
|
26
|
-
**Async**, **
|
|
29
|
+
**Async**, **pure-Python** rendering engine.
|
|
27
30
|
|
|
28
31
|
## Key features
|
|
29
32
|
|
|
@@ -32,6 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
32
35
|
- Sync and async **function components** with **decorator syntax**.
|
|
33
36
|
- All baseline **HTML** tags built-in.
|
|
34
37
|
- Built-in, easy to use `ErrorBoundary` component for graceful error handling.
|
|
38
|
+
- **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
|
|
35
39
|
- Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
|
|
36
40
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
37
41
|
- **Fully-typed**.
|
|
@@ -150,11 +154,14 @@ user_table = html.table(
|
|
|
150
154
|
|
|
151
155
|
### Built-in components
|
|
152
156
|
|
|
153
|
-
`htmy` has a rich set of built-in
|
|
157
|
+
`htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
|
|
154
158
|
|
|
155
159
|
- `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
|
|
156
|
-
- `BaseTag`, `TagWithProps`, `
|
|
160
|
+
- `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
|
|
157
161
|
- `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
|
|
162
|
+
- `Snippet`: utility class for loading and customizing document snippets from the file system.
|
|
163
|
+
- `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
|
|
164
|
+
- `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
|
|
158
165
|
|
|
159
166
|
### Rendering
|
|
160
167
|
|
|
@@ -297,7 +304,10 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
|
|
|
297
304
|
|
|
298
305
|
## Dependencies
|
|
299
306
|
|
|
300
|
-
The library
|
|
307
|
+
The library aims to minimze its dependencies. Currently the following dependencies are required:
|
|
308
|
+
|
|
309
|
+
- `anyio`: for async file operations and networking.
|
|
310
|
+
- `markdown`: for markdown parsing.
|
|
301
311
|
|
|
302
312
|
## Development
|
|
303
313
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
htmy/__init__.py,sha256=_hffMH5lgK2E8cV62Fr90dF2fjfdjM5WMPgWMe9EP4M,1782
|
|
2
|
+
htmy/core.py,sha256=9BvzVMc3ZL94It6Yw4qxodbxLvw_PolJO1uzzxW4Rbo,18562
|
|
3
|
+
htmy/etree.py,sha256=zZkKY82t5fX85unS9oHuG6KEBsJY_iz6E7SJto8lSVQ,3097
|
|
4
|
+
htmy/html.py,sha256=pxmE-KU5OgwNp6MyxOfdS0Ohpzu2RNYCeGGFlHLDGUM,20940
|
|
5
|
+
htmy/io.py,sha256=iebJOZp7L0kZ9SWdqMatKtW5VGRIkEd-eD0_vTAldH8,41
|
|
6
|
+
htmy/md/__init__.py,sha256=lxBJnYplkDuxYuiese6My9KYp1DeGdzo70iUdYTvMnE,334
|
|
7
|
+
htmy/md/core.py,sha256=-EKucDFKMUtGgs9k_q9134oXY2GXtdKX1KOJXG4YmKc,3342
|
|
8
|
+
htmy/md/typing.py,sha256=LbsoAJ8OnZod9UbFiNVvWAEyFDqGRKhGmRpLWqbi8VY,642
|
|
9
|
+
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
htmy/renderer.py,sha256=nGHbAmPqoC0VRku3HtakaiQr5_HkhwLuCAxgjy-aZoI,3539
|
|
11
|
+
htmy/typing.py,sha256=QdqbObdnrVvHjIpVlEH4ylZtvZCuOYU9XygF3oI1IQ8,2672
|
|
12
|
+
htmy/utils.py,sha256=7_CyA39l2m6jzDqparPKkKgRB2wiGuBZXbiPgiZOXKA,1093
|
|
13
|
+
htmy-0.2.0.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
|
|
14
|
+
htmy-0.2.0.dist-info/METADATA,sha256=DWm5jlr6uJ_GA9aYH5ENrjutPnv7p2hA3mZ529IylmA,15971
|
|
15
|
+
htmy-0.2.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
16
|
+
htmy-0.2.0.dist-info/RECORD,,
|
htmy-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
htmy/__init__.py,sha256=EiVFNXpgdN5jeb0l2zH2u2Fl33i_dsvoXu6q7GLFLJw,1718
|
|
2
|
-
htmy/core.py,sha256=zguaJ9zC2nEt_AURnTMJKFlpBvnpRlDpp86PlgGOW_8,15586
|
|
3
|
-
htmy/html.py,sha256=ShXj5-kPA26ZikIhvom1R4auv3TGvTbDnZolLT-TYs4,20936
|
|
4
|
-
htmy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
htmy/renderer.py,sha256=nGHbAmPqoC0VRku3HtakaiQr5_HkhwLuCAxgjy-aZoI,3539
|
|
6
|
-
htmy/typing.py,sha256=neCEmfzlvPjlfOE2Dj5jHujz8c_70O-hzwTae2s7aAo,2448
|
|
7
|
-
htmy/utils.py,sha256=7_CyA39l2m6jzDqparPKkKgRB2wiGuBZXbiPgiZOXKA,1093
|
|
8
|
-
htmy-0.1.0.dist-info/LICENSE,sha256=rFtoGU_3c_rlacXgOZapTHfMErN-JFPT5Bq_col4bqI,1067
|
|
9
|
-
htmy-0.1.0.dist-info/METADATA,sha256=IVMzWa9d21GdO48UYOgcz3fvZCee0z3Es1f3JwT8DYU,15299
|
|
10
|
-
htmy-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
11
|
-
htmy-0.1.0.dist-info/RECORD,,
|
|
File without changes
|