1 /**
2  * Temple (C) Dylan Knutson, 2013, distributed under the:
3  * Boost Software License - Version 1.0 - August 17th, 2003
4  *
5  * Permission is hereby granted, free of charge, to any person or organization
6  * obtaining a copy of the software and accompanying documentation covered by
7  * this license (the "Software") to use, reproduce, display, distribute,
8  * execute, and transmit the Software, and to prepare derivative works of the
9  * Software, and to permit third-parties to whom the Software is furnished to
10  * do so, all subject to the following:
11  *
12  * The copyright notices in the Software and this entire statement, including
13  * the above license grant, this restriction and the following disclaimer,
14  * must be included in all copies of the Software, in whole or in part, and
15  * all derivative works of the Software, unless such copies or derivative
16  * works are solely in the form of machine-executable object code generated by
17  * a source language processor.
18  */
19 
20 module temple.func_string_gen;
21 
22 private import
23 	temple,
24 	temple.util,
25 	temple.delims,
26 	std.conv,
27 	std.string,
28 	std.array,
29 	std.exception,
30 	std.uni,
31 	std.algorithm;
32 
33 /**
34  * Stack and generator for unique temporary variable names
35  */
36 private struct TempBufferNameStack
37 {
38 private:
39 	const string base;
40 	uint counter = 0;
41 	string[] stack;
42 
43 public:
44 	this(string base)
45 	{
46 		this.base = base;
47 	}
48 
49 	/**
50 	 * getNew
51 	 * Gets a new unique buffer variable name
52 	 */
53 	string pushNew()
54 	{
55 		auto name = base ~ counter.to!string;
56 		counter++;
57 		stack ~= name;
58 		return name;
59 	}
60 
61 	/**
62 	 * Pops the topmost unique variable name off the stack
63 	 */
64 	string pop()
65 	{
66 		auto ret = stack[$-1];
67 		stack.length--;
68 		return ret;
69 	}
70 
71 	/**
72 	 * Checks if there are any names to pop off
73 	 */
74 	bool empty()
75 	{
76 		return !stack.length;
77 	}
78 }
79 
80 /**
81  * Represents a unit of code that makes up the template function
82  */
83 private struct FuncPart {
84 	enum Type {
85 		StrLit, // String literal appended to the buffer
86 		Expr,   // Runtime computed expression appended to the buffer
87 		Stmt,   // Any arbitrary statement/declaration making up the function
88 		Line,   // #line directive
89 	}
90 
91 	Type type;
92 	string value;
93 	uint indent;
94 }
95 
96 /**
97  * __temple_gen_temple_func_string
98  * Generates the function string to be mixed into a template which renders
99  * a temple file.
100  */
101 package string __temple_gen_temple_func_string(
102 	string temple_str, in string temple_name, in string filter_ident = "")
103 {
104 	// Output function string being composed
105 	FuncPart[] func_parts;
106 
107 	// Indendation level for a line being pushed
108 	uint indent_level = 0;
109 
110 	// Current line number in the temple_str being scanned
111 	size_t line_number = 0;
112 
113 	// Generates temporary variable names and keeps them on a stack
114 	auto temp_var_names = TempBufferNameStack("__temple_capture_var_");
115 
116 	// Content removed from the head of temple_str is put on the tail of this
117 	string prev_temple_str = "";
118 
119 	/* ----- func_parts appending functions ----- */
120 	void push_expr(string expr) {
121 		func_parts ~= FuncPart(FuncPart.Type.Expr, expr, indent_level);
122 	}
123 
124 	void push_stmt(string stmt)
125 	{
126 		func_parts ~= FuncPart(FuncPart.Type.Stmt, stmt ~ '\n', indent_level);
127 	}
128 
129 	void push_string_literal(string str)
130 	{
131 		func_parts ~= FuncPart(FuncPart.Type.StrLit, str, indent_level);
132 	}
133 
134 	void push_linenum()
135 	{
136 		func_parts ~= FuncPart(
137 			FuncPart.Type.Line,
138 			`#line %d "%s"`.format(line_number+1, temple_name) ~ "\n",
139 			indent_level);
140 	}
141 	/* ----------------------------------------- */
142 
143 	void indent()  { indent_level++; }
144 	void outdent() { indent_level--; }
145 
146 	// Tracks if the block that the parser has just
147 	// finished processing should be printed (e.g., is
148 	// it the block who's contents are assigned to the last tmp_buffer_var)
149 	bool[] printStartBlockTracker;
150 	void sawBlockStart(bool will_be_printed)
151 	{
152 		printStartBlockTracker ~= will_be_printed;
153 	}
154 	bool sawBlockEnd()
155 	{
156 		auto will_be_printed = printStartBlockTracker[$-1];
157 		printStartBlockTracker.length--;
158 		return will_be_printed;
159 	}
160 
161 	// Generate the function signature, taking into account if it has a
162 	// FilterParam to use
163 	push_stmt(build_function_head(filter_ident));
164 
165 	indent();
166 	if(filter_ident.length)
167 	{
168 		push_stmt(`with(%s)`.format(filter_ident));
169 	}
170 	push_stmt(`with(__temple_context) {`);
171 	indent();
172 
173 	// Keeps infinite loops from outright crashing the compiler
174 	// The limit should be set to some arbitrary large number
175 	uint safeswitch = 0;
176 
177 	while(temple_str.length)
178 	{
179 		// This imposes the limiatation of a max of 10_000 delimers parsed for
180 		// a template function. Probably will never ever hit this in a single
181 		// template file without running out of compiler memory
182 		if(safeswitch++ > 10_000)
183 		{
184 			assert(false, "nesting level too deep; throwing saftey switch: \n" ~ temple_str);
185 		}
186 
187 		DelimPos!(OpenDelim)* oDelimPos = temple_str.nextDelim(OpenDelims);
188 
189 		if(oDelimPos is null)
190 		{
191 			//No more delims; append the rest as a string
192 			push_linenum();
193 			push_string_literal(temple_str);
194 			prev_temple_str.munchHeadOf(temple_str, temple_str.length);
195 		}
196 		else
197 		{
198 			immutable OpenDelim  oDelim = oDelimPos.delim;
199 			immutable CloseDelim cDelim = OpenToClose[oDelim];
200 
201 			if(oDelimPos.pos == 0)
202 			{
203 				if(oDelim.isShort())
204 				{
205 					if(!prev_temple_str.validBeforeShort())
206 					{
207 						// Chars before % weren't all whitespace, assume it's part of a
208 						// string literal.
209 						push_linenum();
210 						push_string_literal(temple_str[0..oDelim.toString().length]);
211 						prev_temple_str.munchHeadOf(temple_str, oDelim.toString().length);
212 						continue;
213 					}
214 				}
215 
216 				// If we made it this far, we've got valid open/close delims
217 				DelimPos!(CloseDelim)* cDelimPos = temple_str.nextDelim([cDelim]);
218 				if(cDelimPos is null)
219 				{
220 					if(oDelim.isShort())
221 					{
222 						// don't require a short close delim at the end of the template
223 						temple_str ~= cDelim.toString();
224 						cDelimPos = enforce(temple_str.nextDelim([cDelim]));
225 					}
226 					else
227 					{
228 						assert(false, "Missing close delimer: " ~ cDelim.toString());
229 					}
230 				}
231 
232 				// Made it this far, we've got the position of the close delimer.
233 				push_linenum();
234 
235 				// Get a slice to the content between the delimers
236 				immutable string inbetween_delims =
237 					temple_str[oDelim.toString().length .. cDelimPos.pos];
238 
239 				// track block starts
240 				immutable bool is_block_start = inbetween_delims.isBlockStart();
241 				immutable bool is_block_end   = inbetween_delims.isBlockEnd();
242 
243 				// Invariant
244 				assert(!(is_block_start && is_block_end), "Internal bug: " ~ inbetween_delims);
245 
246 				if(is_block_start)
247 				{
248 					sawBlockStart(oDelim.isStr());
249 				}
250 
251 				if(oDelim.isStr())
252 				{
253 					// Check if this is a block; in that case, put the block's
254 					// contents into a temporary variable, then render that
255 					// variable after the block close delim
256 
257 					// The line would look like:
258 					// <%= capture(() { %>
259 					//  <% }); %>
260 					// so look for something like "){" or ") {" at the end
261 
262 					if(is_block_start)
263 					{
264 						string tmp_name = temp_var_names.pushNew();
265 						push_stmt(`auto %s = %s`.format(tmp_name, inbetween_delims));
266 						indent();
267 					}
268 					else
269 					{
270 						push_expr(inbetween_delims);
271 
272 						if(cDelim == CloseDelim.CloseShort)
273 						{
274 							push_stmt(`__temple_buff_filtered_put("\n");`);
275 						}
276 					}
277 				}
278 				else
279 				{
280 					// It's just raw code, push it into the function body
281 					push_stmt(inbetween_delims);
282 
283 					// Check if the code looks like the ending to a block;
284 					// e.g. for block:
285 					// <%= capture(() { %>
286 					// <% }, "foo"); %>`
287 					// look for it starting with }<something>);
288 					// If it does, output the last tmp buffer var on the stack
289 					if(is_block_end && !temp_var_names.empty)
290 					{
291 
292 						// the block at this level should be printed
293 						if(sawBlockEnd())
294 						{
295 							outdent();
296 							push_stmt(`__temple_context.put(%s);`.format(temp_var_names.pop()));
297 						}
298 					}
299 				}
300 
301 				// remove up to the closing delimer
302 				prev_temple_str.munchHeadOf(
303 					temple_str,
304 					cDelimPos.pos + cDelim.toString().length);
305 			}
306 			else
307 			{
308 				// Move ahead to the next open delimer, rendering
309 				// everything between here and there as a string literal
310 				push_linenum();
311 				immutable delim_pos = oDelimPos.pos;
312 				push_string_literal(temple_str[0..delim_pos]);
313 				prev_temple_str.munchHeadOf(temple_str, delim_pos);
314 			}
315 		}
316 
317 		// count the number of newlines in the previous part of the template;
318 		// that's the current line number
319 		line_number = prev_temple_str.count('\n');
320 	}
321 
322 	outdent();
323 	push_stmt("}");
324 	outdent();
325 	push_stmt("}");
326 
327 
328 	return buildFromParts(func_parts);
329 }
330 
331 private:
332 
333 string build_function_head(string filter_ident) {
334 	string ret = "";
335 
336 	string function_type_params =
337 		filter_ident.length ?
338 			"(%s)".format(filter_ident) :
339 			"" ;
340 
341 	ret ~= (
342 `static void TempleFunc%s(TempleContext __temple_context) {`
343 	.format(function_type_params));
344 
345 	// This isn't just an overload of __temple_buff_filtered_put because D doesn't allow
346 	// overloading of nested functions
347 	ret ~= `
348 	// Ensure that __temple_context is never null
349 	assert(__temple_context);
350 
351 	void __temple_put_expr(T)(T expr) {
352 
353 		// TempleInputStream should never be passed through
354 		// a filter; it should be directly appended to the stream
355 		static if(is(typeof(expr) == TempleInputStream))
356 		{
357 			expr.into(__temple_context.sink);
358 		}
359 
360 		// But other content should be filtered
361 		else
362 		{
363 			__temple_buff_filtered_put(expr);
364 		}
365 	}
366 
367 	deprecated auto renderWith(string __temple_file)(TempleContext tc = null)
368 	{
369 		return render_with!__temple_file(tc);
370 	}
371 	TempleInputStream render(string __temple_file)() {
372 		return render_with!__temple_file(__temple_context);
373 	}
374 	`;
375 
376 	// Is the template using a filter?
377 	if(filter_ident.length)
378 	{
379 		ret ~= `
380 	/// Run 'thing' through the Filter's templeFilter static
381 	void __temple_buff_filtered_put(T)(T thing)
382 	{
383 		static if(__traits(compiles, __fp__.templeFilter(__temple_context.sink, thing)))
384 		{
385 			pragma(msg, "Deprecated: templeFilter on filters is deprecated; please use temple_filter");
386 			__fp__.templeFilter(__temple_context.sink, thing);
387 		}
388 		else static if(__traits(compiles, __fp__.templeFilter(thing)))
389 		{
390 			pragma(msg, "Deprecated: templeFilter on filters is deprecated; please use temple_filter");
391 			__temple_context.put(__fp__.templeFilter(thing));
392 		}
393 		else static if(__traits(compiles, __fp__.temple_filter(__temple_context.sink, thing))) {
394 			__fp__.temple_filter(__temple_context.sink, thing);
395 		}
396 		else static if(__traits(compiles, __fp__.temple_filter(thing)))
397 		{
398 			__temple_context.put(__fp__.temple_filter(thing));
399 		}
400 		else {
401 			// Fall back to templeFilter returning a string
402 			static assert(false, "Filter does not have a case that accepts a " ~ T.stringof);
403 		}
404 	}
405 
406 	/// with filter, render subtemplate with an explicit context (which defaults to null)
407 	TempleInputStream render_with(string __temple_file)(TempleContext tc = null)
408 	{
409 		return TempleInputStream(delegate(ref TempleOutputStream s) {
410 			auto nested = compile_temple_file!(__temple_file, __fp__)();
411 			nested.render(s, tc);
412 		});
413 	}
414 	`.replace("__fp__", filter_ident);
415 	}
416 	else
417 	{
418 		// No filter means just directly append the thing to the
419 		// buffer, converting it to a string if needed
420 		ret ~= `
421 	void __temple_buff_filtered_put(T)(T thing)
422 	{
423 		__temple_context.put(std.conv.to!string(thing));
424 	}
425 
426 	/// without filter, render subtemplate with an explicit context (which defaults to null)
427 	TempleInputStream render_with(string __temple_file)(TempleContext tc = null)
428 	{
429 		return TempleInputStream(delegate(ref TempleOutputStream s) {
430 			auto nested = compile_temple_file!(__temple_file)();
431 			nested.render(s, tc);
432 		});
433 	}
434 	`;
435 	}
436 
437 	return ret;
438 }
439 
440 string buildFromParts(in FuncPart[] parts) {
441 	string func_str = "";
442 
443 	foreach(index, immutable part; parts) {
444 		string indent() {
445 			string ret = "";
446 			for(int i = 0; i < part.indent; i++)
447 				ret ~= '\t';
448 			return ret;
449 		}
450 
451 		func_str ~= indent();
452 
453 		final switch(part.type)
454 		with(FuncPart.Type) {
455 			case Stmt:
456 			case Line:
457 				func_str ~= part.value;
458 				break;
459 
460 			case Expr:
461 				func_str ~= "__temple_put_expr(" ~ part.value.strip ~ ");\n";
462 				break;
463 
464 			case StrLit:
465 				if(index > 1 && (index + 2) < parts.length) {
466 					// look ahead/behind 2 because the generator inserts
467 					// #line annotations after each statement/expr/literal
468 					immutable prev_type = parts[index-2].type;
469 					immutable next_type = parts[index+2].type;
470 
471 					// if the previous and next parts are statements, and this part is all
472 					// whitespace, skip inserting it into the template
473 					if(
474 						prev_type == FuncPart.Type.Stmt &&
475 						next_type == FuncPart.Type.Stmt &&
476 						part.value.all!((chr) => chr.isWhite()))
477 					{
478 						break;
479 					}
480 				}
481 
482 				func_str ~= `__temple_context.put("` ~ part.value.replace("\n", "\\n").escapeQuotes() ~ "\");\n";
483 				break;
484 		}
485 	}
486 
487 	return func_str;
488 }