a few little web enhancements

This commit is contained in:
Adam D. Ruppe 2021-07-28 22:23:38 -04:00
parent ae0fbaa89f
commit 81dba0f46d
4 changed files with 318 additions and 102 deletions

119
cgi.d
View File

@ -3277,6 +3277,37 @@ mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentL
mixin CustomCgiMain!(Cgi, fun, maxContentLength);
}
/++
Boilerplate mixin for a main function that uses the [dispatcher] function.
You can send `typeof(null)` as the `Presenter` argument to use a generic one.
History:
Added July 9, 2021
+/
mixin template DispatcherMain(Presenter, DispatcherArgs...) {
/++
Handler to the generated presenter you can use from your objects, etc.
+/
Presenter activePresenter;
/++
Request handler that creates the presenter then forwards to the [dispatcher] function.
Renders 404 if the dispatcher did not handle the request.
+/
void handler(Cgi cgi) {
auto presenter = new Presenter;
activePresenter = presenter;
scope(exit) activePresenter = null;
if(cgi.dispatcher!DispatcherArgs(presenter))
return;
presenter.renderBasicError(cgi, 404);
}
mixin GenericMain!handler;
}
private string simpleHtmlEncode(string s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br />\n");
}
@ -4068,6 +4099,17 @@ void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaul
+/
/++
The stack size when a fiber is created. You can set this from your main or from a shared static constructor
to optimize your memory use if you know you don't need this much space. Be careful though, some functions use
more stack space than you realize and a recursive function (including ones like in dom.d) can easily grow fast!
History:
Added July 10, 2021. Previously, it used the druntime default of 16 KB.
+/
version(cgi_use_fiber)
__gshared size_t fiberStackSize = 4096 * 100;
version(cgi_use_fiber)
class CgiFiber : Fiber {
private void function(Socket) f_handler;
@ -4081,8 +4123,7 @@ class CgiFiber : Fiber {
this(void delegate(Socket) handler) {
this.handler = handler;
// FIXME: stack size
super(&run);
super(&run, fiberStackSize);
}
Socket connection;
@ -8106,8 +8147,34 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
*what = T.init;
return true;
} else {
// could be a child
if(name[paramName.length] == '.') {
// could be a child. gonna allow either obj.field OR obj[field]
string afterName;
if(name[paramName.length] == '[') {
int count = 1;
auto idx = paramName.length + 1;
while(idx < name.length && count > 0) {
if(name[idx] == '[')
count++;
else if(name[idx] == ']') {
count--;
if(count == 0) break;
}
idx++;
}
if(idx == name.length)
return false; // malformed
auto insideBrackets = name[paramName.length + 1 .. idx];
afterName = name[idx + 1 .. $];
name = name[0 .. paramName.length];
paramName = insideBrackets;
} else if(name[paramName.length] == '.') {
paramName = name[paramName.length + 1 .. $];
name = paramName;
int p = 0;
@ -8117,17 +8184,23 @@ auto callFromCgi(alias method, T)(T dg, Cgi cgi) {
p++;
}
// set the child member
switch(paramName) {
foreach(idx, memberName; __traits(allMembers, T))
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
// data member!
case memberName:
return setVariable(name, paramName, &(__traits(getMember, *what, memberName)), value);
}
default:
// ok, not a member
afterName = paramName[p .. $];
paramName = paramName[0 .. p];
} else {
return false;
}
if(paramName.length)
// set the child member
switch(paramName) {
foreach(idx, memberName; __traits(allMembers, T))
static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) {
// data member!
case memberName:
return setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value);
}
default:
// ok, not a member
}
}
@ -8539,13 +8612,13 @@ html", true, true);
}
/// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime
void presentSuccessfulReturn(T : MultipleResponses!Types, Types...)(Cgi cgi, T ret, typeof(null) meta, string format) {
void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) {
bool outputted = false;
foreach(index, type; Types) {
if(ret.contains == index) {
assert(!outputted);
outputted = true;
(cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret.payload[index], meta);
(cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format);
}
}
if(!outputted)
@ -8655,7 +8728,19 @@ html", true, true);
auto div = Element.make("div");
div.addClass("form-field");
static if(is(T == struct)) {
static if(is(T == Cgi.UploadedFile)) {
Element lbl;
if(displayName !is null) {
lbl = div.addChild("label");
lbl.addChild("span", displayName, "label-text");
lbl.appendText(" ");
} else {
lbl = div;
}
auto i = lbl.addChild("input", name);
i.attrs.name = name;
i.attrs.type = "file";
} else static if(is(T == struct)) {
if(displayName !is null)
div.addChild("span", displayName, "label-text");
auto fieldset = div.addChild("fieldset");

119
dom.d
View File

@ -94,7 +94,10 @@ bool isConvenientAttribute(string name) {
/// The main document interface, including a html parser.
/// Group: core_functionality
class Document : FileResource {
class Document : FileResource, DomParent {
inout(Document) asDocument() inout { return this; }
inout(Element) asElement() inout { return null; }
/// Convenience method for web scraping. Requires [arsd.http2] to be
/// included in the build as well as [arsd.characterencodings].
static Document fromUrl()(string url, bool strictMode = false) {
@ -1130,6 +1133,7 @@ class Document : FileResource {
} while (r.type != 0 || r.element.nodeType != 1); // we look past the xml prologue and doctype; root only begins on a regular node
root = r.element;
root.parent_ = this;
if(!strict) // in strict mode, we'll just ignore stuff after the xml
while(r.type != 4) {
@ -1353,7 +1357,6 @@ class Document : FileResource {
name = name.toLower();
auto e = Element.make(name, null, null, selfClosedElements);
e.parentDocument = this;
return e;
@ -1475,9 +1478,17 @@ class Document : FileResource {
}
}
interface DomParent {
inout(Document) asDocument() inout;
inout(Element) asElement() inout;
}
/// This represents almost everything in the DOM.
/// Group: core_functionality
class Element {
class Element : DomParent {
inout(Document) asDocument() inout { return null; }
inout(Element) asElement() inout { return this; }
/// Returns a collection of elements by selector.
/// See: [Document.opIndex]
ElementCollection opIndex(string selector) {
@ -1926,44 +1937,64 @@ class Element {
/// Instead, this flag tells if it should be. It is based on the source document's notation and a html element list.
private bool selfClosed;
private DomParent parent_;
/// Get the parent Document object that contains this element.
/// It may be null, so remember to check for that.
Document parentDocument;
@property inout(Document) parentDocument() inout {
if(this.parent_ is null)
return null;
auto p = cast() this.parent_.asElement;
auto prev = cast() this;
while(p) {
prev = p;
if(p.parent_ is null)
return null;
p = cast() p.parent_.asElement;
}
return cast(inout) prev.parent_.asDocument;
}
deprecated @property void parentDocument(Document doc) {
parent_ = doc;
}
///.
inout(Element) parentNode() inout {
auto p = _parentNode;
if(parent_ is null)
return null;
auto p = parent_.asElement;
if(cast(DocumentFragment) p)
return p._parentNode;
return p.parent_.asElement;
return p;
}
//protected
Element parentNode(Element e) {
return _parentNode = e;
parent_ = e;
return e;
}
private Element _parentNode;
// the next few methods are for implementing interactive kind of things
private CssStyle _computedStyle;
// these are here for event handlers. Don't forget that this library never fires events.
// (I'm thinking about putting this in a version statement so you don't have the baggage. The instance size of this class is 56 bytes right now.)
EventHandler[][string] bubblingEventHandlers;
EventHandler[][string] capturingEventHandlers;
EventHandler[string] defaultEventHandlers;
void addEventListener(string event, EventHandler handler, bool useCapture = false) {
if(event.length > 2 && event[0..2] == "on")
event = event[2 .. $];
version(dom_with_events) {
EventHandler[][string] bubblingEventHandlers;
EventHandler[][string] capturingEventHandlers;
EventHandler[string] defaultEventHandlers;
if(useCapture)
capturingEventHandlers[event] ~= handler;
else
bubblingEventHandlers[event] ~= handler;
void addEventListener(string event, EventHandler handler, bool useCapture = false) {
if(event.length > 2 && event[0..2] == "on")
event = event[2 .. $];
if(useCapture)
capturingEventHandlers[event] ~= handler;
else
bubblingEventHandlers[event] ~= handler;
}
}
@ -2091,7 +2122,6 @@ class Element {
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
parentDocument = _parentDocument;
tagName = _tagName;
if(_attributes !is null)
attributes = _attributes;
@ -2128,8 +2158,6 @@ class Element {
}
private this(Document _parentDocument) {
parentDocument = _parentDocument;
version(dom_node_indexes)
this.dataset.nodeIndex = to!string(&(this.attributes));
}
@ -2600,6 +2628,10 @@ class Element {
// if you change something here, it won't apply... FIXME const? but changing it would be nice if it applies to the style attribute too though you should use style there.
// the next few methods are for implementing interactive kind of things
private CssStyle _computedStyle;
/// Don't use this.
@property CssStyle computedStyle() {
if(_computedStyle is null) {
@ -2713,12 +2745,16 @@ class Element {
selfClosed = false;
e.parentNode = this;
e.parentDocument = this.parentDocument;
if(auto frag = cast(DocumentFragment) e)
children ~= frag.children;
else
children ~= e;
/+
foreach(item; e.tree)
item.parentDocument = this.parentDocument;
+/
sendObserverEvent(DomMutationOperations.appendChild, null, null, e);
return e;
@ -2746,7 +2782,6 @@ class Element {
children = children[0..i] ~ frag.children ~ children[i..$];
else
children = children[0..i] ~ what ~ children[i..$];
what.parentDocument = this.parentDocument;
what.parentNode = this;
return what;
}
@ -2781,7 +2816,6 @@ class Element {
else
children = children[0 .. i + 1] ~ what ~ children[i + 1 .. $];
what.parentNode = this;
what.parentDocument = this.parentDocument;
return what;
}
}
@ -2810,7 +2844,6 @@ class Element {
c.parentNode = null;
c = replacement;
c.parentNode = this;
c.parentDocument = this.parentDocument;
return child;
}
assert(0);
@ -2888,7 +2921,6 @@ class Element {
else
children = children[0..i] ~ child ~ children[i..$];
child.parentNode = this;
child.parentDocument = this.parentDocument;
break;
}
}
@ -2920,7 +2952,6 @@ class Element {
do {
foreach(c; e.children) {
c.parentNode = this;
c.parentDocument = this.parentDocument;
}
if(position is null)
children ~= e.children;
@ -2954,7 +2985,6 @@ class Element {
}
do {
e.parentNode = this;
e.parentDocument = this.parentDocument;
if(auto frag = cast(DocumentFragment) e)
children = e.children ~ children;
else
@ -3000,13 +3030,10 @@ class Element {
doc.parseUtf8("<innerhtml>" ~ html ~ "</innerhtml>", strict, strict); // FIXME: this should preserve the strictness of the parent document
children = doc.root.children;
foreach(c; children) {
foreach(c; doc.root.tree) {
c.parentNode = this;
c.parentDocument = this.parentDocument;
}
reparentTreeDocuments();
doc.root.children = null;
return this;
@ -3017,11 +3044,6 @@ class Element {
return this.innerHTML = html.source;
}
private void reparentTreeDocuments() {
foreach(c; this.tree)
c.parentDocument = this.parentDocument;
}
/**
Replaces this node with the given html string, which is parsed
@ -3037,13 +3059,8 @@ class Element {
children = doc.root.children;
foreach(c; children) {
c.parentNode = this;
c.parentDocument = this.parentDocument;
}
reparentTreeDocuments();
stripOut();
return doc.root.children;
@ -3093,7 +3110,6 @@ class Element {
replace.parentNode = this;
children[i].parentNode = null;
children[i] = replace;
replace.parentDocument = this.parentDocument;
return replace;
}
}
@ -3132,7 +3148,6 @@ class Element {
children[i] = replace[0];
foreach(e; replace) {
e.parentNode = this;
e.parentDocument = this.parentDocument;
}
children = .insertAfter(children, i, replace[1..$]);
@ -3263,7 +3278,6 @@ class Element {
/// Clones the node. If deepClone is true, clone all inner tags too. If false, only do this tag (and its attributes), but it will have no contents.
Element cloneNode(bool deepClone) {
auto e = Element.make(this.tagName);
e.parentDocument = this.parentDocument;
e.attributes = this.attributes.aadup;
e.selfClosed = this.selfClosed;
@ -3565,6 +3579,9 @@ class Element {
}
}
// computedStyle could argubaly be removed to bring size down
//pragma(msg, __traits(classInstanceSize, Element));
//pragma(msg, Element.tupleof);
// FIXME: since Document loosens the input requirements, it should probably be the sub class...
/// Specializes Document for handling generic XML. (always uses strict mode, uses xml mime type and file header)
@ -4053,7 +4070,7 @@ class DocumentFragment : Element {
}
*/
override Element parentNode(Element p) {
this._parentNode = p;
this.parent_ = p;
foreach(child; children)
child.parentNode = p;
return p;
@ -8499,9 +8516,11 @@ private string[string] aadup(in string[string] arr) {
// dom event support, if you want to use it
/// used for DOM events
version(dom_with_events)
alias EventHandler = void delegate(Element handlerAttachedTo, Event event);
/// This is a DOM event, like in javascript. Note that this library never fires events - it is only here for you to use if you want it.
version(dom_with_events)
class Event {
this(string eventName, Element target) {
this.eventName = eventName;

View File

@ -693,6 +693,9 @@ struct var {
// so prewrapped stuff can be easily passed.
this._type = Type.Object;
this._payload._object = t;
} else static if(is(T == enum)) {
this._type = Type.String;
this._payload._string = to!string(t);
} else static if(isFloatingPoint!T) {
this._type = Type.Floating;
this._payload._floating = t;

View File

@ -20,6 +20,11 @@
there were no items.
</or-else>
<form>
<!-- new on July 17, 2021 (dub v10.3) -->
<hidden-form-data from="data_var" name="arg_name" />
</form>
<render-template file="partial.html" />
</main>
```
@ -49,44 +54,58 @@ class TemplateException : Exception {
}
}
void addDefaultFunctions(var context) {
import std.conv;
// FIXME: I prolly want it to just set the prototype or something
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;
};
// don't want checking meta or data to be an error
if(context.meta == null)
context.meta = var.emptyObject;
if(context.data == null)
context.data = var.emptyObject;
}
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;
};
addDefaultFunctions(context);
addDefaultFunctions(skeletonContext);
auto skeleton = new Document(readText("templates/skeleton.html"), true, true);
auto document = new Document();
@ -212,6 +231,18 @@ void expandTemplate(Element root, var context) {
fragment.stealChildren(document.root);
debug fragment.appendChild(new HtmlComment(null, "end " ~ templateName));
ele.replaceWith(fragment);
} else if(ele.tagName == "hidden-form-data") {
auto from = interpret(ele.attrs.from, context);
auto name = ele.attrs.name;
auto form = new Form(null);
populateForm(form, from, name);
auto fragment = new DocumentFragment(null);
fragment.stealChildren(form);
ele.replaceWith(fragment);
} else if(auto asp = cast(AspCode) ele) {
auto code = asp.source[1 .. $-1];
@ -238,7 +269,7 @@ void expandTemplate(Element root, var context) {
if(root.hasAttribute("onrender")) {
var nc = var.emptyObject(context);
nc["this"] = wrapNativeObject(root);
nc["this"]["populateFrom"]._function = delegate var(var this_, var[] args) {
nc["this"]["populateFrom"] = delegate var(var this_, var[] args) {
auto form = cast(Form) root;
if(form is null) return this_;
foreach(k, v; args[0]) {
@ -256,9 +287,12 @@ void populateForm(Form form, var obj, string name) {
import std.string;
if(obj.payloadType == var.Type.Object) {
form.setValue(name, "");
foreach(k, v; obj) {
auto fn = name.replace("%", k.get!string);
// should I unify structs and assoctiavite arrays?
populateForm(form, v, fn ~ "["~k.get!string~"]");
//populateForm(form, v, fn ~"."~k.get!string);
}
} else {
//import std.stdio; writeln("SET ", name, " ", obj, " ", obj.payloadType);
@ -291,6 +325,18 @@ struct Template {
struct Skeleton {
string name;
}
/++
UDA to attach runtime metadata to a function. Will be available in the template.
History:
Added July 12, 2021
+/
struct meta {
string name;
string value;
}
/++
Can be used as a return value from one of your own methods when rendering websites with [WebPresenterWithTemplateSupport].
+/
@ -319,6 +365,8 @@ template WebPresenterWithTemplateSupport(CTRP) {
typeof(null) at;
string templateName;
string skeletonName;
string[string] meta;
Form function(WebPresenterWithTemplateSupport presenter) automaticForm;
alias at this;
}
template methodMeta(alias method) {
@ -332,6 +380,12 @@ template WebPresenterWithTemplateSupport(CTRP) {
ret.templateName = attr.name;
else static if(is(typeof(attr) == Skeleton))
ret.skeletonName = attr.name;
else static if(is(typeof(attr) == .meta))
ret.meta[attr.name] = attr.value;
ret.automaticForm = function Form(WebPresenterWithTemplateSupport presenter) {
return presenter.createAutomaticFormForFunction!(method, typeof(&method))(null);
};
return ret;
}
@ -351,11 +405,66 @@ template WebPresenterWithTemplateSupport(CTRP) {
void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, Meta meta) {
if(meta.templateName.length) {
var sobj = var.emptyObject;
var obj = var.emptyObject;
obj.data = ret;
presentSuccessfulReturnAsHtml(cgi, RenderTemplate(meta.templateName, obj), meta);
/+
sobj.meta = var.emptyObject;
foreach(k,v; meta.meta)
sobj.meta[k] = v;
+/
obj.meta = var.emptyObject;
foreach(k,v; meta.meta)
obj.meta[k] = v;
obj.meta.currentPath = cgi.pathInfo;
obj.meta.automaticForm = { return meta.automaticForm(this).toString; };
presentSuccessfulReturnAsHtml(cgi, RenderTemplate(meta.templateName, obj, sobj), meta);
} else
super.presentSuccessfulReturnAsHtml(cgi, ret, meta);
}
}
}
auto serveTemplateDirectory()(string urlPrefix, string directory = null, string skeleton = null, string extension = ".html") {
import arsd.cgi;
import std.file;
assert(urlPrefix[0] == '/');
assert(urlPrefix[$-1] == '/');
static struct DispatcherDetails {
string directory;
string skeleton;
string extension;
}
if(directory is null)
directory = urlPrefix[1 .. $];
assert(directory[$-1] == '/');
static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
auto file = cgi.pathInfo[urlPrefix.length .. $];
if(file.indexOf("/") != -1 || file.indexOf("\\") != -1)
return false;
auto fn = "templates/" ~ details.directory ~ file ~ details.extension;
if(std.file.exists(fn)) {
cgi.setCache(true);
auto doc = renderTemplate(fn["templates/".length.. $]);
cgi.gzipResponse = true;
cgi.write(doc.toString, true);
return true;
} else {
return false;
}
}
return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, skeleton, extension));
}