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`, "&quot;Insecure&quot; string");
185         test(Element.make!"div", "<div></div>");
186         test(Element.make!"?xml", "<?xml ?>");
187         test(Element.make!"div", `<XSS>`, "<div></div>&lt;XSS&gt;");
188 
189         test(["hello, ", "<XSS>!"], "hello, &lt;XSS&gt;!");
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>&lt;b&gt;foo&lt;/b&gt;"
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>") == "&lt;script&gt;");
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&lt;bar&gt;test</div>");
291     assert(elem!"div"(bar) == "<div><span>Hello, </span><strong>World!</strong></div>");
292 
293     assert(elem!"div".add(foo) == "<div>foo&lt;bar&gt;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 }