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);  // &lt;div&gt;&lt;/div&gt;
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`, "&quot;Insecure&quot; string");
283         test(Element.make!"div", "<div></div>");
284         test(Element.make!"?xml", "<?xml ?>");
285         test(Element.make!"div", `<XSS>`, "<div></div>&lt;XSS&gt;");
286 
287         test(["hello, ", "<XSS>!"], "hello, &lt;XSS&gt;!");
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>&lt;b&gt;foo&lt;/b&gt;"
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>") == "&lt;script&gt;");
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&lt;bar&gt;test</div>");
394     assert(elem!"div"(bar) == "<div><span>Hello, </span><strong>World!</strong></div>");
395 
396     assert(elem!"div".add(foo) == "<div>foo&lt;bar&gt;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 }