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 +/