1 /// This module provides syntax for generating HTML and XML documents from inside of D.
2 /// It supports embedded control flow (`if`, `foreach`, etc) and writing to output ranges.
3 ///
4 /// $(B See the [#examples|examples] to get started!)
5 ///
6 /// Note that the generator, at the moment, only supports HTML generation. While its output should
7 /// be a valid XML document, it is unable to create tags other than HTML's own. The old
8 /// [XML API](elemi.xml.html) should still be suitable for use with XML.
9 ///
10 /// History: $(LIST
11 ///     * Introduced in Elemi 1.4.0
12 /// )
13 module elemi.generator;
14 
15 /// The [buildHTML] function is the entrypoint to generating HTML. Connect it to a HTML source:
16 /// a function to fill the HTML document with content.
17 @safe unittest {
18     string output = buildHTML() ~ (html) {
19 
20     };
21 }
22 
23 /// Elements are created using a tilde, followed by curly braces. This syntax is called an
24 /// $(B element block).
25 @safe unittest {
26     string output = buildHTML() ~ (html) {
27         html.div ~ {
28             html.span ~ { };
29         };
30     };
31     assert(output == "<div><span></span></div>");
32 }
33 
34 /// Instead of a block, you can follow an element with a string to specify text content.
35 @safe unittest {
36     string output = buildHTML() ~ (html) {
37         html.p ~ "Hello, World!";
38     };
39     assert(output == "<p>Hello, World!</p>");
40 }
41 
42 /// You can add attributes to an element using the [Tag.attr] method.
43 @safe unittest {
44     string output = buildHTML() ~ (html) {
45         html.a.attr("href", "https://example.com") ~ "Visit my website!";
46     };
47     assert(output == `<a href="https://example.com">Visit my website!</a>`);
48 }
49 
50 /// Common HTML attributes are available through specialized methods.
51 @safe unittest {
52     string output = buildHTML() ~ (html) {
53         html.div.id("my-div") ~ { };
54         html.div.classes("one", "two", "three") ~ { };
55         html.a.href("https://example.com") ~ { };
56     };
57     assert(output == `<div id="my-div"></div>`
58         ~ `<div class="one two three"></div>`
59         ~ `<a href="https://example.com"></a>`);
60 }
61 
62 /// Use control flow statements like `if` and `foreach` to generate HTML with code.
63 @safe unittest {
64     string output = buildHTML() ~ (html) {
65         html.ul ~ {
66             foreach (item; ["1", "2", "3"]) {
67                 html.li ~ item;
68             }
69         };
70     };
71     assert(output == `<ul><li>1</li><li>2</li><li>3</li></ul>`);
72 }
73 
74 /// Omit the tag name to append text directly.
75 @safe unittest {
76     string output = buildHTML() ~ (html) {
77         html ~ "Hello, ";
78         html.strong ~ "World!";
79     };
80     assert(output == "Hello, <strong>World!</strong>");
81 }
82 
83 /// Keep your document safe from XSS injections: all HTML content is escaped automatically.
84 ///
85 /// Warning: Elemi will $(B not) escape
86 /// [javascript links](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript).
87 /// If you let your users input URLs, make sure to block the `javascript:` schema.
88 @safe unittest {
89     string output = buildHTML() ~ (html) {
90         html ~ "<script>alert('fool!')</script>";
91         html.input.attr("value", "\" can't quit me") ~ { };
92         html.div.classes("classes are <no exception>") ~ { };
93     };
94 
95     assert(output == `&lt;script&gt;alert(&#39;fool!&#39;)&lt;/script&gt;`
96         ~ `<input value="&quot; can&#39;t quit me"/>`
97         ~ `<div class="classes are &lt;no exception&gt;"></div>`);
98 }
99 
100 static if (withInterpolation) {
101     /// Interpolated expression strings, aka istrings, are supported.
102     unittest {
103         string output = buildHTML() ~ (html) {
104             const user = "<user>";
105             html ~ i"Hello $(user)";
106             html.div ~ i"Hello $(user)";
107             html.div.attr("name", i"$(user)") ~ { };
108             html.a.href(i"https://example.com/$(user)") ~ { };
109             html.a
110                 .classes("button", "special")
111                 .href("https://example.com/") ~ { };
112         };
113         assert(output == `Hello &lt;user&gt;`
114             ~ `<div>Hello &lt;user&gt;</div>`
115             ~ `<div name="&lt;user&gt;"></div>`
116             ~ `<a href="https://example.com/&lt;user&gt;"></a>`
117             ~ `<a class="button special" href="https://example.com/"></a>`);
118     }
119 }
120 
121 static if (withInterpolation) {
122 
123     /// This syntax can be mixed with elements generated by the old syntax.
124     @safe unittest {
125         string output = buildHTML() ~ (html) {
126             html ~ elem!"span"();
127             html.div ~ elem!"span"();
128             html ~ i"$(elem!"span"())";
129         };
130         assert(output == `<span></span>`
131             ~ `<div><span></span></div>`
132             ~ `<span></span>`);
133     }
134 
135 }
136 
137 /// Elemi works at compile time!
138 @safe unittest {
139     enum output = buildHTML ~ (html) {
140         html.div ~ {
141             html.p ~ "Hello, World!";
142         };
143     };
144     assert(output == "<div><p>Hello, World!</p></div>");
145 }
146 
147 
148 @safe unittest {
149     string output = buildHTML() ~ (html) {
150         html ~ elem!"p"("Hello, World!");
151     };
152 
153     assert(output == "<p>Hello, World!</p>");
154 }
155 
156 @safe unittest {
157     // const(edgecase)
158     const element = elem!"p"("Hello, World!");
159     string output = buildHTML() ~ (html) {
160         html ~ element;
161         html ~ i" with $(element)";
162     };
163 
164     assert(output == "<p>Hello, World!</p> with <p>Hello, World!</p>");
165 }
166 
167 @safe unittest {
168     // buildHTML() ~ null should work
169     void delegate(HTML) @safe htmlBuilder;
170     void delegate(HTML) @safe builder;
171 
172     string output1 = buildHTML() ~ htmlBuilder;
173     assert(output1 == "");
174 
175     string output2 = buildHTML() ~ (html) {
176         html ~ builder;
177     };
178     assert(output2 == "");
179 
180     string output3 = buildHTML ~ builder;
181     assert(output3 == "");
182 
183 }
184 
185 import std.meta;
186 import std.range;
187 import std.string;
188 import std.functional;
189 
190 import elemi.html;
191 import elemi.element;
192 import elemi.internal;
193 import elemi.attribute;
194 
195 static if (__traits(compiles, { import core.attribute : mustuse; })) {
196     import core.attribute : mustuse;
197 }
198 else {
199     alias mustuse = AliasSeq!();
200 }
201 
202 @safe:
203 
204 /// Generic XML or HTML tag.
205 @mustuse
206 struct Tag {
207 
208     /// Output target for this tag; XML/HTML code will be written to this destination.
209     DocumentOutput output;
210 
211     /// Name of the XML or HTML tag, for example `book` for `<book>` or `div` for `<div></div>`.
212     string tagName;
213 
214     /// If true, this is a "self-closing" tag. No child nodes or text can be added.
215     bool isSelfClosing;
216 
217     /// The tag can be marked as self-closing so it does not generate content nor an end tag.
218     unittest {
219         auto normal = buildHTML() ~ (html) {
220             Tag(html, "a") ~ { };
221         };
222         auto selfClosing = buildHTML() ~ (html) {
223             Tag(html, "a").makeSelfClosing() ~ { };
224         };
225 
226         assert(normal == "<a></a>");
227         assert(selfClosing == "<a/>");
228     }
229 
230     unittest {
231         auto normal = buildHTML() ~ (html) {
232             Tag(html, "a").attr("k", "k") ~ { };
233         };
234         auto selfClosing = buildHTML() ~ (html) {
235             Tag(html, "a").attr("k", "k").makeSelfClosing() ~ { };
236         };
237 
238         assert(normal == `<a k="k"></a>`);
239         assert(selfClosing == `<a k="k"/>`);
240     }
241 
242     /// True if an attribute has been added to this tag.
243     ///
244     /// This changes whether the whole opening tag will be added when content starts (no
245     /// attributes), or just the right bracket (with attributes).
246     bool withAttributes;
247 
248     alias output this;
249 
250     /// Returns:
251     ///     The same tag, but marked using [withAttributes]. This should be used to return from
252     ///     attribute-adding methods.
253     Tag attributed() const @safe {
254         return Tag(output, tagName, isSelfClosing, true);
255     }
256 
257     /// Returns:
258     ///     This tag, but edited to be self-closing.
259     Tag makeSelfClosing() const @safe {
260         return Tag(output, tagName, true, withAttributes);
261     }
262 
263     void opBinary(string op : "~")(typeof(null)) @safe {
264         begin();
265         end();
266     }
267 
268     void opBinary(string op : "~", T)(T content) @safe {
269         begin();
270         if (!isSelfClosing) {
271             static if (is(T : const Element)) {
272                 pushElementMarkup(content);
273             }
274             else {
275                 pushElementText(content);
276             }
277             end();
278         }
279     }
280 
281     static if (withInterpolation)
282     void opBinary(string op : "~", Ts...)(InterpolationHeader, Ts text) {
283         begin();
284         if (!isSelfClosing) {
285             pushElementText(text);
286             end();
287         }
288     }
289 
290     void opBinary(string op : "~")(void delegate() @safe builder) @safe {
291         begin();
292         if (!isSelfClosing && builder !is null) {
293             builder();
294         }
295         end();
296     }
297 
298     void opBinary(string op : "~")(void delegate() @system builder) @system {
299         begin();
300         if (!isSelfClosing && builder !is null) {
301             builder();
302         }
303         end();
304     }
305 
306     /// Specify attributes using strings as key-value pairs. Interpolated strings are also
307     /// accepted.
308     ///
309     /// Note:
310     ///     At present, when using interpolated strings, the equals sign MUST be part of the
311     ///     literal, and cannot be interpolated.
312     ///
313     ///     An expression like this is allowed: `i"$(key)=$(value)"`. However, if the
314     ///     equal sign is interpolated, it will produce malformed code. Thus, a piece like
315     ///     `i"$("key=value")"` would fail.
316     Tag opIndex(Ts...)(Ts args) @safe {
317         import std.algorithm : findSplit, filter, joiner;
318 
319         bool isValue;
320 
321         foreach (i, arg; args) {
322             alias A = typeof(arg);
323             static if (isInterpolation!(i, Ts)) {
324                 static if (is(A == InterpolationHeader)) {
325                     beginAttributes();
326                     withAttributes = true;
327                     isValue = false;
328                     pushElementMarkup(" ");
329                 }
330                 else static if (is(A == InterpolationFooter)) {
331                     if (isValue) {
332                         pushElementMarkup(`"`);
333                     }
334                     else {
335                         pushElementMarkup(`=""`);
336                     }
337                 }
338                 else static if (is(A == InterpolatedLiteral!text, string text)) {
339                     auto pair = text.findSplit("=");
340                     if (!isValue && pair) {
341                         pushElementText(pair[0]);
342                         pushElementMarkup(`="`);
343                         pushElementText(pair[2]);
344                         isValue = true;
345                     }
346                     else {
347                         pushElementText(text);
348                     }
349                 }
350                 else static if (isInputRange!A && is(ElementType!A : string)) {
351                     pushElementText(arg
352                         .filter!(a => a != "")
353                         .joiner(" "));
354                 }
355                 else {
356                     pushElementText(arg);
357                 }
358             }
359             else static if (is(A == string)) {
360                 auto pair = arg.findSplit("=");
361                 this = attr(pair[0], pair[2]);                
362             }
363             else static assert(false, "Unsupported argument type " ~ A.stringof);
364         }
365 
366         return attributed;
367     }
368 
369     /// Each string (or istring) defines a single attribute. The first equals-sign in the string
370     /// separates the key from the value:
371     unittest {
372         string output = buildHTML() ~ (html) {
373             html.div["id=mydiv", "class=apple orange"] ~ { };
374         };
375         assert(output == `<div id="mydiv" class="apple orange"></div>`);
376     }
377 
378     /// The equals sign is optional. If missing, the value will be empty.
379     unittest {
380         string output = buildHTML() ~ (html) {
381             html.input["type=checkbox", "checked"] ~ { };
382         };
383         assert(output == `<input type="checkbox" checked=""/>`);
384     }
385 
386     /// Interpolated strings are supported. Ranges of strings are automatically joined with spaces.
387     static if (withInterpolation) {
388         unittest {
389             int number = 1;
390             string[] classes = ["two", "three", "four"];
391             
392             string output = buildHTML() ~ (html) {
393                 html.div[i"id=item-$(number)", i"class=one $(classes) five", "checked"] ~ { };
394             };
395             assert(output == `<div id="item-1" class="one two three four five" checked=""></div>`);
396         }
397 
398         /// Attribute names can also be interpolated.
399         unittest {
400             string key = "data-foo";
401             string output1 = buildHTML() ~ (html) {
402                 html.div[i"$(key)=value"] ~ { };
403             };
404             assert(output1 == `<div data-foo="value"></div>`);
405 
406             string state = "checked";
407             string output2 = buildHTML() ~ (html) {
408                 html.input[i"$(state)", "type=checkbox"] ~ { };
409             };
410             assert(output2 == `<input checked="" type="checkbox"/>`);
411         }
412 
413         unittest {
414             string key = "data";
415             string output = buildHTML() ~ (html) {
416                 html.div[i"$(key)=$(key)=$(key)"] ~ { };
417             };
418             assert(output == `<div data="data=data"></div>`);
419         }
420 
421         static assert( __traits(compiles, Tag()[i"$(1)"]));
422         static assert(!__traits(compiles, Tag()[1]));
423     }
424 
425     /// Add an attribute to the element.
426     /// Params:
427     ///     name  = Name of the attribute.
428     ///     value = Value for the attribute. Supports istrings.
429     /// Returns:
430     ///     Tag builder.
431     Tag attr(string name, string value) @safe {
432         return attr(Attribute(name, value));
433     }
434 
435     /// ditto
436     static if (withInterpolation) {
437         Tag attr(Ts...)(string name, InterpolationHeader, Ts value) @safe {
438             beginAttributes();
439             pushElementMarkup(" ");
440             pushElementMarkup(name);
441             pushElementMarkup(`="`);
442             pushElementText(value);
443             pushElementMarkup(`"`);
444             return attributed;
445         }
446     }
447 
448     /// Add a prepared set of attributes to the element.
449     /// Params:
450     ///     attributes = Attributes to add to the element.
451     /// Returns:
452     ///     Tag builder.
453     Tag attr(Attribute[] attributes...) @safe {
454         beginAttributes();
455         foreach (attribute; attributes) {
456             pushElementMarkup(" ");
457             pushElementMarkup(attribute.name);
458             pushElementMarkup(`="`);
459             pushElementText(attribute.value);
460             pushElementMarkup(`"`);
461         }
462         return attributed;
463     }
464 
465     void begin() @safe {
466         if (tagName is null) return;
467         if (!withAttributes) {
468             pushElementMarkup("<");
469             pushElementMarkup(tagName);
470         }
471         if (isSelfClosing) {
472             pushElementMarkup("/>");
473         }
474         else {
475             pushElementMarkup(">");
476         }
477     }
478 
479     void beginAttributes() @safe {
480         if (tagName is null) return;
481         if (!withAttributes) {
482             pushElementMarkup("<");
483             pushElementMarkup(tagName);
484         }
485     }
486 
487     void end() @safe {
488         if (tagName is null) return;
489         if (!isSelfClosing) {
490             pushElementMarkup("</");
491             pushElementMarkup(tagName);
492             pushElementMarkup(">");
493         }
494     }
495 
496 }
497 
498 /// A wrapper over a function, with additional methods added to support XML/HTML generation.
499 struct DocumentOutput {
500 
501     /// Function to call to append a string fragment to the output.
502     void delegate(string fragment) @safe elementOutput;
503 
504     /// Append a string or [Element] to the stream.
505     /// Params:
506     ///     rhs = `string` or `Element` to append.
507     void opBinary(string op : "~", T : string)(T rhs) @safe {
508         static if (is(T : const Element)) {
509             pushElementMarkup(rhs);
510         }
511         else {
512             pushElementText(rhs);
513         }
514     }
515 
516     static if (withInterpolation) {
517         /// Write an [istring](https://dlang.org/spec/istring.html) to the stream.
518         /// Params:
519         ///     text = Text to write.
520         void opBinary(string op : "~", Ts...)(InterpolationHeader, Ts text) {
521             pushElementText(text);
522         }
523     }
524 
525     /// Low-level function to write elements into current document context. Raw markup can be used,
526     /// i.e. ("<b>Hi</b>")` will include unescaped HTML code.
527     ///
528     /// If escaping is desired, try [pushElementText].
529     ///
530     /// Params:
531     ///     content = Markup to output.
532     void pushElementMarkup(string content) @safe {
533         assert(elementOutput !is null,
534             "No Elemi context is currently active. Try prepending the document with "
535             ~ "`buildDocument() ~`.");
536         elementOutput(content);
537     }
538 
539     /// Low-level function to write escaped text.
540     ///
541     /// Any of the characters `<` (less than sign), `>` (greater than sign), `&` (ampersand),
542     /// `"` (quote mark), or `'` (apostrophe) will be replaced with the corresponding XML escape
543     /// code.
544     ///
545     /// Params:
546     ///     content = Content to write. Interpolated expression strings (istrings) are supported.
547     void pushElementText(string content) {
548         while (!content.empty) {
549             const nextMarkup = content.indexOfAny(`<>&"'`);
550 
551             // No markup remains to be escaped
552             if (nextMarkup == -1) {
553                 pushElementMarkup(content);
554                 return;
555             }
556 
557             // Escape the character
558             else {
559                 pushElementMarkup(content[0 .. nextMarkup]);
560                 pushElementMarkup(content[nextMarkup].escapeXML);
561                 content = content[nextMarkup + 1 .. $];
562             }
563 
564         }
565     }
566 
567     /// ditto
568     void pushElementText(Ts...)(Ts content) {
569 
570         import std.format.write;
571 
572         auto writer = EscapingElementWriter(this);
573 
574         foreach (item; content) {
575             static if (is(typeof(item) : Element)) {
576                 pushElementMarkup(item);
577             }
578             else {
579                 formattedWrite!"%s"(writer, item);
580             }
581         }
582 
583     }
584 
585     /// This output range writes escapes the text it writes.
586     private struct EscapingElementWriter {
587 
588         DocumentOutput output;
589 
590         void put(char content) {
591             if (auto escaped = escapeXML(content)) {
592                 output.pushElementMarkup(escaped);
593             }
594             else {
595                 immutable(char)[1] c = content;
596                 output.pushElementMarkup(c[]);
597             }
598         }
599 
600         void put(string content) {
601             output.pushElementText(content);
602         }
603 
604     }
605 
606 }
607 
608 /// Escape an ASCII character using HTML escape codes.
609 /// Returns:
610 ///     A corresponding HTML escape code, or null if there isn't one.
611 private string escapeXML(char ch) {
612     switch (ch) {
613         case '<':  return "&lt;";
614         case '>':  return "&gt;";
615         case '&':  return "&amp;";
616         case '"':  return "&quot;";
617         case '\'': return "&#39;";
618         default:   return null;
619     }
620 }
621 
622 /// HTML tag builder.
623 ///
624 /// Params:
625 ///     name = Name of the tag, for example `div`.
626 struct HTMLTag(string name) {
627 
628     /// True if the tag is one of HTML's
629     /// [void elements](https://developer.mozilla.org/en-US/docs/Glossary/Void_element).
630     /// Void elements do not have an end tag, and thus, cannot have child nodes.
631     enum isVoidTag = name == "area"
632         || name == "base"
633         || name == "br"
634         || name == "col"
635         || name == "command"
636         || name == "embed"
637         || name == "hr"
638         || name == "img"
639         || name == "input"
640         || name == "keygen"
641         || name == "link"
642         || name == "meta"
643         || name == "param"
644         || name == "source"
645         || name == "track"
646         || name == "wbr";
647 
648     /// XML tag wrapped by this struct; tag this struct corresponds to.
649     Tag tag;
650 
651     alias tag this;
652 
653     /// Create a new instance of this tag for the specified document output.
654     /// Params:
655     ///     output = Document output instance this generator should write to.
656     this(DocumentOutput output) {
657         this.tag = Tag(output, name, isVoidTag);
658     }
659 
660     /// Pass any other tag as an instance of this tag.
661     /// Params:
662     ///     tag = XML or HTML tag to wrap.
663     this(Tag tag) {
664         this.tag = tag;
665     }
666 
667     /// Add an attribute to this element. This function wraps [Tag.attr].
668     HTMLTag attr(Ts...)(Ts args) {
669         return HTMLTag(
670             tag.attr(args));
671     }
672 
673     /// Returns:
674     ///     The same tag, but marked using [Tag.withAttributes]. This should be used to return
675     ///     from attribute-adding methods.
676     HTMLTag attributed() {
677         return HTMLTag(
678             tag.attributed());
679     }
680 
681     /// Add an `id` attribute to the HTML tag.
682     /// Params:
683     ///     value = Value to use for the attribute. Supports istrings.
684     /// Returns:
685     ///     Tag builder, for chaining.
686     HTMLTag id(string value) @safe {
687         return attr("id", value);
688     }
689 
690     static if (withInterpolation) {
691         /// ditto
692         HTMLTag id(Ts...)(InterpolationHeader header, Ts value) @safe {
693             return attr("id", header, value);
694         }
695     }
696 
697     /// Add a `class` attribute to a HTML tag from an array of strings.
698     /// Params:
699     ///     values = Classes to write, as an array. This array will be joined with spaces.
700     /// Returns:
701     ///     Tag builder, for chaining.
702     HTMLTag classes(string[] values...) @safe {
703         beginAttributes();
704         pushElementMarkup(` class="`);
705         foreach (i, value; values) {
706             if (i) pushElementMarkup(" ");
707             pushElementText(value);
708         }
709         pushElementMarkup(`"`);
710         return attributed;
711     }
712 
713     /// Set the "href" attribute for an `<a>` element.
714     /// Params:
715     ///     value = Value to use for the attribute. Supports istrings.
716     /// Returns:
717     ///     A tag builder.
718     HTMLTag href(string value) @safe {
719         return attr("href", value);
720     }
721 
722     static if (withInterpolation) {
723         /// ditto
724         HTMLTag href(Ts...)(InterpolationHeader header, Ts value) @safe {
725             return attr("href", header, value);
726         }
727     }
728 
729 }
730 
731 /// Write HTML elements to an output range.
732 /// Params:
733 ///     range = Output range to write the output to. If not given, the builder will return
734 ///         a string.
735 /// Returns:
736 ///     A struct that accepts an element block `~ (html) { }` and writes the content to the output
737 ///     range.
738 /// See_Also:
739 ///     [HTML], the stream structure used to output data.
740 HTML buildHTML(T)(ref T range)
741 if (isOutputRange!(T, char))
742 do {
743     return HTML(
744         DocumentOutput(fragment => put(range, fragment)));
745 }
746 
747 /// Write HTML elements to a string or [Element].
748 ///
749 /// Returns:
750 ///     A struct that accepts an element block `~ (html) { }` and writes the content to a string.
751 ///
752 ///     The resulting content will be wrapped in an [Element], making it compatible with the
753 ///     [old elemi API](elemi.html.html). Note that metadata is not preserved, so adding
754 ///     attributes or child nodes from `Element` will not be possible.
755 /// See_Also:
756 ///     [HTML], the stream structure used to output data.
757 TextHTML buildHTML() @safe {
758     return TextHTML();
759 }
760 
761 /// If no arguments are specified, `buildHTML()` will output to a string. Under the hood,
762 /// it uses an [std.array.Appender].
763 @safe unittest {
764     import std.array;
765     string stringOutput = buildHTML() ~ (html) {
766         html.p ~ "Hello!";
767     };
768 
769     Appender!string rangeOutput;
770     buildHTML(rangeOutput) ~ (html) {
771         html.p ~ "Hello!";
772     };
773     assert(stringOutput == rangeOutput[]);
774 }
775 
776 /// Supporting structure for [buildHTML].
777 struct TextHTML {
778     import std.array;
779 
780     Element opBinary(string op : "~")(void delegate(HTML) @system rhs) const @system {
781         Appender!string output;
782         if (rhs !is null) {
783             rhs(
784                 HTML(
785                     DocumentOutput(fragment => output ~= fragment)));
786         }
787         return elemTrusted(output[]);
788     }
789 
790     Element opBinary(string op : "~")(void delegate(HTML) @safe rhs) const @safe {
791         Appender!string output;
792         if (rhs !is null) {
793             rhs(
794                 HTML(
795                     DocumentOutput(fragment => output ~= fragment)));
796         }
797         return elemTrusted(output[]);
798     }
799 
800 }
801 
802 /// A set of HTML tags to build documents with.
803 ///
804 /// This structure includes a set of methods for creating HTML tags.
805 struct HTML {
806 
807     DocumentOutput documentOutput;
808 
809     alias documentOutput this;
810 
811     /// Write a string to the output stream.
812     /// Params:
813     ///     rhs = String to write.
814     void opBinary(string op : "~", T : string)(T rhs) @safe {
815         documentOutput ~ rhs;
816     }
817 
818     static if (withInterpolation) {
819         /// Write an [istring](https://dlang.org/spec/istring.html) to the output stream.
820         /// Params:
821         ///     rhs = String to write.
822         void opBinary(string op : "~", Ts...)(InterpolationHeader, Ts text) {
823             pushElementText(text);
824         }
825     }
826 
827     /// Pass self to a function.
828     /// Params:
829     ///     rhs = The function to read the output.
830     void opBinary(string op : "~")(void delegate(HTML o) @system rhs) @system {
831         if (rhs !is null) {
832             rhs(this);
833         }
834     }
835 
836     /// ditto
837     void opBinary(string op : "~")(void delegate(HTML o) @safe rhs) @safe {
838         if (rhs !is null) {
839             rhs(this);
840         }
841     }
842 
843     @safe:
844 
845     ///
846     HTMLTag!"a" a() {
847         return typeof(return)(this);
848     }
849     ///
850     HTMLTag!"abbr" abbr() {
851         return typeof(return)(this);
852     }
853     ///
854     HTMLTag!"acronym" acronym() {
855         return typeof(return)(this);
856     }
857     ///
858     HTMLTag!"address" address() {
859         return typeof(return)(this);
860     }
861     ///
862     HTMLTag!"area" area() {
863         return typeof(return)(this);
864     }
865     ///
866     HTMLTag!"article" article() {
867         return typeof(return)(this);
868     }
869     ///
870     HTMLTag!"aside" aside() {
871         return typeof(return)(this);
872     }
873     ///
874     HTMLTag!"audio" audio() {
875         return typeof(return)(this);
876     }
877     ///
878     HTMLTag!"b" b() {
879         return typeof(return)(this);
880     }
881     ///
882     HTMLTag!"base" base() {
883         return typeof(return)(this);
884     }
885     ///
886     HTMLTag!"bdi" bdi() {
887         return typeof(return)(this);
888     }
889     ///
890     HTMLTag!"bdo" bdo() {
891         return typeof(return)(this);
892     }
893     ///
894     HTMLTag!"big" big() {
895         return typeof(return)(this);
896     }
897     ///
898     HTMLTag!"blockquote" blockquote() {
899         return typeof(return)(this);
900     }
901     ///
902     HTMLTag!"body" body() {
903         return typeof(return)(this);
904     }
905     ///
906     HTMLTag!"br" br() {
907         return typeof(return)(this);
908     }
909     ///
910     HTMLTag!"button" button() {
911         return typeof(return)(this);
912     }
913     ///
914     HTMLTag!"canvas" canvas() {
915         return typeof(return)(this);
916     }
917     ///
918     HTMLTag!"caption" caption() {
919         return typeof(return)(this);
920     }
921     ///
922     HTMLTag!"center" center() {
923         return typeof(return)(this);
924     }
925     ///
926     HTMLTag!"cite" cite() {
927         return typeof(return)(this);
928     }
929     ///
930     HTMLTag!"code" code() {
931         return typeof(return)(this);
932     }
933     ///
934     HTMLTag!"col" col() {
935         return typeof(return)(this);
936     }
937     ///
938     HTMLTag!"colgroup" colgroup() {
939         return typeof(return)(this);
940     }
941     ///
942     HTMLTag!"command" command() {
943         return typeof(return)(this);
944     }
945     ///
946     HTMLTag!"data" data() {
947         return typeof(return)(this);
948     }
949     ///
950     HTMLTag!"datalist" datalist() {
951         return typeof(return)(this);
952     }
953     ///
954     HTMLTag!"dd" dd() {
955         return typeof(return)(this);
956     }
957     ///
958     HTMLTag!"del" del() {
959         return typeof(return)(this);
960     }
961     ///
962     HTMLTag!"details" details() {
963         return typeof(return)(this);
964     }
965     ///
966     HTMLTag!"dfn" dfn() {
967         return typeof(return)(this);
968     }
969     ///
970     HTMLTag!"dialog" dialog() {
971         return typeof(return)(this);
972     }
973     ///
974     HTMLTag!"dir" dir() {
975         return typeof(return)(this);
976     }
977     ///
978     HTMLTag!"div" div() {
979         return typeof(return)(this);
980     }
981     ///
982     HTMLTag!"dl" dl() {
983         return typeof(return)(this);
984     }
985     ///
986     HTMLTag!"dt" dt() {
987         return typeof(return)(this);
988     }
989     ///
990     HTMLTag!"em" em() {
991         return typeof(return)(this);
992     }
993     ///
994     HTMLTag!"embed" embed() {
995         return typeof(return)(this);
996     }
997     ///
998     HTMLTag!"fencedframe" fencedframe() {
999         return typeof(return)(this);
1000     }
1001     ///
1002     HTMLTag!"fieldset" fieldset() {
1003         return typeof(return)(this);
1004     }
1005     ///
1006     HTMLTag!"figcaption" figcaption() {
1007         return typeof(return)(this);
1008     }
1009     ///
1010     HTMLTag!"figure" figure() {
1011         return typeof(return)(this);
1012     }
1013     ///
1014     HTMLTag!"font" font() {
1015         return typeof(return)(this);
1016     }
1017     ///
1018     HTMLTag!"footer" footer() {
1019         return typeof(return)(this);
1020     }
1021     ///
1022     HTMLTag!"form" form() {
1023         return typeof(return)(this);
1024     }
1025     ///
1026     HTMLTag!"frame" frame() {
1027         return typeof(return)(this);
1028     }
1029     ///
1030     HTMLTag!"frameset" frameset() {
1031         return typeof(return)(this);
1032     }
1033     ///
1034     HTMLTag!"h1" h1() {
1035         return typeof(return)(this);
1036     }
1037     ///
1038     HTMLTag!"h2" h2() {
1039         return typeof(return)(this);
1040     }
1041     ///
1042     HTMLTag!"h3" h3() {
1043         return typeof(return)(this);
1044     }
1045     ///
1046     HTMLTag!"h4" h4() {
1047         return typeof(return)(this);
1048     }
1049     ///
1050     HTMLTag!"h5" h5() {
1051         return typeof(return)(this);
1052     }
1053     ///
1054     HTMLTag!"h6" h6() {
1055         return typeof(return)(this);
1056     }
1057     ///
1058     HTMLTag!"head" head() {
1059         return typeof(return)(this);
1060     }
1061     ///
1062     HTMLTag!"header" header() {
1063         return typeof(return)(this);
1064     }
1065     ///
1066     HTMLTag!"hgroup" hgroup() {
1067         return typeof(return)(this);
1068     }
1069     ///
1070     HTMLTag!"hr" hr() {
1071         return typeof(return)(this);
1072     }
1073     ///
1074     HTMLTag!"html" html() {
1075         return typeof(return)(this);
1076     }
1077     ///
1078     HTMLTag!"i" i() {
1079         return typeof(return)(this);
1080     }
1081     ///
1082     HTMLTag!"iframe" iframe() {
1083         return typeof(return)(this);
1084     }
1085     ///
1086     HTMLTag!"img" img() {
1087         return typeof(return)(this);
1088     }
1089     ///
1090     HTMLTag!"input" input() {
1091         return typeof(return)(this);
1092     }
1093     ///
1094     HTMLTag!"ins" ins() {
1095         return typeof(return)(this);
1096     }
1097     ///
1098     HTMLTag!"kbd" kbd() {
1099         return typeof(return)(this);
1100     }
1101     ///
1102     HTMLTag!"keygen" keygen() {
1103         return typeof(return)(this);
1104     }
1105     ///
1106     HTMLTag!"label" label() {
1107         return typeof(return)(this);
1108     }
1109     ///
1110     HTMLTag!"legend" legend() {
1111         return typeof(return)(this);
1112     }
1113     ///
1114     HTMLTag!"li" li() {
1115         return typeof(return)(this);
1116     }
1117     ///
1118     HTMLTag!"link" link() {
1119         return typeof(return)(this);
1120     }
1121     ///
1122     HTMLTag!"main" main() {
1123         return typeof(return)(this);
1124     }
1125     ///
1126     HTMLTag!"map" map() {
1127         return typeof(return)(this);
1128     }
1129     ///
1130     HTMLTag!"mark" mark() {
1131         return typeof(return)(this);
1132     }
1133     ///
1134     HTMLTag!"marquee" marquee() {
1135         return typeof(return)(this);
1136     }
1137     ///
1138     HTMLTag!"math" math() {
1139         return typeof(return)(this);
1140     }
1141     ///
1142     HTMLTag!"menu" menu() {
1143         return typeof(return)(this);
1144     }
1145     ///
1146     HTMLTag!"meta" meta() {
1147         return typeof(return)(this);
1148     }
1149     ///
1150     HTMLTag!"meter" meter() {
1151         return typeof(return)(this);
1152     }
1153     ///
1154     HTMLTag!"nav" nav() {
1155         return typeof(return)(this);
1156     }
1157     ///
1158     HTMLTag!"nobr" nobr() {
1159         return typeof(return)(this);
1160     }
1161     ///
1162     HTMLTag!"noembed" noembed() {
1163         return typeof(return)(this);
1164     }
1165     ///
1166     HTMLTag!"noframes" noframes() {
1167         return typeof(return)(this);
1168     }
1169     ///
1170     HTMLTag!"noscript" noscript() {
1171         return typeof(return)(this);
1172     }
1173     ///
1174     HTMLTag!"object" object() {
1175         return typeof(return)(this);
1176     }
1177     ///
1178     HTMLTag!"ol" ol() {
1179         return typeof(return)(this);
1180     }
1181     ///
1182     HTMLTag!"optgroup" optgroup() {
1183         return typeof(return)(this);
1184     }
1185     ///
1186     HTMLTag!"option" option() {
1187         return typeof(return)(this);
1188     }
1189     ///
1190     HTMLTag!"output" output() {
1191         return typeof(return)(this);
1192     }
1193     ///
1194     HTMLTag!"p" p() {
1195         return typeof(return)(this);
1196     }
1197     ///
1198     HTMLTag!"param" param() {
1199         return typeof(return)(this);
1200     }
1201     ///
1202     HTMLTag!"picture" picture() {
1203         return typeof(return)(this);
1204     }
1205     ///
1206     HTMLTag!"plaintext" plaintext() {
1207         return typeof(return)(this);
1208     }
1209     ///
1210     HTMLTag!"pre" pre() {
1211         return typeof(return)(this);
1212     }
1213     ///
1214     HTMLTag!"progress" progress() {
1215         return typeof(return)(this);
1216     }
1217     ///
1218     HTMLTag!"q" q() {
1219         return typeof(return)(this);
1220     }
1221     ///
1222     HTMLTag!"rb" rb() {
1223         return typeof(return)(this);
1224     }
1225     ///
1226     HTMLTag!"rp" rp() {
1227         return typeof(return)(this);
1228     }
1229     ///
1230     HTMLTag!"rt" rt() {
1231         return typeof(return)(this);
1232     }
1233     ///
1234     HTMLTag!"rtc" rtc() {
1235         return typeof(return)(this);
1236     }
1237     ///
1238     HTMLTag!"ruby" ruby() {
1239         return typeof(return)(this);
1240     }
1241     ///
1242     HTMLTag!"s" s() {
1243         return typeof(return)(this);
1244     }
1245     ///
1246     HTMLTag!"samp" samp() {
1247         return typeof(return)(this);
1248     }
1249     ///
1250     HTMLTag!"script" script() {
1251         return typeof(return)(this);
1252     }
1253     ///
1254     HTMLTag!"search" search() {
1255         return typeof(return)(this);
1256     }
1257     ///
1258     HTMLTag!"section" section() {
1259         return typeof(return)(this);
1260     }
1261     ///
1262     HTMLTag!"select" select() {
1263         return typeof(return)(this);
1264     }
1265     ///
1266     HTMLTag!"slot" slot() {
1267         return typeof(return)(this);
1268     }
1269     ///
1270     HTMLTag!"small" small() {
1271         return typeof(return)(this);
1272     }
1273     ///
1274     HTMLTag!"source" source() {
1275         return typeof(return)(this);
1276     }
1277     ///
1278     HTMLTag!"span" span() {
1279         return typeof(return)(this);
1280     }
1281     ///
1282     HTMLTag!"strike" strike() {
1283         return typeof(return)(this);
1284     }
1285     ///
1286     HTMLTag!"strong" strong() {
1287         return typeof(return)(this);
1288     }
1289     ///
1290     HTMLTag!"style" style() {
1291         return typeof(return)(this);
1292     }
1293     ///
1294     HTMLTag!"sub" sub() {
1295         return typeof(return)(this);
1296     }
1297     ///
1298     HTMLTag!"summary" summary() {
1299         return typeof(return)(this);
1300     }
1301     ///
1302     HTMLTag!"sup" sup() {
1303         return typeof(return)(this);
1304     }
1305     ///
1306     HTMLTag!"svg" svg() {
1307         return typeof(return)(this);
1308     }
1309     ///
1310     HTMLTag!"table" table() {
1311         return typeof(return)(this);
1312     }
1313     ///
1314     HTMLTag!"tbody" tbody() {
1315         return typeof(return)(this);
1316     }
1317     ///
1318     HTMLTag!"td" td() {
1319         return typeof(return)(this);
1320     }
1321     ///
1322     HTMLTag!"template" template_() {
1323         return typeof(return)(this);
1324     }
1325     ///
1326     HTMLTag!"textarea" textarea() {
1327         return typeof(return)(this);
1328     }
1329     ///
1330     HTMLTag!"tfoot" tfoot() {
1331         return typeof(return)(this);
1332     }
1333     ///
1334     HTMLTag!"th" th() {
1335         return typeof(return)(this);
1336     }
1337     ///
1338     HTMLTag!"thead" thead() {
1339         return typeof(return)(this);
1340     }
1341     ///
1342     HTMLTag!"time" time() {
1343         return typeof(return)(this);
1344     }
1345     ///
1346     HTMLTag!"title" title() {
1347         return typeof(return)(this);
1348     }
1349     ///
1350     HTMLTag!"tr" tr() {
1351         return typeof(return)(this);
1352     }
1353     ///
1354     HTMLTag!"track" track() {
1355         return typeof(return)(this);
1356     }
1357     ///
1358     HTMLTag!"tt" tt() {
1359         return typeof(return)(this);
1360     }
1361     ///
1362     HTMLTag!"u" u() {
1363         return typeof(return)(this);
1364     }
1365     ///
1366     HTMLTag!"ul" ul() {
1367         return typeof(return)(this);
1368     }
1369     ///
1370     HTMLTag!"var" var() {
1371         return typeof(return)(this);
1372     }
1373     ///
1374     HTMLTag!"video" video() {
1375         return typeof(return)(this);
1376     }
1377     ///
1378     HTMLTag!"wbr" wbr() {
1379         return typeof(return)(this);
1380     }
1381     ///
1382     HTMLTag!"xmp" xmp() {
1383         return typeof(return)(this);
1384     }
1385 
1386 }