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 			<render-template file="partial.html" />
24 		</main>
25 	```
26 
27 	Functions available:
28 		`encodeURIComponent`, `formatDate`, `dayOfWeek`, `formatTime`
29 +/
30 module arsd.webtemplate;
31 
32 // FIXME: make script exceptions show line from the template it was in too
33 
34 import arsd.script;
35 import arsd.dom;
36 
37 public import arsd.jsvar : var;
38 
39 class TemplateException : Exception {
40 	string templateName;
41 	var context;
42 	Exception e;
43 	this(string templateName, var context, Exception e) {
44 		this.templateName = templateName;
45 		this.context = context;
46 		this.e = e;
47 
48 		super("Exception in template " ~ templateName ~ ": " ~ e.msg);
49 	}
50 }
51 
52 Document renderTemplate(string templateName, var context = var.emptyObject, var skeletonContext = var.emptyObject) {
53 	import std.file;
54 	import arsd.cgi;
55 
56 	try {
57 		context.encodeURIComponent = function string(var f) {
58 			import std.uri;
59 			return encodeComponent(f.get!string);
60 		};
61 
62 		context.formatDate = function string(string s) {
63 			if(s.length < 10)
64 				return s;
65 			auto year = s[0 .. 4];
66 			auto month = s[5 .. 7];
67 			auto day = s[8 .. 10];
68 
69 			return month ~ "/" ~ day ~ "/" ~ year;
70 		};
71 
72 		context.dayOfWeek = function string(string s) {
73 			import std.datetime;
74 			return daysOfWeekFullNames[Date.fromISOExtString(s[0 .. 10]).dayOfWeek];
75 		};
76 
77 		context.formatTime = function string(string s) {
78 			if(s.length < 20)
79 				return s;
80 			auto hour = s[11 .. 13].to!int;
81 			auto minutes = s[14 .. 16].to!int;
82 			auto seconds = s[17 .. 19].to!int;
83 
84 			auto am = (hour >= 12) ? "PM" : "AM";
85 			if(hour > 12)
86 				hour -= 12;
87 
88 			return hour.to!string ~ (minutes < 10 ? ":0" : ":") ~ minutes.to!string ~ " " ~ am;
89 		};
90 
91 		auto skeleton = new Document(readText("templates/skeleton.html"), true, true);
92 		auto document = new Document();
93 		document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom
94 		document.parse("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", true, true);
95 
96 		expandTemplate(skeleton.root, skeletonContext);
97 
98 		foreach(nav; skeleton.querySelectorAll("nav[data-relative-to]")) {
99 			auto r = nav.getAttribute("data-relative-to");
100 			foreach(a; nav.querySelectorAll("a")) {
101 				a.attrs.href = Uri(a.attrs.href).basedOn(Uri(r));// ~ a.attrs.href;
102 			}
103 		}
104 
105 		expandTemplate(document.root, context);
106 
107 		// also do other unique elements and move them over.
108 		// and try partials.
109 
110 		auto templateMain = document.requireSelector(":root > main");
111 		if(templateMain.hasAttribute("body-class")) {
112 			skeleton.requireSelector("body").addClass(templateMain.getAttribute("body-class"));
113 			templateMain.removeAttribute("body-class");
114 		}
115 
116 		skeleton.requireSelector("main").replaceWith(templateMain.removeFromTree);
117 		if(auto title = document.querySelector(":root > title"))
118 			skeleton.requireSelector(":root > head > title").innerHTML = title.innerHTML;
119 
120 		debug
121 		skeleton.root.prependChild(new HtmlComment(null, templateName ~ " inside skeleton.html"));
122 
123 		return skeleton;
124 	} catch(Exception e) {
125 		throw new TemplateException(templateName, context, e);
126 		//throw e;
127 	}
128 }
129 
130 // I don't particularly like this
131 void expandTemplate(Element root, var context) {
132 	import std..string;
133 
134 	string replaceThingInString(string v) {
135 		auto idx = v.indexOf("<%=");
136 		if(idx == -1)
137 			return v;
138 		auto n = v[0 .. idx];
139 		auto r = v[idx + "<%=".length .. $];
140 
141 		auto end = r.indexOf("%>");
142 		if(end == -1)
143 			throw new Exception("unclosed asp code in attribute");
144 		auto code = r[0 .. end];
145 		r = r[end + "%>".length .. $];
146 
147 		import arsd.script;
148 		auto res = interpret(code, context).get!string;
149 
150 		return n ~ res ~ replaceThingInString(r);
151 	}
152 
153 	foreach(k, v; root.attributes) {
154 		if(k == "onrender") {
155 			continue;
156 		}
157 
158 		v = replaceThingInString(v);
159 
160 		root.setAttribute(k, v);
161 	}
162 
163 	bool lastBoolResult;
164 
165 	foreach(ele; root.children) {
166 		if(ele.tagName == "if-true") {
167 			auto fragment = new DocumentFragment(null);
168 			import arsd.script;
169 			auto got = interpret(ele.attrs.cond, context).opCast!bool;
170 			if(got) {
171 				ele.tagName = "root";
172 				expandTemplate(ele, context);
173 				fragment.stealChildren(ele);
174 			}
175 			lastBoolResult = got;
176 			ele.replaceWith(fragment);
177 		} else if(ele.tagName == "or-else") {
178 			auto fragment = new DocumentFragment(null);
179 			if(!lastBoolResult) {
180 				ele.tagName = "root";
181 				expandTemplate(ele, context);
182 				fragment.stealChildren(ele);
183 			}
184 			ele.replaceWith(fragment);
185 		} else if(ele.tagName == "for-each") {
186 			auto fragment = new DocumentFragment(null);
187 			var nc = var.emptyObject(context);
188 			lastBoolResult = false;
189 			auto got = interpret(ele.attrs.over, context);
190 			foreach(item; got) {
191 				lastBoolResult = true;
192 				nc[ele.attrs.as] = item;
193 				auto clone = ele.cloneNode(true);
194 				clone.tagName = "root"; // it certainly isn't a for-each anymore!
195 				expandTemplate(clone, nc);
196 
197 				fragment.stealChildren(clone);
198 			}
199 			ele.replaceWith(fragment);
200 		} else if(ele.tagName == "render-template") {
201 			import std.file;
202 			auto templateName = ele.getAttribute("file");
203 			auto document = new Document();
204 			document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom
205 			document.parse("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", true, true);
206 
207 			expandTemplate(document.root, context);
208 
209 			auto fragment = new DocumentFragment(null);
210 
211 			debug fragment.appendChild(new HtmlComment(null, templateName));
212 			fragment.stealChildren(document.root);
213 			debug fragment.appendChild(new HtmlComment(null, "end " ~ templateName));
214 
215 			ele.replaceWith(fragment);
216 		} else if(auto asp = cast(AspCode) ele) {
217 			auto code = asp.source[1 .. $-1];
218 			auto fragment = new DocumentFragment(null);
219 			if(code[0] == '=') {
220 				import arsd.script;
221 				if(code.length > 5 && code[1 .. 5] == "HTML") {
222 					auto got = interpret(code[5 .. $], context);
223 					if(auto native = got.getWno!Element)
224 						fragment.appendChild(native);
225 					else
226 						fragment.innerHTML = got.get!string;
227 				} else {
228 					auto got = interpret(code[1 .. $], context).get!string;
229 					fragment.innerText = got;
230 				}
231 			}
232 			asp.replaceWith(fragment);
233 		} else {
234 			expandTemplate(ele, context);
235 		}
236 	}
237 
238 	if(root.hasAttribute("onrender")) {
239 		var nc = var.emptyObject(context);
240 		nc["this"] = wrapNativeObject(root);
241 		nc["this"]["populateFrom"]._function = delegate var(var this_, var[] args) {
242 			auto form = cast(Form) root;
243 			if(form is null) return this_;
244 			foreach(k, v; args[0]) {
245 				populateForm(form, v, k.get!string);
246 			}
247 			return this_;
248 		};
249 		interpret(root.getAttribute("onrender"), nc);
250 
251 		root.removeAttribute("onrender");
252 	}
253 }
254 
255 void populateForm(Form form, var obj, string name) {
256 	import std..string;
257 
258 	if(obj.payloadType == var.Type.Object) {
259 		foreach(k, v; obj) {
260 			auto fn = name.replace("%", k.get!string);
261 			populateForm(form, v, fn ~ "["~k.get!string~"]");
262 		}
263 	} else {
264 		//import std.stdio; writeln("SET ", name, " ", obj, " ", obj.payloadType);
265 		form.setValue(name, obj.get!string);
266 	}
267 
268 }
269 
270 immutable daysOfWeekFullNames = [
271 	"Sunday",
272 	"Monday",
273 	"Tuesday",
274 	"Wednesday",
275 	"Thursday",
276 	"Friday",
277 	"Saturday"
278 ];
279 
280 /++
281 	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.
282 
283 	Inside the template, the value returned by the function will be available in the context as the variable `data`.
284 +/
285 struct Template {
286 	string name;
287 }
288 /++
289 	UDA to put on a method when using [WebPresenterWithTemplateSupport]. Overrides the default template skeleton file name.
290 +/
291 struct Skeleton {
292 	string name;
293 }
294 /++
295 	Can be used as a return value from one of your own methods when rendering websites with [WebPresenterWithTemplateSupport].
296 +/
297 struct RenderTemplate {
298 	string name;
299 	var context = var.emptyObject;
300 	var skeletonContext = var.emptyObject;
301 }
302 
303 
304 /++
305 	Make a class that inherits from this with your further customizations, or minimally:
306 	---
307 	class MyPresenter : WebPresenterWithTemplateSupport!MyPresenter { }
308 	---
309 +/
310 template WebPresenterWithTemplateSupport(CTRP) {
311 	import arsd.cgi;
312 	class WebPresenterWithTemplateSupport : WebPresenter!(CTRP) {
313 		override Element htmlContainer() {
314 			auto skeleton = renderTemplate("generic.html");
315 			return skeleton.requireSelector("main");
316 		}
317 
318 		static struct Meta {
319 			typeof(null) at;
320 			string templateName;
321 			string skeletonName;
322 			alias at this;
323 		}
324 		template methodMeta(alias method) {
325 			static Meta helper() {
326 				Meta ret;
327 
328 				// ret.at = typeof(super).methodMeta!method;
329 
330 				foreach(attr; __traits(getAttributes, method))
331 					static if(is(typeof(attr) == Template))
332 						ret.templateName = attr.name;
333 					else static if(is(typeof(attr) == Skeleton))
334 						ret.skeletonName = attr.name;
335 
336 				return ret;
337 			}
338 			enum methodMeta = helper();
339 		}
340 
341 		/// You can override this
342 		void addContext(Cgi cgi, var ctx) {}
343 
344 		void presentSuccessfulReturnAsHtml(T : RenderTemplate)(Cgi cgi, T ret, Meta meta) {
345 			addContext(cgi, ret.context);
346 			auto skeleton = renderTemplate(ret.name, ret.context, ret.skeletonContext);
347 			cgi.setResponseContentType("text/html; charset=utf8");
348 			cgi.gzipResponse = true;
349 			cgi.write(skeleton.toString(), true);
350 		}
351 
352 		void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, Meta meta) {
353 			if(meta.templateName.length) {
354 				var obj = var.emptyObject;
355 				obj.data = ret;
356 				presentSuccessfulReturnAsHtml(cgi, RenderTemplate(meta.templateName, obj), meta);
357 			} else
358 				super.presentSuccessfulReturnAsHtml(cgi, ret, meta);
359 		}
360 	}
361 }
Suggestion Box / Bug Report