From fd85b03bbaca00c2a5541607b5ee4ca290ed31e1 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Mon, 11 Dec 2023 12:06:45 -0500 Subject: [PATCH] allow more placeholder replacements with document fragments --- webtemplate.d | 142 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 17 deletions(-) diff --git a/webtemplate.d b/webtemplate.d index 9f232cb..f493f2a 100644 --- a/webtemplate.d +++ b/webtemplate.d @@ -27,6 +27,8 @@ + + @@ -144,11 +146,57 @@ void addDefaultFunctions(var context) { context.data = var.emptyObject; } -/// -Document renderTemplate(string templateName, var context = var.emptyObject, var skeletonContext = var.emptyObject, string skeletonName = null) { - import std.file; +/++ + A loader object for reading raw template, so you can use something other than files if you like. + + See [TemplateLoader.forDirectory] to a pre-packaged class that implements a loader for a particular directory. + + History: + Added December 11, 2023 (dub v11.3) ++/ +interface TemplateLoader { + /++ + This is the main method to look up a template name and return its HTML as a string. + + Typical implementation is to just `return std.file.readText(directory ~ name);` + +/ + string loadTemplateHtml(string name); + + /++ + Returns a loader for files in the given directory. + +/ + static TemplateLoader forDirectory(string directoryName) { + if(directoryName.length && directoryName[$-1] != '/') + directoryName ~= "/"; + + return new class TemplateLoader { + string loadTemplateHtml(string name) { + import std.file; + return readText(directoryName ~ name); + } + }; + } +} + +/++ + 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. + + Parameters: + 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) + context = the global object available to scripts inside the template + skeletonContext = the global object available to the skeleton template + 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 + 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. + + History: + Parameter `loader` was added on December 11, 2023 (dub v11.3) ++/ +Document renderTemplate(string templateName, var context = var.emptyObject, var skeletonContext = var.emptyObject, string skeletonName = null, TemplateLoader loader = null) { import arsd.cgi; + if(loader is null) + loader = TemplateLoader.forDirectory("templates/"); + try { addDefaultFunctions(context); addDefaultFunctions(skeletonContext); @@ -156,12 +204,12 @@ Document renderTemplate(string templateName, var context = var.emptyObject, var if(skeletonName.length == 0) skeletonName = "skeleton.html"; - auto skeleton = new Document(readText("templates/" ~ skeletonName), true, true); + auto skeleton = new Document(loader.loadTemplateHtml(skeletonName), true, true); auto document = new Document(); document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom - document.parse("" ~ readText("templates/" ~ templateName) ~ "", true, true); + document.parse("" ~ loader.loadTemplateHtml(templateName) ~ "", true, true); - expandTemplate(skeleton.root, skeletonContext); + expandTemplate(skeleton.root, skeletonContext, loader); foreach(nav; skeleton.querySelectorAll("nav[data-relative-to]")) { auto r = nav.getAttribute("data-relative-to"); @@ -170,9 +218,11 @@ Document renderTemplate(string templateName, var context = var.emptyObject, var } } - expandTemplate(document.root, context); + expandTemplate(document.root, context, loader); // also do other unique elements and move them over. + // and have some kind of that can be just reduced when going out in the final result. + // and try partials. auto templateMain = document.requireSelector(":root > main"); @@ -182,9 +232,17 @@ Document renderTemplate(string templateName, var context = var.emptyObject, var } skeleton.requireSelector("main").replaceWith(templateMain.removeFromTree); + if(auto title = document.querySelector(":root > title")) skeleton.requireSelector(":root > head > title").innerHTML = title.innerHTML; + // also allow top-level unique id replacements + foreach(item; document.querySelectorAll(":root > [id]")) + skeleton.requireElementById(item.id).replaceWith(item.removeFromTree); + + foreach(df; skeleton.querySelectorAll("document-fragment")) + df.stripOut(); + debug skeleton.root.prependChild(new HtmlComment(null, templateName ~ " inside skeleton.html")); @@ -195,8 +253,47 @@ Document renderTemplate(string templateName, var context = var.emptyObject, var } } -// I don't particularly like this -void expandTemplate(Element root, var context) { +/++ + Shows how top-level things from the template are moved to their corresponding items on the skeleton. ++/ +unittest { + // for the unittest, we want to inject a loader that uses plain strings instead of files. + auto testLoader = new class TemplateLoader { + string loadTemplateHtml(string name) { + switch(name) { + case "skeleton": + return ` + + + + + + + +
+ + + `; + case "main": + return ` +
Hello
+ + My title + + `; + default: assert(0); + } + } + }; + + Document doc = renderTemplate("main", var.emptyObject, var.emptyObject, "skeleton", testLoader); + + assert(doc.querySelector("document-fragment") is null); // the items are stripped out + assert(doc.querySelector("title") !is null); // but the stuff from inside it is brought in + assert(doc.requireSelector("main").textContent == "Hello"); // and the main from the template is moved to the skeelton +} + +private void expandTemplate(Element root, var context, TemplateLoader loader) { import std.string; string replaceThingInString(string v) { @@ -237,7 +334,7 @@ void expandTemplate(Element root, var context) { auto got = interpret(ele.attrs.cond, context).opCast!bool; if(got) { ele.tagName = "root"; - expandTemplate(ele, context); + expandTemplate(ele, context, loader); fragment.stealChildren(ele); } lastBoolResult = got; @@ -246,7 +343,7 @@ void expandTemplate(Element root, var context) { auto fragment = new DocumentFragment(null); if(!lastBoolResult) { ele.tagName = "root"; - expandTemplate(ele, context); + expandTemplate(ele, context, loader); fragment.stealChildren(ele); } ele.replaceWith(fragment); @@ -262,7 +359,7 @@ void expandTemplate(Element root, var context) { nc[ele.attrs.index] = k; auto clone = ele.cloneNode(true); clone.tagName = "root"; // it certainly isn't a for-each anymore! - expandTemplate(clone, nc); + expandTemplate(clone, nc, loader); fragment.stealChildren(clone); } @@ -272,7 +369,7 @@ void expandTemplate(Element root, var context) { auto templateName = ele.getAttribute("file"); auto document = new Document(); document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom - document.parse("" ~ readText("templates/" ~ templateName) ~ "", true, true); + document.parse("" ~ loader.loadTemplateHtml(templateName) ~ "", true, true); var obj = var.emptyObject; obj.prototype = context; @@ -282,7 +379,7 @@ void expandTemplate(Element root, var context) { obj["data"] = var.fromJson(data); } - expandTemplate(document.root, obj); + expandTemplate(document.root, obj, loader); auto fragment = new DocumentFragment(null); @@ -347,7 +444,7 @@ void expandTemplate(Element root, var context) { ele.innerRawSource = newCode; } } else { - expandTemplate(ele, context); + expandTemplate(ele, context, loader); } } @@ -488,7 +585,7 @@ template WebPresenterWithTemplateSupport(CTRP) { import arsd.cgi; class WebPresenterWithTemplateSupport : WebPresenter!(CTRP) { override Element htmlContainer() { - auto skeleton = renderTemplate("generic.html"); + auto skeleton = renderTemplate("generic.html", var.emptyObject, var.emptyObject, "skeleton.html", templateLoader()); return skeleton.requireSelector("main"); } @@ -526,9 +623,20 @@ template WebPresenterWithTemplateSupport(CTRP) { /// You can override this void addContext(Cgi cgi, var ctx) {} + /++ + You can override this. The default is "templates/". Your returned string must end with '/'. + (in future versions it will probably allow a null return too, but right now it must be a /). + + History: + Added December 6, 2023 (dub v11.3) + +/ + TemplateLoader templateLoader() { + return null; + } + void presentSuccessfulReturnAsHtml(T : RenderTemplate)(Cgi cgi, T ret, Meta meta) { addContext(cgi, ret.context); - auto skeleton = renderTemplate(ret.name, ret.context, ret.skeletonContext, ret.skeletonName); + auto skeleton = renderTemplate(ret.name, ret.context, ret.skeletonContext, ret.skeletonName, templateLoader()); cgi.setResponseContentType("text/html; charset=utf8"); cgi.gzipResponse = true; cgi.write(skeleton.toString(), true);