1 /// This module holds the [Element] type, used and emitted by [elemi.html](elemi.html.html) 2 /// and [elemi.xml](elemi.xml.html). 3 module elemi.element; 4 5 import std.string; 6 7 import elemi; 8 import elemi.internal; 9 10 /// Represents an HTML element. Elements can be created using [elem]. 11 /// 12 /// `Element` implicitly converts to a string, but when passed to `elem`, it will be recognized 13 /// as HTML code, and it will not be escaped. 14 /// 15 /// --- 16 /// Element e1 = elem!"div"(); 17 /// string e2 = elem!"div"(); 18 /// elems(e1); // <div></div> 19 /// elems(e2); // <div></div> 20 /// --- 21 /// 22 /// `Element` does not store its data in a structured manner, so its attributes and content cannot 23 /// be read without using an XML parser. It does, however, have the capability of inserting 24 /// attributes and content at runtime: 25 /// 26 /// --- 27 /// Element e = elem!"div"(); 28 /// e ~= attr("class") = "one"; 29 /// e ~= elem!"div"("two"); 30 /// e ~= "three"; 31 /// --- 32 struct Element { 33 34 /// [Document type declaration](https://en.wikipedia.org/wiki/Document_type_declaration) for 35 /// HTML, used to enable standards mode in browsers. 36 enum HTMLDoctype = elemX!("!DOCTYPE", "html"); 37 38 /// Prepend a document type declaration to your document. 39 pure @safe unittest { 40 auto html = elems( 41 Element.HTMLDoctype, 42 elem!"html"(), 43 ); 44 45 assert(html == "<!DOCTYPE html><html></html>"); 46 } 47 48 /// XML declaration element. Uses version 1.1. 49 enum XMLDeclaration1_1 = elemX!"?xml"( 50 attr!"version" = "1.1", 51 attr!"encoding" = "UTF-8", 52 ); 53 54 /// XML declaration element. Uses version 1.0. 55 enum XMLDeclaration1_0 = elemX!"?xml"( 56 attr!"version" = "1.0", 57 attr!"encoding" = "UTF-8", 58 ); 59 60 /// Default XML declaration element, uses version 1.1. 61 alias XMLDeclaration = XMLDeclaration1_1; 62 63 /// Enables UTF-8 encoding for the document. 64 enum EncodingUTF8 = elemH!"meta"( 65 attr!"charset" = "utf-8", 66 ); 67 68 /// A common head element for adjusting the viewport to mobile devices. 69 enum MobileViewport = elemH!"meta"( 70 attr!"name" = "viewport", 71 attr!"content" = "width=device-width, initial-scale=1" 72 ); 73 74 package { 75 76 bool directive; 77 string startTag; 78 string attributes; 79 string trail; 80 string content; 81 string endTag; 82 83 } 84 85 static package Element make(string tagName)() pure @safe 86 in(tagName.length != 0, "Tag name cannot be empty") 87 do { 88 89 Element that; 90 91 // Self-closing tag 92 static if (tagName[$-1] == '/') { 93 94 // Enforce CTFE 95 enum startTag = format!"<%s"(tagName[0..$-1].stripRight); 96 97 that.startTag = startTag; 98 that.trail = "/>"; 99 100 } 101 102 // XML tag 103 else static if (tagName[0] == '?') { 104 105 that.startTag = format!"<%s"(tagName); 106 that.trail = " "; 107 that.endTag = " ?>"; 108 that.directive = true; 109 110 } 111 112 // Declaration 113 else static if (tagName[0] == '!') { 114 115 that.startTag = format!"<%s"(tagName); 116 that.trail = " "; 117 that.endTag = ">"; 118 that.directive = true; 119 120 } 121 122 else { 123 124 that.startTag = format!"<%s"(tagName); 125 that.trail = ">"; 126 that.endTag = format!"</%s>"(tagName); 127 128 } 129 130 return that; 131 132 } 133 134 /// Returns: 135 /// True if the element allows content. 136 /// 137 /// To accept content, the element must have an end tag. 138 /// In HTML, void elements such as `elemH!"input"` do not have one, and thus, cannot have 139 /// content. In XML, a self closing tag (marked with `/`) `elemX!"tag/"` also cannot 140 /// have content. 141 bool acceptsContent() const pure @safe { 142 143 return endTag.length || !startTag.length; 144 145 } 146 147 /// Add trusted XML/HTML code as a child of this node. 148 /// See_Also: 149 /// [elemTrusted] to construct an `Element` from XML/HTML source code. 150 /// Params: 151 /// code = Raw XML/HTML code to insert into the element. 152 /// Returns: 153 /// This node, to allow chaining. 154 Element addTrusted(string code) pure @safe { 155 156 assert(acceptsContent, "This element doesn't accept content"); 157 158 content ~= code; 159 return this; 160 161 } 162 163 /// Append content to this node. 164 /// 165 /// Params: 166 /// args = Content to append. The content can include: $(LIST 167 /// * A child `Element`, 168 /// * An [Attribute] for this element 169 /// * An [i-string](https://dlang.org/spec/istring.html) 170 /// * A regular [string] 171 /// ) 172 void opOpAssign(string op = "~", Ts...)(Ts args) { 173 174 import std.meta; 175 176 // Interpolate arguments 177 static if (withInterpolation) { 178 179 template inString(size_t index) { 180 static if (is(Ts[index] == InterpolationHeader)) 181 enum inString = true; 182 else static if (is(Ts[index] == InterpolationFooter)) 183 enum inString = false; 184 else static if (index == 0) 185 enum inString = false; 186 else 187 enum inString = inString!(index - 1); 188 } 189 190 static foreach (i, Type; Ts) { 191 static if (!is(Type == InterpolationFooter)) { 192 addItem!(inString!i)(args[i]); 193 } 194 } 195 } 196 197 // Check each argument 198 else { 199 static foreach (i, Type; Ts) { 200 addItem(args[i]); 201 } 202 } 203 204 } 205 206 @safe unittest { 207 assert(elem!"p"(i"Hello, $(123)!", " ... ", i"Hello, $(123)!") == 208 "<p>Hello, 123! ... Hello, 123!</p>"); 209 assert(!__traits(compiles, elem!"p"(123))); 210 assert(!__traits(compiles, elem!"p"(i"Hello ", 123))); 211 212 assert(elem!"p"(i"Hello, $(elem!"b"("Woo!"))~") == 213 "<p>Hello, <b>Woo!</b>~</p>"); 214 } 215 216 private void addItem(bool allowInterpolation = false, Type)(Type item) { 217 218 import std.range; 219 import std.traits; 220 221 // Element 222 static if (is(Type : Element)) { 223 224 assert(acceptsContent, "This element doesn't accept content"); 225 content ~= item; 226 227 } 228 229 // Attribute 230 else static if (is(Type : Attribute)) { 231 232 attributes ~= " " ~ item; 233 234 } 235 236 // String 237 else static if (isSomeString!Type) { 238 239 addText(item); 240 241 } 242 243 // Range 244 else static if (isInputRange!Type && __traits(compiles, addItem(item.front))) { 245 246 // TODO Needs tests 247 foreach (content; item) addItem(content); 248 249 } 250 251 // Perform interpolation 252 else static if (allowInterpolation) { 253 254 addText(item); 255 256 } 257 258 // No idea what is this 259 else static assert(false, "Unsupported element type " ~ fullyQualifiedName!Type); 260 261 } 262 263 private void addText(T)(T item) { 264 265 import std.conv; 266 267 assert(acceptsContent, "This element doesn't accept content"); 268 content ~= directive ? item.to!string : escapeHTML(item.to!string); 269 270 } 271 272 pure @safe unittest { 273 274 void test(T...)(T things, string expectedResult) { 275 276 Element elem; 277 elem ~= things; 278 assert(elem == expectedResult, format!"wrong result: `%s`"(elem.toString)); 279 280 } 281 282 test(`"Insecure" string`, ""Insecure" string"); 283 test(Element.make!"div", "<div></div>"); 284 test(Element.make!"?xml", "<?xml ?>"); 285 test(Element.make!"div", `<XSS>`, "<div></div><XSS>"); 286 287 test(["hello, ", "<XSS>!"], "hello, <XSS>!"); 288 289 } 290 291 /// Convert the element to a string. 292 /// 293 /// If `Element` is passed to something that expects a `string`, it will be casted implicitly. 294 string toString() const pure @safe { 295 296 import std.conv; 297 298 // Special case: prevent space between trail and endTag in directives 299 if (directive && content == null) { 300 301 return startTag ~ attributes ~ content ~ endTag; 302 303 } 304 305 return startTag ~ attributes ~ trail ~ content ~ endTag; 306 307 } 308 309 alias toString this; 310 311 } 312 313 /// Creates an element to function as an element collection to place within other elements. 314 /// For Elemi, it acts like an element, so it can accept child nodes, and can be appended to, 315 /// but it is invisible for the generated document. 316 Element elems(T...)(T content) { 317 318 Element element; 319 element ~= content; 320 return element; 321 322 } 323 324 /// 325 pure @safe unittest { 326 327 const collection = elems("Hello, ", elem!"span"("world!")); 328 329 assert(collection == `Hello, <span>world!</span>`); 330 assert(elem!"div"(collection) == `<div>Hello, <span>world!</span></div>`); 331 332 } 333 334 /// Create an element from trusted HTML/XML code. 335 /// 336 /// Warning: This element cannot have children added after being created. They will be added as 337 /// siblings instead. 338 Element elemTrusted(string code) pure @safe { 339 340 Element element; 341 element.content = code; 342 return element; 343 344 } 345 346 /// 347 pure @safe unittest { 348 349 assert(elemTrusted("<p>test</p>") == "<p>test</p>"); 350 assert( 351 elem!"p"( 352 elemTrusted("<b>foo</b>bar"), 353 ) == "<p><b>foo</b>bar</p>" 354 ); 355 assert( 356 elemTrusted("<b>test</b>").add("<b>foo</b>") 357 == "<b>test</b><b>foo</b>" 358 ); 359 360 } 361 362 363 // Other related tests 364 365 pure @safe unittest { 366 367 const Element element; 368 assert(element == ""); 369 assert(element == elems()); 370 assert(element == Element()); 371 372 assert(elems("<script>") == "<script>"); 373 374 } 375 376 pure @safe unittest { 377 378 assert( 379 elem!"p".addTrusted("<b>test</b>") 380 == "<p><b>test</b></p>" 381 ); 382 383 } 384 385 pure @safe unittest { 386 387 auto foo = ["foo", "<bar>", "test"]; 388 auto bar = [ 389 elem!"span"("Hello, "), 390 elem!"strong"("World!"), 391 ]; 392 393 assert(elem!"div"(foo) == "<div>foo<bar>test</div>"); 394 assert(elem!"div"(bar) == "<div><span>Hello, </span><strong>World!</strong></div>"); 395 396 assert(elem!"div".add(foo) == "<div>foo<bar>test</div>"); 397 assert(elem!"div".addTrusted(foo.join) == "<div>foo<bar>test</div>"); 398 assert(elem!"div".add(bar) == "<div><span>Hello, </span><strong>World!</strong></div>"); 399 400 } 401 402 pure @safe unittest { 403 404 auto attributes = [ 405 attr("rel") = "me", 406 attr("href") = "https://samerion.com", 407 ]; 408 409 assert(elem!"meta"( 410 attributes, 411 attr("x") = "woo", 412 ) == `<meta rel="me" href="https://samerion.com" x="woo"/>`); 413 414 }