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         ~ `&lt;injection /&gt;`
37         ~ `<bold>Can&#39;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>&lt;XSS&gt;</xml>");
318     assert(elem!"xml".addX!"span"("<XSS>") == "<xml><span>&lt;XSS&gt;</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 }