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 }