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 }