1 module dhtags.attrs.attribute;
2 
3 import dhtags.utils.html;
4 
5 import std..string : format, toLower;
6 import std.array : join;
7 import std.algorithm : map;
8 import std.typecons : Nullable, Typedef;
9 import std.conv : to;
10 
11 alias BoolAttribute = Typedef!string;
12 
13 struct HtmlAttribute {
14    string name;
15    string value;
16 
17    this(string name, string value) {
18       this.name = name;
19       this.value = value;
20    }
21 
22    string toString() const {
23       return format(q{%s="%s"}, name, value);
24    }
25 
26    /**
27     * Find if a value is allowed for attributes that define specific values
28     */
29    mixin template FindAllowedValue(Attr, bool ct = false) {
30       template toSymbol(string name) {
31          mixin(format(q{ enum allowedValue = Attr.%s; }, name));
32       }
33 
34       bool isValueAllowed(int i = 0)() {
35          static if (i < Attr.TValues.length) {
36             enum allowedName = "_allowedValue" ~ i.to!string;
37             static if (__traits(hasMember, Attr, allowedName)) {
38                mixin toSymbol!allowedName;
39                return (allowedValue == strValue.toLower) || isValueAllowed!(i + 1);
40             } else {
41                return false || isValueAllowed!(i + 1);
42             }
43          } else {
44             return false;
45          }
46       }
47 
48       auto findAllowedValue() {
49          auto msg = format("'%s' is not a defined value for %s (must be one of the following: %s)", attr.value.to!string, Attr.stringof, Attr.TValues.stringof);
50          static if (ct) {
51             static assert(isValueAllowed, msg);
52          } else {
53             assert(isValueAllowed, msg);
54          }
55 
56          return HtmlAttribute(attr.name, strValue.escape);
57       }
58    }
59 
60    /** 
61     * Convert an attribute value to a string
62     */
63    mixin template ConvertToString(TVal, bool ct = false) {
64       import std.traits : isSomeString, isArray;
65 
66       auto convertToString() {
67          static if (is(TVal == BoolAttribute)) {
68             if (attr.value) {
69                return HtmlAttribute(attr.name, attr.name);
70             }
71          } else static if (!isSomeString!TVal && isArray!TVal && !typeof(attr).hasCustomAttr) {
72             return HtmlAttribute(attr.name, attr.value.map!(x => asString!x).join(" ").escape);
73          } else {
74             return HtmlAttribute(attr.name, strValue.escape);
75          }
76 
77          assert(false, format("Could not convert value for attribute %s to string", attr.name));
78       }
79    }
80 
81    /**
82     * Creates an HtmlAttribute from an AttrPair
83     */
84    static auto create(alias attr)() {
85       Nullable!(typeof(this)) result;
86       string strValue;
87 
88       if (__ctfe) {
89          enum value = attr.value;
90          strValue = asString!(value);
91       } else {
92          strValue = attr.value.to!string;
93       }
94 
95       // If the value's type is one of the attribute's defined types then we can convert it directly to a string
96       static if (attr.valueTypeIsDefined) {
97          mixin ConvertToString!(typeof(attr.value), true);
98          result = convertToString;
99       // Otherwise we'll check if the attribute defines a specific value that is allowed
100       } else {
101          mixin FindAllowedValue!(typeof(attr).AttrType, true);
102          result = findAllowedValue;
103       }
104 
105       return result;
106    }
107 
108    static auto create(AttrPair)(AttrPair attr) {
109       // TODO: Convert double to string at compile-time
110       string strValue = attr.value.to!string;
111 
112       static if (typeof(attr).valueTypeIsDefined) {
113          mixin ConvertToString!(typeof(attr.value), false);
114          return convertToString;
115       } else {
116          mixin FindAllowedValue!(typeof(attr).AttrType, false);
117          return findAllowedValue;
118       }
119    }
120 }
121 
122 abstract class Attr {
123    HtmlAttribute create();
124 }
125 
126 class AttrPair(TAttr, TVal) : Attr {
127    alias AttrType = TAttr;
128 
129    immutable string name;
130    const TVal value;
131 
132    this(string name, TVal value) {
133       this.name = name.camelCaseToHyphens.underscoreToHyphens;
134       this.value = value;
135    }
136 
137    /**
138     * Create an HtmlAttribute from an AttrPair.
139     */
140    override HtmlAttribute create() {
141       return HtmlAttribute.create(this);
142    }
143 
144    /**
145     * Checks if the type of this pair's value is one of the types defined by the actual attribute.
146     */
147    static bool valueTypeIsDefined(int i = 0)() {
148       static if (__traits(hasMember, AttrType, "TValues")) {
149          alias types = AttrType.TValues;
150 
151          static if (i < types.length) {
152             static if (is(types[i] == BoolAttribute)) {
153                // We always allow boolean attributes to pass since we know the underlying type is a string
154                // (e.g. async="async")
155                return true || valueTypeIsDefined!(i + 1);
156             } else static if (!__traits(compiles, typeof(types[i]))) {
157                return (is(types[i] == TVal)) || valueTypeIsDefined!(i + 1);
158             } else {
159                return false || valueTypeIsDefined!(i + 1);
160             }
161          } else {
162             return false;
163          }
164       } else {
165          // If value types are not defined then assume any type is allowed.
166          return true;
167       }
168    }
169 
170    /**
171     * Check if this pair was created from a user-defined attribute.
172     */
173    static bool hasCustomAttr() { 
174       return is(AttrType : CustomAttribute);
175    }
176 }
177 
178 /** 
179  * Represents a custom attribute created using 'attr' function, e.g. "my-attribute".attr
180  */
181 struct CustomAttribute {
182    immutable string name;
183 
184    this(string name) {
185       this.name = name;
186    }
187 
188    enum opAssign(TVal)(TVal value) {
189       return new AttrPair!(typeof(this), TVal)(name, value);
190    }
191 }
192 
193 /**
194  * Represents a formatted attribute. e.g. 'data-%s' or 'aria-%s'
195  */
196 struct FmtAttribute {
197    this(string formatName) {
198       this.formatName = formatName;
199    }
200 
201    auto ref opCall(string name) {
202       return CustomAttribute(format(formatName, name));
203    }
204 
205    auto ref opDispatch(string name, TVal)(TVal val) {
206       return CustomAttribute(format(formatName, name)) = val;
207    }
208 
209    private {
210       immutable string formatName;
211    }
212 }