1 module dhtags.tags.tag;
2 
3 import dhtags.tags.properties;
4 import dhtags.attrs.attribute : HtmlAttribute, Attr;
5 import dhtags.utils.html;
6 import dhtags.utils.prettyprint;
7 
8 import std..string : format;
9 import std.array : array, join;
10 import std.range.primitives : isInputRange, ElementType;
11 import std.traits : isSomeString, isArray;
12 
13 interface TagProperties {
14    string name() const;
15    string before() const;
16    string open(string attrs) const;
17    string close() const;
18    bool isVoid() const;
19 }
20 
21 interface HtmlFragment {
22    string toString() const;
23    string toString(bool escaped) const;
24    string toPrettyString(bool escaped);
25 
26    mixin PrettyPrintFuncs;
27 }
28 
29 /**
30  * Bare-bones html tag with children and attributes.
31  */
32 alias Tag = HtmlTag;
33 class HtmlTag : HtmlFragment {
34    import std.algorithm : map, each;
35    import std.conv : to;
36 
37    HtmlAttribute[] attrs;
38    HtmlFragment[] children;
39 
40    override string toString() const {
41       return toString(true);
42    }
43 
44    /**
45     * Generate an HTML string by iterating through all of the tag's children.
46     */
47    string toString(bool escaped) const {
48       return
49          tag.before ~
50          tag.open(attrsToString) ~
51          children.map!(x => x.toString(escaped)).join ~
52          tag.close;
53    }
54 
55    /**
56     * Generate a pretty HTML string.
57     */
58    string toPrettyString(bool escaped = true) {
59       import std.range : enumerate;
60 
61       auto hasOneString = children.length == 1 && isHtmlString(children[0]);
62       if (hasOneString) {
63          children[0].isOnlyChild = true;
64       }
65 
66       auto newline = "\n";
67       auto indent = createIndent.join;
68 
69       auto childIndent = (children.length) ? HtmlProperties.Indent : "";
70       auto childNewline = children.length > 0 && !hasOneString;
71       auto childLength = children.length;
72       auto thisDepth = depth;
73 
74       return
75          tag.before ~
76          indent ~ // Indent opening tag to proper depth
77          tag.open(attrsToString) ~ // Open tag with attributes
78          ((childNewline) ? newline : "") ~ // Put children on new line if this tag has at least one child which is not a string
79          children.enumerate.map!(x =>
80             x.value
81             .hasNextSibling(x.index != childLength - 1)
82             .depth(thisDepth + 1)
83             .toPrettyString(escaped)
84          ).join ~
85          ((childNewline) ? newline ~ indent : "") ~ // Put closing tag on indented new line to match opening tag
86          tag.close ~ // Close tag
87          ((hasNextSibling) ? newline : ""); // Put starting tag of next sibling on new line
88    }
89 
90    protected {
91       void createAttrs(AttrPairs...)(AttrPairs attrPairs) {
92          import dhtags.utils.range : isAttrRange, isAttrArray;
93          import std.algorithm : map;
94 
95          void iteratePairs(int i = 0)() {
96             static if (i < attrPairs.length) {
97                alias T = typeof(attrPairs[i]);
98 
99                static if (isAttrRange!(T)) {
100                   attrs ~= (attrPairs[i])[].map!(x => x.create).array;
101                } else static if (isAttrArray!(T)) {
102                   foreach (x; attrPairs[i]) { attrs ~= x.create; }
103                } else {
104                   attrs ~= attrPairs[i].create;
105                }
106 
107                iteratePairs!(i + 1);
108             }
109          }
110 
111          iteratePairs;
112       }
113 
114       void setChildren(Children...)(Children c) {
115          this.children = verifyChildren(c);
116 
117          if (tag.isVoid) {
118             assert(children.length == 0, format("Void tag %s has %d children (should be 0)", tag.name, children.length));
119          }
120       }
121 
122       @property const(TagProperties) tag() const {
123          assert(false, format("Tag for fragment '%s' not found", this.stringof));
124       }
125 
126       string attrsToString() const { return attrs.map!(x => x.toString).join(" "); }
127    }
128 
129    mixin PrettyPrintImpl;
130 
131    private {
132       enum ChildType : string {
133          FRAGMENT = "Fragment",
134          FRAGMENT_RANGE = "FragmentRange",
135          FRAGMENT_ARRAY = "FragmentArray",
136          STRING = "string",
137          BUILTIN = "built-in",
138          UNKNOWN = "unknown"
139       }
140 
141       /**
142        * Verify that the tag's children are either tags, strings, or built-in types that can be converted to strings.
143        */
144       HtmlFragment[] verifyChildren(Children...)(Children children) {
145          import std.traits : isBuiltinType;
146          import dhtags.utils.range : isRange;
147 
148          HtmlFragment[] frags;
149 
150          foreach (c; children) {
151             alias T = typeof(c);
152             string type;
153 
154             static if (is(T : HtmlFragment) || is(T : const(HtmlFragment))) {
155                frags ~= cast(HtmlFragment) c;
156                type = ChildType.FRAGMENT;
157             } else static if (isRange!(T) && !is(T : string)) {
158                alias EType = ElementType!(T);
159 
160                static if (is(EType : HtmlFragment) || is(EType : const(HtmlFragment))) {
161                   c.each!(x => frags ~= x);
162                } else static if (is(EType : string)) {
163                   c.each!(x => frags ~= new HtmlString(x));
164                } else static if (isBuiltinType!(EType)) {
165                   c.each!(x => frags ~= new HtmlString(x.to!string));
166                } else {
167                   static assert(false, format("Unknown array type %s found in %s", T.stringof, tag.name));
168                }
169 
170                type = ChildType.FRAGMENT_RANGE;
171             } else static if (isArray!(T) && !is(T : string)) {
172                alias EType = ElementType!(T);
173 
174                static if (is(EType : HtmlFragment) || is(EType : const(HtmlFragment))) {
175                   foreach (x; c) { frags ~= x; }
176                } else static if (is(EType : string)) {
177                   foreach (x; c) { frags ~= new HtmlString(x); }
178                } else static if (isBuiltinType!(EType)) {
179                   foreach (x; c) { frags ~= new HtmlString(x.to!string); }
180                } else {
181                   static assert(false, format("Unknown array type %s found in %s", T.stringof, tag.name));
182                }
183 
184                type = ChildType.FRAGMENT_ARRAY;
185             } else static if (is(T : string)) {
186                frags ~= new HtmlString(c);
187                type = ChildType.STRING;
188             } else static if (isBuiltinType!(T)) {
189                frags ~= new HtmlString(c.to!string);
190                type = ChildType.BUILTIN;
191             } else {
192                static assert(false, format("Unknown tag %s found", T.stringof));
193             }
194 
195             debug {
196                // printChild(c, type);
197             }
198          }
199 
200          return frags;
201       }
202 
203       bool isHtmlString(inout HtmlFragment frag) inout {
204          return (cast(HtmlString) frag) !is null;
205       }
206 
207       void print() {
208          import std.stdio : writeln;
209          debug {
210             if (!__ctfe) {
211                writeln("Tag: ", tag.name);
212                writeln("Attributes: ", attrs);
213                writeln("Children: ", children);
214             }
215          }
216       }
217 
218       /**
219        * Print child with its type
220        */
221       void printChild(T)(T child, string type = "") {
222          if (!__ctfe) {
223             import std.stdio : writeln;
224             writeln(format("[%s -> %s]: ", T.stringof, type), child);
225          }
226       }
227    }
228 }
229 
230 /**
231  * Wraps a string that appears inside a tag's contents.
232  */
233 class HtmlString : HtmlFragment {
234    string text;
235 
236    this(string text) {
237       this.text = text;
238    }
239 
240    override string toString() const {
241       return toString(true);
242    }
243 
244    string toString(bool escaped) const {
245       return (escaped) ? text.escape : text;
246    }
247 
248    string toPrettyString(bool escaped = true) {
249       return (!isOnlyChild) ? createIndent.join ~ toString(escaped)  ~ "\n" : toString(escaped);
250    }
251 
252    mixin PrettyPrintImpl;
253 }