1 /** Definitions to support customization of the Diet compilation process. 2 */ 3 module diet.traits; 4 5 import diet.dom; 6 7 8 /** Marks a struct as a Diet traits container. 9 10 A traits struct can contain any of the following: 11 12 $(UL 13 $(LI `string translate(string)` - A function that takes a `string` and 14 returns the translated version of that string. This is used for 15 translating the text of nodes marked with `&` at compile time. Note 16 that the input string may contain string interpolations.) 17 $(LI `void filterX(string)` - Any number of compile-time filter 18 functions, where "X" is a placeholder for the actual filter name. 19 The first character will be converted to lower case, so that a 20 function `filterCss` will be available as `:css` within the Diet 21 template.) 22 $(LI `SafeFilterCallback[string] filters` - A dictionary of runtime filter 23 functions that will be used to for filter nodes that don't have an 24 available compile-time filter or contain string interpolations.) 25 $(LI `alias processors = AliasSeq!(...)` - A list of callables taking 26 a `Document` to modify its contents) 27 $(LI `HTMLOutputStyle htmlOutputStyle` - An enum to configure 28 the output style of the generated HTML, e.g. compact or pretty) 29 ) 30 */ 31 @property DietTraitsAttribute dietTraits() { return DietTraitsAttribute.init; } 32 33 /// 34 unittest { 35 import diet.html : compileHTMLDietString; 36 import std.array : appender, array; 37 import std..string : toUpper; 38 39 @dietTraits 40 static struct CTX { 41 static string translate(string text) { 42 return text == "Hello, World!" ? "Hallo, Welt!" : text; 43 } 44 45 static string filterUppercase(I)(I input) { 46 return input.toUpper(); 47 } 48 } 49 50 auto dst = appender!string; 51 dst.compileHTMLDietString!("p& Hello, World!", CTX); 52 assert(dst.data == "<p>Hallo, Welt!</p>"); 53 54 dst = appender!string; 55 dst.compileHTMLDietString!(":uppercase testing", CTX); 56 assert(dst.data == "TESTING"); 57 } 58 59 60 /** Translates a line of text based on the traits passed to the Diet parser. 61 62 The input text may contain string interpolations of the form `#{...}` or 63 `!{...}`, where the contents form an arbitrary D expression. The 64 translation function is required to pass these through unmodified. 65 */ 66 string translate(TRAITS...)(string text) 67 { 68 import std.traits : hasUDA; 69 70 foreach (T; TRAITS) { 71 static assert(hasUDA!(T, DietTraitsAttribute)); 72 static if (is(typeof(&T.translate))) 73 text = T.translate(text); 74 } 75 return text; 76 } 77 78 79 /** Applies any transformations that are defined in the supplied traits list. 80 81 Transformations are defined by declaring a `processors` sequence in a 82 traits struct. 83 84 See_also: `dietTraits` 85 */ 86 Document applyTraits(TRAITS...)(Document doc) 87 { 88 import diet.defs : enforcep; 89 import std.algorithm.searching : startsWith; 90 import std.array : split; 91 92 void processNode(ref Node n, bool in_filter) 93 { 94 bool is_filter = n.name == Node.SpecialName.filter; 95 96 // process children first 97 for (size_t i = 0; i < n.contents.length;) { 98 auto nc = n.contents[i]; 99 if (nc.kind == NodeContent.Kind.node) { 100 processNode(nc.node, is_filter || in_filter); 101 if ((is_filter || in_filter) && nc.node.name == Node.SpecialName.text) { 102 n.contents = n.contents[0 .. i] ~ nc.node.contents ~ n.contents[i+1 .. $]; 103 i += nc.node.contents.length; 104 } else i++; 105 } else i++; 106 } 107 108 // then consolidate text 109 for (size_t i = 1; i < n.contents.length;) { 110 if (n.contents[i-1].kind == NodeContent.Kind.text && n.contents[i].kind == NodeContent.Kind.text) { 111 n.contents[i-1].value ~= n.contents[i].value; 112 n.contents = n.contents[0 .. i] ~ n.contents[i+1 .. $]; 113 } else i++; 114 } 115 116 // finally process filters 117 if (is_filter) { 118 enforcep(n.isProceduralTextNode, "Only text is supported as filter contents.", n.loc); 119 auto chain = n.getAttribute("filterChain").expectText().split(' '); 120 n.attributes = null; 121 n.attribs = NodeAttribs.none; 122 123 if (n.isTextNode) { 124 while (chain.length) { 125 if (hasFilterCT!TRAITS(chain[$-1])) { 126 n.contents[0].value = runFilterCT!TRAITS(n.contents[0].value, chain[$-1]); 127 chain.length--; 128 } else break; 129 } 130 } 131 132 if (!chain.length) n.name = Node.SpecialName.text; 133 else { 134 n.name = Node.SpecialName.code; 135 n.contents = [NodeContent.text(generateFilterChainMixin(chain, n.contents), n.loc)]; 136 } 137 } 138 } 139 140 foreach (ref n; doc.nodes) processNode(n, false); 141 142 // apply DOM processors 143 foreach (T; TRAITS) { 144 static if (is(typeof(T.processors.length))) { 145 foreach (p; T.processors) 146 p(doc); 147 } 148 } 149 150 return doc; 151 } 152 153 deprecated("Use SafeFilterCallback instead.") 154 alias FilterCallback = void delegate(in char[] input, scope CharacterSink output); 155 alias SafeFilterCallback = void delegate(in char[] input, scope CharacterSink output) @safe; 156 alias CharacterSink = void delegate(in char[]) @safe; 157 158 void filter(ALIASES...)(in char[] input, string filter, CharacterSink output) 159 { 160 import std.traits : hasUDA; 161 162 foreach (A; ALIASES) 163 static if (hasUDA!(A, DietTraitsAttribute)) { 164 static if (is(typeof(A.filters))) 165 if (auto pf = filter in A.filters) { 166 (*pf)(input, output); 167 return; 168 } 169 } 170 171 // FIXME: output location information 172 throw new Exception("Unknown filter: "~filter); 173 } 174 175 private string generateFilterChainMixin(string[] chain, NodeContent[] contents) 176 { 177 import std.format : format; 178 import diet.defs : enforcep, dietOutputRangeName; 179 import diet.internal..string : dstringEscape; 180 181 string ret = `{ import std.array : appender; import std.format : formattedWrite; `; 182 auto tloname = format("__f%s", chain.length); 183 184 if (contents.length == 1 && contents[0].kind == NodeContent.Kind.text) { 185 ret ~= q{enum %s = "%s";}.format(tloname, dstringEscape(contents[0].value)); 186 } else { 187 ret ~= q{auto %s_app = appender!(char[])();}.format(tloname); 188 foreach (c; contents) { 189 switch (c.kind) { 190 default: assert(false, "Unexpected node content in filter."); 191 case NodeContent.Kind.text: 192 ret ~= q{%s_app.put("%s");}.format(tloname, dstringEscape(c.value)); 193 break; 194 case NodeContent.Kind.rawInterpolation: 195 ret ~= q{%s_app.formattedWrite("%%s", %s);}.format(tloname, c.value); 196 break; 197 case NodeContent.Kind.interpolation: 198 enforcep(false, "Non-raw interpolations are not supported within filter contents.", c.loc); 199 break; 200 } 201 ret ~= "\n"; 202 } 203 ret ~= q{auto %s = %s_app.data;}.format(tloname, tloname); 204 } 205 206 foreach_reverse (i, f; chain) { 207 ret ~= "\n"; 208 string iname = format("__f%s", i+1); 209 string oname; 210 if (i > 0) { 211 oname = format("__f%s_app", i); 212 ret ~= q{auto %s = appender!(char[]);}.format(oname); 213 } else oname = dietOutputRangeName; 214 ret ~= q{%s.filter!ALIASES("%s", (in char[] s) @safe { %s.put(s); });}.format(iname, dstringEscape(f), oname); 215 if (i > 0) ret ~= q{auto __f%s = %s.data;}.format(i, oname); 216 } 217 218 return ret ~ `}`; 219 } 220 221 unittest { 222 import std.array : appender; 223 import diet.html : compileHTMLDietString; 224 225 @dietTraits 226 static struct CTX { 227 static string filterFoo(string str) { return "("~str~")"; } 228 static SafeFilterCallback[string] filters; 229 } 230 231 CTX.filters["foo"] = (input, scope output) { output("(R"); output(input); output("R)"); }; 232 CTX.filters["bar"] = (input, scope output) { output("(RB"); output(input); output("RB)"); }; 233 234 auto dst = appender!string; 235 dst.compileHTMLDietString!(":foo text", CTX); 236 assert(dst.data == "(text)"); 237 238 dst = appender!string; 239 dst.compileHTMLDietString!(":foo text\n\tmore", CTX); 240 assert(dst.data == "(text\nmore)"); 241 242 dst = appender!string; 243 dst.compileHTMLDietString!(":foo :foo text", CTX); 244 assert(dst.data == "((text))"); 245 246 dst = appender!string; 247 dst.compileHTMLDietString!(":bar :foo text", CTX); 248 assert(dst.data == "(RB(text)RB)"); 249 250 dst = appender!string; 251 dst.compileHTMLDietString!(":foo :bar text", CTX); 252 assert(dst.data == "(R(RBtextRB)R)"); 253 254 dst = appender!string; 255 dst.compileHTMLDietString!(":foo text !{1}", CTX); 256 assert(dst.data == "(Rtext 1R)"); 257 } 258 259 @safe unittest { 260 import diet.html : compileHTMLDietString; 261 262 static struct R { 263 void put(char) @safe {} 264 void put(in char[]) @safe {} 265 void put(dchar) @safe {} 266 } 267 268 @dietTraits 269 static struct CTX { 270 static SafeFilterCallback[string] filters; 271 } 272 CTX.filters["foo"] = (input, scope output) { output(input); }; 273 274 R r; 275 r.compileHTMLDietString!(":foo bar", CTX); 276 } 277 278 package struct DietTraitsAttribute {} 279 280 private bool hasFilterCT(TRAITS...)(string filter) 281 { 282 alias Filters = FiltersFromTraits!TRAITS; 283 static if (Filters.length) { 284 switch (filter) { 285 default: break; 286 foreach (i, F; Filters) { 287 case FilterName!(Filters[i]): return true; 288 } 289 } 290 } 291 return false; 292 } 293 294 private string runFilterCT(TRAITS...)(string text, string filter) 295 { 296 alias Filters = FiltersFromTraits!TRAITS; 297 static if (Filters.length) { 298 switch (filter) { 299 default: break; 300 foreach (i, F; Filters) { 301 case FilterName!(Filters[i]): return F(text); 302 } 303 } 304 } 305 return text; // FIXME: error out? 306 } 307 308 private template FiltersFromTraits(TRAITS...) 309 { 310 import std.meta : AliasSeq; 311 template impl(size_t i) { 312 static if (i < TRAITS.length) { 313 // FIXME: merge lists avoiding duplicates 314 alias impl = AliasSeq!(FiltersFromContext!(TRAITS[i]), impl!(i+1)); 315 } else alias impl = AliasSeq!(); 316 } 317 alias FiltersFromTraits = impl!0; 318 } 319 320 /** Extracts all Diet traits structs from a set of aliases as passed to a render function. 321 */ 322 template DietTraits(ALIASES...) 323 { 324 import std.meta : AliasSeq; 325 import std.traits : hasUDA; 326 327 template impl(size_t i) { 328 static if (i < ALIASES.length) { 329 static if (is(ALIASES[i]) && hasUDA!(ALIASES[i], DietTraitsAttribute)) { 330 alias impl = AliasSeq!(ALIASES[i], impl!(i+1)); 331 } else alias impl = impl!(i+1); 332 } else alias impl = AliasSeq!(); 333 } 334 alias DietTraits = impl!0; 335 } 336 337 private template FiltersFromContext(Context) 338 { 339 import std.meta : AliasSeq; 340 import std.algorithm.searching : startsWith; 341 342 alias members = AliasSeq!(__traits(allMembers, Context)); 343 template impl(size_t i) { 344 static if (i < members.length) { 345 static if (members[i].startsWith("filter") && members[i].length > 6 && members[i] != "filters") 346 alias impl = AliasSeq!(__traits(getMember, Context, members[i]), impl!(i+1)); 347 else alias impl = impl!(i+1); 348 } else alias impl = AliasSeq!(); 349 } 350 alias FiltersFromContext = impl!0; 351 } 352 353 private template FilterName(alias FilterFunction) 354 { 355 import std.algorithm.searching : startsWith; 356 import std.ascii : toLower; 357 358 enum ident = __traits(identifier, FilterFunction); 359 static if (ident.startsWith("filter") && ident.length > 6) 360 enum FilterName = ident[6].toLower ~ ident[7 .. $]; 361 else static assert(false, 362 "Filter function must start with \"filter\" and must have a non-zero length suffix: " ~ ident); 363 }