module arsd.web; /* FIXME: in params on the wrapped functions generally don't work Running from the command line: ./myapp function positional args.... ./myapp --format=json function _GET _POST _PUT _DELETE ./myapp --make-nested-call Procedural vs Object Oriented right now it is procedural: root/function root/module/function what about an object approach: root/object root/class/object static ApiProvider.getObject Formatting data: CoolApi.myFunc().getFormat('Element', [...same as get...]); You should also be able to ask for json, but with a particular format available as toString format("json", "html") -- gets json, but each object has it's own toString. Actually, the object adds a member called formattedSecondarily that is the other thing. Note: the array itself cannot be changed in format, only it's members. Note: the literal string of the formatted object is often returned. This may more than double the bandwidth of the call Note: BUG: it only works with built in formats right now when doing secondary // formats are: text, html, json, table, and xml // except json, they are all represented as strings in json values string toString -> formatting as text Element makeHtmlElement -> making it html (same as fragment) JSONValue makeJsonValue -> formatting to json Table makeHtmlTable -> making a table (not implemented) toXml -> making it into an xml document Arrays can be handled too: static (converts to) string makeHtmlArray(typeof(this)[] arr); Envelope format: document (default), json, none */ public import arsd.dom; public import arsd.cgi; // you have to import this in the actual usage file or else it won't link; surely a compiler bug import arsd.sha; public import std.string; public import std.array; public import std.stdio : writefln; public import std.conv; import std.random; public import std.range; public import std.traits; import std.json; struct Envelope { bool success; string type; string errorMessage; string userData; JSONValue result; // use result.str if the format was anything other than json debug string dFullString; } string linkTo(alias func, T...)(T args) { auto reflection = __traits(parent, func).reflection; assert(reflection !is null); auto name = func.stringof; int idx = name.indexOf("("); if(idx != -1) name = name[0 .. idx]; auto funinfo = reflection.functions[name]; return funinfo.originalName; } /// Everything should derive from this instead of the old struct namespace used before /// Your class must provide a default constructor. class ApiProvider { Cgi cgi; static immutable(ReflectionInfo)* reflection; string _baseUrl; // filled based on where this is called from on this request /// Override this if you have initialization work that must be done *after* cgi and reflection is ready. /// It should be used instead of the constructor for most work. void _initialize() {} /// This one is called at least once per call. (_initialize is only called once per process) void _initializePerCall() {} /// Override this if you want to do something special to the document void _postProcess(Document document) {} /// This tentatively redirects the user - depends on the envelope fomat void redirect(string location) { if(cgi.request("envelopeFormat", "document") == "document") cgi.setResponseLocation(location, false); } Element _sitemap() { auto container = _getGenericContainer(); auto list = container.addChild("ul"); string[string] handled; foreach(func; reflection.functions) { if(func.originalName in handled) continue; handled[func.originalName] = func.originalName; list.addChild("li", new Link(_baseUrl ~ "/" ~ func.name, beautify(func.originalName))); } return list.parentNode.removeChild(list); } Document _defaultPage() { throw new Exception("no default"); return null; } Element _getGenericContainer() out(ret) { assert(ret !is null); } body { auto document = new Document("
"); auto container = document.getElementById("body"); return container; } void _catchAll(string path) { throw new NoSuchPageException(_errorMessageForCatchAll); } private string _errorMessageForCatchAll; private void _catchallEntry(string path, string funName, string errorMessage) { if(!errorMessage.length) { string allFuncs, allObjs; foreach(n, f; reflection.functions) allFuncs ~= n ~ "\n"; foreach(n, f; reflection.objects) allObjs ~= n ~ "\n"; errorMessage = "no such function " ~ funName ~ "\n functions are:\n" ~ allFuncs ~ "\n\nObjects are:\n" ~ allObjs; } _errorMessageForCatchAll = errorMessage; } /// When in website mode, you can use this to beautify the error message Document delegate(Throwable) _errorFunction; } class ApiObject { /* abstract this(ApiProvider parent, string identifier) */ } struct ReflectionInfo { FunctionInfo[string] functions; EnumInfo[string] enums; StructInfo[string] structs; const(ReflectionInfo)*[string] objects; bool needsInstantiation; // the overall namespace string name; // this is also used as the object name in the JS api string defaultOutputFormat = "html"; int versionOfOutputFormat = 2; // change this in your constructor if you still need the (deprecated) old behavior // bool apiMode = false; // no longer used - if format is json, apiMode behavior is assumed. if format is html, it is not. // FIXME: what if you want the data formatted server side, but still in a json envelope? // should add format-payload: } struct EnumInfo { string name; int[] values; string[] names; } struct StructInfo { string name; // a struct is sort of like a function constructor... StructMemberInfo[] members; } struct StructMemberInfo { string name; string staticType; string defaultValue; } struct FunctionInfo { WrapperFunction dispatcher; JSONValue delegate(Cgi cgi, in string[][string] sargs) documentDispatcher; // should I also offer dispatchers for other formats like Variant[]? string name; string originalName; //string uriPath; Parameter[] parameters; string returnType; bool returnTypeIsDocument; Document function(in string[string] args) createForm; } struct Parameter { string name; string value; string type; string staticType; string validator; // for radio and select boxes string[] options; string[] optionValues; } string makeJavascriptApi(const ReflectionInfo* mod, string base) { assert(mod !is null); string script = `var `~mod.name~` = { "_apiBase":'`~base~`',`; script ~= javascriptBase; script ~= "\n\t"; bool[string] alreadyDone; bool outp = false; foreach(s; mod.enums) { if(outp) script ~= ",\n\t"; else outp = true; script ~= "'"~s.name~"': {\n"; bool outp2 = false; foreach(i, n; s.names) { if(outp2) script ~= ",\n"; else outp2 = true; // auto v = s.values[i]; auto v = "'" ~ n ~ "'"; // we actually want to use the name here because to!enum() uses member name. script ~= "\t\t'"~n~"':" ~ to!string(v); } script ~= "\n\t}"; } foreach(s; mod.structs) { if(outp) script ~= ",\n\t"; else outp = true; script ~= "'"~s.name~"': function("; bool outp2 = false; foreach(n; s.members) { if(outp2) script ~= ", "; else outp2 = true; script ~= n.name; } script ~= ") { return {\n"; outp2 = false; script ~= "\t\t'_arsdTypeOf':'"~s.name~"'"; if(s.members.length) script ~= ","; script ~= " // metadata, ought to be read only\n"; // outp2 is still false because I put the comma above foreach(n; s.members) { if(outp2) script ~= ",\n"; else outp2 = true; auto v = n.defaultValue; script ~= "\t\t'"~n.name~"': (typeof "~n.name~" == 'undefined') ? "~n.name~" : '" ~ to!string(v) ~ "'"; } script ~= "\n\t}; }"; } // FIXME: it should output the classes too /* foreach(obj; mod.objects) { if(outp) script ~= ",\n\t"; else outp = true; script ~= makeJavascriptApi(obj, base); } */ foreach(func; mod.functions) { if(func.originalName in alreadyDone) continue; // there's url friendly and code friendly, only need one alreadyDone[func.originalName] = true; if(outp) script ~= ",\n\t"; else outp = true; string args; string obj; bool outputted = false; foreach(i, arg; func.parameters) { if(outputted) { args ~= ","; obj ~= ","; } else outputted = true; args ~= arg.name; // FIXME: we could probably do better checks here too like on type obj ~= `'`~arg.name~`':(typeof `~arg.name ~ ` == "undefined" ? this._raiseError('InsufficientParametersException', '`~func.originalName~`: argument `~to!string(i) ~ " (" ~ arg.staticType~` `~arg.name~`) is not present') : `~arg.name~`)`; } /* if(outputted) args ~= ","; args ~= "callback"; */ script ~= `'` ~ func.originalName ~ `'`; script ~= ":"; script ~= `function(`~args~`) {`; if(obj.length) script ~= ` var argumentsObject = { `~obj~` }; return this._serverCall('`~func.name~`', argumentsObject, '`~func.returnType~`');`; else script ~= ` return this._serverCall('`~func.name~`', null, '`~func.returnType~`');`; script ~= ` }`; } script ~= "\n}"; // some global stuff to put in script ~= ` if(typeof arsdGlobalStuffLoadedForWebDotD == "undefined") { arsdGlobalStuffLoadedForWebDotD = true; var oldObjectDotPrototypeDotToString = Object.prototype.toString; Object.prototype.toString = function() { if(this.formattedSecondarily) return this.formattedSecondarily; return oldObjectDotPrototypeDotToString.call(this); } } `; return script; } template isEnum(alias T) if(is(T)) { static if (is(T == enum)) enum bool isEnum = true; else enum bool isEnum = false; } // WTF, shouldn't is(T == xxx) already do this? template isEnum(T) if(!is(T)) { enum bool isEnum = false; } template isStruct(alias T) if(is(T)) { static if (is(T == struct)) enum bool isStruct = true; else enum bool isStruct = false; } // WTF template isStruct(T) if(!is(T)) { enum bool isStruct = false; } template isApiObject(alias T) if(is(T)) { static if (is(T : ApiObject)) enum bool isApiObject = true; else enum bool isApiObject = false; } // WTF template isApiObject(T) if(!is(T)) { enum bool isApiObject = false; } template isApiProvider(alias T) if(is(T)) { static if (is(T : ApiProvider)) enum bool isApiProvider = true; else enum bool isApiProvider = false; } // WTF template isApiProvider(T) if(!is(T)) { enum bool isApiProvider = false; } template Passthrough(T) { T Passthrough; } template PassthroughType(T) { alias T PassthroughType; } auto generateGetter(PM, Parent, string member, alias hackToEnsureMultipleFunctionsWithTheSameSignatureGetTheirOwnInstantiations)(string io, Parent instantiation) { static if(is(PM : ApiObject)) { auto i = new PM(instantiation, io); return &__traits(getMember, i, member); } else { return &__traits(getMember, instantiation, member); } } immutable(ReflectionInfo*) prepareReflection(alias PM)(Cgi cgi, PM instantiation, ApiObject delegate(string) instantiateObject = null) if(is(PM : ApiProvider) || is(PM: ApiObject) ) { return prepareReflectionImpl!(PM, PM)(cgi, instantiation, instantiateObject); } immutable(ReflectionInfo*) prepareReflectionImpl(alias PM, alias Parent)(Cgi cgi, Parent instantiation, ApiObject delegate(string) instantiateObject = null) if((is(PM : ApiProvider) || is(PM: ApiObject)) && is(Parent : ApiProvider) ) { assert(instantiation !is null); ReflectionInfo* reflection = new ReflectionInfo; reflection.name = PM.stringof; static if(is(PM: ApiObject)) reflection.needsInstantiation = true; // derivedMembers is changed from allMembers foreach(member; __traits(derivedMembers, PM)) { // FIXME: the filthiest of all hacks... static if(!__traits(compiles, !is(typeof(__traits(getMember, PM, member)) == function) && isEnum!(__traits(getMember, PM, member)))) continue; // must be a data member or something... else // DONE WITH FILTHIEST OF ALL HACKS //if(member.length == 0) // continue; static if( !is(typeof(__traits(getMember, PM, member)) == function) && isEnum!(__traits(getMember, PM, member)) && member[0] != '_' ) { EnumInfo i; i.name = member; foreach(m; __traits(allMembers, __traits(getMember, PM, member))) { i.names ~= m; i.values ~= cast(int) __traits(getMember, __traits(getMember, PM, member), m); } reflection.enums[member] = i; } else static if( !is(typeof(__traits(getMember, PM, member)) == function) && isStruct!(__traits(getMember, PM, member)) && member[0] != '_' ) { StructInfo i; i.name = member; typeof(Passthrough!(__traits(getMember, PM, member))) s; foreach(idx, m; s.tupleof) { StructMemberInfo mem; mem.name = s.tupleof[idx].stringof[2..$]; mem.staticType = typeof(m).stringof; mem.defaultValue = null; // FIXME i.members ~= mem; } reflection.structs[member] = i; } else static if( is(typeof(__traits(getMember, PM, member)) == function) && ( member[0] != '_' && ( member.length < 5 || ( member[$ - 5 .. $] != "_Page" && member[$ - 5 .. $] != "_Form") && !(member.length > 16 && member[$ - 16 .. $] == "_PermissionCheck") ))) { FunctionInfo f; ParameterTypeTuple!(__traits(getMember, PM, member)) fargs; f.returnType = ReturnType!(__traits(getMember, PM, member)).stringof; f.returnTypeIsDocument = is(ReturnType!(__traits(getMember, PM, member)) : Document); f.name = toUrlName(member); f.originalName = member; assert(instantiation !is null); f.dispatcher = generateWrapper!( generateGetter!(PM, Parent, member, __traits(getMember, PM, member)), __traits(getMember, PM, member), Parent, member )(reflection, instantiation); //f.uriPath = f.originalName; auto names = parameterNamesOf!(__traits(getMember, PM, member)); foreach(idx, param; fargs) { Parameter p; p.name = names[idx]; p.staticType = typeof(fargs[idx]).stringof; static if( is( typeof(param) == enum )) { p.type = "select"; foreach(opt; __traits(allMembers, typeof(param))) { p.options ~= opt; p.optionValues ~= to!string(__traits(getMember, param, opt)); } } else static if (is(typeof(param) == bool)) { p.type = "checkbox"; } else static if (is(Unqual!(typeof(param)) == Cgi.UploadedFile)) { p.type = "file"; } else { if(p.name.toLower.indexOf("password") != -1) // hack to support common naming convention p.type = "password"; else p.type = "text"; } f.parameters ~= p; } static if(__traits(hasMember, PM, member ~ "_Form")) { f.createForm = &__traits(getMember, PM, member ~ "_Form"); } reflection.functions[f.name] = f; // also offer the original name if it doesn't // conflict //if(f.originalName !in reflection.functions) reflection.functions[f.originalName] = f; } else static if( !is(typeof(__traits(getMember, PM, member)) == function) && isApiObject!(__traits(getMember, PM, member)) && member[0] != '_' ) { reflection.objects[member] = prepareReflectionImpl!( __traits(getMember, PM, member), Parent) (cgi, instantiation); } else static if( // child ApiProviders are like child modules !is(typeof(__traits(getMember, PM, member)) == function) && isApiProvider!(__traits(getMember, PM, member)) && member[0] != '_' ) { PassthroughType!(__traits(getMember, PM, member)) i; i = new typeof(i)(); auto r = prepareReflection!(__traits(getMember, PM, member))(cgi, i); reflection.objects[member] = r; if(toLower(member) !in reflection.objects) // web filenames are often lowercase too reflection.objects[member.toLower] = r; } } static if(is(PM: ApiProvider)) { instantiation.cgi = cgi; instantiation.reflection = cast(immutable) reflection; instantiation._initialize(); } return cast(immutable) reflection; } void run(Provider)(Cgi cgi, Provider instantiation, int pathInfoStartingPoint = 0) if(is(Provider : ApiProvider)) { assert(instantiation !is null); immutable(ReflectionInfo)* reflection; if(instantiation.reflection is null) prepareReflection!(Provider)(cgi, instantiation); reflection = instantiation.reflection; instantiation._baseUrl = cgi.scriptName ~ cgi.pathInfo[0 .. pathInfoStartingPoint]; if(cgi.pathInfo[pathInfoStartingPoint .. $].length <= 1) { auto document = instantiation._defaultPage(); if(document !is null) { instantiation._postProcess(document); cgi.write(document.toString()); } cgi.close(); return; } string funName = cgi.pathInfo[pathInfoStartingPoint + 1..$]; // kinda a hack, but this kind of thing should be available anyway if(funName == "functions.js") { cgi.setResponseContentType("text/javascript"); cgi.write(makeJavascriptApi(reflection, replace(cast(string) cgi.requestUri, "functions.js", ""))); cgi.close(); return; } instantiation._initializePerCall(); // what about some built in functions? /* // Basic integer operations builtin.opAdd builtin.opSub builtin.opMul builtin.opDiv // Basic array operations builtin.opConcat // use to combine calls easily builtin.opIndex builtin.opSlice builtin.length // Basic floating point operations builtin.round builtin.floor builtin.ceil // Basic object operations builtin.getMember // Basic functional operations builtin.filter // use to slice down on stuff to transfer builtin.map // call a server function on a whole array builtin.reduce // Access to the html items builtin.getAutomaticForm(method) */ const(FunctionInfo)* fun; auto envelopeFormat = cgi.request("envelopeFormat", "document"); Envelope result; result.userData = cgi.request("passedThroughUserData"); string instantiator; string objectName; try { // Built-ins string errorMessage; if(funName.length > 8 && funName[0..8] == "builtin.") { funName = funName[8..$]; switch(funName) { default: assert(0); case "getAutomaticForm": auto mfun = new FunctionInfo; mfun.returnType = "Form"; mfun.dispatcher = delegate JSONValue (Cgi cgi, string, in string[][string] sargs, in string format, in string secondaryFormat = null) { auto rfun = cgi.request("method") in reflection.functions; if(rfun is null) throw new NoSuchPageException("no such function " ~ cgi.request("method")); auto form = createAutomaticForm(new Document, *rfun); auto idx = cgi.requestUri.indexOf("builtin.getAutomaticForm"); form.action = cgi.requestUri[0 .. idx] ~ form.action; // make sure it works across the site JSONValue v; v.type = JSON_TYPE.STRING; v.str = form.toString(); return v; }; fun = cast(immutable) mfun; break; } } else { // User-defined // FIXME: modules? should be done with dots since slashes is used for api objects fun = funName in reflection.functions; if(fun is null) { auto parts = funName.split("/"); const(ReflectionInfo)* currentReflection = reflection; if(parts.length > 1) while(parts.length) { if(parts.length > 1) { objectName = parts[0]; auto object = objectName in reflection.objects; if(object is null) { // || object.instantiate is null) errorMessage = "no such object: " ~ objectName; goto noSuchFunction; } currentReflection = *object; if(!currentReflection.needsInstantiation) { parts = parts[1 .. $]; continue; } auto objectIdentifier = parts[1]; instantiator = objectIdentifier; //obj = object.instantiate(objectIdentifier); parts = parts[2 .. $]; if(parts.length == 0) { // gotta run the default function fun = (to!string(cgi.requestMethod)) in currentReflection.functions; } } else { fun = parts[0] in currentReflection.functions; if(fun is null) errorMessage = "no such method in class "~objectName~": " ~ parts[0]; parts = parts[1 .. $]; } } } } if(fun is null) { noSuchFunction: instantiation._catchallEntry( cgi.pathInfo[pathInfoStartingPoint + 1..$], funName, errorMessage); } assert(fun !is null); assert(fun.dispatcher !is null); assert(cgi !is null); result.type = fun.returnType; string format = cgi.request("format", reflection.defaultOutputFormat); string secondaryFormat = cgi.request("secondaryFormat", ""); if(secondaryFormat.length == 0) secondaryFormat = null; JSONValue res; if(envelopeFormat == "document" && fun.documentDispatcher !is null) { res = fun.documentDispatcher(cgi, cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray); envelopeFormat = "html"; } else res = fun.dispatcher(cgi, instantiator, cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.postArray : cgi.getArray, format, secondaryFormat); //if(cgi) // cgi.setResponseContentType("application/json"); result.success = true; result.result = res; } catch (Throwable e) { result.success = false; result.errorMessage = e.msg; result.type = e.classinfo.name; debug result.dFullString = e.toString(); if(envelopeFormat == "document" || envelopeFormat == "html") { auto ipe = cast(InsufficientParametersException) e; if(ipe !is null) { assert(fun !is null); Form form; if(0 || fun.createForm !is null) { // FIXME: if 0 // go ahead and use it to make the form page auto doc = fun.createForm(cgi.requestMethod == Cgi.RequestMethod.POST ? cgi.post : cgi.get); } else { Parameter[] params = fun.parameters.dup; foreach(i, p; fun.parameters) { string value = ""; if(p.name in cgi.get) value = cgi.get[p.name]; if(p.name in cgi.post) value = cgi.post[p.name]; params[i].value = value; } form = createAutomaticForm(new Document, *fun);// params, beautify(fun.originalName)); foreach(k, v; cgi.get) form.setValue(k, v); form.setValue("envelopeFormat", envelopeFormat); auto n = form.getElementById("function-name"); if(n) n.innerText = beautify(fun.originalName); } assert(form !is null); result.result.str = form.toString(); } else { if(instantiation._errorFunction !is null) { auto document = instantiation._errorFunction(e); if(document is null) goto gotnull; result.result.str = (document.toString()); } else { gotnull: auto document = new Document; auto code = document.createElement("pre"); code.innerText = e.toString(); result.result.str = (code.toString()); } } } } finally { switch(envelopeFormat) { case "redirect": auto redirect = cgi.request("_arsd_redirect_location", cgi.referrer); // FIXME: is this safe? it'd make XSS super easy // add result to url if(!result.success) goto case "none"; cgi.setResponseLocation(redirect, false); break; case "json": // this makes firefox ugly //cgi.setResponseContentType("application/json"); auto json = toJsonValue(result); cgi.write(toJSON(&json)); break; case "none": cgi.setResponseContentType("text/plain"); if(result.success) { if(result.result.type == JSON_TYPE.STRING) { cgi.write(result.result.str); } else { cgi.write(toJSON(&result.result)); } } else { cgi.write(result.errorMessage); } break; case "document": case "html": default: cgi.setResponseContentType("text/html"); if(result.result.type == JSON_TYPE.STRING) { auto returned = result.result.str; if((fun !is null) && envelopeFormat != "html") { Document document; if(fun.returnTypeIsDocument) { // probably not super efficient... document = new TemplatedDocument(returned); } else { auto e = instantiation._getGenericContainer(); document = e.parentDocument; // FIXME: slow, esp if func return element e.innerHTML = returned; } if(envelopeFormat == "document") instantiation._postProcess(document); returned = document.toString; } cgi.write(returned); } else cgi.write(htmlEntitiesEncode(toJSON(&result.result))); break; } cgi.close(); } } mixin template FancyMain(T, Args...) { void fancyMainFunction(Cgi cgi) { //string[] args) { // auto cgi = new Cgi; // there must be a trailing slash for relative links.. if(cgi.pathInfo.length == 0) { cgi.setResponseLocation(cgi.requestUri ~ "/"); cgi.close(); return; } // FIXME: won't work for multiple objects T instantiation = new T(); auto reflection = prepareReflection!(T)(cgi, instantiation); run(cgi, instantiation); /+ if(args.length > 1) { string[string][] namedArgs; foreach(arg; args[2..$]) { auto lol = arg.indexOf("="); if(lol == -1) throw new Exception("use named args for all params"); //namedArgs[arg[0..lol]] = arg[lol+1..$]; // FIXME } if(!(args[1] in reflection.functions)) { throw new Exception("No such function"); } //writefln("%s", reflection.functions[args[1]].dispatcher(null, namedArgs, "string")); } else { +/ // } } mixin GenericMain!(fancyMainFunction, Args); } Form createAutomaticForm(Document document, in FunctionInfo func, string[string] fieldTypes = null) { return createAutomaticForm(document, func.name, func.parameters, beautify(func.originalName), "POST", fieldTypes); } Form createAutomaticForm(Document document, string action, in Parameter[] parameters, string submitText = "Submit", string method = "POST", string[string] fieldTypes = null) { assert(document !is null); auto form = cast(Form) document.createElement("form"); form.action = action; assert(form !is null); form.method = method; auto fieldset = document.createElement("fieldset"); auto legend = document.createElement("legend"); legend.innerText = submitText; fieldset.appendChild(legend); auto table = cast(Table) document.createElement("table"); assert(table !is null); form.appendChild(fieldset); fieldset.appendChild(table); table.appendChild(document.createElement("tbody")); static int count = 0; foreach(param; parameters) { Element input; string type = param.type; if(param.name in fieldTypes) type = fieldTypes[param.name]; if(type == "select") { input = document.createElement("select"); foreach(idx, opt; param.options) { auto option = document.createElement("option"); option.name = opt; option.value = param.optionValues[idx]; option.innerText = beautify(opt); if(option.value == param.value) option.selected = "selected"; input.appendChild(option); } input.name = param.name; } else if (type == "radio") { assert(0, "FIXME"); } else { if(type.startsWith("textarea")) { input = document.createElement("textarea"); input.name = param.name; input.innerText = param.value; auto idx = type.indexOf("-"); if(idx != -1) { idx++; input.rows = type[idx .. $]; } } else { input = document.createElement("input"); input.type = type; input.name = param.name; input.value = param.value; if(type == "file") { form.method = "POST"; form.enctype = "multipart/form-data"; } } } string n = param.name ~ "_auto-form-" ~ to!string(count); input.id = n; if(type == "hidden") { form.appendChild(input); } else { auto th = document.createElement("th"); auto label = document.createElement("label"); label.setAttribute("for", n); label.innerText = beautify(param.name) ~ ": "; th.appendChild(label); table.appendRow(th, input); } count++; }; auto fmt = document.createElement("select"); fmt.name = "format"; fmt.addChild("option", "html").setAttribute("value", "html"); fmt.addChild("option", "table").setAttribute("value", "table"); fmt.addChild("option", "json").setAttribute("value", "json"); fmt.addChild("option", "string").setAttribute("value", "string"); auto th = table.th(""); th.addChild("label", "Format:"); table.appendRow(th, fmt).className = "format-row"; auto submit = document.createElement("input"); submit.value = submitText; submit.type = "submit"; table.appendRow(Html(" "), submit); // form.setValue("format", reflection.defaultOutputFormat); return form; } /** * Returns the parameter names of the given function * * Params: * func = the function alias to get the parameter names of * * Returns: an array of strings containing the parameter names */ /+ string parameterNamesOf( alias fn )( ) { string fullName = typeof(&fn).stringof; int pos = fullName.lastIndexOf( ')' ); int end = pos; int count = 0; do { if ( fullName[pos] == ')' ) { count++; } else if ( fullName[pos] == '(' ) { count--; } pos--; } while ( count > 0 ); return fullName[pos+2..end]; } +/ template parameterNamesOf (alias func) { const parameterNamesOf = parameterNamesOfImpl!(func); } int indexOfNew(string s, char a) { foreach(i, c; s) if(c == a) return i; return -1; } /** * Returns the parameter names of the given function * * Params: * func = the function alias to get the parameter names of * * Returns: an array of strings containing the parameter names */ private string[] parameterNamesOfImpl (alias func) () { string funcStr = typeof(&func).stringof; auto start = funcStr.indexOfNew('('); auto end = funcStr.indexOfNew(')'); const firstPattern = ' '; const secondPattern = ','; funcStr = funcStr[start + 1 .. end]; if (funcStr == "") return null; funcStr ~= secondPattern; string token; string[] arr; foreach (c ; funcStr) { if (c != firstPattern && c != secondPattern) token ~= c; else { if (token) arr ~= token; token = null; } } if (arr.length == 1) return arr; string[] result; bool skip = false; foreach (str ; arr) { skip = !skip; if (skip) continue; result ~= str; } return result; } ///////////////////////////////// string toHtml(T)(T a) { string ret; static if(is(T : Document)) ret = a.toString(); else static if(isArray!(T)) { static if(__traits(compiles, typeof(T[0]).makeHtmlArray(a))) ret = to!string(typeof(T[0]).makeHtmlArray(a)); else foreach(v; a) ret ~= toHtml(v); } else static if(is(T : Element)) ret = a.toString(); else static if(__traits(compiles, a.makeHtmlElement().toString())) ret = a.makeHtmlElement().toString(); else static if(is(T == Html)) ret = a.source; else ret = htmlEntitiesEncode(std.array.replace(to!string(a), "\n", "