1 module elemi;
2 
3 public {
4 
5     import elemi.xml;
6     import elemi.html;
7     import elemi.element;
8     import elemi.generator;
9     import elemi.attribute;
10     import elemi.wrapper;
11 
12 }
13 
14 alias elem = elemi.html.elem;
15 alias add = elemi.html.add;
16 
17 
18 ///
19 pure @safe unittest {
20 
21     // Compile-time empty type detection
22     assert(elem!"input" == "<input/>");
23     assert(elem!"hr" == "<hr/>");
24     assert(elem!"p" == "<p></p>");
25 
26     // Content
27     assert(elem!"p"("Hello, World!") == "<p>Hello, World!</p>");
28 
29     // Compile-time attributes — variant A
30     assert(
31 
32         elem!("a", [ "href": "about:blank", "title": "Destroy this page" ])("Hello, World!")
33 
34         == `<a href="about:blank" title="Destroy this page">Hello, World!</a>`
35 
36     );
37 
38     // Compile-time attributes — variant B
39     assert(
40 
41         elem!("a", q{
42             href="about:blank"
43             title="Destroy this page" })(
44             "Hello, World!"
45         )
46         == `<a href="about:blank" title="Destroy this page">Hello, World!</a>`
47 
48     );
49 
50     // Nesting and input sanitization
51     assert(
52 
53         elem!"div"(
54             elem!"p"("Hello, World!"),
55             "-> Sanitized"
56         )
57 
58         == "<div><p>Hello, World!</p>-&gt; Sanitized</div>"
59 
60     );
61 
62     // Sanitized user input in attributes
63     assert(
64         elem!"input"(
65             attr("type") = "text",
66             attr("value") = `"XSS!"`
67         ) == `<input type="text" value="&quot;XSS!&quot;"/>`
68     );
69 
70     assert(
71 
72         elem!"input"(["type": "text", "value": `"XSS!"`])
73         == `<input type="text" value="&quot;XSS!&quot;"/>`
74 
75     );
76     assert(
77         elem!("input", q{ type="text" })(["value": `"XSS!"`])
78         == `<input type="text" value="&quot;XSS!&quot;"/>`
79     );
80 
81     // Alternative method of nesting
82     assert(
83 
84         elem!("div", q{ style="background:#500" })
85             .add!"p"("Hello, World!")
86             .add("-> Sanitized")
87             .add(
88                 " and",
89                 " clear"
90             )
91 
92         == `<div style="background:#500"><p>Hello, World!</p>-&gt; Sanitized and clear</div>`
93 
94     );
95 
96     import std.range : repeat;
97 
98     // Adding elements by ranges
99     assert(
100         elem!"ul"(
101             "element".elem!"li".repeat(3)
102         )
103         == "<ul><li>element</li><li>element</li><li>element</li></ul>"
104 
105     );
106 
107     // Significant whitespace
108     assert(elem!"span"(" Foo ") == "<span> Foo </span>");
109 
110     // Also with tilde
111     auto myElem = elem!"div";
112     myElem ~= elem!"span"("Sample");
113     myElem ~= " ";
114     myElem ~= elem!"span"("Text");
115     myElem ~= attr("class") = "test";
116 
117     assert(
118         myElem == `<div class="test"><span>Sample</span> <span>Text</span></div>`
119     );
120 
121 }
122 
123 /// A general example page
124 pure @system unittest {
125 
126     import std.stdio : writeln;
127     import std.base64 : Base64;
128     import std.array : split, join;
129     import std.algorithm : map, filter;
130 
131     enum page = Element.HTMLDoctype ~ elem!"html"(
132 
133         elem!"head"(
134 
135             elem!("title")("An example document"),
136 
137             // Metadata
138             Element.MobileViewport,
139             Element.EncodingUTF8,
140 
141             elem!("style")(`
142 
143                 html, body {
144                     height: 100%;
145                    font-family: sans-serif;
146                     padding: 0;
147                     margin: 0;
148                 }
149                 .header {
150                     background: #f7a;
151                     font-size: 1.5em;
152                     margin: 0;
153                     padding: 5px;
154                 }
155                 .article {
156                     padding-left: 2em;
157                 }
158 
159             `.split("\n").map!"a.strip".filter!"a.length".join),
160 
161         ),
162 
163         elem!"body"(
164 
165             elem!("header", q{ class="header" })(
166                 elem!"h1"("Example website")
167             ),
168 
169             elem!"h1"("Welcome to my website!"),
170             elem!"p"("Hello there,", elem!"br", "may you want to read some of my articles?"),
171 
172             elem!("div", q{ class="article" })(
173                 elem!"h2"("Stuff"),
174                 elem!"p"("Description")
175             )
176 
177         )
178 
179     );
180 
181     enum target = cast(string) Base64.decode([
182         `PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHRpdGxlPkFuIGV4YW1wbGUgZG9jdW1lbnQ8L3Rp`,
183         `dGxlPjxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGlu`,
184         `aXRpYWwtc2NhbGU9MSIvPjxtZXRhIGNoYXJzZXQ9InV0Zi04Ii8+PHN0eWxlPmh0bWwsIGJvZHkg`,
185         `e2hlaWdodDogMTAwJTtmb250LWZhbWlseTogc2Fucy1zZXJpZjtwYWRkaW5nOiAwO21hcmdpbjog`,
186         `MDt9LmhlYWRlciB7YmFja2dyb3VuZDogI2Y3YTtmb250LXNpemU6IDEuNWVtO21hcmdpbjogMDtw`,
187         `YWRkaW5nOiA1cHg7fS5hcnRpY2xlIHtwYWRkaW5nLWxlZnQ6IDJlbTt9PC9zdHlsZT48L2hlYWQ+`,
188         `PGJvZHk+PGhlYWRlciBjbGFzcz0iaGVhZGVyIj48aDE+RXhhbXBsZSB3ZWJzaXRlPC9oMT48L2hl`,
189         `YWRlcj48aDE+V2VsY29tZSB0byBteSB3ZWJzaXRlITwvaDE+PHA+SGVsbG8gdGhlcmUsPGJyLz5t`,
190         `YXkgeW91IHdhbnQgdG8gcmVhZCBzb21lIG9mIG15IGFydGljbGVzPzwvcD48ZGl2IGNsYXNzPSJh`,
191         `cnRpY2xlIj48aDI+U3R1ZmY8L2gyPjxwPkRlc2NyaXB0aW9uPC9wPjwvZGl2PjwvYm9keT48L2h0`,
192         `bWw+`,
193     ].join);
194 
195     assert(page == target);
196 
197 }
198 
199 // README example
200 pure @safe unittest {
201 
202     import elemi;
203     import std.conv;
204 
205     // HTML document
206     auto document = text(
207         Element.HTMLDoctype,
208         elem!"html"(
209 
210             elem!"head"(
211                 elem!"title"("Hello, World!"),
212                 Element.MobileViewport,
213                 Element.EncodingUTF8,
214             ),
215 
216             elem!"body"(
217                 attr("class") = ["home", "logged-in"],
218 
219                 elem!"main"(
220 
221                     elem!"img"(
222                         attr("src") = "/logo.png",
223                         attr("alt") = "Website logo"
224                     ),
225 
226                     // All input is sanitized.
227                     "<Welcome to my website!>"
228 
229                 )
230 
231             ),
232 
233         ),
234 
235     );
236 
237     // XML document
238     // You may `import elemi.xml` if you prefer to type `elem` over `elemX`
239     auto xml = text(
240         Element.XMLDeclaration1_0,
241         elemX!"feed"(
242 
243             attr("xmlns") = "http://www.w3.org/2005/Atom",
244 
245             elemX!"title"("Example feed"),
246             elemX!"subtitle"("Showcasing using elemi for generating XML"),
247             elemX!"updated"("2021-10-30T20:30:00Z"),
248 
249             elemX!"entry"(
250                 elemX!"title"("Elemi home page"),
251                 elemX!"link"(
252                     attr("href") = "https://git.samerion.com/Artha/Elemi",
253                 ),
254                 elemX!"updated"("2021-10-30T20:30:00Z"),
255                 elemX!"summary"("Elemi repository on GitHub"),
256                 elemX!"author"(
257                      elemX!"Artha",
258                      elemX!"artha@samerion.com"
259                 )
260             )
261 
262         )
263 
264     );
265 
266 }
267 
268 // UTF-32 test: generally `string` is preferred and in most cases, is required. There's one exception, content, and it
269 // must preserve the support.
270 //
271 // In the future, it might be preferable to introduce support for any UTF encoding.
272 pure @safe unittest {
273 
274     import elemi;
275 
276     auto data = "Foo bar"d;
277     dchar[] dataArr = "Foo bar"d.dup;
278 
279     assert(elem!"div"("Hello, World!"d) == "<div>Hello, World!</div>");
280     assert(elem!"div"(elem!"span"("Hello, World!"d)) == "<div><span>Hello, World!</span></div>");
281     assert(elem!"div"(["class": "foo bar"], "Hello, World!"d) == `<div class="foo bar">Hello, World!</div>`);
282     assert(elem!"p"(data) == `<p>Foo bar</p>`);
283     assert(elem!"p"(dataArr) == `<p>Foo bar</p>`);
284 
285 }
286 
287 // readme.md example
288 @safe unittest {
289 
290     import elemi;
291 
292     auto document = buildHTML() ~ (html) {
293         html ~ Element.HTMLDoctype;
294         html.html ~ {
295             html.head ~ {
296                 html.title ~ "Hello, World!";
297                 html ~ Element.MobileViewport;
298                 html ~ Element.EncodingUTF8;
299             };
300             html.body.classes("home", "logged-in") ~ {
301                 html.main ~ {
302 
303                     html.img
304                         .attr("src", "/logo.png")
305                         .attr("alt", "Website logo") ~ { };
306 
307                     html.p ~ {
308                         // All input is sanitized
309                         html ~ "My <hobbies>:";
310                     };
311 
312                     html.ul ~ {
313                         foreach (item; ["Web development", "Music", "Trains"]) {
314                             html.li ~ item;
315                         }
316                     };
317 
318                     // i-strings are supported
319                     html.p ~ i"1+2 is $(1+2).";
320                 };
321             };
322         };
323     };
324 
325     assert(document == `<!DOCTYPE html><html><head>`
326         ~ `<title>Hello, World!</title>`
327         ~ `<meta name="viewport" content="width=device-width, initial-scale=1"/>`
328         ~ `<meta charset="utf-8"/>`
329         ~ `</head>`
330         ~ `<body class="home logged-in"><main>`
331         ~ `<img src="/logo.png" alt="Website logo"/>`
332         ~ `<p>My &lt;hobbies&gt;:</p>`
333         ~ `<ul>`
334         ~ `<li>Web development</li>`
335         ~ `<li>Music</li>`
336         ~ `<li>Trains</li>`
337         ~ `</ul>`
338         ~ `<p>1+2 is 3.</p>`
339         ~ "</main></body></html>");
340 
341 }