arsd/webtemplate.d

265 lines
7.0 KiB
D

/++
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);
};
context.formatDate = function string(string s) {
if(s.length < 10)
return s;
auto year = s[0 .. 4];
auto month = s[5 .. 7];
auto day = s[8 .. 10];
return month ~ "/" ~ day ~ "/" ~ year;
};
context.dayOfWeek = function string(string s) {
import std.datetime;
return daysOfWeekFullNames[Date.fromISOExtString(s[0 .. 10]).dayOfWeek];
};
context.formatTime = function string(string s) {
if(s.length < 20)
return s;
auto hour = s[11 .. 13].to!int;
auto minutes = s[14 .. 16].to!int;
auto seconds = s[17 .. 19].to!int;
auto am = (hour >= 12) ? "PM" : "AM";
if(hour > 12)
hour -= 12;
return hour.to!string ~ (minutes < 10 ? ":0" : ":") ~ minutes.to!string ~ " " ~ am;
};
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("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", 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;
string replaceThingInString(string v) {
auto idx = v.indexOf("<%=");
if(idx == -1)
return v;
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;
return n ~ res ~ replaceThingInString(r);
}
foreach(k, v; root.attributes) {
if(k == "onrender") {
continue;
}
v = replaceThingInString(v);
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("<root>" ~ readText("templates/" ~ templateName) ~ "</root>", 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);
}
}
if(root.hasAttribute("onrender")) {
var nc = var.emptyObject(context);
nc["this"] = wrapNativeObject(root);
nc["this"]["populateFrom"]._function = delegate var(var this_, var[] args) {
auto form = cast(Form) root;
if(form is null) return this_;
foreach(k, v; args[0]) {
populateForm(form, v, k.get!string);
}
return this_;
};
interpret(root.getAttribute("onrender"), nc);
root.removeAttribute("onrender");
}
}
void populateForm(Form form, var obj, string name) {
import std.string;
if(obj.payloadType == var.Type.Object) {
foreach(k, v; obj) {
auto fn = name.replace("%", k.get!string);
populateForm(form, v, fn ~ "["~k.get!string~"]");
}
} else {
//import std.stdio; writeln("SET ", name, " ", obj, " ", obj.payloadType);
form.setValue(name, obj.get!string);
}
}
immutable daysOfWeekFullNames = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
];
/+
mixin template WebTemplatePresenterSupport() {
}
+/