diff --git a/webtemplate.d b/webtemplate.d new file mode 100644 index 0000000..2caeca1 --- /dev/null +++ b/webtemplate.d @@ -0,0 +1,188 @@ +/++ + 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() { + +} ++/