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 }