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>-> 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=""XSS!""/>` 68 ); 69 70 assert( 71 72 elem!"input"(["type": "text", "value": `"XSS!"`]) 73 == `<input type="text" value=""XSS!""/>` 74 75 ); 76 assert( 77 elem!("input", q{ type="text" })(["value": `"XSS!"`]) 78 == `<input type="text" value=""XSS!""/>` 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>-> 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 <hobbies>:</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 }