1 module elemi.element; 2 3 import std.string; 4 5 import elemi; 6 import elemi.internal; 7 8 /// Represents a HTML element. 9 /// 10 /// Use `elem` to generate. 11 struct Element { 12 13 // Commonly used elements 14 enum { 15 16 /// Doctype info for HTML. 17 HTMLDoctype = elemX!("!DOCTYPE", "html"), 18 19 /// XML declaration element. Uses version 1.1. 20 XMLDeclaration1_1 = elemX!"?xml"( 21 attr!"version" = "1.1", 22 attr!"encoding" = "UTF-8", 23 ), 24 XMLDeclaration1_0 = elemX!"?xml"( 25 attr!"version" = "1.0", 26 attr!"encoding" = "UTF-8", 27 ), 28 XMLDeclaration = XMLDeclaration1_1, 29 30 /// Enables UTF-8 encoding for the document 31 EncodingUTF8 = elemH!"meta"( 32 attr!"charset" = "utf-8", 33 ), 34 35 /// A common head element for adjusting the viewport to mobile devices. 36 MobileViewport = elemH!"meta"( 37 attr!"name" = "viewport", 38 attr!"content" = "width=device-width, initial-scale=1" 39 ), 40 41 } 42 43 package { 44 45 bool directive; 46 string startTag; 47 string attributes; 48 string trail; 49 string content; 50 string endTag; 51 52 } 53 54 /// Create the tag. 55 static package Element make(string tagName)() pure @safe 56 in(tagName.length != 0, "Tag name cannot be empty") 57 do { 58 59 Element that; 60 61 // Self-closing tag 62 static if (tagName[$-1] == '/') { 63 64 // Enforce CTFE 65 enum startTag = format!"<%s"(tagName[0..$-1].stripRight); 66 67 that.startTag = startTag; 68 that.trail = "/>"; 69 70 } 71 72 // XML tag 73 else static if (tagName[0] == '?') { 74 75 that.startTag = format!"<%s"(tagName); 76 that.trail = " "; 77 that.endTag = " ?>"; 78 that.directive = true; 79 80 } 81 82 // Declaration 83 else static if (tagName[0] == '!') { 84 85 that.startTag = format!"<%s"(tagName); 86 that.trail = " "; 87 that.endTag = ">"; 88 that.directive = true; 89 90 } 91 92 else { 93 94 that.startTag = format!"<%s"(tagName); 95 that.trail = ">"; 96 that.endTag = format!"</%s>"(tagName); 97 98 } 99 100 return that; 101 102 } 103 104 /// Check if the element allows content. 105 bool acceptsContent() const pure @safe { 106 107 return endTag.length || !startTag.length; 108 109 } 110 111 /// Add trusted XML/HTML code as a child of this node. 112 /// Returns: This node, to allow chaining. 113 Element addTrusted(string code) pure @safe { 114 115 assert(acceptsContent, "This element doesn't accept content"); 116 117 content ~= code; 118 return this; 119 120 } 121 122 void opOpAssign(string op = "~", Ts...)(Ts args) { 123 124 // Check each argument 125 static foreach (i, Type; Ts) { 126 127 addItem(args[i]); 128 129 } 130 131 } 132 133 private void addItem(Attribute item) pure @safe { 134 135 attributes ~= " " ~ item; 136 137 } 138 139 private void addItem(Type)(Type item) { 140 141 import std.conv; 142 import std.range; 143 import std.traits; 144 145 assert(acceptsContent, "This element doesn't accept content"); 146 147 // Element 148 static if (is(Type : Element)) { 149 150 content ~= item; 151 152 } 153 154 // String 155 else static if (isSomeString!Type) { 156 157 content ~= directive ? item.to!string : escapeHTML(item.to!string); 158 159 } 160 161 // Range 162 else static if (isInputRange!Type) { 163 164 // TODO Needs tests 165 foreach (content; item) addItem(content); 166 167 } 168 169 // No idea what is this 170 else static assert(false, "Unsupported element type " ~ fullyQualifiedName!Type); 171 172 } 173 174 pure @safe unittest { 175 176 void test(T...)(T things, string expectedResult) { 177 178 Element elem; 179 elem ~= things; 180 assert(elem == expectedResult, format!"wrong result: `%s`"(elem.toString)); 181 182 } 183 184 test(`"Insecure" string`, ""Insecure" string"); 185 test(Element.make!"div", "<div></div>"); 186 test(Element.make!"?xml", "<?xml ?>"); 187 test(Element.make!"div", `<XSS>`, "<div></div><XSS>"); 188 189 test(["hello, ", "<XSS>!"], "hello, <XSS>!"); 190 191 } 192 193 string toString() const pure @safe { 194 195 import std.conv; 196 197 // Special case: prevent space between trail and endTag in directives 198 if (directive && content == null) { 199 200 return startTag ~ attributes ~ content ~ endTag; 201 202 } 203 204 return startTag ~ attributes ~ trail ~ content ~ endTag; 205 206 } 207 208 alias toString this; 209 210 } 211 212 /// Creates an element to function as an element collection to place within other elements. This is functionally 213 /// equivalent to a regular element, server-side, but is transparent for the rendered document. 214 Element elems(T...)(T content) { 215 216 Element element; 217 element ~= content; 218 return element; 219 220 } 221 222 /// 223 pure @safe unittest { 224 225 const collection = elems("Hello, ", elem!"span"("world!")); 226 227 assert(collection == `Hello, <span>world!</span>`); 228 assert(elem!"div"(collection) == `<div>Hello, <span>world!</span></div>`); 229 230 } 231 232 /// Create an element from trusted HTML/XML code. 233 /// 234 /// Warning: This element cannot have children added after being created. They will be added as siblings instead. 235 Element elemTrusted(string code) pure @safe { 236 237 Element element; 238 element.content = code; 239 return element; 240 241 } 242 243 /// 244 pure @safe unittest { 245 246 assert(elemTrusted("<p>test</p>") == "<p>test</p>"); 247 assert( 248 elem!"p"( 249 elemTrusted("<b>foo</b>bar"), 250 ) == "<p><b>foo</b>bar</p>" 251 ); 252 assert( 253 elemTrusted("<b>test</b>").add("<b>foo</b>") 254 == "<b>test</b><b>foo</b>" 255 ); 256 257 } 258 259 260 // Other related tests 261 262 pure @safe unittest { 263 264 const Element element; 265 assert(element == ""); 266 assert(element == elems()); 267 assert(element == Element()); 268 269 assert(elems("<script>") == "<script>"); 270 271 } 272 273 pure @safe unittest { 274 275 assert( 276 elem!"p".addTrusted("<b>test</b>") 277 == "<p><b>test</b></p>" 278 ); 279 280 } 281 282 pure @safe unittest { 283 284 auto foo = ["foo", "<bar>", "test"]; 285 auto bar = [ 286 elem!"span"("Hello, "), 287 elem!"strong"("World!"), 288 ]; 289 290 assert(elem!"div"(foo) == "<div>foo<bar>test</div>"); 291 assert(elem!"div"(bar) == "<div><span>Hello, </span><strong>World!</strong></div>"); 292 293 assert(elem!"div".add(foo) == "<div>foo<bar>test</div>"); 294 assert(elem!"div".addTrusted(foo.join) == "<div>foo<bar>test</div>"); 295 assert(elem!"div".add(bar) == "<div><span>Hello, </span><strong>World!</strong></div>"); 296 297 }