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() {
+
+}
++/