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 }