/++ This provides a kind of web template support, built on top of [arsd.dom] and [arsd.script], in support of [arsd.cgi]. +/ module arsd.webtemplate; // FIXME: make script exceptions show line from the template it was in too import arsd.script; import arsd.dom; public import arsd.jsvar : var; struct RenderTemplate { string name; var context = var.emptyObject; var skeletonContext = var.emptyObject; } class TemplateException : Exception { string templateName; var context; Exception e; this(string templateName, var context, Exception e) { this.templateName = templateName; this.context = context; this.e = e; super("Exception in template " ~ templateName ~ ": " ~ e.msg); } } Document renderTemplate(string templateName, var context = var.emptyObject, var skeletonContext = var.emptyObject) { import std.file; import arsd.cgi; try { context.encodeURIComponent = function string(var f) { import std.uri; return encodeComponent(f.get!string); }; auto skeleton = new Document(readText("templates/skeleton.html"), true, true); auto document = new Document(); document.parseSawAspCode = (string) => true; // enable adding <% %> to the dom document.parse("" ~ readText("templates/" ~ templateName) ~ "", true, true); expandTemplate(skeleton.root, skeletonContext); foreach(nav; skeleton.querySelectorAll("nav[data-relative-to]")) { auto r = nav.getAttribute("data-relative-to"); foreach(a; nav.querySelectorAll("a")) { a.attrs.href = Uri(a.attrs.href).basedOn(Uri(r));// ~ a.attrs.href; } } expandTemplate(document.root, context); // also do other unique elements and move them over. // and try partials. auto templateMain = document.requireSelector(":root > main"); if(templateMain.hasAttribute("body-class")) { skeleton.requireSelector("body").addClass(templateMain.getAttribute("body-class")); templateMain.removeAttribute("body-class"); } skeleton.requireSelector("main").replaceWith(templateMain.removeFromTree); if(auto title = document.querySelector(":root > title")) skeleton.requireSelector(":root > head > title").innerHTML = title.innerHTML; debug skeleton.root.prependChild(new HtmlComment(null, templateName ~ " inside skeleton.html")); return skeleton; } catch(Exception e) { throw new TemplateException(templateName, context, e); } } // I don't particularly like this void expandTemplate(Element root, var context) { import std.string; foreach(k, v; root.attributes) { if(k == "onrender") { // FIXME } auto idx = v.indexOf("<%="); if(idx == -1) continue; auto n = v[0 .. idx]; auto r = v[idx + "<%=".length .. $]; auto end = r.indexOf("%>"); if(end == -1) throw new Exception("unclosed asp code in attribute"); auto code = r[0 .. end]; r = r[end + "%>".length .. $]; import arsd.script; auto res = interpret(code, context).get!string; v = n ~ res ~ r; root.setAttribute(k, v); } bool lastBoolResult; foreach(ele; root.children) { if(ele.tagName == "if-true") { auto fragment = new DocumentFragment(null); import arsd.script; auto got = interpret(ele.attrs.cond, context).get!bool; if(got) { ele.tagName = "root"; expandTemplate(ele, context); fragment.stealChildren(ele); } lastBoolResult = got; ele.replaceWith(fragment); } else if(ele.tagName == "or-else") { auto fragment = new DocumentFragment(null); if(!lastBoolResult) { ele.tagName = "root"; expandTemplate(ele, context); fragment.stealChildren(ele); } ele.replaceWith(fragment); } else if(ele.tagName == "for-each") { auto fragment = new DocumentFragment(null); var nc = var.emptyObject(context); lastBoolResult = false; auto got = interpret(ele.attrs.over, context); foreach(item; got) { lastBoolResult = true; nc[ele.attrs.as] = item; auto clone = ele.cloneNode(true); clone.tagName = "root"; // it certainly isn't a for-each anymore! expandTemplate(clone, nc); fragment.stealChildren(clone); } ele.replaceWith(fragment); } else if(ele.tagName == "render-template") { import std.file; 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); expandTemplate(document.root, context); auto fragment = new DocumentFragment(null); debug fragment.appendChild(new HtmlComment(null, templateName)); fragment.stealChildren(document.root); debug fragment.appendChild(new HtmlComment(null, "end " ~ templateName)); ele.replaceWith(fragment); } else if(auto asp = cast(AspCode) ele) { auto code = asp.source[1 .. $-1]; auto fragment = new DocumentFragment(null); if(code[0] == '=') { import arsd.script; if(code.length > 5 && code[1 .. 5] == "HTML") { auto got = interpret(code[5 .. $], context); if(auto native = got.getWno!Element) fragment.appendChild(native); else fragment.innerHTML = got.get!string; } else { auto got = interpret(code[1 .. $], context).get!string; fragment.innerText = got; } } asp.replaceWith(fragment); } else { expandTemplate(ele, context); } } } /+ mixin template WebTemplatePresenterSupport() { } +/