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 body-class="foo">
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" index="idx">
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 30 <document-fragment></document-fragment>
31 32 <script>
33 var a = <%= some_var %>; // it will be json encoded in a script tag, so it can be safely used from Javascript
34 </script>
35 </main>
36 ```
37 38 Functions available:
39 `encodeURIComponent`, `formatDate`, `dayOfWeek`, `formatTime`, `filterKeys`
40 41 History:
42 Things inside script tag were added on January 7, 2022.
43 44 This module was added to dub on September 11, 2023 (dub v11.2).
45 46 It was originally written in July 2019 to support a demonstration of moving a ruby on rails app to D.
47 +/48 modulearsd.webtemplate;
49 50 // FIXME: make script exceptions show line from the template it was in too51 52 importarsd.script;
53 importarsd.dom;
54 55 publicimportarsd.jsvar : var;
56 57 /++
58 A class to render web template files into HTML documents.
59 60 61 You can customize various parts of this with subclassing and dependency injection. Customization hook points include:
62 63 $(NUMBERED_LIST
64 * You pass a [TemplateLoader] instance to the constructor. This object is responsible for loading a particular
65 named template and returning a string of its html text. If you don't pass one, the default behavior is to load a
66 particular file out of the templates directory.
67 68 * The next step is transforming the string the TemplateLoader returned into a document object model. This is done
69 by a private function at this time. If you want to use a different format than HTML, you should either embed the other
70 language in your template (you can pass a translator to the constructor, details to follow later in this document)
71 72 * Next, the contexts must be prepared. It will call [addDefaultFunctions] on each one to prepare them. You can override that
73 to provide more or fewer functions.
74 75 * Now, it is time to expand the template. This is done by a private function, so you cannot replace this step, but you can
76 customize it in some ways by passing functions to the constructor's `embeddedTagTranslators` argument.
77 78 * At this point, it combines the expanded template with the skeleton to form the complete, expanded document.
79 80 * Finally, it will call your custom post-processing function right before returning the document. You can override the [postProcess] method to add custom behavior to this step.
81 )
82 83 ### Custom Special Tags
84 85 You can define translator for special tags, such as to embed a block of custom markup inside your template.
86 87 Let's suppose we want to add a `<plaintext>...</plaintext>` tag that does not need HTML entity encoding.
88 89 ```html
90 <main>
91 I can use <b>HTML</b> & need to respect entity encoding here.
92 93 <plaintext>
94 But here, I can write & as plain text and <b>html</b> will not work.
95 </plaintext>
96 </main>
97 ```
98 99 We can make that possible by defining a custom special tag when constructing the `WebTemplateRenderer`, like this:
100 101 ---
102 auto renderer = new WebTemplateRenderer(null /* no special loader needed */, [
103 // this argument is the special tag name and a function to work with it
104 // listed as associative arrays.
105 "plaintext": function(string content, string[string] attributes) {
106 import arsd.dom;
107 return WebTemplateRenderer.EmbeddedTagResult(new TextNode(content));
108 }
109 ]);
110 ---
111 112 The associative array keys are the special tag name. For each one, this instructs the HTML parser to treat them similarly to `<script>` - it will read until the closing tag, making no attempt to parse anything else inside it. It just scoops of the content, then calls your function to decide what to do with it.
113 114 $(SIDEBAR
115 Note: just like with how you cannot use `"</script>"` in a Javascript block in HTML, you also need to avoid using the closing tag as a string in your custom thing!
116 )
117 118 Your function is given an associative array of attributes on the special tag and its inner content, as raw source, from the file. You must construct an appropriate DOM element from the content (including possibly a `DocumentFragment` object if you need multiple tags inside) and return it, along with, optionally, an enumerated value telling the renderer if it should try to expand template text inside this new element. If you don't provide a value, it will try to automatically guess what it should do based on the returned element type. (That is, if you return a text node, it will try to do a string-based replacement, and if you return another node, it will descend into it the same as any other node written in the document looking for `AspCode` elements.)
119 120 The example given here returns a `TextNode`, so we let it do the default string-based template content processing. But if we returned `WebTemplateRenderer.EmbeddedTagResult(new TextNode(content), false);`, it would not support embedded templates and any `<% .. %>` stuff would be left as-is.
121 122 $(TIP
123 You can trim some of that namespace spam if you make a subclass and pass it to `super` inside your constructor.
124 )
125 126 History:
127 Added February 5, 2024 (dub v11.5)
128 +/129 classWebTemplateRenderer {
130 privateTemplateLoaderloader;
131 privateEmbeddedTagResultfunction(stringcontent, AttributesHolderattributes)[string] embeddedTagTranslators;
132 133 /++
134 135 +/136 this(TemplateLoaderloader = null, EmbeddedTagResultfunction(stringcontent, AttributesHolderattributes)[string] embeddedTagTranslators = null) {
137 if(loaderisnull)
138 loader = TemplateLoader.forDirectory("templates/");
139 this.loader = loader;
140 this.embeddedTagTranslators = embeddedTagTranslators;
141 }
142 143 /++
144 145 +/146 structEmbeddedTagResult {
147 Elementelement;
148 boolscanForTemplateContent = true;
149 }
150 151 /++
152 153 +/154 finalDocumentrenderTemplate(stringtemplateName, varcontext = var.emptyObject, varskeletonContext = var.emptyObject, stringskeletonName = null) {
155 importarsd.cgi;
156 157 try {
158 addDefaultFunctions(context);
159 addDefaultFunctions(skeletonContext);
160 161 if(skeletonName.length == 0)
162 skeletonName = "skeleton.html";
163 164 autoskeleton = parseTemplateString(loader.loadTemplateHtml(skeletonName), WrapTemplateIn.nothing);
165 autodocument = parseTemplateString(loader.loadTemplateHtml(templateName), WrapTemplateIn.rootElement);
166 167 expandTemplate(skeleton.root, skeletonContext);
168 169 foreach(nav; skeleton.querySelectorAll("nav[data-relative-to]")) {
170 autor = nav.getAttribute("data-relative-to");
171 foreach(a; nav.querySelectorAll("a")) {
172 a.attrs.href = Uri(a.attrs.href).basedOn(Uri(r));// ~ a.attrs.href;173 }
174 }
175 176 expandTemplate(document.root, context);
177 178 // also do other unique elements and move them over.179 // and have some kind of <document-fragment> that can be just reduced when going out in the final result.180 181 // and try partials.182 183 autotemplateMain = document.requireSelector(":root > main");
184 if(templateMain.hasAttribute("body-class")) {
185 skeleton.requireSelector("body").addClass(templateMain.getAttribute("body-class"));
186 templateMain.removeAttribute("body-class");
187 }
188 189 skeleton.requireSelector("main").replaceWith(templateMain.removeFromTree);
190 191 if(autotitle = document.querySelector(":root > title"))
192 skeleton.requireSelector(":root > head > title").innerHTML = title.innerHTML;
193 194 // also allow top-level unique id replacements195 foreach(item; document.querySelectorAll(":root > [id]"))
196 skeleton.requireElementById(item.id).replaceWith(item.removeFromTree);
197 198 foreach(df; skeleton.querySelectorAll("document-fragment"))
199 df.stripOut();
200 201 debug202 skeleton.root.prependChild(newHtmlComment(null, templateName ~ " inside skeleton.html"));
203 204 postProcess(skeleton);
205 206 returnskeleton;
207 } catch(Exceptione) {
208 thrownewTemplateException(templateName, context, e);
209 //throw e;210 }
211 }
212 213 privateDocumentparseTemplateString(stringtemplateHtml, WrapTemplateInwrapTemplateIn) {
214 autodocument = newDocument();
215 document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom216 finalswitch(wrapTemplateIn) {
217 caseWrapTemplateIn.nothing:
218 // no change needed219 break;
220 caseWrapTemplateIn.rootElement:
221 templateHtml = "<root>" ~ templateHtml ~ "</root>";
222 break;
223 }
224 foreach(k, v; embeddedTagTranslators)
225 document.rawSourceElements ~= k;
226 document.parse(templateHtml, true, true);
227 returndocument;
228 }
229 230 privateenumWrapTemplateIn {
231 nothing,
232 rootElement233 }
234 235 /++
236 Adds the default functions to the context. You can override this to add additional default functions (or static data) to the context objects.
237 +/238 voidaddDefaultFunctions(varcontext) {
239 importstd.conv;
240 // FIXME: I prolly want it to just set the prototype or something241 242 /+
243 foo |> filterKeys(["foo", "bar"]);
244 245 It needs to match the filter, then if it is -pattern, it is removed and if it is +pattern, it is retained.
246 247 First one that matches applies to the key, so the last one in the list is your default.
248 249 Default is to reject. Putting a "*" at the end will keep everything not removed though.
250 251 ["-foo", "*"] // keep everything except foo
252 +/253 context.filterKeys = functionvar(varf, string[] filters) {
254 importstd.path;
255 varo = var.emptyObject;
256 foreach(k, v; f) {
257 boolkeep = false;
258 foreach(filter; filters) {
259 if(filter.length == 0)
260 thrownewException("invalid filter");
261 boolfilterOff = filter[0] == '-';
262 if(filterOff)
263 filter = filter[1 .. $];
264 if(globMatch(k.get!string, filter)) {
265 keep = !filterOff;
266 break;
267 }
268 }
269 if(keep)
270 o[k] = v;
271 }
272 returno;
273 };
274 275 context.encodeURIComponent = functionstring(varf) {
276 importarsd.core;
277 returnencodeUriComponent(f.get!string);
278 };
279 280 context.formatDate = functionstring(strings) {
281 if(s.length < 10)
282 returns;
283 autoyear = s[0 .. 4];
284 automonth = s[5 .. 7];
285 autoday = s[8 .. 10];
286 287 returnmonth ~ "/" ~ day ~ "/" ~ year;
288 };
289 290 context.dayOfWeek = functionstring(strings) {
291 importstd.datetime;
292 returndaysOfWeekFullNames[Date.fromISOExtString(s[0 .. 10]).dayOfWeek];
293 };
294 295 context.formatTime = functionstring(strings) {
296 if(s.length < 20)
297 returns;
298 autohour = s[11 .. 13].to!int;
299 autominutes = s[14 .. 16].to!int;
300 autoseconds = s[17 .. 19].to!int;
301 302 autoam = (hour >= 12) ? "PM" : "AM";
303 if(hour > 12)
304 hour -= 12;
305 306 returnhour.to!string ~ (minutes < 10 ? ":0" : ":") ~ minutes.to!string ~ " " ~ am;
307 };
308 309 // don't want checking meta or data to be an error310 if(context.meta == null)
311 context.meta = var.emptyObject;
312 if(context.data == null)
313 context.data = var.emptyObject;
314 }
315 316 /++
317 The default is currently to do nothing. This function only exists for you to override it.
318 319 However, this may change in the future. To protect yourself, if you subclass and override
320 this method, always call `super.postProcess(document);` before doing your own customizations.
321 +/322 voidpostProcess(Documentdocument) {
323 324 }
325 326 privatevoidexpandTemplate(Elementroot, varcontext) {
327 importstd.string;
328 329 stringreplaceThingInString(stringv) {
330 autoidx = v.indexOf("<%=");
331 if(idx == -1)
332 returnv;
333 auton = v[0 .. idx];
334 autor = v[idx + "<%=".length .. $];
335 336 autoend = r.indexOf("%>");
337 if(end == -1)
338 thrownewException("unclosed asp code in attribute");
339 autocode = r[0 .. end];
340 r = r[end + "%>".length .. $];
341 342 importarsd.script;
343 autores = interpret(code, context).get!string;
344 345 returnn ~ res ~ replaceThingInString(r);
346 }
347 348 foreach(k, v; root.attributes) {
349 if(k == "onrender") {
350 continue;
351 }
352 353 v = replaceThingInString(v);
354 355 root.setAttribute(k, v);
356 }
357 358 boollastBoolResult;
359 360 foreach(ele; root.children) {
361 if(ele.tagName == "if-true") {
362 autofragment = newDocumentFragment(null);
363 importarsd.script;
364 autogot = interpret(ele.attrs.cond, context).opCast!bool;
365 if(got) {
366 ele.tagName = "root";
367 expandTemplate(ele, context);
368 fragment.stealChildren(ele);
369 }
370 lastBoolResult = got;
371 ele.replaceWith(fragment);
372 } elseif(ele.tagName == "or-else") {
373 autofragment = newDocumentFragment(null);
374 if(!lastBoolResult) {
375 ele.tagName = "root";
376 expandTemplate(ele, context);
377 fragment.stealChildren(ele);
378 }
379 ele.replaceWith(fragment);
380 } elseif(ele.tagName == "for-each") {
381 autofragment = newDocumentFragment(null);
382 varnc = var.emptyObject(context);
383 lastBoolResult = false;
384 autogot = interpret(ele.attrs.over, context);
385 foreach(k, item; got) {
386 lastBoolResult = true;
387 nc[ele.attrs.as] = item;
388 if(ele.attrs.index.length)
389 nc[ele.attrs.index] = k;
390 autoclone = ele.cloneNode(true);
391 clone.tagName = "root"; // it certainly isn't a for-each anymore!392 expandTemplate(clone, nc);
393 394 fragment.stealChildren(clone);
395 }
396 ele.replaceWith(fragment);
397 } elseif(ele.tagName == "render-template") {
398 importstd.file;
399 autotemplateName = ele.getAttribute("file");
400 autodocument = newDocument();
401 document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom402 document.parse("<root>" ~ loader.loadTemplateHtml(templateName) ~ "</root>", true, true);
403 404 varobj = var.emptyObject;
405 obj.prototype = context;
406 407 // FIXME: there might be other data you pass from the parent...408 if(autodata = ele.getAttribute("data")) {
409 obj["data"] = var.fromJson(data);
410 }
411 412 expandTemplate(document.root, obj);
413 414 autofragment = newDocumentFragment(null);
415 416 debugfragment.appendChild(newHtmlComment(null, templateName));
417 fragment.stealChildren(document.root);
418 debugfragment.appendChild(newHtmlComment(null, "end " ~ templateName));
419 420 ele.replaceWith(fragment);
421 } elseif(ele.tagName == "hidden-form-data") {
422 autofrom = interpret(ele.attrs.from, context);
423 autoname = ele.attrs.name;
424 425 autoform = newForm(null);
426 427 populateForm(form, from, name);
428 429 autofragment = newDocumentFragment(null);
430 fragment.stealChildren(form);
431 432 ele.replaceWith(fragment);
433 } elseif(autoasp = cast(AspCode) ele) {
434 autocode = asp.source[1 .. $-1];
435 autofragment = newDocumentFragment(null);
436 if(code[0] == '=') {
437 importarsd.script;
438 if(code.length > 5 && code[1 .. 5] == "HTML") {
439 autogot = interpret(code[5 .. $], context);
440 if(autonative = got.getWno!Element)
441 fragment.appendChild(native);
442 else443 fragment.innerHTML = got.get!string;
444 } else {
445 autogot = interpret(code[1 .. $], context).get!string;
446 fragment.innerText = got;
447 }
448 }
449 asp.replaceWith(fragment);
450 } elseif(ele.tagName == "script") {
451 autosource = ele.innerHTML;
452 stringnewCode;
453 check_more:
454 autoidx = source.indexOf("<%=");
455 if(idx != -1) {
456 newCode ~= source[0 .. idx];
457 autoremaining = source[idx + 3 .. $];
458 idx = remaining.indexOf("%>");
459 if(idx == -1)
460 thrownewException("unclosed asp code in script");
461 autocode = remaining[0 .. idx];
462 463 autodata = interpret(code, context);
464 newCode ~= data.toJson();
465 466 source = remaining[idx + 2 .. $];
467 gotocheck_more;
468 }
469 470 if(newCodeisnull)
471 {} // nothing needed472 else {
473 newCode ~= source;
474 ele.innerRawSource = newCode;
475 }
476 } elseif(autopTranslator = ele.tagNameinembeddedTagTranslators) {
477 autoreplacement = (*pTranslator)(ele.innerHTML, ele.attributes);
478 if(replacement.elementisnull)
479 ele.stripOut();
480 else {
481 ele.replaceWith(replacement.element);
482 if(replacement.scanForTemplateContent) {
483 if(autotn = cast(TextNode) replacement.element)
484 tn.textContent = replaceThingInString(tn.nodeValue);
485 else486 expandTemplate(replacement.element, context);
487 }
488 }
489 } else {
490 expandTemplate(ele, context);
491 }
492 }
493 494 if(root.hasAttribute("onrender")) {
495 varnc = var.emptyObject(context);
496 nc["this"] = wrapNativeObject(root);
497 nc["this"]["populateFrom"] = delegatevar(varthis_, var[] args) {
498 autoform = cast(Form) root;
499 if(formisnull) returnthis_;
500 foreach(k, v; args[0]) {
501 populateForm(form, v, k.get!string);
502 }
503 returnthis_;
504 };
505 interpret(root.getAttribute("onrender"), nc);
506 507 root.removeAttribute("onrender");
508 }
509 }
510 }
511 512 /+
513 unittest {
514 515 }
516 +/517 518 deprecated("Use a WebTemplateRenderer class instead")
519 voidaddDefaultFunctions(varcontext) {
520 scoperenderer = newWebTemplateRenderer(null);
521 renderer.addDefaultFunctions(context);
522 }
523 524 525 // FIXME: want to show additional info from the exception, neatly integrated, whenever possible.526 classTemplateException : Exception {
527 stringtemplateName;
528 varcontext;
529 Exceptione;
530 this(stringtemplateName, varcontext, Exceptione) {
531 this.templateName = templateName;
532 this.context = context;
533 this.e = e;
534 535 super("Exception in template " ~ templateName ~ ": " ~ e.msg);
536 }
537 }
538 539 /++
540 A loader object for reading raw template, so you can use something other than files if you like.
541 542 See [TemplateLoader.forDirectory] to a pre-packaged class that implements a loader for a particular directory.
543 544 History:
545 Added December 11, 2023 (dub v11.3)
546 +/547 interfaceTemplateLoader {
548 /++
549 This is the main method to look up a template name and return its HTML as a string.
550 551 Typical implementation is to just `return std.file.readText(directory ~ name);`
552 +/553 stringloadTemplateHtml(stringname);
554 555 /++
556 Returns a loader for files in the given directory.
557 +/558 staticTemplateLoaderforDirectory(stringdirectoryName) {
559 if(directoryName.length && directoryName[$-1] != '/')
560 directoryName ~= "/";
561 562 returnnewclassTemplateLoader {
563 stringloadTemplateHtml(stringname) {
564 importstd.file;
565 returnreadText(directoryName ~ name);
566 }
567 };
568 }
569 }
570 571 /++
572 Loads a template from the template directory, applies the given context variables, and returns the html document in dom format. You can use [Document.toString] to make a string.
573 574 Parameters:
575 templateName = the name of the main template to load. This is usually a .html filename in the `templates` directory (but see also the `loader` param)
576 context = the global object available to scripts inside the template
577 skeletonContext = the global object available to the skeleton template
578 skeletonName = the name of the skeleton template to load. This is usually a .html filename in the `templates` directory (but see also the `loader` param), and the skeleton file has the boilerplate html and defines placeholders for the main template
579 loader = a class that defines how to load templates by name. If you pass `null`, it uses a default implementation that loads files from the `templates/` directory.
580 581 History:
582 Parameter `loader` was added on December 11, 2023 (dub v11.3)
583 584 See_Also:
585 [WebTemplateRenderer] gives you more control than the argument list here provides.
586 +/587 DocumentrenderTemplate(stringtemplateName, varcontext = var.emptyObject, varskeletonContext = var.emptyObject, stringskeletonName = null, TemplateLoaderloader = null) {
588 scopeautorenderer = newWebTemplateRenderer(loader);
589 returnrenderer.renderTemplate(templateName, context, skeletonContext, skeletonName);
590 }
591 592 /++
593 Shows how top-level things from the template are moved to their corresponding items on the skeleton.
594 +/595 unittest {
596 // for the unittest, we want to inject a loader that uses plain strings instead of files.597 autotestLoader = newclassTemplateLoader {
598 stringloadTemplateHtml(stringname) {
599 switch(name) {
600 case"skeleton":
601 return`
602 <html>
603 <head>
604 <!-- you can define replaceable things with ids -->
605 <!-- including <document-fragment>s which are stripped out when the template is finalized -->
606 <document-fragment id="header-stuff" />
607 </head>
608 <body>
609 <main></main>
610 </body>
611 </html>
612 `;
613 case"main":
614 return`
615 <main>Hello</main>
616 <document-fragment id="header-stuff">
617 <title>My title</title>
618 </document-fragment>
619 `;
620 default: assert(0);
621 }
622 }
623 };
624 625 Documentdoc = renderTemplate("main", var.emptyObject, var.emptyObject, "skeleton", testLoader);
626 627 assert(doc.querySelector("document-fragment") isnull); // the <document-fragment> items are stripped out628 assert(doc.querySelector("title") !isnull); // but the stuff from inside it is brought in629 assert(doc.requireSelector("main").textContent == "Hello"); // and the main from the template is moved to the skeelton630 }
631 632 voidpopulateForm(Formform, varobj, stringname) {
633 importstd.string;
634 635 if(obj.payloadType == var.Type.Object) {
636 form.setValue(name, "");
637 foreach(k, v; obj) {
638 autofn = name.replace("%", k.get!string);
639 // should I unify structs and assoctiavite arrays?640 populateForm(form, v, fn ~ "["~k.get!string~"]");
641 //populateForm(form, v, fn ~"."~k.get!string);642 }
643 } else {
644 //import std.stdio; writeln("SET ", name, " ", obj, " ", obj.payloadType);645 form.setValue(name, obj.get!string);
646 }
647 648 }
649 650 /++
651 Replaces `things[0]` with `things[1]` in `what` all at once.
652 Returns the new string.
653 654 History:
655 Added February 12, 2022. I might move it later.
656 +/657 stringmultiReplace(stringwhat, string[] things...) {
658 importstd.string; // FIXME: indexOf not actually ideal but meh659 if(things.length == 0)
660 returnwhat;
661 662 assert(things.length % 2 == 0);
663 664 stringn;
665 666 while(what.length) {
667 intnextIndex = cast(int) what.length;
668 intnextThing = -1;
669 670 foreach(i, thing; things) {
671 if(i & 1)
672 continue;
673 674 autoidx = what.indexOf(thing);
675 if(idx != -1 && idx < nextIndex) {
676 nextIndex = cast(int) idx;
677 nextThing = cast(int) i;
678 }
679 }
680 681 if(nextThing == -1) {
682 n ~= what;
683 what = null;
684 } else {
685 n ~= what[0 .. nextIndex];
686 what = what[nextIndex + things[nextThing].length .. $];
687 n ~= things[nextThing + 1];
688 continue;
689 }
690 }
691 692 returnn;
693 }
694 695 immutabledaysOfWeekFullNames = [
696 "Sunday",
697 "Monday",
698 "Tuesday",
699 "Wednesday",
700 "Thursday",
701 "Friday",
702 "Saturday"703 ];
704 705 /++
706 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.
707 708 Inside the template, the value returned by the function will be available in the context as the variable `data`.
709 +/710 structTemplate {
711 stringname;
712 }
713 /++
714 UDA to put on a method when using [WebPresenterWithTemplateSupport]. Overrides the default template skeleton file name.
715 +/716 structSkeleton {
717 stringname;
718 }
719 720 /++
721 UDA to attach runtime metadata to a function. Will be available in the template.
722 723 History:
724 Added July 12, 2021
725 +/726 structmeta {
727 stringname;
728 stringvalue;
729 }
730 731 /++
732 Can be used as a return value from one of your own methods when rendering websites with [WebPresenterWithTemplateSupport].
733 +/734 structRenderTemplate {
735 this(stringname, varcontext = var.emptyObject, varskeletonContext = var.emptyObject, stringskeletonName = null) {
736 this.name = name;
737 this.context = context;
738 this.skeletonContext = skeletonContext;
739 this.skeletonName = skeletonName;
740 }
741 742 stringname;
743 varcontext;
744 varskeletonContext;
745 stringskeletonName;
746 }
747 748 749 /++
750 Make a class that inherits from this with your further customizations, or minimally:
751 ---
752 class MyPresenter : WebPresenterWithTemplateSupport!MyPresenter { }
753 ---
754 +/755 templateWebPresenterWithTemplateSupport(CTRP) {
756 importarsd.cgi;
757 classWebPresenterWithTemplateSupport : WebPresenter!(CTRP) {
758 overrideElementhtmlContainer() {
759 try {
760 autoskeleton = renderTemplate("generic.html", var.emptyObject, var.emptyObject, "skeleton.html", templateLoader());
761 returnskeleton.requireSelector("main");
762 } catch(Exceptione) {
763 autodocument = newDocument("<html><body><p>generic.html trouble: <span id=\"ghe\"></span></p> <main></main></body></html>");
764 document.requireSelector("#ghe").textContent = e.msg;
765 returndocument.requireSelector("main");
766 }
767 }
768 769 staticstructMeta {
770 typeof(null) at;
771 stringtemplateName;
772 stringskeletonName;
773 string[string] meta;
774 Formfunction(WebPresenterWithTemplateSupportpresenter) automaticForm;
775 aliasatthis;
776 }
777 templatemethodMeta(aliasmethod) {
778 staticMetahelper() {
779 Metaret;
780 781 // ret.at = typeof(super).methodMeta!method;782 783 foreach(attr; __traits(getAttributes, method))
784 staticif(is(typeof(attr) == Template))
785 ret.templateName = attr.name;
786 elsestaticif(is(typeof(attr) == Skeleton))
787 ret.skeletonName = attr.name;
788 elsestaticif(is(typeof(attr) == .meta))
789 ret.meta[attr.name] = attr.value;
790 791 ret.automaticForm = functionForm(WebPresenterWithTemplateSupportpresenter) {
792 returnpresenter.createAutomaticFormForFunction!(method, typeof(&method))(null);
793 };
794 795 returnret;
796 }
797 enummethodMeta = helper();
798 }
799 800 /// You can override this801 voidaddContext(Cgicgi, varctx) {}
802 803 /++
804 You can override this. The default is "templates/". Your returned string must end with '/'.
805 (in future versions it will probably allow a null return too, but right now it must be a /).
806 807 History:
808 Added December 6, 2023 (dub v11.3)
809 +/810 TemplateLoadertemplateLoader() {
811 returnnull;
812 }
813 814 /++
815 You can override this.
816 817 History:
818 Added February 5, 2024 (dub v11.5)
819 +/820 WebTemplateRendererwebTemplateRenderer() {
821 returnnewWebTemplateRenderer(templateLoader());
822 }
823 824 voidpresentSuccessfulReturnAsHtml(T : RenderTemplate)(Cgicgi, Tret, Metameta) {
825 addContext(cgi, ret.context);
826 827 autorenderer = this.webTemplateRenderer();
828 829 autoskeleton = renderer.renderTemplate(ret.name, ret.context, ret.skeletonContext, ret.skeletonName);
830 cgi.setResponseContentType("text/html; charset=utf8");
831 cgi.gzipResponse = true;
832 cgi.write(skeleton.toString(), true);
833 }
834 835 voidpresentSuccessfulReturnAsHtml(T)(Cgicgi, Tret, Metameta) {
836 if(meta.templateName.length) {
837 varsobj = var.emptyObject;
838 839 varobj = var.emptyObject;
840 841 obj.data = ret;
842 843 /+
844 sobj.meta = var.emptyObject;
845 foreach(k,v; meta.meta)
846 sobj.meta[k] = v;
847 +/848 849 obj.meta = var.emptyObject;
850 foreach(k,v; meta.meta)
851 obj.meta[k] = v;
852 853 obj.meta.currentPath = cgi.pathInfo;
854 obj.meta.automaticForm = { returnmeta.automaticForm(this).toString; };
855 856 presentSuccessfulReturnAsHtml(cgi, RenderTemplate(meta.templateName, obj, sobj, meta.skeletonName), meta);
857 } else858 super.presentSuccessfulReturnAsHtml(cgi, ret, meta);
859 }
860 }
861 }
862 863 WebTemplateRendererDefaultWtrFactory(TemplateLoaderloader) {
864 returnnewWebTemplateRenderer(loader);
865 }
866 867 /++
868 Serves up a directory of template files as html. This is meant to be used for some near-static html in the midst of an application, giving you a little bit of dynamic content and conveniences with the ease of editing files without recompiles.
869 870 Parameters:
871 urlPrefix = the url prefix to trigger this handler, relative to the current dispatcher base
872 directory = the directory, under the template directory, to find the template files
873 skeleton = the name of the skeleton file inside the template directory
874 extension = the file extension to add to the url name to get the template name
875 wtrFactory = an alias to a function of type `WebTemplateRenderer function(TemplateLoader loader)` that returns `new WebTemplateRenderer(loader)` (or similar subclasses/argument lists);
876 877 To get the filename of the template from the url, it will:
878 879 1) Strip the url prefixes off to get just the filename
880 881 2) Concatenate the directory with the template directory
882 883 3) Add the extension to the givenname
884 885 $(PITFALL
886 The `templateDirectory` parameter may be removed or changed in the near future.
887 )
888 889 History:
890 Added July 28, 2021 (documented dub v11.0)
891 892 The `wtrFactory` parameter was added on February 5, 2024 (dub v11.5).
893 +/894 autoserveTemplateDirectory(aliaswtrFactory = DefaultWtrFactory)(stringurlPrefix, stringdirectory = null, stringskeleton = null, stringextension = ".html", stringtemplateDirectory = "templates/") {
895 importarsd.cgi;
896 importstd.file;
897 898 assert(urlPrefix[0] == '/');
899 assert(urlPrefix[$-1] == '/');
900 901 assert(templateDirectory[$-1] == '/');
902 903 staticstructDispatcherDetails {
904 stringdirectory;
905 stringskeleton;
906 stringextension;
907 stringtemplateDirectory;
908 }
909 910 if(directoryisnull)
911 directory = urlPrefix[1 .. $];
912 913 if(directory.length == 0)
914 directory = "./";
915 916 assert(directory[$-1] == '/');
917 918 staticboolinternalHandler(stringurlPrefix, Cgicgi, Objectpresenter, DispatcherDetailsdetails) {
919 autofile = cgi.pathInfo[urlPrefix.length .. $];
920 if(file.indexOf("/") != -1 || file.indexOf("\\") != -1)
921 returnfalse;
922 923 autofn = details.templateDirectory ~ details.directory ~ file ~ details.extension;
924 if(std.file.exists(fn)) {
925 cgi.setResponseExpiresRelative(600, true); // 10 minute cache expiration by default, FIXME it should be configurable926 927 autoloader = TemplateLoader.forDirectory(details.templateDirectory);
928 929 WebTemplateRendererrenderer = wtrFactory(loader);
930 931 autodoc = renderer.renderTemplate(fn[details.templateDirectory.length.. $], var.emptyObject, var.emptyObject, details.skeleton);
932 cgi.gzipResponse = true;
933 cgi.write(doc.toString, true);
934 returntrue;
935 } else {
936 returnfalse;
937 }
938 }
939 940 returnDispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, skeleton, extension, templateDirectory));
941 }