Simple HTML Generation https://minihtml.trendels.name/
1from contextvars import ContextVar
2from textwrap import dedent
3
4from minihtml import (
5 Component,
6 Element,
7 Slots,
8 component,
9 component_scripts,
10 component_styles,
11 template,
12 text,
13)
14from minihtml.tags import body, div, head, html, main, script, style, title
15
16
17def test_template_renders_as_html_with_doctype_and_trailing_newline():
18 @template()
19 def my_template(message: str) -> Element:
20 return div(message)
21
22 assert my_template("hello").render() == dedent("""\
23 <!doctype html>
24 <div>hello</div>
25 """)
26
27
28def test_template_can_disable_doctype():
29 @template()
30 def my_template(message: str) -> Element:
31 return div(message)
32
33 assert my_template("hello").render(doctype=False) == "<div>hello</div>\n"
34
35
36def test_template_with_layout_component():
37 @component(slots=["title", "content"], default="content")
38 def my_layout(slots: Slots) -> Element:
39 with html as elem:
40 with head, title:
41 slots.slot("title")
42 with body:
43 with div["#content"]:
44 slots.slot()
45
46 return elem
47
48 @template(layout=my_layout)
49 def my_template(layout: Component, message: str) -> None:
50 with layout.slot("title"):
51 text("my title")
52 div(message)
53
54 assert my_template("hello").render() == dedent("""\
55 <!doctype html>
56 <html>
57 <head>
58 <title>my title</title>
59 </head>
60 <body>
61 <div id="content">
62 <div>hello</div>
63 </div>
64 </body>
65 </html>
66 """)
67
68 # Test that layout is not cached
69 assert my_template("goodbye").render() == dedent("""\
70 <!doctype html>
71 <html>
72 <head>
73 <title>my title</title>
74 </head>
75 <body>
76 <div id="content">
77 <div>goodbye</div>
78 </div>
79 </body>
80 </html>
81 """)
82
83
84def test_template_collects_and_deduplicates_component_styles_and_scripts():
85 @component(
86 style=style(".my-component { background: #ccc }"),
87 script=[
88 script("// 1st script goes here"),
89 script("// 2nd script goes here"),
90 ],
91 )
92 def my_component(slots: Slots) -> Element:
93 return div["my-component"]
94
95 @component(
96 style=style("main { background: #eee }"),
97 script=script("// layout script goes here"),
98 )
99 def my_layout(slots: Slots) -> Element:
100 with html as elem:
101 with head:
102 component_styles()
103 with body:
104 with main:
105 slots.slot()
106 component_scripts()
107
108 return elem
109
110 @template(layout=my_layout)
111 def my_template(layout: Component) -> None:
112 my_component()
113 my_component()
114
115 assert my_template().render() == dedent("""\
116 <!doctype html>
117 <html>
118 <head>
119 <style>main { background: #eee }</style>
120 <style>.my-component { background: #ccc }</style>
121 </head>
122 <body>
123 <main>
124 <div class="my-component"></div>
125 <div class="my-component"></div>
126 </main>
127 <script>// layout script goes here</script>
128 <script>// 1st script goes here</script>
129 <script>// 2nd script goes here</script>
130 </body>
131 </html>
132 """)
133
134
135def test_passing_component_styles_and_scripts_as_arguments():
136 @component(
137 style=style(".my-component { background: #ccc }"),
138 script=[
139 script("// 1st script goes here"),
140 script("// 2nd script goes here"),
141 ],
142 )
143 def my_component(slots: Slots) -> Element:
144 return div["my-component"]
145
146 @template()
147 def my_template() -> Element:
148 return html(
149 head(
150 component_styles(),
151 ),
152 body(
153 my_component(),
154 my_component(),
155 component_scripts(),
156 ),
157 )
158
159 assert my_template().render() == dedent("""\
160 <!doctype html>
161 <html>
162 <head>
163 <style>.my-component { background: #ccc }</style>
164 </head>
165 <body>
166 <div class="my-component"></div>
167 <div class="my-component"></div>
168 <script>// 1st script goes here</script>
169 <script>// 2nd script goes here</script>
170 </body>
171 </html>
172 """)
173
174
175name_context = ContextVar[str]("name")
176
177
178def test_template_is_rendered_lazily():
179 @template()
180 def my_template() -> Element:
181 return div(name_context.get())
182
183 t = my_template()
184
185 name_context.set("fred")
186 assert t.render(doctype=False) == "<div>fred</div>\n"
187
188 name_context.set("barney")
189 assert t.render(doctype=False) == "<div>barney</div>\n"