From 81dba0f46daa2ad2f231ee1cc169258f05478ad8 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Wed, 28 Jul 2021 22:23:38 -0400 Subject: [PATCH] a few little web enhancements --- cgi.d | 119 ++++++++++++++++++++++++++++----- dom.d | 119 +++++++++++++++++++-------------- jsvar.d | 3 + webtemplate.d | 179 ++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 318 insertions(+), 102 deletions(-) diff --git a/cgi.d b/cgi.d index 83df269..e8d4e00 100644 --- a/cgi.d +++ b/cgi.d @@ -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("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
\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"); diff --git a/dom.d b/dom.d index 9604eeb..69b2613 100644 --- a/dom.d +++ b/dom.d @@ -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("" ~ html ~ "", 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; diff --git a/jsvar.d b/jsvar.d index ff9d83e..fdde32b 100644 --- a/jsvar.d +++ b/jsvar.d @@ -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; diff --git a/webtemplate.d b/webtemplate.d index 4fc1272..d37f03e 100644 --- a/webtemplate.d +++ b/webtemplate.d @@ -20,6 +20,11 @@ there were no items. +
+ + + + ``` @@ -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)); +}