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 }