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 }