htmy 0.1.0__tar.gz → 0.3.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.
- htmy-0.1.0/README.md → htmy-0.3.0/PKG-INFO +34 -4
- htmy-0.1.0/PKG-INFO → htmy-0.3.0/README.md +15 -19
- {htmy-0.1.0 → htmy-0.3.0}/htmy/__init__.py +3 -1
- {htmy-0.1.0 → htmy-0.3.0}/htmy/core.py +128 -23
- htmy-0.3.0/htmy/etree.py +90 -0
- {htmy-0.1.0 → htmy-0.3.0}/htmy/html.py +18 -18
- htmy-0.3.0/htmy/i18n.py +165 -0
- htmy-0.3.0/htmy/io.py +1 -0
- htmy-0.3.0/htmy/md/__init__.py +6 -0
- htmy-0.3.0/htmy/md/core.py +111 -0
- htmy-0.3.0/htmy/md/typing.py +20 -0
- {htmy-0.1.0 → htmy-0.3.0}/htmy/typing.py +12 -4
- {htmy-0.1.0 → htmy-0.3.0}/pyproject.toml +10 -3
- {htmy-0.1.0 → htmy-0.3.0}/LICENSE +0 -0
- {htmy-0.1.0 → htmy-0.3.0}/htmy/py.typed +0 -0
- {htmy-0.1.0 → htmy-0.3.0}/htmy/renderer.py +0 -0
- {htmy-0.1.0 → htmy-0.3.0}/htmy/utils.py +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: htmy
|
|
3
|
+
Version: 0.3.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: async-lru (>=2.0.4,<3.0.0)
|
|
16
|
+
Requires-Dist: markdown (>=3.7,<4.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
1
19
|

|
|
2
20
|

|
|
3
21
|

|
|
@@ -9,7 +27,7 @@
|
|
|
9
27
|
|
|
10
28
|
# `htmy`
|
|
11
29
|
|
|
12
|
-
**Async**, **
|
|
30
|
+
**Async**, **pure-Python** rendering engine.
|
|
13
31
|
|
|
14
32
|
## Key features
|
|
15
33
|
|
|
@@ -17,7 +35,10 @@
|
|
|
17
35
|
- **Powerful**, React-like **context support**, so you can avoid prop-drilling.
|
|
18
36
|
- Sync and async **function components** with **decorator syntax**.
|
|
19
37
|
- All baseline **HTML** tags built-in.
|
|
38
|
+
- **Markdown** support with tools for customization.
|
|
39
|
+
- Async, JSON based **internationalization**.
|
|
20
40
|
- Built-in, easy to use `ErrorBoundary` component for graceful error handling.
|
|
41
|
+
- **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
|
|
21
42
|
- Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
|
|
22
43
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
23
44
|
- **Fully-typed**.
|
|
@@ -136,11 +157,15 @@ user_table = html.table(
|
|
|
136
157
|
|
|
137
158
|
### Built-in components
|
|
138
159
|
|
|
139
|
-
`htmy` has a rich set of built-in
|
|
160
|
+
`htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
|
|
140
161
|
|
|
141
162
|
- `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
|
|
142
|
-
- `
|
|
163
|
+
- `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
|
|
164
|
+
- `i18n`: utilities for async, JSON based internationalization.
|
|
165
|
+
- `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
|
|
143
166
|
- `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
|
|
167
|
+
- `Snippet`: utility class for loading and customizing document snippets from the file system.
|
|
168
|
+
- `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
|
|
144
169
|
|
|
145
170
|
### Rendering
|
|
146
171
|
|
|
@@ -283,7 +308,11 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
|
|
|
283
308
|
|
|
284
309
|
## Dependencies
|
|
285
310
|
|
|
286
|
-
The library
|
|
311
|
+
The library aims to minimze its dependencies. Currently the following dependencies are required:
|
|
312
|
+
|
|
313
|
+
- `anyio`: for async file operations and networking.
|
|
314
|
+
- `async-lru`: for async caching.
|
|
315
|
+
- `markdown`: for markdown parsing.
|
|
287
316
|
|
|
288
317
|
## Development
|
|
289
318
|
|
|
@@ -298,3 +327,4 @@ All contributions are welcome, including more documentation, examples, code, and
|
|
|
298
327
|
## License - MIT
|
|
299
328
|
|
|
300
329
|
The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
|
|
330
|
+
|
|
@@ -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
|

|
|
16
2
|

|
|
17
3
|

|
|
@@ -23,7 +9,7 @@ Description-Content-Type: text/markdown
|
|
|
23
9
|
|
|
24
10
|
# `htmy`
|
|
25
11
|
|
|
26
|
-
**Async**, **
|
|
12
|
+
**Async**, **pure-Python** rendering engine.
|
|
27
13
|
|
|
28
14
|
## Key features
|
|
29
15
|
|
|
@@ -31,7 +17,10 @@ Description-Content-Type: text/markdown
|
|
|
31
17
|
- **Powerful**, React-like **context support**, so you can avoid prop-drilling.
|
|
32
18
|
- Sync and async **function components** with **decorator syntax**.
|
|
33
19
|
- All baseline **HTML** tags built-in.
|
|
20
|
+
- **Markdown** support with tools for customization.
|
|
21
|
+
- Async, JSON based **internationalization**.
|
|
34
22
|
- Built-in, easy to use `ErrorBoundary` component for graceful error handling.
|
|
23
|
+
- **Unopinionated**: use the backend, CSS, and JS frameworks of your choice, the way you want to use them.
|
|
35
24
|
- Everything is **easily customizable**, from the rendering engine to components, formatting and context management.
|
|
36
25
|
- Automatic and customizable **property-name conversion** from snake case to kebab case.
|
|
37
26
|
- **Fully-typed**.
|
|
@@ -150,11 +139,15 @@ user_table = html.table(
|
|
|
150
139
|
|
|
151
140
|
### Built-in components
|
|
152
141
|
|
|
153
|
-
`htmy` has a rich set of built-in
|
|
142
|
+
`htmy` has a rich set of built-in utilities and components for both HTML and other use-cases:
|
|
154
143
|
|
|
155
144
|
- `html` module: a complete set of [baseline HTML tags](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
|
|
156
|
-
- `
|
|
145
|
+
- `md`: `MarkdownParser` utility and `MD` component for loading, parsing, converting, and rendering markdown content.
|
|
146
|
+
- `i18n`: utilities for async, JSON based internationalization.
|
|
147
|
+
- `BaseTag`, `TagWithProps`, `Tag`, `WildcardTag`: base classes for custom XML tags.
|
|
157
148
|
- `ErrorBoundary`, `Fragment`, `SafeStr`, `WithContext`: utilities for error handling, component wrappers, context providers, and formatting.
|
|
149
|
+
- `Snippet`: utility class for loading and customizing document snippets from the file system.
|
|
150
|
+
- `etree.ETreeConverter`: utility that converts XML to a component tree with support for custom HTMY components.
|
|
158
151
|
|
|
159
152
|
### Rendering
|
|
160
153
|
|
|
@@ -297,7 +290,11 @@ The primary aim of `htmy` is to be an **async**, pure-Python rendering engine, w
|
|
|
297
290
|
|
|
298
291
|
## Dependencies
|
|
299
292
|
|
|
300
|
-
The library
|
|
293
|
+
The library aims to minimze its dependencies. Currently the following dependencies are required:
|
|
294
|
+
|
|
295
|
+
- `anyio`: for async file operations and networking.
|
|
296
|
+
- `async-lru`: for async caching.
|
|
297
|
+
- `markdown`: for markdown parsing.
|
|
301
298
|
|
|
302
299
|
## Development
|
|
303
300
|
|
|
@@ -312,4 +309,3 @@ All contributions are welcome, including more documentation, examples, code, and
|
|
|
312
309
|
## License - MIT
|
|
313
310
|
|
|
314
311
|
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
|
|
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
|
-
|
|
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-0.3.0/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
|
|
@@ -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-0.3.0/htmy/i18n.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, ClassVar, overload
|
|
5
|
+
|
|
6
|
+
from async_lru import alru_cache
|
|
7
|
+
|
|
8
|
+
from .core import ContextAware
|
|
9
|
+
from .io import open_file
|
|
10
|
+
|
|
11
|
+
TranslationResource = Mapping[str, Any]
|
|
12
|
+
"""Translation resource type."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class I18nError(Exception): ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class I18nKeyError(I18nError): ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class I18nValueError(I18nError): ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class I18n(ContextAware):
|
|
25
|
+
"""
|
|
26
|
+
Context-aware async internationalization utility.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__slots__ = ("_path", "_fallback")
|
|
30
|
+
|
|
31
|
+
_root_keys: ClassVar[frozenset[str]] = frozenset(("", "."))
|
|
32
|
+
"""Special keys that represent the "root" object, i.e. the entire translation resource file."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, path: str | Path, fallback: str | Path | None = None) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Initialization.
|
|
37
|
+
|
|
38
|
+
Arguments:
|
|
39
|
+
path: Path to the root directory that contains the translation resources.
|
|
40
|
+
fallback: Optional fallback path to use if `path` doesn't contain the required resources.
|
|
41
|
+
"""
|
|
42
|
+
self._path: Path = Path(path) if isinstance(path, str) else path
|
|
43
|
+
self._fallback: Path | None = Path(fallback) if isinstance(fallback, str) else fallback
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
async def get(self, dotted_path: str, key: str) -> Any: ...
|
|
47
|
+
|
|
48
|
+
@overload
|
|
49
|
+
async def get(self, dotted_path: str, key: str, **kwargs: Any) -> str: ...
|
|
50
|
+
|
|
51
|
+
async def get(self, dotted_path: str, key: str, **kwargs: Any) -> Any:
|
|
52
|
+
"""
|
|
53
|
+
Returns the translation resource at the given location.
|
|
54
|
+
|
|
55
|
+
If keyword arguments are provided, it's expected that the referenced data
|
|
56
|
+
is a [format string](https://docs.python.org/3/library/string.html#formatstrings)
|
|
57
|
+
which can be fully formatted using the given keyword arguments.
|
|
58
|
+
|
|
59
|
+
Arguments:
|
|
60
|
+
dotted_path: A package-like (dot separated) path to the file that contains
|
|
61
|
+
the required translation resource, relative to `path`.
|
|
62
|
+
key: The key in the translation resource whose value is requested. Use dots to reference
|
|
63
|
+
embedded attributes.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The loaded value.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
I18nError: If the given translation resource is not found or invalid.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return await self._resolve(self._path, dotted_path, key, **kwargs)
|
|
73
|
+
except I18nError:
|
|
74
|
+
if self._fallback is None:
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
return await self._resolve(self._fallback, dotted_path, key, **kwargs)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
async def _resolve(cls, root: Path, dotted_subpath: str, key: str, **kwargs: Any) -> Any:
|
|
81
|
+
"""
|
|
82
|
+
Resolves the given translation resource.
|
|
83
|
+
|
|
84
|
+
Arguments:
|
|
85
|
+
root: The root path to use.
|
|
86
|
+
dotted_subpath: Subpath under `root` with dots as separators.
|
|
87
|
+
key: The key in the translation resource.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The resolved translation resource.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
I18nKeyError: If the translation resource doesn't contain the requested key.
|
|
94
|
+
I18nValueError: If the translation resource is not found or its content is invalid.
|
|
95
|
+
"""
|
|
96
|
+
result = await load_translation_resource(resolve_json_path(root, dotted_subpath))
|
|
97
|
+
if key in cls._root_keys:
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
for k in key.split("."):
|
|
101
|
+
try:
|
|
102
|
+
result = result[k]
|
|
103
|
+
except KeyError as e:
|
|
104
|
+
raise I18nKeyError(f"Key not found: {key}") from e
|
|
105
|
+
|
|
106
|
+
if len(kwargs) > 0:
|
|
107
|
+
if not isinstance(result, str):
|
|
108
|
+
raise I18nValueError("Formatting is only supported for strings.")
|
|
109
|
+
|
|
110
|
+
return result.format(**kwargs)
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@alru_cache(8)
|
|
116
|
+
async def load_translation_resource(path: Path) -> TranslationResource:
|
|
117
|
+
"""
|
|
118
|
+
Loads the translation resource from the given path.
|
|
119
|
+
|
|
120
|
+
Arguments:
|
|
121
|
+
path: The path of the translation resource to load.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The loaded translation resource.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
I18nValueError: If the translation resource is not a JSON dict.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
async with await open_file(path, "r") as f:
|
|
132
|
+
content = await f.read()
|
|
133
|
+
except FileNotFoundError as e:
|
|
134
|
+
raise I18nValueError(f"Translation resource not found: {str(path)}") from e
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
result = json.loads(content)
|
|
138
|
+
except json.JSONDecodeError as e:
|
|
139
|
+
raise I18nValueError("Translation resource decoding failed.") from e
|
|
140
|
+
|
|
141
|
+
if isinstance(result, dict):
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
raise I18nValueError("Only dict translation resources are allowed.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def resolve_json_path(root: Path, dotted_subpath: str) -> Path:
|
|
148
|
+
"""
|
|
149
|
+
Resolves the given dotted subpath relative to root.
|
|
150
|
+
|
|
151
|
+
Arguments:
|
|
152
|
+
root: The root path.
|
|
153
|
+
dotted_subpath: Subpath under `root` with dots as separators.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The resolved path.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
I18nValueError: If the given dotted path is invalid.
|
|
160
|
+
"""
|
|
161
|
+
*dirs, name = dotted_subpath.split(".")
|
|
162
|
+
if not name:
|
|
163
|
+
raise I18nValueError("Invalid path.")
|
|
164
|
+
|
|
165
|
+
return root / Path(*dirs) / f"{name}.json"
|
htmy-0.3.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,16 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "htmy"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Async,
|
|
3
|
+
version = "0.3.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
|
+
async-lru = "^2.0.4"
|
|
13
|
+
markdown = "^3.7"
|
|
11
14
|
|
|
12
15
|
[tool.poetry.group.dev.dependencies]
|
|
13
16
|
mkdocs-material = "^9.5.39"
|
|
@@ -18,6 +21,7 @@ pytest = "^8.3.3"
|
|
|
18
21
|
pytest-asyncio = "^0.24.0"
|
|
19
22
|
pytest-random-order = "^1.1.1"
|
|
20
23
|
ruff = "^0.6.8"
|
|
24
|
+
types-markdown = "^3.7.0.20240822"
|
|
21
25
|
|
|
22
26
|
[build-system]
|
|
23
27
|
requires = ["poetry-core"]
|
|
@@ -27,6 +31,9 @@ build-backend = "poetry.core.masonry.api"
|
|
|
27
31
|
strict = true
|
|
28
32
|
show_error_codes = true
|
|
29
33
|
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
addopts = "--random-order"
|
|
36
|
+
|
|
30
37
|
[tool.ruff]
|
|
31
38
|
line-length = 108
|
|
32
39
|
exclude = [
|
|
@@ -57,7 +64,7 @@ format = "ruff format ."
|
|
|
57
64
|
lint = "ruff check ."
|
|
58
65
|
lint-fix = "ruff . --fix"
|
|
59
66
|
mypy = "mypy ."
|
|
60
|
-
test = "python -m pytest tests
|
|
67
|
+
test = "python -m pytest tests"
|
|
61
68
|
static-checks.sequence = ["lint", "check-format", "mypy"]
|
|
62
69
|
static-checks.ignore_fail = "return_non_zero"
|
|
63
70
|
serve-docs = "mkdocs serve"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|