1 /++
2 	This provides a kind of web template support, built on top of [arsd.dom] and [arsd.script], in support of [arsd.cgi].
3 
4 	```html
5 		<main>
6 			<%=HTML some_var_with_html %>
7 			<%= some_var %>
8 
9 			<if-true cond="whatever">
10 				whatever == true
11 			</if-true>
12 			<or-else>
13 				whatever == false
14 			</or-else>
15 
16 			<for-each over="some_array" as="item">
17 				<%= item %>
18 			</for-each>
19 			<or-else>
20 				there were no items.
21 			</or-else>
22 
23 			<form>
24 				<!-- new on July 17, 2021 (dub v10.3) -->
25 				<hidden-form-data from="data_var" name="arg_name" />
26 			</form>
27 
28 			<render-template file="partial.html" />
29 		</main>
30 	```
31 
32 	Functions available:
33 		`encodeURIComponent`, `formatDate`, `dayOfWeek`, `formatTime`
34 +/
35 module arsd.webtemplate;
36 
37 // FIXME: make script exceptions show line from the template it was in too
38 
39 import arsd.script;
40 import arsd.dom;
41 
42 public import arsd.jsvar : var;
43 
44 class TemplateException : Exception {
45 	string templateName;
46 	var context;
47 	Exception e;
48 	this(string templateName, var context, Exception e) {
49 		this.templateName = templateName;
50 		this.context = context;
51 		this.e = e;
52 
53 		super("Exception in template " ~ templateName ~ ": " ~ e.msg);
54 	}
55 }
56 
57 void addDefaultFunctions(var context) {
58 	import std.conv;
59 	// FIXME: I prolly want it to just set the prototype or something
60 
61 	context.encodeURIComponent = function string(var f) {
62 		import std.uri;
63 		return encodeComponent(f.get!string);
64 	};
65 
66 	context.formatDate = function string(string s) {
67 		if(s.length < 10)
68 			return s;
69 		auto year = s[0 .. 4];
70 		auto month = s[5 .. 7];
71 		auto day = s[8 .. 10];
72 
73 		return month ~ "/" ~ day ~ "/" ~ year;
74 	};
75 
76 	context.dayOfWeek = function string(string s) {
77 		import std.datetime;
78 		return daysOfWeekFullNames[Date.fromISOExtString(s[0 .. 10]).dayOfWeek];
79 	};
80 
81 	context.formatTime = function string(string s) {
82 		if(s.length < 20)
83 			return s;
84 		auto hour = s[11 .. 13].to!int;
85 		auto minutes = s[14 .. 16].to!int;
86 		auto seconds = s[17 .. 19].to!int;
87 
88 		auto am = (hour >= 12) ? "PM" : "AM";
89 		if(hour > 12)
90 			hour -= 12;
91 
92 		return hour.to!string ~ (minutes < 10 ? ":0" : ":") ~ minutes.to!string ~ " " ~ am;
93 	};
94 
95 	// don't want checking meta or data to be an error
96 	if(context.meta == null)
97 		context.meta = var.emptyObject;
98 	if(context.data == null)
99 		context.data = var.emptyObject;
100 }
101 
102 Document renderTemplate(string templateName, var context = var.emptyObject, var skeletonContext = var.emptyObject) {
103 	import std.file;
104 	import arsd.cgi;
105 
106 	try {
107 		addDefaultFunctions(context);
108 		addDefaultFunctions(skeletonContext);
109 
110 		auto skeleton = new Document(readText("templates/skeleton.html"), true, true);
111 		auto document = new Document();
112 		document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom
113 		document.parse("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", true, true);
114 
115 		expandTemplate(skeleton.root, skeletonContext);
116 
117 		foreach(nav; skeleton.querySelectorAll("nav[data-relative-to]")) {
118 			auto r = nav.getAttribute("data-relative-to");
119 			foreach(a; nav.querySelectorAll("a")) {
120 				a.attrs.href = Uri(a.attrs.href).basedOn(Uri(r));// ~ a.attrs.href;
121 			}
122 		}
123 
124 		expandTemplate(document.root, context);
125 
126 		// also do other unique elements and move them over.
127 		// and try partials.
128 
129 		auto templateMain = document.requireSelector(":root > main");
130 		if(templateMain.hasAttribute("body-class")) {
131 			skeleton.requireSelector("body").addClass(templateMain.getAttribute("body-class"));
132 			templateMain.removeAttribute("body-class");
133 		}
134 
135 		skeleton.requireSelector("main").replaceWith(templateMain.removeFromTree);
136 		if(auto title = document.querySelector(":root > title"))
137 			skeleton.requireSelector(":root > head > title").innerHTML = title.innerHTML;
138 
139 		debug
140 		skeleton.root.prependChild(new HtmlComment(null, templateName ~ " inside skeleton.html"));
141 
142 		return skeleton;
143 	} catch(Exception e) {
144 		throw new TemplateException(templateName, context, e);
145 		//throw e;
146 	}
147 }
148 
149 // I don't particularly like this
150 void expandTemplate(Element root, var context) {
151 	import std.string;
152 
153 	string replaceThingInString(string v) {
154 		auto idx = v.indexOf("<%=");
155 		if(idx == -1)
156 			return v;
157 		auto n = v[0 .. idx];
158 		auto r = v[idx + "<%=".length .. $];
159 
160 		auto end = r.indexOf("%>");
161 		if(end == -1)
162 			throw new Exception("unclosed asp code in attribute");
163 		auto code = r[0 .. end];
164 		r = r[end + "%>".length .. $];
165 
166 		import arsd.script;
167 		auto res = interpret(code, context).get!string;
168 
169 		return n ~ res ~ replaceThingInString(r);
170 	}
171 
172 	foreach(k, v; root.attributes) {
173 		if(k == "onrender") {
174 			continue;
175 		}
176 
177 		v = replaceThingInString(v);
178 
179 		root.setAttribute(k, v);
180 	}
181 
182 	bool lastBoolResult;
183 
184 	foreach(ele; root.children) {
185 		if(ele.tagName == "if-true") {
186 			auto fragment = new DocumentFragment(null);
187 			import arsd.script;
188 			auto got = interpret(ele.attrs.cond, context).opCast!bool;
189 			if(got) {
190 				ele.tagName = "root";
191 				expandTemplate(ele, context);
192 				fragment.stealChildren(ele);
193 			}
194 			lastBoolResult = got;
195 			ele.replaceWith(fragment);
196 		} else if(ele.tagName == "or-else") {
197 			auto fragment = new DocumentFragment(null);
198 			if(!lastBoolResult) {
199 				ele.tagName = "root";
200 				expandTemplate(ele, context);
201 				fragment.stealChildren(ele);
202 			}
203 			ele.replaceWith(fragment);
204 		} else if(ele.tagName == "for-each") {
205 			auto fragment = new DocumentFragment(null);
206 			var nc = var.emptyObject(context);
207 			lastBoolResult = false;
208 			auto got = interpret(ele.attrs.over, context);
209 			foreach(item; got) {
210 				lastBoolResult = true;
211 				nc[ele.attrs.as] = item;
212 				auto clone = ele.cloneNode(true);
213 				clone.tagName = "root"; // it certainly isn't a for-each anymore!
214 				expandTemplate(clone, nc);
215 
216 				fragment.stealChildren(clone);
217 			}
218 			ele.replaceWith(fragment);
219 		} else if(ele.tagName == "render-template") {
220 			import std.file;
221 			auto templateName = ele.getAttribute("file");
222 			auto document = new Document();
223 			document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom
224 			document.parse("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", true, true);
225 
226 			expandTemplate(document.root, context);
227 
228 			auto fragment = new DocumentFragment(null);
229 
230 			debug fragment.appendChild(new HtmlComment(null, templateName));
231 			fragment.stealChildren(document.root);
232 			debug fragment.appendChild(new HtmlComment(null, "end " ~ templateName));
233 
234 			ele.replaceWith(fragment);
235 		} else if(ele.tagName == "hidden-form-data") {
236 			auto from = interpret(ele.attrs.from, context);
237 			auto name = ele.attrs.name;
238 
239 			auto form = new Form(null);
240 
241 			populateForm(form, from, name);
242 
243 			auto fragment = new DocumentFragment(null);
244 			fragment.stealChildren(form);
245 
246 			ele.replaceWith(fragment);
247 		} else if(auto asp = cast(AspCode) ele) {
248 			auto code = asp.source[1 .. $-1];
249 			auto fragment = new DocumentFragment(null);
250 			if(code[0] == '=') {
251 				import arsd.script;
252 				if(code.length > 5 && code[1 .. 5] == "HTML") {
253 					auto got = interpret(code[5 .. $], context);
254 					if(auto native = got.getWno!Element)
255 						fragment.appendChild(native);
256 					else
257 						fragment.innerHTML = got.get!string;
258 				} else {
259 					auto got = interpret(code[1 .. $], context).get!string;
260 					fragment.innerText = got;
261 				}
262 			}
263 			asp.replaceWith(fragment);
264 		} else {
265 			expandTemplate(ele, context);
266 		}
267 	}
268 
269 	if(root.hasAttribute("onrender")) {
270 		var nc = var.emptyObject(context);
271 		nc["this"] = wrapNativeObject(root);
272 		nc["this"]["populateFrom"] = delegate var(var this_, var[] args) {
273 			auto form = cast(Form) root;
274 			if(form is null) return this_;
275 			foreach(k, v; args[0]) {
276 				populateForm(form, v, k.get!string);
277 			}
278 			return this_;
279 		};
280 		interpret(root.getAttribute("onrender"), nc);
281 
282 		root.removeAttribute("onrender");
283 	}
284 }
285 
286 void populateForm(Form form, var obj, string name) {
287 	import std.string;
288 
289 	if(obj.payloadType == var.Type.Object) {
290 		form.setValue(name, "");
291 		foreach(k, v; obj) {
292 			auto fn = name.replace("%", k.get!string);
293 			// should I unify structs and assoctiavite arrays?
294 			populateForm(form, v, fn ~ "["~k.get!string~"]");
295 			//populateForm(form, v, fn ~"."~k.get!string);
296 		}
297 	} else {
298 		//import std.stdio; writeln("SET ", name, " ", obj, " ", obj.payloadType);
299 		form.setValue(name, obj.get!string);
300 	}
301 
302 }
303 
304 immutable daysOfWeekFullNames = [
305 	"Sunday",
306 	"Monday",
307 	"Tuesday",
308 	"Wednesday",
309 	"Thursday",
310 	"Friday",
311 	"Saturday"
312 ];
313 
314 /++
315 	UDA to put on a method when using [WebPresenterWithTemplateSupport]. Overrides default generic element formatting and instead uses the specified template name to render the return value.
316 
317 	Inside the template, the value returned by the function will be available in the context as the variable `data`.
318 +/
319 struct Template {
320 	string name;
321 }
322 /++
323 	UDA to put on a method when using [WebPresenterWithTemplateSupport]. Overrides the default template skeleton file name.
324 +/
325 struct Skeleton {
326 	string name;
327 }
328 
329 /++
330 	UDA to attach runtime metadata to a function. Will be available in the template.
331 
332 	History:
333 		Added July 12, 2021
334 +/
335 struct meta {
336 	string name;
337 	string value;
338 }
339 
340 /++
341 	Can be used as a return value from one of your own methods when rendering websites with [WebPresenterWithTemplateSupport].
342 +/
343 struct RenderTemplate {
344 	string name;
345 	var context = var.emptyObject;
346 	var skeletonContext = var.emptyObject;
347 }
348 
349 
350 /++
351 	Make a class that inherits from this with your further customizations, or minimally:
352 	---
353 	class MyPresenter : WebPresenterWithTemplateSupport!MyPresenter { }
354 	---
355 +/
356 template WebPresenterWithTemplateSupport(CTRP) {
357 	import arsd.cgi;
358 	class WebPresenterWithTemplateSupport : WebPresenter!(CTRP) {
359 		override Element htmlContainer() {
360 			auto skeleton = renderTemplate("generic.html");
361 			return skeleton.requireSelector("main");
362 		}
363 
364 		static struct Meta {
365 			typeof(null) at;
366 			string templateName;
367 			string skeletonName;
368 			string[string] meta;
369 			Form function(WebPresenterWithTemplateSupport presenter) automaticForm;
370 			alias at this;
371 		}
372 		template methodMeta(alias method) {
373 			static Meta helper() {
374 				Meta ret;
375 
376 				// ret.at = typeof(super).methodMeta!method;
377 
378 				foreach(attr; __traits(getAttributes, method))
379 					static if(is(typeof(attr) == Template))
380 						ret.templateName = attr.name;
381 					else static if(is(typeof(attr) == Skeleton))
382 						ret.skeletonName = attr.name;
383 					else static if(is(typeof(attr) == .meta))
384 						ret.meta[attr.name] = attr.value;
385 
386 				ret.automaticForm = function Form(WebPresenterWithTemplateSupport presenter) {
387 					return presenter.createAutomaticFormForFunction!(method, typeof(&method))(null);
388 				};
389 
390 				return ret;
391 			}
392 			enum methodMeta = helper();
393 		}
394 
395 		/// You can override this
396 		void addContext(Cgi cgi, var ctx) {}
397 
398 		void presentSuccessfulReturnAsHtml(T : RenderTemplate)(Cgi cgi, T ret, Meta meta) {
399 			addContext(cgi, ret.context);
400 			auto skeleton = renderTemplate(ret.name, ret.context, ret.skeletonContext);
401 			cgi.setResponseContentType("text/html; charset=utf8");
402 			cgi.gzipResponse = true;
403 			cgi.write(skeleton.toString(), true);
404 		}
405 
406 		void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, Meta meta) {
407 			if(meta.templateName.length) {
408 				var sobj = var.emptyObject;
409 
410 				var obj = var.emptyObject;
411 
412 				obj.data = ret;
413 
414 				/+
415 				sobj.meta = var.emptyObject;
416 				foreach(k,v; meta.meta)
417 					sobj.meta[k] = v;
418 				+/
419 
420 				obj.meta = var.emptyObject;
421 				foreach(k,v; meta.meta)
422 					obj.meta[k] = v;
423 
424 				obj.meta.currentPath = cgi.pathInfo;
425 				obj.meta.automaticForm = { return meta.automaticForm(this).toString; };
426 
427 				presentSuccessfulReturnAsHtml(cgi, RenderTemplate(meta.templateName, obj, sobj), meta);
428 			} else
429 				super.presentSuccessfulReturnAsHtml(cgi, ret, meta);
430 		}
431 	}
432 }
433 
434 auto serveTemplateDirectory()(string urlPrefix, string directory = null, string skeleton = null, string extension = ".html") {
435 	import arsd.cgi;
436 	import std.file;
437 
438 	assert(urlPrefix[0] == '/');
439 	assert(urlPrefix[$-1] == '/');
440 
441 	static struct DispatcherDetails {
442 		string directory;
443 		string skeleton;
444 		string extension;
445 	}
446 
447 	if(directory is null)
448 		directory = urlPrefix[1 .. $];
449 
450 	assert(directory[$-1] == '/');
451 
452 	static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
453 		auto file = cgi.pathInfo[urlPrefix.length .. $];
454 		if(file.indexOf("/") != -1 || file.indexOf("\\") != -1)
455 			return false;
456 
457 		auto fn = "templates/" ~ details.directory ~ file ~ details.extension;
458 		if(std.file.exists(fn)) {
459 			cgi.setCache(true);
460 			auto doc = renderTemplate(fn["templates/".length.. $]);
461 			cgi.gzipResponse = true;
462 			cgi.write(doc.toString, true);
463 			return true;
464 		} else {
465 			return false;
466 		}
467 	}
468 
469 	return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, skeleton, extension));
470 }