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 module arsd.webtemplate;
5 
6 // FIXME: make script exceptions show line from the template it was in too
7 
8 import arsd.script;
9 import arsd.dom;
10 
11 public import arsd.jsvar : var;
12 
13 struct RenderTemplate {
14 	string name;
15 	var context = var.emptyObject;
16 	var skeletonContext = var.emptyObject;
17 }
18 
19 class TemplateException : Exception {
20 	string templateName;
21 	var context;
22 	Exception e;
23 	this(string templateName, var context, Exception e) {
24 		this.templateName = templateName;
25 		this.context = context;
26 		this.e = e;
27 
28 		super("Exception in template " ~ templateName ~ ": " ~ e.msg);
29 	}
30 }
31 
32 Document renderTemplate(string templateName, var context = var.emptyObject, var skeletonContext = var.emptyObject) {
33 	import std.file;
34 	import arsd.cgi;
35 
36 	try {
37 		context.encodeURIComponent = function string(var f) {
38 			import std.uri;
39 			return encodeComponent(f.get!string);
40 		};
41 
42 		context.formatDate = function string(string s) {
43 			if(s.length < 10)
44 				return s;
45 			auto year = s[0 .. 4];
46 			auto month = s[5 .. 7];
47 			auto day = s[8 .. 10];
48 
49 			return month ~ "/" ~ day ~ "/" ~ year;
50 		};
51 
52 		context.dayOfWeek = function string(string s) {
53 			import std.datetime;
54 			return daysOfWeekFullNames[Date.fromISOExtString(s[0 .. 10]).dayOfWeek];
55 		};
56 
57 		context.formatTime = function string(string s) {
58 			if(s.length < 20)
59 				return s;
60 			auto hour = s[11 .. 13].to!int;
61 			auto minutes = s[14 .. 16].to!int;
62 			auto seconds = s[17 .. 19].to!int;
63 
64 			auto am = (hour >= 12) ? "PM" : "AM";
65 			if(hour > 12)
66 				hour -= 12;
67 
68 			return hour.to!string ~ (minutes < 10 ? ":0" : ":") ~ minutes.to!string ~ " " ~ am;
69 		};
70 
71 		auto skeleton = new Document(readText("templates/skeleton.html"), true, true);
72 		auto document = new Document();
73 		document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom
74 		document.parse("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", true, true);
75 
76 		expandTemplate(skeleton.root, skeletonContext);
77 
78 		foreach(nav; skeleton.querySelectorAll("nav[data-relative-to]")) {
79 			auto r = nav.getAttribute("data-relative-to");
80 			foreach(a; nav.querySelectorAll("a")) {
81 				a.attrs.href = Uri(a.attrs.href).basedOn(Uri(r));// ~ a.attrs.href;
82 			}
83 		}
84 
85 		expandTemplate(document.root, context);
86 
87 		// also do other unique elements and move them over.
88 		// and try partials.
89 
90 		auto templateMain = document.requireSelector(":root > main");
91 		if(templateMain.hasAttribute("body-class")) {
92 			skeleton.requireSelector("body").addClass(templateMain.getAttribute("body-class"));
93 			templateMain.removeAttribute("body-class");
94 		}
95 
96 		skeleton.requireSelector("main").replaceWith(templateMain.removeFromTree);
97 		if(auto title = document.querySelector(":root > title"))
98 			skeleton.requireSelector(":root > head > title").innerHTML = title.innerHTML;
99 
100 		debug
101 		skeleton.root.prependChild(new HtmlComment(null, templateName ~ " inside skeleton.html"));
102 
103 		return skeleton;
104 	} catch(Exception e) {
105 		throw new TemplateException(templateName, context, e);
106 	}
107 }
108 
109 // I don't particularly like this
110 void expandTemplate(Element root, var context) {
111 	import std.string;
112 
113 	string replaceThingInString(string v) {
114 		auto idx = v.indexOf("<%=");
115 		if(idx == -1)
116 			return v;
117 		auto n = v[0 .. idx];
118 		auto r = v[idx + "<%=".length .. $];
119 
120 		auto end = r.indexOf("%>");
121 		if(end == -1)
122 			throw new Exception("unclosed asp code in attribute");
123 		auto code = r[0 .. end];
124 		r = r[end + "%>".length .. $];
125 
126 		import arsd.script;
127 		auto res = interpret(code, context).get!string;
128 
129 		return n ~ res ~ replaceThingInString(r);
130 	}
131 
132 	foreach(k, v; root.attributes) {
133 		if(k == "onrender") {
134 			continue;
135 		}
136 
137 		v = replaceThingInString(v);
138 
139 		root.setAttribute(k, v);
140 	}
141 
142 	bool lastBoolResult;
143 
144 	foreach(ele; root.children) {
145 		if(ele.tagName == "if-true") {
146 			auto fragment = new DocumentFragment(null);
147 			import arsd.script;
148 			auto got = interpret(ele.attrs.cond, context).get!bool;
149 			if(got) {
150 				ele.tagName = "root";
151 				expandTemplate(ele, context);
152 				fragment.stealChildren(ele);
153 			}
154 			lastBoolResult = got;
155 			ele.replaceWith(fragment);
156 		} else if(ele.tagName == "or-else") {
157 			auto fragment = new DocumentFragment(null);
158 			if(!lastBoolResult) {
159 				ele.tagName = "root";
160 				expandTemplate(ele, context);
161 				fragment.stealChildren(ele);
162 			}
163 			ele.replaceWith(fragment);
164 		} else if(ele.tagName == "for-each") {
165 			auto fragment = new DocumentFragment(null);
166 			var nc = var.emptyObject(context);
167 			lastBoolResult = false;
168 			auto got = interpret(ele.attrs.over, context);
169 			foreach(item; got) {
170 				lastBoolResult = true;
171 				nc[ele.attrs.as] = item;
172 				auto clone = ele.cloneNode(true);
173 				clone.tagName = "root"; // it certainly isn't a for-each anymore!
174 				expandTemplate(clone, nc);
175 
176 				fragment.stealChildren(clone);
177 			}
178 			ele.replaceWith(fragment);
179 		} else if(ele.tagName == "render-template") {
180 			import std.file;
181 			auto templateName = ele.getAttribute("file");
182 			auto document = new Document();
183 			document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom
184 			document.parse("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", true, true);
185 
186 			expandTemplate(document.root, context);
187 
188 			auto fragment = new DocumentFragment(null);
189 
190 			debug fragment.appendChild(new HtmlComment(null, templateName));
191 			fragment.stealChildren(document.root);
192 			debug fragment.appendChild(new HtmlComment(null, "end " ~ templateName));
193 
194 			ele.replaceWith(fragment);
195 		} else if(auto asp = cast(AspCode) ele) {
196 			auto code = asp.source[1 .. $-1];
197 			auto fragment = new DocumentFragment(null);
198 			if(code[0] == '=') {
199 				import arsd.script;
200 				if(code.length > 5 && code[1 .. 5] == "HTML") {
201 					auto got = interpret(code[5 .. $], context);
202 					if(auto native = got.getWno!Element)
203 						fragment.appendChild(native);
204 					else
205 						fragment.innerHTML = got.get!string;
206 				} else {
207 					auto got = interpret(code[1 .. $], context).get!string;
208 					fragment.innerText = got;
209 				}
210 			}
211 			asp.replaceWith(fragment);
212 		} else {
213 			expandTemplate(ele, context);
214 		}
215 	}
216 
217 	if(root.hasAttribute("onrender")) {
218 		var nc = var.emptyObject(context);
219 		nc["this"] = wrapNativeObject(root);
220 		nc["this"]["populateFrom"]._function = delegate var(var this_, var[] args) {
221 			auto form = cast(Form) root;
222 			if(form is null) return this_;
223 			foreach(k, v; args[0]) {
224 				populateForm(form, v, k.get!string);
225 			}
226 			return this_;
227 		};
228 		interpret(root.getAttribute("onrender"), nc);
229 
230 		root.removeAttribute("onrender");
231 	}
232 }
233 
234 void populateForm(Form form, var obj, string name) {
235 	import std.string;
236 
237 	if(obj.payloadType == var.Type.Object) {
238 		foreach(k, v; obj) {
239 			auto fn = name.replace("%", k.get!string);
240 			populateForm(form, v, fn ~ "["~k.get!string~"]");
241 		}
242 	} else {
243 		//import std.stdio; writeln("SET ", name, " ", obj, " ", obj.payloadType);
244 		form.setValue(name, obj.get!string);
245 	}
246 
247 }
248 
249 immutable daysOfWeekFullNames = [
250 	"Sunday",
251 	"Monday",
252 	"Tuesday",
253 	"Wednesday",
254 	"Thursday",
255 	"Friday",
256 	"Saturday"
257 ];
258 
259 
260 /+
261 mixin template WebTemplatePresenterSupport() {
262 
263 }
264 +/