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 }