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