Coverage for src/minihtml/_template.py: 100%
64 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-06 18:07 +0200
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-06 18:07 +0200
1import io
2from collections.abc import Iterable
3from functools import wraps
4from typing import Callable, Concatenate, ParamSpec, TextIO, TypeAlias, overload
6from ._component import Component, ComponentWrapper
7from ._core import HasNodes, Node, iter_nodes, register_with_context
8from ._template_context import get_template_context, template_context
10P = ParamSpec("P")
12TemplateImpl: TypeAlias = Callable[P, Node | HasNodes]
13TemplateImplLayout: TypeAlias = Callable[Concatenate[Component, P], None]
16class Template:
17 """
18 The result of calling a function decorated with :deco:`template`.
19 """
21 def __init__(self, callback: Callable[[], list[Node]]):
22 self._callback = callback
24 def render(self, *, doctype: bool = True) -> str:
25 """
26 Render the template and return a string.
28 Args:
29 doctype: Whether or not to prepend the doctype declaration
30 ``<!doctype html>`` to the output.
31 """
32 nodes = self._callback()
33 buf = io.StringIO()
34 if doctype:
35 buf.write("<!doctype html>\n")
36 Node.render_list(buf, nodes)
37 buf.write("\n")
38 return buf.getvalue()
41@overload
42def template() -> Callable[[TemplateImpl[P]], Callable[P, Template]]: ...
45@overload
46def template(
47 layout: ComponentWrapper[...],
48) -> Callable[[TemplateImplLayout[P]], Callable[P, Template]]: ...
51def template(
52 layout: ComponentWrapper[...] | None = None,
53) -> (
54 Callable[[TemplateImpl[P]], Callable[P, Template]]
55 | Callable[[TemplateImplLayout[P]], Callable[P, Template]]
56):
57 """
58 Decorator to create a template.
60 Args:
61 layout: A component to use as the layout for the template. The layout
62 must have a default slot.
64 When ``layout`` is used, the decorated function will be executed within the
65 context of the layout component when the template is rendered. All elements
66 created in the function body will be added to the default slot of the
67 component. The function will receive the component as the first positional
68 argument, and should return `None`.
70 When ``layout`` is not used, the function should return the content to be
71 rendered.
73 The template will collect and deduplicate the style and script nodes of all
74 components used within the template (including the layout component). These
75 nodes can be inserted into the document by using the
76 :func:`component_styles()` and :func:`component_scripts()` placeholders.
77 """
78 if layout is None:
80 def plain_decorator(fn: TemplateImpl[P]) -> Callable[P, Template]:
81 @wraps(fn)
82 def wrapper(*args: P.args, **kwargs: P.kwargs) -> Template:
83 def callback() -> list[Node]:
84 with template_context():
85 result = fn(*args, **kwargs)
86 return list(iter_nodes([result]))
88 return Template(callback)
90 return wrapper
92 return plain_decorator
94 else:
96 def layout_decorator(fn: TemplateImplLayout[P]) -> Callable[P, Template]:
97 @wraps(fn)
98 def wrapper(*args: P.args, **kwargs: P.kwargs) -> Template:
99 def callback() -> list[Node]:
100 with template_context():
101 with layout() as result:
102 fn(result, *args, **kwargs)
103 return list(iter_nodes([result]))
105 return Template(callback)
107 return wrapper
109 return layout_decorator
112class ResourceWrapper(Node):
113 def __init__(self, nodes: Iterable[Node]):
114 self._nodes = nodes
115 self._inline = False
117 def write(self, f: TextIO, indent: int = 0) -> None:
118 nodes = list(self._nodes)
119 n = len(nodes)
120 for i, node in enumerate(nodes):
121 node.write(f, indent)
122 if i < n - 1:
123 f.write("\n")
124 f.write(" " * indent)
127def component_styles() -> ResourceWrapper:
128 """
129 Placeholder element for component styles.
131 Can only be used in code called from a function decorated with
132 :deco:`template`. Inserts the style nodes collected from all components
133 used in the current template.
134 """
135 wrapper = ResourceWrapper(get_template_context().styles)
136 register_with_context(wrapper)
137 return wrapper
140def component_scripts() -> ResourceWrapper:
141 """
142 Placeholder element for component scripts.
144 Can only be used in code called from a function decorated with
145 :deco:`template`. Inserts the script nodes collected from all components
146 used in the current template.
147 """
148 wrapper = ResourceWrapper(get_template_context().scripts)
149 register_with_context(wrapper)
150 return wrapper