1 /// This module defines the old Elemi syntax for generating XML code. 2 /// 3 /// See [#examples|examples] for more information. 4 module elemi.xml; 5 6 /// Elemi allows creating XML elements using the `elemX` template. 7 pure @safe unittest { 8 auto element = elemX!"paragraph"("Hello, World!"); 9 10 assert(element == "<paragraph>Hello, World!</paragraph>"); 11 } 12 13 /// Elements can be nested. 14 pure @safe unittest { 15 auto element = elemX!"book"( 16 elemX!"title"("Programming in D"), 17 elemX!"author"("Ali Çehreli"), 18 elemX!"link"("https://www.ddili.org/ders/d.en/index.html"), 19 ); 20 21 assert(element == `<book>` 22 ~ `<title>Programming in D</title>` 23 ~ `<author>Ali Çehreli</author>` 24 ~ `<link>https://www.ddili.org/ders/d.en/index.html</link>` 25 ~ `</book>`); 26 } 27 28 /// Text and XML content can be mixed together. Special XML characters are always escaped. 29 pure @safe unittest { 30 auto element = elemX!"paragraph"( 31 "<injection />", 32 elemX!"bold"("Can't get through me!"), 33 ); 34 35 assert(element == `<paragraph>` 36 ~ `<injection />` 37 ~ `<bold>Can't get through me!</bold>` 38 ~ `</paragraph>`); 39 } 40 41 /// Attributes can be added using `attr` at the beginning of your element. 42 pure @safe unittest { 43 auto book = elemX!"website"( 44 attr("title") = "D Programming Language", 45 attr("url") = "https://dlang.org", 46 "D is a general-purpose programming language with static typing, ", 47 "systems-level access, and C-like syntax. With the D Programming ", 48 "Language, write fast, read fast, and run fast." 49 ); 50 51 assert(book == `<website ` 52 ~ `title="D Programming Language" ` 53 ~ `url="https://dlang.org">` 54 ~ `D is a general-purpose programming language with static typing, ` 55 ~ `systems-level access, and C-like syntax. With the D Programming ` 56 ~ `Language, write fast, read fast, and run fast.` 57 ~ `</website>`); 58 } 59 60 /// You can create self-closing tags if you follow the name with a slash. 61 pure @safe unittest { 62 auto element = elemX!"void/"(); 63 assert(element == "<void/>"); 64 } 65 66 /// The `elem` shorthand defaults to HTML mode, but if you import `elemi.xml` directly, 67 /// XML mode will be used. 68 pure @safe unittest { 69 { 70 import elemi : elem; 71 auto e = elem!"input"(); 72 assert(e == "<input/>"); 73 // <input> is a self-closing void tag 74 } 75 { 76 import elemi.xml : elem; 77 auto e = elem!"input"(); 78 assert(e == "<input></input>"); 79 } 80 } 81 82 /// If you need to add children dynamically, you can append them. 83 pure @safe unittest { 84 Element element = elemX!"parent"(); 85 86 // Append elements 87 foreach (content; ["one", "two", "three"]) { 88 element ~= elemX!"child"(content); 89 } 90 91 // Append attributes 92 element ~= attr("id") = "example"; 93 94 assert(element == `<parent id="example">` 95 ~ `<child>one</child>` 96 ~ `<child>two</child>` 97 ~ `<child>three</child>` 98 ~ `</parent>`); 99 } 100 101 /// If you need to group a few elements together, you can do it with `elems`. 102 pure @safe unittest { 103 auto child = elems( 104 "Hello, ", 105 elemX!"strong"("World"), 106 ); 107 auto parent = elemX!"div"( 108 child, 109 ); 110 111 assert(child == "Hello, <strong>World</strong>"); 112 assert(parent == "<div>Hello, <strong>World</strong></div>"); 113 } 114 115 /// If combined with a fresh compiler, Elemi also supports interpolated strings. 116 static if (withInterpolation) 117 pure @safe unittest{ 118 auto e = elemX!"item"( 119 attr("expression") = i"1+2 = $(1+2)", 120 i"1+2 is $(1+2)", 121 ); 122 123 assert(e == `<item expression="1+2 = 3">1+2 is 3</item>`); 124 } 125 126 /// `elemX` also supports DTD declarations like DOCTYPE, XML declarations, and even preprocessor 127 /// tags. 128 pure @safe unittest { 129 assert(elemX!"!DOCTYPE"("html") == `<!DOCTYPE html>`); 130 assert(elemX!"?xml"( 131 attr("version") = "1.0", 132 attr("encoding") = "UTF-8") == `<?xml version="1.0" encoding="UTF-8" ?>`); 133 assert(elemX!"?php"(`$var = "<div></div>";`) == `<?php $var = "<div></div>"; ?>`); 134 assert(elemX!"?="(`$var`) == `<?= $var ?>`); 135 } 136 137 import std.meta; 138 import std.traits; 139 140 import elemi.internal; 141 142 public { 143 144 import elemi.attribute; 145 import elemi.element; 146 147 } 148 149 /// A shorthand for `elemX`. Available if importing `elemi.xml`. 150 /// 151 /// Note that importing both `elemi` and `elemi.xml`, or `elemi.html` and `elemi.xml` may raise 152 /// conflicts. Import whichever is more suitable. 153 /// 154 /// See [elemX] for more information. 155 alias elem = elemX; 156 alias add = addX; 157 158 /// Create an XML element. 159 /// 160 /// `elemX` builds the element with attributes and child content given as arguments. Their kind 161 /// is distinguished by their type — child element should be passed as [Element], attributes 162 /// as [Attribute] and text content as [string]. 163 /// 164 /// * You can pass the result of `elemX` to another `elemX` call to create child nodes: 165 /// `elemX!"parent"(elemX!"child"())`. 166 /// * Use [attr] to specify attributes: `elemX!"element"(attr("key") = "value")`. 167 /// * Pass a string to specify text content: `elemX!"text"("Hello")`. 168 /// 169 /// `elemX` also provides the option to specify attributes from an associative array 170 /// or source: `elemX!"element"(["key": "value"])` or `elemX!("element", `key="value"`)` 171 /// but these are considered legacy and should be avoided in new code. 172 /// 173 /// Params: 174 /// name = Name of the element. 175 /// attrXML = Optional, legacy; unsanitized attributes to insert at compile-time. 176 /// attributes = Optional, legacy; attributes for the element as an associative array mapping 177 /// attribute names to their values. 178 /// content = Attributes ([Attribute]), 179 /// children elements ([Element]) 180 /// and text of the element, as a string. 181 /// Returns: 182 /// An [Element] type, implicitly castable to string. 183 /// Instances of Element can be safely placed 184 Element elemX(string name, string[string] attributes, Ts...)(Ts content) { 185 186 // Overload 1: attributes from a CTFE hash map 187 188 enum attrHTML = attributes.serializeAttributes; 189 190 auto element = Element.make!name; 191 element.attributes = attrHTML; 192 element ~= content; 193 194 return element; 195 196 } 197 198 /// ditto 199 Element elemX(string name, string attrXML = null, T...)(string[string] attributes, T content) { 200 201 // Overload 2: attributes from a CTFE attribute string and from a runtime hash map 202 203 enum attrXML = minifyAttributes(attrXML); 204 205 auto element = Element.make!name; 206 element.attributes = attrXML 207 ~ attributes.serializeAttributes; 208 element ~= content; 209 210 return element; 211 212 } 213 214 /// ditto 215 Element elemX(string name, string attrXML = null, T...)(T content) 216 if (!T.length || (!is(T[0] == typeof(null)) && !is(T[0] == string[string]))) { 217 218 // Overload 3: attributes from a CTFE attribute string 219 220 enum attrXML = minifyAttributes(attrXML); 221 222 auto element = Element.make!name; 223 element.attributes = attrXML; 224 element ~= content; 225 226 return element; 227 228 } 229 230 /// 231 pure @safe unittest { 232 233 enum xml = elemX!"xml"( 234 elemX!"heading"("This is my sample document!"), 235 elemX!("spacing /", q{ height="1em" }), 236 elemX!"spacing /"(["height": "1em"]), 237 elemX!"empty", 238 elemX!"br", 239 elemX!"container" 240 .addX!"paragraph"("Foo") 241 .addX!"paragraph"("Bar"), 242 ); 243 244 assert(xml == "<xml>" ~ ( 245 "<heading>This is my sample document!</heading>" 246 ~ `<spacing height="1em"/>` 247 ~ `<spacing height="1em"/>` 248 ~ `<empty></empty>` 249 ~ `<br></br>` 250 ~ "<container>" ~ ( 251 "<paragraph>Foo</paragraph>" 252 ~ "<paragraph>Bar</paragraph>" 253 ) ~ "</container>" 254 ) ~ "</xml>"); 255 256 } 257 258 259 /// Add a new node as a child. 260 /// 261 /// This overload is considered legacy; you should use the apprend `~=` operator instead. 262 /// 263 /// Params: 264 /// parent = Parent node. 265 /// Returns: 266 /// This node, to allow chaining. 267 Element addX(Ts...)(ref Element parent, Ts args) 268 if (allSatisfy!(isType, Ts)) { 269 270 parent ~= args; 271 return parent; 272 273 } 274 275 /// ditto 276 Element addX(Ts...)(Element parent, Ts args) 277 if (allSatisfy!(isType, Ts)) { 278 279 parent ~= args; 280 return parent; 281 282 } 283 284 /// ditto 285 template addX(Ts...) 286 if (Ts.length != 0) { 287 288 Element addX(Args...)(ref Element parent, Args args) { 289 290 parent ~= elemX!Ts(args); 291 return parent; 292 293 } 294 295 Element addX(Args...)(Element parent, Args args) { 296 297 parent ~= elemX!Ts(args); 298 return parent; 299 300 } 301 302 } 303 304 /// 305 pure @safe unittest { 306 307 auto document = elem!"xml" 308 .addX!"text"("Hello") 309 .addX!"text"("World!"); 310 311 assert(document == "<xml><text>Hello</text><text>World!</text></xml>"); 312 313 } 314 315 pure @safe unittest { 316 317 assert(elem!"xml".add("<XSS>") == "<xml><XSS></xml>"); 318 assert(elem!"xml".addX!"span"("<XSS>") == "<xml><span><XSS></span></xml>"); 319 320 } 321 322 pure @safe unittest { 323 324 assert(elemX!"br" == "<br></br>"); 325 assert(elemX!"br " == "<br ></br >"); 326 assert(elemX!"br /" == "<br/>"); 327 assert(elemX!"br/" == "<br/>"); 328 329 assert(elemX!"myFancyTag" == "<myFancyTag></myFancyTag>"); 330 assert(elemX!"myFancyTag /" == "<myFancyTag/>"); 331 assert(elemX!"myFancyTäg /" == "<myFancyTäg/>"); 332 333 assert(elemX!"?br" == "<?br ?>"); 334 assert(elemX!"!br" == "<!br>"); 335 336 assert(elemX!"?br" == "<?br ?>"); 337 assert(elemX!("?br", "foo") == "<?br foo ?>"); 338 assert(elemX!"?br"(attr("foo", "bar")) == `<?br foo="bar" ?>`); 339 340 } 341 342 // Issue #1 343 pure @safe unittest { 344 345 enum Foo = elem!("p")("<unsafe>code</unsafe>"); 346 347 } 348 349 pure @safe unittest { 350 351 assert(elemX!"p" == "<p></p>"); 352 assert(elemX!"p /" == "<p/>"); 353 assert(elemX!("!DOCTYPE", "html") == "<!DOCTYPE html>"); 354 assert(Element.HTMLDoctype == "<!DOCTYPE html>"); 355 assert(elemX!("!ATTLIST", "pre (preserve) #FIXED 'preserve'") == "<!ATTLIST pre (preserve) #FIXED 'preserve'>"); 356 assert(elemX!"!ATTLIST"("pre (preserve) #FIXED 'preserve'") == "<!ATTLIST pre (preserve) #FIXED 'preserve'>"); 357 assert(elemX!"!ATTLIST".add("pre (preserve) #FIXED 'preserve'") == "<!ATTLIST pre (preserve) #FIXED 'preserve'>"); 358 assert(elemX!"?xml" == "<?xml ?>"); 359 assert(elemX!("?xml", q{ version="1.1" encoding="UTF-8" }) == `<?xml version="1.1" encoding="UTF-8" ?>`); 360 assert(elemX!"?xml"(`version="1.1" encoding="UTF-8"`) == `<?xml version="1.1" encoding="UTF-8" ?>`); 361 assert(elemX!"?xml".add(`version="1.1" encoding="UTF-8"`) == `<?xml version="1.1" encoding="UTF-8" ?>`); 362 assert(Element.XMLDeclaration == `<?xml version="1.1" encoding="UTF-8" ?>`); 363 assert(elemX!"?xml"(["version": "1.1"]).addTrusted(`encoding="UTF-8"`) 364 == `<?xml version="1.1" encoding="UTF-8" ?>`); 365 assert(elemX!"?php" == "<?php ?>"); 366 assert(Element.XMLDeclaration1_0 == `<?xml version="1.0" encoding="UTF-8" ?>`); 367 assert(Element.XMLDeclaration1_1 == `<?xml version="1.1" encoding="UTF-8" ?>`); 368 assert(elemX!"?php"(`echo "Hello, World!";`) == `<?php echo "Hello, World!"; ?>`); 369 assert(elemX!"?="(`"Hello, World!"`) == `<?= "Hello, World!" ?>`); 370 // ↑ I will not special-case this to remove spaces. 371 372 auto php = elemX!"?php"; 373 php.add(`$target = "World!";`); 374 php.add(`echo "Hello, " . $target;`); 375 assert(php == `<?php $target = "World!";echo "Hello, " . $target; ?>`); 376 377 assert(elemX!("?xml", "test").add("foo") == "<?xml test foo ?>"); 378 assert(elemX!("!XML", "test").add("foo") == "<!XML test foo>"); 379 380 }