htmy 0.1.0__tar.gz → 0.2.0__tar.gz

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.

@@ -1,3 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: htmy
3
+ Version: 0.2.0
4
+ Summary: Async, pure-Python rendering engine.
5
+ License: MIT
6
+ Author: Peter Volf
7
+ Author-email: do.volfp@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
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)
16
+ Description-Content-Type: text/markdown
17
+
1
18
  ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
2
19
  ![Linters](https://github.com/volfpeter/htmy/actions/workflows/linters.yml/badge.svg)
3
20
  ![Documentation](https://github.com/volfpeter/htmy/actions/workflows/build-docs.yml/badge.svg)
@@ -9,7 +26,7 @@
9
26
 
10
27
  # `htmy`
11
28
 
12
- **Async**, **zero-dependency**, **pure-Python** rendering engine.
29
+ **Async**, **pure-Python** rendering engine.
13
30
 
14
31
  ## Key features
15
32
 
@@ -18,6 +35,7 @@
18
35
  - Sync and async **function components** with **decorator syntax**.
19
36
  - All baseline **HTML** tags built-in.
20
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.
21
39
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
22
40
  - Automatic and customizable **property-name conversion** from snake case to kebab case.
23
41
  - **Fully-typed**.
@@ -136,11 +154,14 @@ user_table = html.table(
136
154
 
137
155
  ### Built-in components
138
156
 
139
- `htmy` has a rich set of built-in base and utility components for both HTML and other use-cases:
157
+ `htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
140
158
 
141
159
  - `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
142
- - `BaseTag`, `TagWithProps`, `StandaloneTag`, `Tag`: base classes for custom XML tags.
160
+ - `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
143
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.
144
165
 
145
166
  ### Rendering
146
167
 
@@ -283,7 +304,10 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
283
304
 
284
305
  ## Dependencies
285
306
 
286
- The library has **no dependencies**.
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.
287
311
 
288
312
  ## Development
289
313
 
@@ -298,3 +322,4 @@ All contributions are welcome, including more documentation, examples, code, and
298
322
  ## License - MIT
299
323
 
300
324
  The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
325
+
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: htmy
3
- Version: 0.1.0
4
- Summary: Async, zero-dependency, pure-Python rendering engine.
5
- License: MIT
6
- Author: Peter Volf
7
- Author-email: do.volfp@gmail.com
8
- Requires-Python: >=3.11,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Description-Content-Type: text/markdown
14
-
15
1
  ![Tests](https://github.com/volfpeter/htmy/actions/workflows/tests.yml/badge.svg)
16
2
  ![Linters](https://github.com/volfpeter/htmy/actions/workflows/linters.yml/badge.svg)
17
3
  ![Documentation](https://github.com/volfpeter/htmy/actions/workflows/build-docs.yml/badge.svg)
@@ -23,7 +9,7 @@ Description-Content-Type: text/markdown
23
9
 
24
10
  # `htmy`
25
11
 
26
- **Async**, **zero-dependency**, **pure-Python** rendering engine.
12
+ **Async**, **pure-Python** rendering engine.
27
13
 
28
14
  ## Key features
29
15
 
@@ -32,6 +18,7 @@ Description-Content-Type: text/markdown
32
18
  - Sync and async **function components** with **decorator syntax**.
33
19
  - All baseline **HTML** tags built-in.
34
20
  - Built-in, easy to use `ErrorBoundary` component for graceful error handling.
21
+ - **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
35
22
  - Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
36
23
  - Automatic and customizable **property-name conversion** from snake case to kebab case.
37
24
  - **Fully-typed**.
@@ -150,11 +137,14 @@ user_table = html.table(
150
137
 
151
138
  ### Built-in components
152
139
 
153
- `htmy` has a rich set of built-in base and utility components for both HTML and other use-cases:
140
+ `htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
154
141
 
155
142
  - `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
156
- - `BaseTag`, `TagWithProps`, `StandaloneTag`, `Tag`: base classes for custom XML tags.
143
+ - `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
157
144
  - `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
145
+ - `Snippet`: utility class for loading and customizing document snippets from the file system.
146
+ - `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
147
+ - `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
158
148
 
159
149
  ### Rendering
160
150
 
@@ -297,7 +287,10 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
297
287
 
298
288
  ## Dependencies
299
289
 
300
- The library has **no dependencies**.
290
+ The library aims to minimze its dependencies. Currently the following dependencies are required:
291
+
292
+ - `anyio`: for async file operations and networking.
293
+ - `markdown`: for markdown parsing.
301
294
 
302
295
  ## Development
303
296
 
@@ -312,4 +305,3 @@ All contributions are welcome, including more documentation, examples, code, and
312
305
  ## License - MIT
313
306
 
314
307
  The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
315
-
@@ -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 StandaloneTag as StandaloneTag
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
@@ -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
- return tuple(join_components(self._children, "\n"))
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
- __slot__ = ()
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 SafeStr(str):
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 _htmy_name(self) -> str:
444
- return type(self).__name__
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
- name = self._htmy_name
529
+ """Renders the component."""
530
+ name = self.htmy_name
461
531
  props = self._htmy_format_props(context=context)
462
- return (SafeStr(f"<{name} {props}>"), SafeStr(f"</{name}>"))
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
- self.props = props
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
- separator = self.tag_config.get("child_separator", None)
493
- opening, closing = cast(tuple[ComponentType, ComponentType], super().htmy(context))
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from .core import SafeStr, StandaloneTag, Tag, TagConfig, TagWithProps
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
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_(StandaloneTag):
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
- @property
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(StandaloneTag):
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
- @property
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
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(StandaloneTag):
1202
+ class area(TagWithProps):
1203
1203
  """
1204
1204
  `<area>` element.
1205
1205
 
htmy-0.2.0/htmy/io.py ADDED
@@ -0,0 +1 @@
1
+ from anyio import open_file as open_file
@@ -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
@@ -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))
@@ -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."""
@@ -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,13 +1,15 @@
1
1
  [tool.poetry]
2
2
  name = "htmy"
3
- version = "0.1.0"
4
- description = "Async, zero-dependency, pure-Python rendering engine."
3
+ version = "0.2.0"
4
+ description = "Async, pure-Python rendering engine."
5
5
  authors = ["Peter Volf <do.volfp@gmail.com>"]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
8
 
9
9
  [tool.poetry.dependencies]
10
10
  python = "^3.11"
11
+ anyio = "^4.6.2.post1"
12
+ markdown = "^3.7"
11
13
 
12
14
  [tool.poetry.group.dev.dependencies]
13
15
  mkdocs-material = "^9.5.39"
@@ -18,6 +20,7 @@ pytest = "^8.3.3"
18
20
  pytest-asyncio = "^0.24.0"
19
21
  pytest-random-order = "^1.1.1"
20
22
  ruff = "^0.6.8"
23
+ types-markdown = "^3.7.0.20240822"
21
24
 
22
25
  [build-system]
23
26
  requires = ["poetry-core"]
File without changes
File without changes
File without changes
File without changes