1 module dhtags.tags.define; 2 3 /** 4 * Define tag classes from a tag name (e.g. "div") and some properties. 5 * This allows us to generate all our required classes with just a list of names. 6 */ 7 template DefineTag(alias name, bool isVoidTag = false) { 8 import dhtags.tags.tag : HtmlTag, TagProperties; 9 import dhtags.attrs.attribute : HtmlAttribute; 10 import std.traits : isSomeString; 11 import std..string : format, replace, capitalize; 12 13 static if (isSomeString!(typeof(name))) { 14 // If a single name is provided it will be both the tag symbol and name 15 enum tagSymbol = name; 16 enum tagName = name; 17 } else { 18 // Otherwise assume an array or tuple (symbol, name) was provided 19 enum tagSymbol = name[0]; 20 static if (name.length > 1) { enum tagName = name[1]; } 21 else { enum tagName = tagSymbol; } 22 } 23 24 enum capitalizedTagName = capitalize(tagName); 25 26 mixin(format(q{ 27 /** 28 * Mixin an html tag, e.g. 'DivTag' 29 */ 30 static class _%1$sTag : HtmlTag { 31 this() {} 32 33 /** 34 * Create an tag with an attribute array and a child tag list. 35 * This constructor is called when the attributes and children are passed in successively using the function call operator. 36 * e.g. div(id="Foo")("Bar") 37 */ 38 this(Children...)(HtmlAttribute[] attrs, Children c) { 39 this.attrs = attrs; 40 setChildren(c); 41 } 42 43 override @property const(_%1$sTagProperties) tag() const { return _tag; } 44 45 private { 46 /** 47 * Mixin some helper properties for this tag. 48 */ 49 static class _%1$sTagProperties : TagProperties { 50 mixin TagImpl; 51 } 52 53 auto const _tag = new typeof(tag); 54 } 55 } 56 57 /** 58 * Mixin an html tag builder, e.g. 'DivTagBuilder' 59 * Defining a separate builder allows us to better control the amount of successive parameter lists a tag accepts. 60 */ 61 static class _%1$sTagBuilder(bool isAttributeList) : _%1$sTag { 62 /** 63 * Create an tag by supplying an attribute list. 64 * This constructor is always called for a tag with attributes. 65 */ 66 this(Attrs...)(Attrs attrs) { 67 createAttrs(attrs); 68 } 69 70 /** 71 * If attributes were supplied, then any child tags are passed in using the overloaded function call operator. 72 */ 73 auto ref opCall(Children...)(Children c) { 74 return new _%1$sTag(attrs, c); 75 } 76 77 /** 78 * Build a tag with either an attribute list or children list. 79 * If it's a children list then we assume this tag has no attributes. 80 */ 81 static auto ref build(Args...)(Args a) { 82 static if (isAttributeList) { 83 return new _%1$sTagBuilder!(isAttributeList)(a); 84 } else { 85 return new _%1$sTag([], a); 86 } 87 } 88 } 89 90 mixin TagHelper!(tagSymbol, tagName); 91 92 }, capitalizedTagName)); 93 94 /** 95 * We separate the implementation so the 'replace' function doesn't have to parse irrelevant text 96 * which slows down compilation. 97 */ 98 mixin template TagImpl() { 99 string name() const { return tagName; } 100 string before() const { static if (tagSymbol == "htmlWithDoctype") { return "<!DOCTYPE html>"; } else { return ""; } } 101 string open(string attrs) const { return (attrs.length) ? format("<%s %s>", tagName, attrs) : format("<%s>", tagName); } 102 string close() const { return (!isVoidTag) ? format("</%s>", tagName) : ""; } 103 bool isVoid() const { return isVoidTag; } 104 } 105 106 /** 107 * Generate a helper function to create the tag using just the tag name instead of the full class name. 108 */ 109 mixin template TagHelper(string symbol, string name) { 110 import dhtags.attrs.attribute : Attr; 111 import dhtags.utils.range : isAttrRange, isAttrArray; 112 113 mixin template ExamineArguments() { 114 // TODO: Find a way to combine these into a compile-time function 115 116 static bool isAttribute(alias a)() { 117 return is(typeof(a) : Attr) || isAttrRange!(typeof(a)) || isAttrArray!(typeof(a)); 118 } 119 120 bool doesContainAttributes(int i = 0)() { 121 static if (i < d.length) { return isAttribute!(d[i]) || doesContainAttributes!(i + 1); } 122 else { return false; } 123 } 124 125 bool doesContainChildren(int i = 0)() { 126 static if (i < d.length) { return !isAttribute!(d[i]) || doesContainChildren!(i + 1); } 127 else { return false; } 128 } 129 } 130 131 mixin(format(q{ 132 auto %s(Args...)(Args d) { 133 mixin ExamineArguments; 134 135 enum isAttributeList = doesContainAttributes; 136 137 static if (isAttributeList && doesContainChildren) { 138 static assert(false, format(`Error: Tag '%%s' mixes attributes with content`, name)); 139 } 140 141 return _%sTagBuilder!(isAttributeList).build(d); 142 } 143 }, symbol, capitalize(name))); 144 } 145 } 146 147 template DefineVoidTag(alias name) { 148 alias DefineVoidTag = DefineTag!(name, true); 149 }