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.temple,
24 	temple.util,
25 	temple.delims,
26 	std.conv,
27 	std.string,
28 	std.array,
29 	std.exception;
30 
31 /**
32  * Stack and generator for unique temporary variable names
33  */
34 private static struct TempBufferNameStack
35 {
36 private:
37 	const string base;
38 	uint counter = 0;
39 	string[] stack;
40 
41 public:
42 	this(string base)
43 	{
44 		this.base = base;
45 	}
46 
47 	/**
48 	 * getNew
49 	 * Gets a new unique buffer variable name
50 	 */
51 	string pushNew()
52 	{
53 		auto name = base ~ counter.to!string;
54 		counter++;
55 		stack ~= name;
56 		return name;
57 	}
58 
59 	/**
60 	 * Pops the topmost unique variable name off the stack
61 	 */
62 	string pop()
63 	{
64 		auto ret = stack[$-1];
65 		stack.length--;
66 		return ret;
67 	}
68 
69 	/**
70 	 * Checks if there are any names to pop off
71 	 */
72 	bool empty()
73 	{
74 		return !stack.length;
75 	}
76 }
77 
78 /**
79  * __temple_gen_temple_func_string
80  * Generates the function string to be mixed into a template which renders
81  * a temple file.
82  */
83 package string __temple_gen_temple_func_string(
84 	string temple_str, string temple_name, string filter_ident = "")
85 {
86 	// Output function string being composed
87 	auto function_str = "";
88 
89 	// Indendation level for a line being pushed
90 	auto indent_level = 0;
91 
92 	// Current line number in the temple_str being scanned
93 	size_t line_number = 0;
94 
95 	void push_line(string[] stmts...)
96 	{
97 		foreach(i; 0..indent_level)
98 		{
99 			function_str ~= '\t';
100 		}
101 		foreach(stmt; stmts)
102 		{
103 			function_str ~= stmt;
104 		}
105 		function_str ~= '\n';
106 	}
107 
108 	void push_linenum()
109 	{
110 		push_line(`#line ` ~ (line_number + 1).to!string ~ ` "` ~ temple_name ~ `"`);
111 	}
112 
113 	void push_string_literal(string str)
114 	{
115 		if(str.length == 0)
116 			return;
117 
118 		push_line(`__temple_buff.put("` ~ str.escapeQuotes() ~ `");`);
119 	}
120 
121 	void indent()  { indent_level++; }
122 	void outdent() { indent_level--; }
123 
124 
125 	auto temp_var_names = TempBufferNameStack("__temple_capture_var_");
126 
127 	// Tracks if the block that the parser has just
128 	// finished processing should be printed (e.g., is
129 	// it the block who's contents are assigned to the last tmp_buffer_var)
130 	bool[] printStartBlockTracker;
131 	void sawBlockStart(bool will_be_printed)
132 	{
133 		printStartBlockTracker ~= will_be_printed;
134 	}
135 	bool sawBlockEnd()
136 	{
137 		auto will_be_printed = printStartBlockTracker[$-1];
138 		printStartBlockTracker.length--;
139 		return will_be_printed;
140 	}
141 
142 	string function_type_params = "";
143 	if(filter_ident.length)
144 	{
145 		function_type_params = "(%s)".format(filter_ident);
146 	}
147 	push_line(`static void TempleFunc%s(OutputStream __temple_buff, TempleContext __temple_context = null) {`.format(function_type_params));
148 
149 	// This isn't just an overload of __templeBuffFilteredPut because D doesn't allow
150 	// overloading of nested functions
151 	push_line(q{
152 		void __templeBuffPutStream(AppenderOutputStream os)
153 		{
154 			__temple_buff.put(os.data);
155 		}
156 
157 	});
158 
159 	push_line(q{
160 		/// Calls renderWith, with the current Temple context
161 		AppenderOutputStream render(string __temple_file)()
162 		{
163 			return renderWith!__temple_file(__temple_context);
164 		}
165 	});
166 
167 	// Is the template using a filter?
168 	if(filter_ident.length)
169 	{
170 		push_line(q{
171 			/// Run 'thing' through the Filter's templeFilter static
172 			void __templeBuffFilteredPut(T)(T thing)
173 			{
174 				static if(__traits(compiles, __fp__.templeFilter(cast(OutputStream) __temple_buff, thing))) {
175 					// The filter defines a method that takes an OutputBuffer,
176 					// prefer that to appending an entire string
177 					__fp__.templeFilter(__temple_buff, thing);
178 				}
179 				else {
180 					// Fall back to templeFilter returning a string
181 					__temple_buff.put( __fp__.templeFilter(thing) );
182 				}
183 			}
184 
185 			/// Renders a subtemplate here with an explicitly defined context
186 			/// By default, the context is null, so a blank context will be
187 			/// used to render the nested template
188 			AppenderOutputStream renderWith(string __temple_file)(TempleContext __ctx = null)
189 			{
190 				alias __temple_render_func = TempleFile!(__temple_file, __fp__);
191 				return __temple_context.__templeRenderWith(&__temple_render_func, __ctx);
192 			}
193 
194 		}.replace("__fp__", filter_ident));
195 	}
196 	else
197 	{
198 		// No filter means just directly append the thing to the
199 		// buffer, converting it to a string if needed
200 		push_line(q{
201 			void __templeBuffFilteredPut(T)(T thing)
202 			{
203 				__temple_buff.put(.std.conv.to!string(thing));
204 			}
205 
206 			/// Same as the renderWith when a filter is given, just
207 			/// without the filter
208 			AppenderOutputStream renderWith(string __temple_file)(TempleContext __ctx = null)
209 			{
210 				alias __temple_render_func = TempleFile!__temple_file;
211 				return __temple_context.__templeRenderWith(&__temple_render_func, __ctx);
212 			}
213 		});
214 	}
215 
216 	push_line(q{
217 		// Ensure that __temple_context is never null
218 		if(__temple_context is null)
219 		{
220 			__temple_context = new TempleContext();
221 		}
222 
223 		// A stack of the current temple buffers, used to render nested
224 		// templates with
225 		OutputStream[] __temple_buffers;
226 		void __pushBuff(OutputStream __new_buff)
227 		{
228 			__temple_buffers ~= __temple_buff;
229 			__temple_buff = __new_buff;
230 		}
231 
232 		void __popBuff()
233 		{
234 			__temple_buff = __temple_buffers[$-1];
235 			__temple_buffers.length--;
236 		}
237 
238 		// Push this template's hooks to the current context
239 		__temple_context.__templePushHooks(&__pushBuff, &__popBuff);
240 		scope(exit) { __temple_context.__templePopHooks(); }
241 	});
242 
243 	indent();
244 	if(filter_ident.length)
245 	{
246 		push_line(`with(%s)`.format(filter_ident));
247 	}
248 	push_line(`with(__temple_context) {`);
249 	indent();
250 
251 	// Keeps infinite loops from outright crashing the compiler
252 	// The limit should be set to some arbitrary large number
253 	uint safeswitch = 0;
254 
255 	string prevTempl = "";
256 
257 	while(temple_str.length)
258 	{
259 		// This imposes the limiatation of a max of 10_000 delimers parsed for
260 		// a template function. Probably will never ever hit this in a single
261 		// template file without running out of compiler memory
262 		if(safeswitch++ > 10_000)
263 		{
264 			assert(false, "nesting level too deep; throwing saftey switch: \n" ~ temple_str);
265 		}
266 
267 		DelimPos!(OpenDelim)* oDelimPos = temple_str.nextDelim(OpenDelims);
268 
269 		if(oDelimPos is null)
270 		{
271 			//No more delims; append the rest as a string
272 			push_linenum();
273 			push_string_literal(temple_str);
274 			prevTempl.munchHeadOf(temple_str, temple_str.length);
275 		}
276 		else
277 		{
278 			immutable OpenDelim  oDelim = oDelimPos.delim;
279 			immutable CloseDelim cDelim = OpenToClose[oDelim];
280 
281 			if(oDelimPos.pos == 0)
282 			{
283 				if(oDelim.isShort())
284 				{
285 					if(!prevTempl.validBeforeShort())
286 					{
287 						// Chars before % weren't all whitespace, assume it's part of a
288 						// string literal.
289 						push_linenum();
290 						push_string_literal(temple_str[0..oDelim.toString().length]);
291 						prevTempl.munchHeadOf(temple_str, oDelim.toString().length);
292 						continue;
293 					}
294 				}
295 
296 				// If we made it this far, we've got valid open/close delims
297 				auto cDelimPos = temple_str.nextDelim([cDelim]);
298 				if(cDelimPos is null)
299 				{
300 					if(oDelim.isShort())
301 					{
302 						// don't require a short close delim at the end of the template
303 						temple_str ~= cDelim.toString();
304 						cDelimPos = enforce(temple_str.nextDelim([cDelim]));
305 					}
306 					else
307 					{
308 						assert(false, "Missing close delimer: " ~ cDelim.toString());
309 					}
310 				}
311 
312 				// Made it this far, we've got the position of the close delimer.
313 				push_linenum();
314 
315 				// Get a slice to the content between the delimers
316 				immutable string inbetween_delims =
317 					temple_str[oDelim.toString().length .. cDelimPos.pos];
318 
319 				// track block starts
320 				immutable bool is_block_start = inbetween_delims.isBlockStart();
321 				immutable bool is_block_end   = inbetween_delims.isBlockEnd();
322 
323 				// Invariant
324 				assert(!(is_block_start && is_block_end), "Internal bug: " ~ inbetween_delims);
325 
326 				if(is_block_start)
327 				{
328 					sawBlockStart(oDelim.isStr());
329 				}
330 
331 				if(oDelim.isStr())
332 				{
333 					// Check if this is a block; in that case, put the block's
334 					// contents into a temporary variable, then render that
335 					// variable after the block close delim
336 
337 					// The line would look like:
338 					// <%= capture(() { %>
339 					//  <% }); %>
340 					// so look for something like "){" or ") {" at the end
341 
342 					if(is_block_start)
343 					{
344 						string tmp_name = temp_var_names.pushNew();
345 						push_line(`auto %s = %s`.format(tmp_name, inbetween_delims));
346 						indent();
347 					}
348 					else
349 					{
350 						push_line(q{
351 							// AppenderOuputStream should never be passed through
352 							// a filter; it should be directly appended to the stream
353 							static if(is(typeof(__expr__) == AppenderOutputStream))
354 							{
355 								__templeBuffPutStream(__expr__);
356 							}
357 
358 							// But other content should be filtered
359 							else
360 							{
361 								__templeBuffFilteredPut(__expr__);
362 							}
363 						}.replace("__expr__", inbetween_delims));
364 
365 						if(cDelim == CloseDelim.CloseShort)
366 						{
367 							push_line(`__templeBuffFilteredPut("\n");`);
368 						}
369 					}
370 				}
371 				else
372 				{
373 					// It's just raw code, push it into the function body
374 					push_line(inbetween_delims);
375 
376 					// Check if the code looks like the ending to a block;
377 					// e.g. for block:
378 					// <%= capture(() { %>
379 					// <% }, "foo"); %>`
380 					// look for it starting with }<something>);
381 					// If it does, output the last tmp buffer var on the stack
382 					if(is_block_end && !temp_var_names.empty)
383 					{
384 
385 						// the block at this level should be printed
386 						if(sawBlockEnd())
387 						{
388 							outdent();
389 							push_line(`__temple_buff.put(%s);`.format(temp_var_names.pop()));
390 						}
391 					}
392 				}
393 
394 				// remove up to the closing delimer
395 				prevTempl.munchHeadOf(
396 					temple_str,
397 					cDelimPos.pos + cDelim.toString().length);
398 			}
399 			else
400 			{
401 				// Move ahead to the next open delimer, rendering
402 				// everything between here and there as a string literal
403 				push_linenum();
404 				immutable delim_pos = oDelimPos.pos;
405 				push_string_literal(temple_str[0..delim_pos]);
406 				prevTempl.munchHeadOf(temple_str, delim_pos);
407 			}
408 		}
409 
410 		// count the number of newlines in the previous part of the template;
411 		// that's the current line number
412 		line_number = prevTempl.count('\n');
413 	}
414 
415 	outdent();
416 	push_line("}");
417 	outdent();
418 	push_line("}");
419 
420 	return function_str;
421 }