Simple HTML Generation https://minihtml.trendels.name/
1from textwrap import dedent
2
3from pytest import raises as assert_raises
4
5from minihtml import Element, Fragment, Slots, component, fragment
6from minihtml.tags import (
7 body,
8 div,
9 h2,
10 head,
11 html,
12 img,
13 main,
14 p,
15 script,
16 style,
17 title,
18)
19
20
21def test_basic_component():
22 @component()
23 def my_component(slots: Slots, name: str) -> Element:
24 return div(name=name)
25
26 with div["container"] as elem:
27 my_component(name="component-name")
28
29 assert str(elem) == dedent("""\
30 <div class="container">
31 <div name="component-name"></div>
32 </div>""")
33
34
35def test_component_slot():
36 @component()
37 def my_component(slots: Slots, name: str) -> Element:
38 with div(name=name) as elem:
39 slots.slot()
40 return elem
41
42 with div["container"] as elem:
43 with my_component(name="component-name"):
44 p("slot content")
45
46 assert str(elem) == dedent("""\
47 <div class="container">
48 <div name="component-name">
49 <p>slot content</p>
50 </div>
51 </div>""")
52
53
54def test_named_slots():
55 @component(slots=("head", "main"))
56 def my_component(slots: Slots) -> Element:
57 with html as elem:
58 with head:
59 slots.slot("head")
60 with body:
61 with main:
62 slots.slot("main")
63 return elem
64
65 with my_component() as comp:
66 with comp.slot("head"):
67 title("My website")
68 with comp.slot("main"):
69 p("My article")
70
71 assert str(comp) == dedent("""\
72 <html>
73 <head>
74 <title>My website</title>
75 </head>
76 <body>
77 <main>
78 <p>My article</p>
79 </main>
80 </body>
81 </html>""")
82
83
84def test_component_without_default_raises_if_default_slot_is_used():
85 @component(slots=["one"])
86 def my_component(slots: Slots) -> Element:
87 with div as elem:
88 slots.slot("one")
89 return elem
90
91 with assert_raises(KeyError):
92 with my_component():
93 p("default slot content")
94
95
96def test_named_slots_with_default():
97 @component(slots=("head", "main"), default="main")
98 def my_component(slots: Slots) -> Element:
99 with html as elem:
100 with head:
101 slots.slot("head")
102 with body:
103 with main:
104 slots.slot()
105 return elem
106
107 with my_component() as comp1:
108 with comp1.slot("head"):
109 title("My website")
110 p("My article")
111
112 assert str(comp1) == dedent("""\
113 <html>
114 <head>
115 <title>My website</title>
116 </head>
117 <body>
118 <main>
119 <p>My article</p>
120 </main>
121 </body>
122 </html>""")
123
124 with my_component() as comp2:
125 with comp2.slot("head"):
126 title("My website")
127 with comp2.slot("main"):
128 p("My article")
129
130 assert str(comp1) == str(comp2)
131
132
133def test_slot_is_filled():
134 @component(slots=("icon", "main"), default="main")
135 def my_component(slots: Slots) -> Element:
136 with div["my-component"] as elem:
137 if slots.is_filled("icon"):
138 with div["icon"]:
139 slots.slot("icon")
140 if slots.is_filled():
141 with div["main"]:
142 slots.slot()
143 return elem
144
145 with my_component() as comp1:
146 p("My article")
147
148 assert str(comp1) == dedent("""\
149 <div class="my-component">
150 <div class="main">
151 <p>My article</p>
152 </div>
153 </div>""")
154
155 with my_component() as comp2:
156 with comp2.slot("icon"):
157 img(src="icon.png")
158
159 assert str(comp2) == dedent("""\
160 <div class="my-component">
161 <div class="icon"><img src="icon.png"></div>
162 </div>""")
163
164
165def test_slots_with_default_content():
166 @component(slots=("title", "content"), default="content")
167 def my_component(slots: Slots) -> Fragment:
168 with fragment() as f:
169 with slots.slot("title"):
170 h2("Default title")
171 with slots.slot():
172 p("Default content")
173 return f
174
175 comp1 = my_component()
176
177 with my_component() as comp2:
178 with comp2.slot("title"):
179 h2("My title")
180 p("My content")
181
182 assert str(comp1) == dedent("""\
183 <h2>Default title</h2>
184 <p>Default content</p>""")
185
186 assert str(comp2) == dedent("""\
187 <h2>My title</h2>
188 <p>My content</p>""")
189
190
191def test_default_slot_must_be_a_valid_slot_name():
192 with assert_raises(ValueError, match="Can't set default without slots: 'x'"):
193 component(default="x")
194
195 with assert_raises(
196 ValueError, match="Invalid default: 'x'. Available slots: 'a', 'b'"
197 ):
198 component(slots=("a", "b"), default="x")
199
200
201def test_stringifying_component_has_no_side_effect():
202 @component()
203 def my_component(slots: Slots) -> Element:
204 with div["my-component"] as elem:
205 slots.slot()
206 return elem
207
208 c1 = my_component()
209
210 with div as elem:
211 str(c1)
212
213 assert str(elem) == "<div></div>"
214
215
216def test_nested_components():
217 @component()
218 def inner(slots: Slots) -> Element:
219 with div["inner"] as elem:
220 slots.slot()
221 return elem
222
223 @component()
224 def outer(slots: Slots) -> Element:
225 with div["outer"] as elem:
226 with inner():
227 slots.slot()
228 return elem
229
230 with div["container"] as elem:
231 with outer():
232 p("content")
233
234 assert str(elem) == dedent("""\
235 <div class="container">
236 <div class="outer">
237 <div class="inner">
238 <p>content</p>
239 </div>
240 </div>
241 </div>""")
242
243
244def test_component_style_and_script_has_no_effect_outside_of_template_context():
245 @component(
246 style=style(".my-component { background: #ccc }"),
247 script=script("// script goes here"),
248 )
249 def my_component(slots: Slots) -> Element:
250 return div["my-component"]
251
252 assert str(my_component()) == '<div class="my-component"></div>'
253
254
255def test_passing_component_as_argument():
256 @component()
257 def my_component(slots: Slots) -> Element:
258 return div["my-component"]
259
260 with div["#container"] as elem:
261 div(my_component())
262
263 assert str(elem) == dedent("""\
264 <div id="container">
265 <div>
266 <div class="my-component"></div>
267 </div>
268 </div>""")