Update.
This commit is contained in:
@@ -3,15 +3,20 @@
|
||||
|
||||
#include "modules.h"
|
||||
|
||||
#include "argparse.wren.inc"
|
||||
#include "base64.wren.inc"
|
||||
#include "bytes.wren.inc"
|
||||
#include "crypto.wren.inc"
|
||||
#include "dataset.wren.inc"
|
||||
#include "datetime.wren.inc"
|
||||
#include "dns.wren.inc"
|
||||
#include "env.wren.inc"
|
||||
#include "html.wren.inc"
|
||||
#include "http.wren.inc"
|
||||
#include "io.wren.inc"
|
||||
#include "jinja.wren.inc"
|
||||
#include "json.wren.inc"
|
||||
#include "markdown.wren.inc"
|
||||
#include "math.wren.inc"
|
||||
#include "net.wren.inc"
|
||||
#include "os.wren.inc"
|
||||
@@ -23,10 +28,19 @@
|
||||
#include "subprocess.wren.inc"
|
||||
#include "timer.wren.inc"
|
||||
#include "tls.wren.inc"
|
||||
#include "uuid.wren.inc"
|
||||
#include "wdantic.wren.inc"
|
||||
#include "web.wren.inc"
|
||||
#include "websocket.wren.inc"
|
||||
|
||||
extern void base64Encode(WrenVM* vm);
|
||||
extern void base64Decode(WrenVM* vm);
|
||||
extern void bytesToList(WrenVM* vm);
|
||||
extern void bytesFromList(WrenVM* vm);
|
||||
extern void bytesXorMask(WrenVM* vm);
|
||||
extern void bytesConcat(WrenVM* vm);
|
||||
extern void bytesSlice(WrenVM* vm);
|
||||
extern void bytesLength(WrenVM* vm);
|
||||
extern void cryptoRandomBytes(WrenVM* vm);
|
||||
extern void cryptoMd5(WrenVM* vm);
|
||||
extern void cryptoSha1(WrenVM* vm);
|
||||
@@ -218,12 +232,24 @@ typedef struct
|
||||
// The array of built-in modules.
|
||||
static ModuleRegistry modules[] =
|
||||
{
|
||||
MODULE(argparse)
|
||||
END_MODULE
|
||||
MODULE(base64)
|
||||
CLASS(Base64)
|
||||
STATIC_METHOD("encode(_)", base64Encode)
|
||||
STATIC_METHOD("decode(_)", base64Decode)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(bytes)
|
||||
CLASS(Bytes)
|
||||
STATIC_METHOD("toList(_)", bytesToList)
|
||||
STATIC_METHOD("fromList(_)", bytesFromList)
|
||||
STATIC_METHOD("xorMask(_,_)", bytesXorMask)
|
||||
STATIC_METHOD("concat(_,_)", bytesConcat)
|
||||
STATIC_METHOD("slice(_,_,_)", bytesSlice)
|
||||
STATIC_METHOD("length(_)", bytesLength)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(crypto)
|
||||
CLASS(Crypto)
|
||||
STATIC_METHOD("randomBytes_(_,_)", cryptoRandomBytes)
|
||||
@@ -234,6 +260,8 @@ static ModuleRegistry modules[] =
|
||||
STATIC_METHOD("sha256_(_)", cryptoSha256)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(dataset)
|
||||
END_MODULE
|
||||
MODULE(datetime)
|
||||
CLASS(DateTime)
|
||||
STATIC_METHOD("now_()", datetimeNow)
|
||||
@@ -255,6 +283,8 @@ static ModuleRegistry modules[] =
|
||||
STATIC_METHOD("all", envAll)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(html)
|
||||
END_MODULE
|
||||
MODULE(http)
|
||||
END_MODULE
|
||||
MODULE(io)
|
||||
@@ -313,6 +343,8 @@ static ModuleRegistry modules[] =
|
||||
STATIC_METHOD("parse(_)", jsonParse)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(markdown)
|
||||
END_MODULE
|
||||
MODULE(math)
|
||||
CLASS(Math)
|
||||
STATIC_METHOD("sin(_)", mathSin)
|
||||
@@ -431,6 +463,12 @@ static ModuleRegistry modules[] =
|
||||
METHOD("close_()", tlsSocketClose)
|
||||
END_CLASS
|
||||
END_MODULE
|
||||
MODULE(uuid)
|
||||
END_MODULE
|
||||
MODULE(wdantic)
|
||||
END_MODULE
|
||||
MODULE(web)
|
||||
END_MODULE
|
||||
MODULE(websocket)
|
||||
END_MODULE
|
||||
|
||||
|
||||
Vendored
+277
@@ -0,0 +1,277 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "os" for Process
|
||||
|
||||
class ArgumentParser {
|
||||
construct new() {
|
||||
_description = ""
|
||||
_positional = []
|
||||
_optional = []
|
||||
_prog = null
|
||||
}
|
||||
|
||||
construct new(description) {
|
||||
_description = description
|
||||
_positional = []
|
||||
_optional = []
|
||||
_prog = null
|
||||
}
|
||||
|
||||
prog { _prog }
|
||||
prog=(value) { _prog = value }
|
||||
description { _description }
|
||||
description=(value) { _description = value }
|
||||
|
||||
addArgument(name) { addArgument(name, {}) }
|
||||
|
||||
addArgument(name, options) {
|
||||
if (name.startsWith("-")) {
|
||||
var arg = {
|
||||
"name": name,
|
||||
"long": options.containsKey("long") ? options["long"] : null,
|
||||
"type": options.containsKey("type") ? options["type"] : "string",
|
||||
"default": options.containsKey("default") ? options["default"] : null,
|
||||
"required": options.containsKey("required") ? options["required"] : false,
|
||||
"help": options.containsKey("help") ? options["help"] : "",
|
||||
"choices": options.containsKey("choices") ? options["choices"] : null,
|
||||
"action": options.containsKey("action") ? options["action"] : "store",
|
||||
"nargs": options.containsKey("nargs") ? options["nargs"] : null,
|
||||
"dest": options.containsKey("dest") ? options["dest"] : null
|
||||
}
|
||||
_optional.add(arg)
|
||||
} else {
|
||||
var arg = {
|
||||
"name": name,
|
||||
"type": options.containsKey("type") ? options["type"] : "string",
|
||||
"default": options.containsKey("default") ? options["default"] : null,
|
||||
"required": options.containsKey("required") ? options["required"] : true,
|
||||
"help": options.containsKey("help") ? options["help"] : "",
|
||||
"choices": options.containsKey("choices") ? options["choices"] : null,
|
||||
"nargs": options.containsKey("nargs") ? options["nargs"] : null
|
||||
}
|
||||
_positional.add(arg)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
destName_(arg) {
|
||||
if (arg["dest"]) return arg["dest"]
|
||||
var name = arg["long"] ? arg["long"] : arg["name"]
|
||||
while (name.startsWith("-")) name = name[1..-1]
|
||||
return name.replace("-", "_")
|
||||
}
|
||||
|
||||
parseArgs() { parseArgs(Process.arguments) }
|
||||
|
||||
parseArgs(args) {
|
||||
var result = {}
|
||||
|
||||
for (arg in _optional) {
|
||||
var dest = destName_(arg)
|
||||
if (arg["action"] == "storeTrue") {
|
||||
result[dest] = false
|
||||
} else if (arg["action"] == "storeFalse") {
|
||||
result[dest] = true
|
||||
} else if (arg["action"] == "count") {
|
||||
result[dest] = 0
|
||||
} else if (arg["action"] == "append") {
|
||||
result[dest] = []
|
||||
} else if (arg["default"] != null) {
|
||||
result[dest] = arg["default"]
|
||||
} else {
|
||||
result[dest] = null
|
||||
}
|
||||
}
|
||||
|
||||
for (arg in _positional) {
|
||||
if (arg["default"] != null) {
|
||||
result[arg["name"]] = arg["default"]
|
||||
} else {
|
||||
result[arg["name"]] = null
|
||||
}
|
||||
}
|
||||
|
||||
var posIdx = 0
|
||||
var i = 0
|
||||
while (i < args.count) {
|
||||
var token = args[i]
|
||||
|
||||
if (token.startsWith("-")) {
|
||||
var matchedArg = null
|
||||
for (arg in _optional) {
|
||||
if (token == arg["name"] || token == arg["long"]) {
|
||||
matchedArg = arg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedArg == null) {
|
||||
Fiber.abort("Unknown option: %(token)")
|
||||
}
|
||||
|
||||
var dest = destName_(matchedArg)
|
||||
if (matchedArg["action"] == "storeTrue") {
|
||||
result[dest] = true
|
||||
i = i + 1
|
||||
} else if (matchedArg["action"] == "storeFalse") {
|
||||
result[dest] = false
|
||||
i = i + 1
|
||||
} else if (matchedArg["action"] == "count") {
|
||||
result[dest] = result[dest] + 1
|
||||
i = i + 1
|
||||
} else if (matchedArg["action"] == "append") {
|
||||
if (i + 1 >= args.count) {
|
||||
Fiber.abort("Option %(token) requires a value")
|
||||
}
|
||||
i = i + 1
|
||||
result[dest].add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
} else {
|
||||
if (matchedArg["nargs"] == "*") {
|
||||
var values = []
|
||||
i = i + 1
|
||||
while (i < args.count && !args[i].startsWith("-")) {
|
||||
values.add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
result[dest] = values
|
||||
} else if (matchedArg["nargs"] == "+") {
|
||||
var values = []
|
||||
i = i + 1
|
||||
while (i < args.count && !args[i].startsWith("-")) {
|
||||
values.add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
if (values.count == 0) {
|
||||
Fiber.abort("Option %(token) requires at least one value")
|
||||
}
|
||||
result[dest] = values
|
||||
} else if (matchedArg["nargs"] is Num) {
|
||||
var values = []
|
||||
var n = matchedArg["nargs"]
|
||||
i = i + 1
|
||||
for (j in 0...n) {
|
||||
if (i >= args.count) {
|
||||
Fiber.abort("Option %(token) requires %(n) values")
|
||||
}
|
||||
values.add(convertValue_(args[i], matchedArg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
result[dest] = values
|
||||
} else {
|
||||
if (i + 1 >= args.count) {
|
||||
Fiber.abort("Option %(token) requires a value")
|
||||
}
|
||||
i = i + 1
|
||||
var value = convertValue_(args[i], matchedArg["type"])
|
||||
if (matchedArg["choices"] && !matchedArg["choices"].contains(value)) {
|
||||
Fiber.abort("Invalid choice '%(value)' for %(token)")
|
||||
}
|
||||
result[dest] = value
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (posIdx < _positional.count) {
|
||||
var arg = _positional[posIdx]
|
||||
if (arg["nargs"] == "*" || arg["nargs"] == "+") {
|
||||
var values = []
|
||||
while (i < args.count && !args[i].startsWith("-")) {
|
||||
values.add(convertValue_(args[i], arg["type"]))
|
||||
i = i + 1
|
||||
}
|
||||
if (arg["nargs"] == "+" && values.count == 0) {
|
||||
Fiber.abort("Argument %(arg["name"]) requires at least one value")
|
||||
}
|
||||
result[arg["name"]] = values
|
||||
} else {
|
||||
var value = convertValue_(token, arg["type"])
|
||||
if (arg["choices"] && !arg["choices"].contains(value)) {
|
||||
Fiber.abort("Invalid choice '%(value)' for %(arg["name"])")
|
||||
}
|
||||
result[arg["name"]] = value
|
||||
i = i + 1
|
||||
}
|
||||
posIdx = posIdx + 1
|
||||
} else {
|
||||
Fiber.abort("Unexpected argument: %(token)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (arg in _positional) {
|
||||
if (arg["required"] && result[arg["name"]] == null) {
|
||||
Fiber.abort("Missing required argument: %(arg["name"])")
|
||||
}
|
||||
}
|
||||
|
||||
for (arg in _optional) {
|
||||
var dest = destName_(arg)
|
||||
if (arg["required"] && result[dest] == null) {
|
||||
Fiber.abort("Missing required option: %(arg["name"])")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
convertValue_(value, type) {
|
||||
if (type == "int") {
|
||||
return Num.fromString(value)
|
||||
} else if (type == "float") {
|
||||
return Num.fromString(value)
|
||||
} else if (type == "bool") {
|
||||
if (value == "true" || value == "1" || value == "yes") return true
|
||||
if (value == "false" || value == "0" || value == "no") return false
|
||||
Fiber.abort("Invalid boolean value: %(value)")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
printHelp() {
|
||||
var prog = _prog ? _prog : "program"
|
||||
System.print("usage: %(prog) [options]%(positionalUsage_())")
|
||||
if (_description.count > 0) {
|
||||
System.print("\n%(description)")
|
||||
}
|
||||
if (_positional.count > 0) {
|
||||
System.print("\npositional arguments:")
|
||||
for (arg in _positional) {
|
||||
var helpText = arg["help"].count > 0 ? arg["help"] : ""
|
||||
System.print(" %(pad_(arg["name"], 20)) %(helpText)")
|
||||
}
|
||||
}
|
||||
if (_optional.count > 0) {
|
||||
System.print("\noptional arguments:")
|
||||
for (arg in _optional) {
|
||||
var names = arg["name"]
|
||||
if (arg["long"]) names = names + ", " + arg["long"]
|
||||
var helpText = arg["help"].count > 0 ? arg["help"] : ""
|
||||
System.print(" %(pad_(names, 20)) %(helpText)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
positionalUsage_() {
|
||||
if (_positional.count == 0) return ""
|
||||
var parts = []
|
||||
for (arg in _positional) {
|
||||
if (arg["nargs"] == "*") {
|
||||
parts.add("[%(arg["name"]) ...]")
|
||||
} else if (arg["nargs"] == "+") {
|
||||
parts.add("%(arg["name"]) [...]")
|
||||
} else if (arg["required"]) {
|
||||
parts.add(arg["name"])
|
||||
} else {
|
||||
parts.add("[%(arg["name"])]")
|
||||
}
|
||||
}
|
||||
return " " + parts.join(" ")
|
||||
}
|
||||
|
||||
pad_(str, width) {
|
||||
var result = str
|
||||
while (result.count < width) result = result + " "
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/argparse.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* argparseModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"os\" for Process\n"
|
||||
"\n"
|
||||
"class ArgumentParser {\n"
|
||||
" construct new() {\n"
|
||||
" _description = \"\"\n"
|
||||
" _positional = []\n"
|
||||
" _optional = []\n"
|
||||
" _prog = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" construct new(description) {\n"
|
||||
" _description = description\n"
|
||||
" _positional = []\n"
|
||||
" _optional = []\n"
|
||||
" _prog = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" prog { _prog }\n"
|
||||
" prog=(value) { _prog = value }\n"
|
||||
" description { _description }\n"
|
||||
" description=(value) { _description = value }\n"
|
||||
"\n"
|
||||
" addArgument(name) { addArgument(name, {}) }\n"
|
||||
"\n"
|
||||
" addArgument(name, options) {\n"
|
||||
" if (name.startsWith(\"-\")) {\n"
|
||||
" var arg = {\n"
|
||||
" \"name\": name,\n"
|
||||
" \"long\": options.containsKey(\"long\") ? options[\"long\"] : null,\n"
|
||||
" \"type\": options.containsKey(\"type\") ? options[\"type\"] : \"string\",\n"
|
||||
" \"default\": options.containsKey(\"default\") ? options[\"default\"] : null,\n"
|
||||
" \"required\": options.containsKey(\"required\") ? options[\"required\"] : false,\n"
|
||||
" \"help\": options.containsKey(\"help\") ? options[\"help\"] : \"\",\n"
|
||||
" \"choices\": options.containsKey(\"choices\") ? options[\"choices\"] : null,\n"
|
||||
" \"action\": options.containsKey(\"action\") ? options[\"action\"] : \"store\",\n"
|
||||
" \"nargs\": options.containsKey(\"nargs\") ? options[\"nargs\"] : null,\n"
|
||||
" \"dest\": options.containsKey(\"dest\") ? options[\"dest\"] : null\n"
|
||||
" }\n"
|
||||
" _optional.add(arg)\n"
|
||||
" } else {\n"
|
||||
" var arg = {\n"
|
||||
" \"name\": name,\n"
|
||||
" \"type\": options.containsKey(\"type\") ? options[\"type\"] : \"string\",\n"
|
||||
" \"default\": options.containsKey(\"default\") ? options[\"default\"] : null,\n"
|
||||
" \"required\": options.containsKey(\"required\") ? options[\"required\"] : true,\n"
|
||||
" \"help\": options.containsKey(\"help\") ? options[\"help\"] : \"\",\n"
|
||||
" \"choices\": options.containsKey(\"choices\") ? options[\"choices\"] : null,\n"
|
||||
" \"nargs\": options.containsKey(\"nargs\") ? options[\"nargs\"] : null\n"
|
||||
" }\n"
|
||||
" _positional.add(arg)\n"
|
||||
" }\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" destName_(arg) {\n"
|
||||
" if (arg[\"dest\"]) return arg[\"dest\"]\n"
|
||||
" var name = arg[\"long\"] ? arg[\"long\"] : arg[\"name\"]\n"
|
||||
" while (name.startsWith(\"-\")) name = name[1..-1]\n"
|
||||
" return name.replace(\"-\", \"_\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" parseArgs() { parseArgs(Process.arguments) }\n"
|
||||
"\n"
|
||||
" parseArgs(args) {\n"
|
||||
" var result = {}\n"
|
||||
"\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" var dest = destName_(arg)\n"
|
||||
" if (arg[\"action\"] == \"storeTrue\") {\n"
|
||||
" result[dest] = false\n"
|
||||
" } else if (arg[\"action\"] == \"storeFalse\") {\n"
|
||||
" result[dest] = true\n"
|
||||
" } else if (arg[\"action\"] == \"count\") {\n"
|
||||
" result[dest] = 0\n"
|
||||
" } else if (arg[\"action\"] == \"append\") {\n"
|
||||
" result[dest] = []\n"
|
||||
" } else if (arg[\"default\"] != null) {\n"
|
||||
" result[dest] = arg[\"default\"]\n"
|
||||
" } else {\n"
|
||||
" result[dest] = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" if (arg[\"default\"] != null) {\n"
|
||||
" result[arg[\"name\"]] = arg[\"default\"]\n"
|
||||
" } else {\n"
|
||||
" result[arg[\"name\"]] = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var posIdx = 0\n"
|
||||
" var i = 0\n"
|
||||
" while (i < args.count) {\n"
|
||||
" var token = args[i]\n"
|
||||
"\n"
|
||||
" if (token.startsWith(\"-\")) {\n"
|
||||
" var matchedArg = null\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" if (token == arg[\"name\"] || token == arg[\"long\"]) {\n"
|
||||
" matchedArg = arg\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (matchedArg == null) {\n"
|
||||
" Fiber.abort(\"Unknown option: %(token)\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var dest = destName_(matchedArg)\n"
|
||||
" if (matchedArg[\"action\"] == \"storeTrue\") {\n"
|
||||
" result[dest] = true\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (matchedArg[\"action\"] == \"storeFalse\") {\n"
|
||||
" result[dest] = false\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (matchedArg[\"action\"] == \"count\") {\n"
|
||||
" result[dest] = result[dest] + 1\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (matchedArg[\"action\"] == \"append\") {\n"
|
||||
" if (i + 1 >= args.count) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires a value\")\n"
|
||||
" }\n"
|
||||
" i = i + 1\n"
|
||||
" result[dest].add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" } else {\n"
|
||||
" if (matchedArg[\"nargs\"] == \"*\") {\n"
|
||||
" var values = []\n"
|
||||
" i = i + 1\n"
|
||||
" while (i < args.count && !args[i].startsWith(\"-\")) {\n"
|
||||
" values.add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" result[dest] = values\n"
|
||||
" } else if (matchedArg[\"nargs\"] == \"+\") {\n"
|
||||
" var values = []\n"
|
||||
" i = i + 1\n"
|
||||
" while (i < args.count && !args[i].startsWith(\"-\")) {\n"
|
||||
" values.add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" if (values.count == 0) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires at least one value\")\n"
|
||||
" }\n"
|
||||
" result[dest] = values\n"
|
||||
" } else if (matchedArg[\"nargs\"] is Num) {\n"
|
||||
" var values = []\n"
|
||||
" var n = matchedArg[\"nargs\"]\n"
|
||||
" i = i + 1\n"
|
||||
" for (j in 0...n) {\n"
|
||||
" if (i >= args.count) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires %(n) values\")\n"
|
||||
" }\n"
|
||||
" values.add(convertValue_(args[i], matchedArg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" result[dest] = values\n"
|
||||
" } else {\n"
|
||||
" if (i + 1 >= args.count) {\n"
|
||||
" Fiber.abort(\"Option %(token) requires a value\")\n"
|
||||
" }\n"
|
||||
" i = i + 1\n"
|
||||
" var value = convertValue_(args[i], matchedArg[\"type\"])\n"
|
||||
" if (matchedArg[\"choices\"] && !matchedArg[\"choices\"].contains(value)) {\n"
|
||||
" Fiber.abort(\"Invalid choice '%(value)' for %(token)\")\n"
|
||||
" }\n"
|
||||
" result[dest] = value\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" if (posIdx < _positional.count) {\n"
|
||||
" var arg = _positional[posIdx]\n"
|
||||
" if (arg[\"nargs\"] == \"*\" || arg[\"nargs\"] == \"+\") {\n"
|
||||
" var values = []\n"
|
||||
" while (i < args.count && !args[i].startsWith(\"-\")) {\n"
|
||||
" values.add(convertValue_(args[i], arg[\"type\"]))\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" if (arg[\"nargs\"] == \"+\" && values.count == 0) {\n"
|
||||
" Fiber.abort(\"Argument %(arg[\"name\"]) requires at least one value\")\n"
|
||||
" }\n"
|
||||
" result[arg[\"name\"]] = values\n"
|
||||
" } else {\n"
|
||||
" var value = convertValue_(token, arg[\"type\"])\n"
|
||||
" if (arg[\"choices\"] && !arg[\"choices\"].contains(value)) {\n"
|
||||
" Fiber.abort(\"Invalid choice '%(value)' for %(arg[\"name\"])\")\n"
|
||||
" }\n"
|
||||
" result[arg[\"name\"]] = value\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" posIdx = posIdx + 1\n"
|
||||
" } else {\n"
|
||||
" Fiber.abort(\"Unexpected argument: %(token)\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" if (arg[\"required\"] && result[arg[\"name\"]] == null) {\n"
|
||||
" Fiber.abort(\"Missing required argument: %(arg[\"name\"])\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" var dest = destName_(arg)\n"
|
||||
" if (arg[\"required\"] && result[dest] == null) {\n"
|
||||
" Fiber.abort(\"Missing required option: %(arg[\"name\"])\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" convertValue_(value, type) {\n"
|
||||
" if (type == \"int\") {\n"
|
||||
" return Num.fromString(value)\n"
|
||||
" } else if (type == \"float\") {\n"
|
||||
" return Num.fromString(value)\n"
|
||||
" } else if (type == \"bool\") {\n"
|
||||
" if (value == \"true\" || value == \"1\" || value == \"yes\") return true\n"
|
||||
" if (value == \"false\" || value == \"0\" || value == \"no\") return false\n"
|
||||
" Fiber.abort(\"Invalid boolean value: %(value)\")\n"
|
||||
" }\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" printHelp() {\n"
|
||||
" var prog = _prog ? _prog : \"program\"\n"
|
||||
" System.print(\"usage: %(prog) [options]%(positionalUsage_())\")\n"
|
||||
" if (_description.count > 0) {\n"
|
||||
" System.print(\"\\n%(description)\")\n"
|
||||
" }\n"
|
||||
" if (_positional.count > 0) {\n"
|
||||
" System.print(\"\\npositional arguments:\")\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" var helpText = arg[\"help\"].count > 0 ? arg[\"help\"] : \"\"\n"
|
||||
" System.print(\" %(pad_(arg[\"name\"], 20)) %(helpText)\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (_optional.count > 0) {\n"
|
||||
" System.print(\"\\noptional arguments:\")\n"
|
||||
" for (arg in _optional) {\n"
|
||||
" var names = arg[\"name\"]\n"
|
||||
" if (arg[\"long\"]) names = names + \", \" + arg[\"long\"]\n"
|
||||
" var helpText = arg[\"help\"].count > 0 ? arg[\"help\"] : \"\"\n"
|
||||
" System.print(\" %(pad_(names, 20)) %(helpText)\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" positionalUsage_() {\n"
|
||||
" if (_positional.count == 0) return \"\"\n"
|
||||
" var parts = []\n"
|
||||
" for (arg in _positional) {\n"
|
||||
" if (arg[\"nargs\"] == \"*\") {\n"
|
||||
" parts.add(\"[%(arg[\"name\"]) ...]\")\n"
|
||||
" } else if (arg[\"nargs\"] == \"+\") {\n"
|
||||
" parts.add(\"%(arg[\"name\"]) [...]\")\n"
|
||||
" } else if (arg[\"required\"]) {\n"
|
||||
" parts.add(arg[\"name\"])\n"
|
||||
" } else {\n"
|
||||
" parts.add(\"[%(arg[\"name\"])]\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return \" \" + parts.join(\" \")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" pad_(str, width) {\n"
|
||||
" var result = str\n"
|
||||
" while (result.count < width) result = result + \" \"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
@@ -0,0 +1,180 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include "bytes.h"
|
||||
#include "wren.h"
|
||||
|
||||
void bytesToList(WrenVM* vm) {
|
||||
int length = 0;
|
||||
const char* data = wrenGetSlotBytes(vm, 1, &length);
|
||||
|
||||
if (data == NULL || length == 0) {
|
||||
wrenSetSlotNewList(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
wrenEnsureSlots(vm, 3);
|
||||
wrenSetSlotNewList(vm, 0);
|
||||
|
||||
const uint8_t* bytes = (const uint8_t*)data;
|
||||
for (int i = 0; i < length; i++) {
|
||||
wrenSetSlotDouble(vm, 2, (double)bytes[i]);
|
||||
wrenInsertInList(vm, 0, -1, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void bytesFromList(WrenVM* vm) {
|
||||
if (wrenGetSlotType(vm, 1) != WREN_TYPE_LIST) {
|
||||
wrenSetSlotString(vm, 0, "Argument must be a list.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
int count = wrenGetListCount(vm, 1);
|
||||
if (count == 0) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
char* buffer = (char*)malloc(count);
|
||||
if (buffer == NULL) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
wrenEnsureSlots(vm, 3);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
wrenGetListElement(vm, 1, i, 2);
|
||||
if (wrenGetSlotType(vm, 2) != WREN_TYPE_NUM) {
|
||||
free(buffer);
|
||||
wrenSetSlotString(vm, 0, "List must contain only numbers.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
double value = wrenGetSlotDouble(vm, 2);
|
||||
int byteVal = (int)value;
|
||||
if (byteVal < 0 || byteVal > 255 || value != (double)byteVal) {
|
||||
free(buffer);
|
||||
wrenSetSlotString(vm, 0, "Byte values must be integers 0-255.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
buffer[i] = (char)(uint8_t)byteVal;
|
||||
}
|
||||
|
||||
wrenSetSlotBytes(vm, 0, buffer, count);
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
void bytesXorMask(WrenVM* vm) {
|
||||
int dataLen = 0;
|
||||
const char* data = wrenGetSlotBytes(vm, 1, &dataLen);
|
||||
int maskLen = 0;
|
||||
const char* mask = wrenGetSlotBytes(vm, 2, &maskLen);
|
||||
|
||||
if (mask == NULL || maskLen == 0) {
|
||||
wrenSetSlotString(vm, 0, "Mask cannot be empty.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data == NULL || dataLen == 0) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
char* result = (char*)malloc(dataLen);
|
||||
if (result == NULL) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* src = (const uint8_t*)data;
|
||||
const uint8_t* maskBytes = (const uint8_t*)mask;
|
||||
uint8_t* dst = (uint8_t*)result;
|
||||
|
||||
if (maskLen == 4) {
|
||||
uint8_t m0 = maskBytes[0], m1 = maskBytes[1];
|
||||
uint8_t m2 = maskBytes[2], m3 = maskBytes[3];
|
||||
int i = 0;
|
||||
int fullBlocks = dataLen / 4;
|
||||
for (int block = 0; block < fullBlocks; block++) {
|
||||
dst[i] = src[i] ^ m0;
|
||||
dst[i+1] = src[i+1] ^ m1;
|
||||
dst[i+2] = src[i+2] ^ m2;
|
||||
dst[i+3] = src[i+3] ^ m3;
|
||||
i += 4;
|
||||
}
|
||||
int rem = dataLen % 4;
|
||||
if (rem >= 1) dst[i] = src[i] ^ m0;
|
||||
if (rem >= 2) dst[i+1] = src[i+1] ^ m1;
|
||||
if (rem >= 3) dst[i+2] = src[i+2] ^ m2;
|
||||
} else {
|
||||
for (int i = 0; i < dataLen; i++) {
|
||||
dst[i] = src[i] ^ maskBytes[i % maskLen];
|
||||
}
|
||||
}
|
||||
|
||||
wrenSetSlotBytes(vm, 0, result, dataLen);
|
||||
free(result);
|
||||
}
|
||||
|
||||
void bytesConcat(WrenVM* vm) {
|
||||
int lenA = 0;
|
||||
const char* a = wrenGetSlotBytes(vm, 1, &lenA);
|
||||
int lenB = 0;
|
||||
const char* b = wrenGetSlotBytes(vm, 2, &lenB);
|
||||
|
||||
if (lenA == 0 && lenB == 0) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
if (lenA == 0) {
|
||||
wrenSetSlotBytes(vm, 0, b, lenB);
|
||||
return;
|
||||
}
|
||||
if (lenB == 0) {
|
||||
wrenSetSlotBytes(vm, 0, a, lenA);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t totalLen = (size_t)lenA + (size_t)lenB;
|
||||
char* result = (char*)malloc(totalLen);
|
||||
if (result == NULL) {
|
||||
wrenSetSlotString(vm, 0, "Memory allocation failed.");
|
||||
wrenAbortFiber(vm, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(result, a, lenA);
|
||||
memcpy(result + lenA, b, lenB);
|
||||
wrenSetSlotBytes(vm, 0, result, (int)totalLen);
|
||||
free(result);
|
||||
}
|
||||
|
||||
void bytesSlice(WrenVM* vm) {
|
||||
int length = 0;
|
||||
const char* data = wrenGetSlotBytes(vm, 1, &length);
|
||||
int start = (int)wrenGetSlotDouble(vm, 2);
|
||||
int end = (int)wrenGetSlotDouble(vm, 3);
|
||||
|
||||
if (start < 0) start = 0;
|
||||
if (end > length) end = length;
|
||||
if (start >= end || start >= length) {
|
||||
wrenSetSlotBytes(vm, 0, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
wrenSetSlotBytes(vm, 0, data + start, end - start);
|
||||
}
|
||||
|
||||
void bytesLength(WrenVM* vm) {
|
||||
int length = 0;
|
||||
wrenGetSlotBytes(vm, 1, &length);
|
||||
wrenSetSlotDouble(vm, 0, (double)length);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
#ifndef bytes_h
|
||||
#define bytes_h
|
||||
|
||||
#include "wren.h"
|
||||
|
||||
void bytesToList(WrenVM* vm);
|
||||
void bytesFromList(WrenVM* vm);
|
||||
void bytesXorMask(WrenVM* vm);
|
||||
void bytesConcat(WrenVM* vm);
|
||||
void bytesSlice(WrenVM* vm);
|
||||
void bytesLength(WrenVM* vm);
|
||||
|
||||
#endif
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
class Bytes {
|
||||
foreign static toList(data)
|
||||
foreign static fromList(list)
|
||||
foreign static xorMask(data, mask)
|
||||
foreign static concat(a, b)
|
||||
foreign static slice(data, start, end)
|
||||
foreign static length(data)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/bytes.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* bytesModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"class Bytes {\n"
|
||||
" foreign static toList(data)\n"
|
||||
" foreign static fromList(list)\n"
|
||||
" foreign static xorMask(data, mask)\n"
|
||||
" foreign static concat(a, b)\n"
|
||||
" foreign static slice(data, start, end)\n"
|
||||
" foreign static length(data)\n"
|
||||
"}\n";
|
||||
Vendored
+263
@@ -0,0 +1,263 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "sqlite" for Database
|
||||
import "uuid" for Uuid
|
||||
import "datetime" for DateTime
|
||||
import "json" for Json
|
||||
|
||||
class Dataset {
|
||||
construct open(path) {
|
||||
_db = Database.open(path)
|
||||
_tables = {}
|
||||
}
|
||||
|
||||
construct memory() {
|
||||
_db = Database.memory()
|
||||
_tables = {}
|
||||
}
|
||||
|
||||
db { _db }
|
||||
|
||||
[tableName] {
|
||||
if (!_tables.containsKey(tableName)) {
|
||||
_tables[tableName] = Table.new_(this, tableName)
|
||||
}
|
||||
return _tables[tableName]
|
||||
}
|
||||
|
||||
tables {
|
||||
var rows = _db.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
var result = []
|
||||
for (row in rows) {
|
||||
result.add(row["name"])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
close() { _db.close() }
|
||||
}
|
||||
|
||||
class Table {
|
||||
construct new_(dataset, name) {
|
||||
_dataset = dataset
|
||||
_name = name
|
||||
_columns = null
|
||||
}
|
||||
|
||||
name { _name }
|
||||
db { _dataset.db }
|
||||
|
||||
columns {
|
||||
if (_columns == null) {
|
||||
_columns = {}
|
||||
var rows = db.query("PRAGMA table_info(" + _name + ")")
|
||||
for (row in rows) {
|
||||
_columns[row["name"]] = row["type"]
|
||||
}
|
||||
}
|
||||
return _columns
|
||||
}
|
||||
|
||||
ensureTable_() {
|
||||
var exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [_name])
|
||||
if (exists.count == 0) {
|
||||
db.execute("CREATE TABLE " + _name + " (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)")
|
||||
_columns = null
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn_(colName, value) {
|
||||
ensureTable_()
|
||||
var cols = columns
|
||||
if (!cols.containsKey(colName)) {
|
||||
var sqlType = getSqlType_(value)
|
||||
db.execute("ALTER TABLE " + _name + " ADD COLUMN " + colName + " " + sqlType)
|
||||
_columns = null
|
||||
}
|
||||
}
|
||||
|
||||
getSqlType_(value) {
|
||||
if (value is Num) {
|
||||
if (value == value.floor) return "INTEGER"
|
||||
return "REAL"
|
||||
}
|
||||
if (value is Bool) return "INTEGER"
|
||||
if (value is Map || value is List) return "TEXT"
|
||||
return "TEXT"
|
||||
}
|
||||
|
||||
serializeValue_(value) {
|
||||
if (value is Bool) return value ? 1 : 0
|
||||
if (value is Map || value is List) return Json.stringify(value)
|
||||
return value
|
||||
}
|
||||
|
||||
deserializeRow_(row) {
|
||||
var result = {}
|
||||
for (key in row.keys) {
|
||||
var value = row[key]
|
||||
if (value is String && (value.startsWith("{") || value.startsWith("["))) {
|
||||
var fiber = Fiber.new { Json.parse(value) }
|
||||
var parsed = fiber.try()
|
||||
if (!fiber.error) {
|
||||
result[key] = parsed
|
||||
continue
|
||||
}
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
insert(record) {
|
||||
ensureTable_()
|
||||
var uid = record.containsKey("uid") ? record["uid"] : Uuid.v4()
|
||||
var createdAt = record.containsKey("created_at") ? record["created_at"] : DateTime.now().toString
|
||||
|
||||
var colNames = ["uid", "created_at"]
|
||||
var placeholders = ["?", "?"]
|
||||
var values = [uid, createdAt]
|
||||
|
||||
for (key in record.keys) {
|
||||
if (key == "uid" || key == "created_at" || key == "deleted_at") continue
|
||||
ensureColumn_(key, record[key])
|
||||
colNames.add(key)
|
||||
placeholders.add("?")
|
||||
values.add(serializeValue_(record[key]))
|
||||
}
|
||||
|
||||
var sql = "INSERT INTO " + _name + " (" + colNames.join(", ") + ") VALUES (" + placeholders.join(", ") + ")"
|
||||
db.execute(sql, values)
|
||||
|
||||
var result = {}
|
||||
for (key in record.keys) {
|
||||
result[key] = record[key]
|
||||
}
|
||||
result["uid"] = uid
|
||||
result["created_at"] = createdAt
|
||||
return result
|
||||
}
|
||||
|
||||
update(record) {
|
||||
if (!record.containsKey("uid")) {
|
||||
Fiber.abort("Record must have a uid for update")
|
||||
}
|
||||
var uid = record["uid"]
|
||||
|
||||
var setParts = []
|
||||
var values = []
|
||||
for (key in record.keys) {
|
||||
if (key == "uid") continue
|
||||
ensureColumn_(key, record[key])
|
||||
setParts.add(key + " = ?")
|
||||
values.add(serializeValue_(record[key]))
|
||||
}
|
||||
values.add(uid)
|
||||
|
||||
var sql = "UPDATE " + _name + " SET " + setParts.join(", ") + " WHERE uid = ? AND deleted_at IS NULL"
|
||||
db.execute(sql, values)
|
||||
return db.changes
|
||||
}
|
||||
|
||||
delete(uid) {
|
||||
var sql = "UPDATE " + _name + " SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL"
|
||||
db.execute(sql, [DateTime.now().toString, uid])
|
||||
return db.changes > 0
|
||||
}
|
||||
|
||||
hardDelete(uid) {
|
||||
var sql = "DELETE FROM " + _name + " WHERE uid = ?"
|
||||
db.execute(sql, [uid])
|
||||
return db.changes > 0
|
||||
}
|
||||
|
||||
find(conditions) {
|
||||
ensureTable_()
|
||||
var where = buildWhere_(conditions)
|
||||
var sql = "SELECT * FROM " + _name + " WHERE deleted_at IS NULL" + where["clause"]
|
||||
var rows = db.query(sql, where["values"])
|
||||
var result = []
|
||||
for (row in rows) {
|
||||
result.add(deserializeRow_(row))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
findOne(conditions) {
|
||||
var results = find(conditions)
|
||||
return results.count > 0 ? results[0] : null
|
||||
}
|
||||
|
||||
all() {
|
||||
ensureTable_()
|
||||
var rows = db.query("SELECT * FROM " + _name + " WHERE deleted_at IS NULL")
|
||||
var result = []
|
||||
for (row in rows) {
|
||||
result.add(deserializeRow_(row))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
count() {
|
||||
ensureTable_()
|
||||
var rows = db.query("SELECT COUNT(*) as cnt FROM " + _name + " WHERE deleted_at IS NULL")
|
||||
return rows[0]["cnt"]
|
||||
}
|
||||
|
||||
buildWhere_(conditions) {
|
||||
var parts = []
|
||||
var values = []
|
||||
|
||||
for (key in conditions.keys) {
|
||||
var value = conditions[key]
|
||||
var op = "="
|
||||
var col = key
|
||||
|
||||
if (key.contains("__")) {
|
||||
var split = key.split("__")
|
||||
col = split[0]
|
||||
var suffix = split[1]
|
||||
if (suffix == "gt") {
|
||||
op = ">"
|
||||
} else if (suffix == "lt") {
|
||||
op = "<"
|
||||
} else if (suffix == "gte") {
|
||||
op = ">="
|
||||
} else if (suffix == "lte") {
|
||||
op = "<="
|
||||
} else if (suffix == "ne") {
|
||||
op = "!="
|
||||
} else if (suffix == "like") {
|
||||
op = "LIKE"
|
||||
} else if (suffix == "in") {
|
||||
if (value is List) {
|
||||
var placeholders = []
|
||||
for (v in value) {
|
||||
placeholders.add("?")
|
||||
values.add(v)
|
||||
}
|
||||
parts.add(col + " IN (" + placeholders.join(", ") + ")")
|
||||
continue
|
||||
}
|
||||
} else if (suffix == "null") {
|
||||
if (value) {
|
||||
parts.add(col + " IS NULL")
|
||||
} else {
|
||||
parts.add(col + " IS NOT NULL")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
parts.add(col + " " + op + " ?")
|
||||
values.add(serializeValue_(value))
|
||||
}
|
||||
|
||||
var clause = ""
|
||||
if (parts.count > 0) {
|
||||
clause = " AND " + parts.join(" AND ")
|
||||
}
|
||||
|
||||
return {"clause": clause, "values": values}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/dataset.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* datasetModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"sqlite\" for Database\n"
|
||||
"import \"uuid\" for Uuid\n"
|
||||
"import \"datetime\" for DateTime\n"
|
||||
"import \"json\" for Json\n"
|
||||
"\n"
|
||||
"class Dataset {\n"
|
||||
" construct open(path) {\n"
|
||||
" _db = Database.open(path)\n"
|
||||
" _tables = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" construct memory() {\n"
|
||||
" _db = Database.memory()\n"
|
||||
" _tables = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" db { _db }\n"
|
||||
"\n"
|
||||
" [tableName] {\n"
|
||||
" if (!_tables.containsKey(tableName)) {\n"
|
||||
" _tables[tableName] = Table.new_(this, tableName)\n"
|
||||
" }\n"
|
||||
" return _tables[tableName]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" tables {\n"
|
||||
" var rows = _db.query(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\")\n"
|
||||
" var result = []\n"
|
||||
" for (row in rows) {\n"
|
||||
" result.add(row[\"name\"])\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" close() { _db.close() }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Table {\n"
|
||||
" construct new_(dataset, name) {\n"
|
||||
" _dataset = dataset\n"
|
||||
" _name = name\n"
|
||||
" _columns = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" name { _name }\n"
|
||||
" db { _dataset.db }\n"
|
||||
"\n"
|
||||
" columns {\n"
|
||||
" if (_columns == null) {\n"
|
||||
" _columns = {}\n"
|
||||
" var rows = db.query(\"PRAGMA table_info(\" + _name + \")\")\n"
|
||||
" for (row in rows) {\n"
|
||||
" _columns[row[\"name\"]] = row[\"type\"]\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return _columns\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" ensureTable_() {\n"
|
||||
" var exists = db.query(\"SELECT name FROM sqlite_master WHERE type='table' AND name=?\", [_name])\n"
|
||||
" if (exists.count == 0) {\n"
|
||||
" db.execute(\"CREATE TABLE \" + _name + \" (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)\")\n"
|
||||
" _columns = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" ensureColumn_(colName, value) {\n"
|
||||
" ensureTable_()\n"
|
||||
" var cols = columns\n"
|
||||
" if (!cols.containsKey(colName)) {\n"
|
||||
" var sqlType = getSqlType_(value)\n"
|
||||
" db.execute(\"ALTER TABLE \" + _name + \" ADD COLUMN \" + colName + \" \" + sqlType)\n"
|
||||
" _columns = null\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" getSqlType_(value) {\n"
|
||||
" if (value is Num) {\n"
|
||||
" if (value == value.floor) return \"INTEGER\"\n"
|
||||
" return \"REAL\"\n"
|
||||
" }\n"
|
||||
" if (value is Bool) return \"INTEGER\"\n"
|
||||
" if (value is Map || value is List) return \"TEXT\"\n"
|
||||
" return \"TEXT\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" serializeValue_(value) {\n"
|
||||
" if (value is Bool) return value ? 1 : 0\n"
|
||||
" if (value is Map || value is List) return Json.stringify(value)\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" deserializeRow_(row) {\n"
|
||||
" var result = {}\n"
|
||||
" for (key in row.keys) {\n"
|
||||
" var value = row[key]\n"
|
||||
" if (value is String && (value.startsWith(\"{\") || value.startsWith(\"[\"))) {\n"
|
||||
" var fiber = Fiber.new { Json.parse(value) }\n"
|
||||
" var parsed = fiber.try()\n"
|
||||
" if (!fiber.error) {\n"
|
||||
" result[key] = parsed\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" result[key] = value\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" insert(record) {\n"
|
||||
" ensureTable_()\n"
|
||||
" var uid = record.containsKey(\"uid\") ? record[\"uid\"] : Uuid.v4()\n"
|
||||
" var createdAt = record.containsKey(\"created_at\") ? record[\"created_at\"] : DateTime.now().toString\n"
|
||||
"\n"
|
||||
" var colNames = [\"uid\", \"created_at\"]\n"
|
||||
" var placeholders = [\"?\", \"?\"]\n"
|
||||
" var values = [uid, createdAt]\n"
|
||||
"\n"
|
||||
" for (key in record.keys) {\n"
|
||||
" if (key == \"uid\" || key == \"created_at\" || key == \"deleted_at\") continue\n"
|
||||
" ensureColumn_(key, record[key])\n"
|
||||
" colNames.add(key)\n"
|
||||
" placeholders.add(\"?\")\n"
|
||||
" values.add(serializeValue_(record[key]))\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var sql = \"INSERT INTO \" + _name + \" (\" + colNames.join(\", \") + \") VALUES (\" + placeholders.join(\", \") + \")\"\n"
|
||||
" db.execute(sql, values)\n"
|
||||
"\n"
|
||||
" var result = {}\n"
|
||||
" for (key in record.keys) {\n"
|
||||
" result[key] = record[key]\n"
|
||||
" }\n"
|
||||
" result[\"uid\"] = uid\n"
|
||||
" result[\"created_at\"] = createdAt\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" update(record) {\n"
|
||||
" if (!record.containsKey(\"uid\")) {\n"
|
||||
" Fiber.abort(\"Record must have a uid for update\")\n"
|
||||
" }\n"
|
||||
" var uid = record[\"uid\"]\n"
|
||||
"\n"
|
||||
" var setParts = []\n"
|
||||
" var values = []\n"
|
||||
" for (key in record.keys) {\n"
|
||||
" if (key == \"uid\") continue\n"
|
||||
" ensureColumn_(key, record[key])\n"
|
||||
" setParts.add(key + \" = ?\")\n"
|
||||
" values.add(serializeValue_(record[key]))\n"
|
||||
" }\n"
|
||||
" values.add(uid)\n"
|
||||
"\n"
|
||||
" var sql = \"UPDATE \" + _name + \" SET \" + setParts.join(\", \") + \" WHERE uid = ? AND deleted_at IS NULL\"\n"
|
||||
" db.execute(sql, values)\n"
|
||||
" return db.changes\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" delete(uid) {\n"
|
||||
" var sql = \"UPDATE \" + _name + \" SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL\"\n"
|
||||
" db.execute(sql, [DateTime.now().toString, uid])\n"
|
||||
" return db.changes > 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" hardDelete(uid) {\n"
|
||||
" var sql = \"DELETE FROM \" + _name + \" WHERE uid = ?\"\n"
|
||||
" db.execute(sql, [uid])\n"
|
||||
" return db.changes > 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" find(conditions) {\n"
|
||||
" ensureTable_()\n"
|
||||
" var where = buildWhere_(conditions)\n"
|
||||
" var sql = \"SELECT * FROM \" + _name + \" WHERE deleted_at IS NULL\" + where[\"clause\"]\n"
|
||||
" var rows = db.query(sql, where[\"values\"])\n"
|
||||
" var result = []\n"
|
||||
" for (row in rows) {\n"
|
||||
" result.add(deserializeRow_(row))\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" findOne(conditions) {\n"
|
||||
" var results = find(conditions)\n"
|
||||
" return results.count > 0 ? results[0] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" all() {\n"
|
||||
" ensureTable_()\n"
|
||||
" var rows = db.query(\"SELECT * FROM \" + _name + \" WHERE deleted_at IS NULL\")\n"
|
||||
" var result = []\n"
|
||||
" for (row in rows) {\n"
|
||||
" result.add(deserializeRow_(row))\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" count() {\n"
|
||||
" ensureTable_()\n"
|
||||
" var rows = db.query(\"SELECT COUNT(*) as cnt FROM \" + _name + \" WHERE deleted_at IS NULL\")\n"
|
||||
" return rows[0][\"cnt\"]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" buildWhere_(conditions) {\n"
|
||||
" var parts = []\n"
|
||||
" var values = []\n"
|
||||
"\n"
|
||||
" for (key in conditions.keys) {\n"
|
||||
" var value = conditions[key]\n"
|
||||
" var op = \"=\"\n"
|
||||
" var col = key\n"
|
||||
"\n"
|
||||
" if (key.contains(\"__\")) {\n"
|
||||
" var split = key.split(\"__\")\n"
|
||||
" col = split[0]\n"
|
||||
" var suffix = split[1]\n"
|
||||
" if (suffix == \"gt\") {\n"
|
||||
" op = \">\"\n"
|
||||
" } else if (suffix == \"lt\") {\n"
|
||||
" op = \"<\"\n"
|
||||
" } else if (suffix == \"gte\") {\n"
|
||||
" op = \">=\"\n"
|
||||
" } else if (suffix == \"lte\") {\n"
|
||||
" op = \"<=\"\n"
|
||||
" } else if (suffix == \"ne\") {\n"
|
||||
" op = \"!=\"\n"
|
||||
" } else if (suffix == \"like\") {\n"
|
||||
" op = \"LIKE\"\n"
|
||||
" } else if (suffix == \"in\") {\n"
|
||||
" if (value is List) {\n"
|
||||
" var placeholders = []\n"
|
||||
" for (v in value) {\n"
|
||||
" placeholders.add(\"?\")\n"
|
||||
" values.add(v)\n"
|
||||
" }\n"
|
||||
" parts.add(col + \" IN (\" + placeholders.join(\", \") + \")\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" } else if (suffix == \"null\") {\n"
|
||||
" if (value) {\n"
|
||||
" parts.add(col + \" IS NULL\")\n"
|
||||
" } else {\n"
|
||||
" parts.add(col + \" IS NOT NULL\")\n"
|
||||
" }\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" parts.add(col + \" \" + op + \" ?\")\n"
|
||||
" values.add(serializeValue_(value))\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var clause = \"\"\n"
|
||||
" if (parts.count > 0) {\n"
|
||||
" clause = \" AND \" + parts.join(\" AND \")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return {\"clause\": clause, \"values\": values}\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
class Html {
|
||||
static isUnreserved_(c) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (code >= 65 && code <= 90) return true
|
||||
if (code >= 97 && code <= 122) return true
|
||||
if (code >= 48 && code <= 57) return true
|
||||
if (c == "-" || c == "_" || c == "." || c == "~") return true
|
||||
return false
|
||||
}
|
||||
|
||||
static hexDigit_(n) {
|
||||
if (n < 10) return String.fromCodePoint(48 + n)
|
||||
return String.fromCodePoint(65 + n - 10)
|
||||
}
|
||||
|
||||
static hexValue_(c) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (code >= 48 && code <= 57) return code - 48
|
||||
if (code >= 65 && code <= 70) return code - 55
|
||||
if (code >= 97 && code <= 102) return code - 87
|
||||
return -1
|
||||
}
|
||||
|
||||
static urlencode(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var result = ""
|
||||
for (c in string) {
|
||||
if (isUnreserved_(c)) {
|
||||
result = result + c
|
||||
} else if (c == " ") {
|
||||
result = result + "+"
|
||||
} else {
|
||||
for (b in c.bytes) {
|
||||
result = result + "\%" + hexDigit_((b >> 4) & 0x0F) + hexDigit_(b & 0x0F)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static decodeUtf8_(bytes) {
|
||||
var result = ""
|
||||
var i = 0
|
||||
while (i < bytes.count) {
|
||||
var b = bytes[i]
|
||||
if (b < 128) {
|
||||
result = result + String.fromCodePoint(b)
|
||||
i = i + 1
|
||||
} else if ((b & 0xE0) == 0xC0 && i + 1 < bytes.count) {
|
||||
var cp = ((b & 0x1F) << 6) | (bytes[i + 1] & 0x3F)
|
||||
result = result + String.fromCodePoint(cp)
|
||||
i = i + 2
|
||||
} else if ((b & 0xF0) == 0xE0 && i + 2 < bytes.count) {
|
||||
var cp = ((b & 0x0F) << 12) | ((bytes[i + 1] & 0x3F) << 6) | (bytes[i + 2] & 0x3F)
|
||||
result = result + String.fromCodePoint(cp)
|
||||
i = i + 3
|
||||
} else if ((b & 0xF8) == 0xF0 && i + 3 < bytes.count) {
|
||||
var cp = ((b & 0x07) << 18) | ((bytes[i + 1] & 0x3F) << 12) | ((bytes[i + 2] & 0x3F) << 6) | (bytes[i + 3] & 0x3F)
|
||||
result = result + String.fromCodePoint(cp)
|
||||
i = i + 4
|
||||
} else {
|
||||
result = result + String.fromCodePoint(0xFFFD)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static urldecode(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var bytes = []
|
||||
var i = 0
|
||||
var chars = string.toList
|
||||
while (i < chars.count) {
|
||||
if (chars[i] == "+") {
|
||||
bytes.add(32)
|
||||
i = i + 1
|
||||
} else if (chars[i] == "\%" && i + 2 < chars.count) {
|
||||
var hi = hexValue_(chars[i + 1])
|
||||
var lo = hexValue_(chars[i + 2])
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
bytes.add((hi << 4) | lo)
|
||||
i = i + 3
|
||||
} else {
|
||||
for (b in chars[i].bytes) bytes.add(b)
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
for (b in chars[i].bytes) bytes.add(b)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return decodeUtf8_(bytes)
|
||||
}
|
||||
|
||||
static slugify(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var result = ""
|
||||
var prevHyphen = true
|
||||
for (c in string.bytes) {
|
||||
if ((c >= 65 && c <= 90)) {
|
||||
result = result + String.fromCodePoint(c + 32)
|
||||
prevHyphen = false
|
||||
} else if ((c >= 97 && c <= 122) || (c >= 48 && c <= 57)) {
|
||||
result = result + String.fromCodePoint(c)
|
||||
prevHyphen = false
|
||||
} else if (!prevHyphen) {
|
||||
result = result + "-"
|
||||
prevHyphen = true
|
||||
}
|
||||
}
|
||||
if (result.count > 0 && result[-1] == "-") {
|
||||
result = result[0..-2]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static quote(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var result = ""
|
||||
for (c in string) {
|
||||
if (c == "&") {
|
||||
result = result + "&"
|
||||
} else if (c == "<") {
|
||||
result = result + "<"
|
||||
} else if (c == ">") {
|
||||
result = result + ">"
|
||||
} else if (c == "\"") {
|
||||
result = result + """
|
||||
} else if (c == "'") {
|
||||
result = result + "'"
|
||||
} else {
|
||||
result = result + c
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static unquote(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
return string
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
}
|
||||
|
||||
static encodeParams(params) {
|
||||
if (!(params is Map)) Fiber.abort("Argument must be a map.")
|
||||
var parts = []
|
||||
for (key in params.keys) {
|
||||
parts.add(urlencode(key.toString) + "=" + urlencode(params[key].toString))
|
||||
}
|
||||
return parts.join("&")
|
||||
}
|
||||
|
||||
static decodeParams(string) {
|
||||
if (!(string is String)) Fiber.abort("Argument must be a string.")
|
||||
var params = {}
|
||||
if (string.count == 0) return params
|
||||
var pairs = string.split("&")
|
||||
for (pair in pairs) {
|
||||
var idx = pair.indexOf("=")
|
||||
if (idx > 0) {
|
||||
var key = urldecode(pair[0...idx])
|
||||
var value = urldecode(pair[idx + 1..-1])
|
||||
params[key] = value
|
||||
} else if (pair.count > 0) {
|
||||
params[urldecode(pair)] = ""
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/html.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* htmlModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"class Html {\n"
|
||||
" static isUnreserved_(c) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (code >= 65 && code <= 90) return true\n"
|
||||
" if (code >= 97 && code <= 122) return true\n"
|
||||
" if (code >= 48 && code <= 57) return true\n"
|
||||
" if (c == \"-\" || c == \"_\" || c == \".\" || c == \"~\") return true\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static hexDigit_(n) {\n"
|
||||
" if (n < 10) return String.fromCodePoint(48 + n)\n"
|
||||
" return String.fromCodePoint(65 + n - 10)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static hexValue_(c) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (code >= 48 && code <= 57) return code - 48\n"
|
||||
" if (code >= 65 && code <= 70) return code - 55\n"
|
||||
" if (code >= 97 && code <= 102) return code - 87\n"
|
||||
" return -1\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static urlencode(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var result = \"\"\n"
|
||||
" for (c in string) {\n"
|
||||
" if (isUnreserved_(c)) {\n"
|
||||
" result = result + c\n"
|
||||
" } else if (c == \" \") {\n"
|
||||
" result = result + \"+\"\n"
|
||||
" } else {\n"
|
||||
" for (b in c.bytes) {\n"
|
||||
" result = result + \"\\%\" + hexDigit_((b >> 4) & 0x0F) + hexDigit_(b & 0x0F)\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static decodeUtf8_(bytes) {\n"
|
||||
" var result = \"\"\n"
|
||||
" var i = 0\n"
|
||||
" while (i < bytes.count) {\n"
|
||||
" var b = bytes[i]\n"
|
||||
" if (b < 128) {\n"
|
||||
" result = result + String.fromCodePoint(b)\n"
|
||||
" i = i + 1\n"
|
||||
" } else if ((b & 0xE0) == 0xC0 && i + 1 < bytes.count) {\n"
|
||||
" var cp = ((b & 0x1F) << 6) | (bytes[i + 1] & 0x3F)\n"
|
||||
" result = result + String.fromCodePoint(cp)\n"
|
||||
" i = i + 2\n"
|
||||
" } else if ((b & 0xF0) == 0xE0 && i + 2 < bytes.count) {\n"
|
||||
" var cp = ((b & 0x0F) << 12) | ((bytes[i + 1] & 0x3F) << 6) | (bytes[i + 2] & 0x3F)\n"
|
||||
" result = result + String.fromCodePoint(cp)\n"
|
||||
" i = i + 3\n"
|
||||
" } else if ((b & 0xF8) == 0xF0 && i + 3 < bytes.count) {\n"
|
||||
" var cp = ((b & 0x07) << 18) | ((bytes[i + 1] & 0x3F) << 12) | ((bytes[i + 2] & 0x3F) << 6) | (bytes[i + 3] & 0x3F)\n"
|
||||
" result = result + String.fromCodePoint(cp)\n"
|
||||
" i = i + 4\n"
|
||||
" } else {\n"
|
||||
" result = result + String.fromCodePoint(0xFFFD)\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static urldecode(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var bytes = []\n"
|
||||
" var i = 0\n"
|
||||
" var chars = string.toList\n"
|
||||
" while (i < chars.count) {\n"
|
||||
" if (chars[i] == \"+\") {\n"
|
||||
" bytes.add(32)\n"
|
||||
" i = i + 1\n"
|
||||
" } else if (chars[i] == \"\\%\" && i + 2 < chars.count) {\n"
|
||||
" var hi = hexValue_(chars[i + 1])\n"
|
||||
" var lo = hexValue_(chars[i + 2])\n"
|
||||
" if (hi >= 0 && lo >= 0) {\n"
|
||||
" bytes.add((hi << 4) | lo)\n"
|
||||
" i = i + 3\n"
|
||||
" } else {\n"
|
||||
" for (b in chars[i].bytes) bytes.add(b)\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" for (b in chars[i].bytes) bytes.add(b)\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return decodeUtf8_(bytes)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static slugify(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var result = \"\"\n"
|
||||
" var prevHyphen = true\n"
|
||||
" for (c in string.bytes) {\n"
|
||||
" if ((c >= 65 && c <= 90)) {\n"
|
||||
" result = result + String.fromCodePoint(c + 32)\n"
|
||||
" prevHyphen = false\n"
|
||||
" } else if ((c >= 97 && c <= 122) || (c >= 48 && c <= 57)) {\n"
|
||||
" result = result + String.fromCodePoint(c)\n"
|
||||
" prevHyphen = false\n"
|
||||
" } else if (!prevHyphen) {\n"
|
||||
" result = result + \"-\"\n"
|
||||
" prevHyphen = true\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (result.count > 0 && result[-1] == \"-\") {\n"
|
||||
" result = result[0..-2]\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static quote(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var result = \"\"\n"
|
||||
" for (c in string) {\n"
|
||||
" if (c == \"&\") {\n"
|
||||
" result = result + \"&\"\n"
|
||||
" } else if (c == \"<\") {\n"
|
||||
" result = result + \"<\"\n"
|
||||
" } else if (c == \">\") {\n"
|
||||
" result = result + \">\"\n"
|
||||
" } else if (c == \"\\\"\") {\n"
|
||||
" result = result + \""\"\n"
|
||||
" } else if (c == \"'\") {\n"
|
||||
" result = result + \"'\"\n"
|
||||
" } else {\n"
|
||||
" result = result + c\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static unquote(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" return string\n"
|
||||
" .replace(\"&\", \"&\")\n"
|
||||
" .replace(\"<\", \"<\")\n"
|
||||
" .replace(\">\", \">\")\n"
|
||||
" .replace(\""\", \"\\\"\")\n"
|
||||
" .replace(\"'\", \"'\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static encodeParams(params) {\n"
|
||||
" if (!(params is Map)) Fiber.abort(\"Argument must be a map.\")\n"
|
||||
" var parts = []\n"
|
||||
" for (key in params.keys) {\n"
|
||||
" parts.add(urlencode(key.toString) + \"=\" + urlencode(params[key].toString))\n"
|
||||
" }\n"
|
||||
" return parts.join(\"&\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static decodeParams(string) {\n"
|
||||
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
|
||||
" var params = {}\n"
|
||||
" if (string.count == 0) return params\n"
|
||||
" var pairs = string.split(\"&\")\n"
|
||||
" for (pair in pairs) {\n"
|
||||
" var idx = pair.indexOf(\"=\")\n"
|
||||
" if (idx > 0) {\n"
|
||||
" var key = urldecode(pair[0...idx])\n"
|
||||
" var value = urldecode(pair[idx + 1..-1])\n"
|
||||
" params[key] = value\n"
|
||||
" } else if (pair.count > 0) {\n"
|
||||
" params[urldecode(pair)] = \"\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return params\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+328
@@ -0,0 +1,328 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "html" for Html
|
||||
|
||||
class Markdown {
|
||||
static toHtml(text) { toHtml(text, {}) }
|
||||
|
||||
static toHtml(text, options) {
|
||||
var safeMode = options.containsKey("safeMode") ? options["safeMode"] : false
|
||||
var lines = text.split("\n")
|
||||
var result = []
|
||||
var inCodeBlock = false
|
||||
var codeBlockContent = []
|
||||
var inList = false
|
||||
var listType = null
|
||||
var inBlockquote = false
|
||||
|
||||
for (i in 0...lines.count) {
|
||||
var line = lines[i]
|
||||
|
||||
if (line.startsWith("```")) {
|
||||
if (inCodeBlock) {
|
||||
result.add("<pre><code>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
|
||||
codeBlockContent = []
|
||||
inCodeBlock = false
|
||||
} else {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
inCodeBlock = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.add(line)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.count == 0) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
if (inBlockquote) {
|
||||
result.add("</blockquote>")
|
||||
inBlockquote = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith("> ")) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
if (!inBlockquote) {
|
||||
result.add("<blockquote>")
|
||||
inBlockquote = true
|
||||
}
|
||||
result.add("<p>" + processInline_(line[2..-1], safeMode) + "</p>")
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith("---") || line.startsWith("***") || line.startsWith("___")) {
|
||||
var isHr = true
|
||||
for (c in line) {
|
||||
if (c != "-" && c != "*" && c != "_" && c != " ") {
|
||||
isHr = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isHr && line.count >= 3) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
result.add("<hr>")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var heading = parseHeading_(line)
|
||||
if (heading) {
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
result.add("<h%(heading["level"])>" + processInline_(heading["text"], safeMode) + "</h%(heading["level"])>")
|
||||
continue
|
||||
}
|
||||
|
||||
var listItem = parseListItem_(line)
|
||||
if (listItem) {
|
||||
var newType = listItem["type"]
|
||||
if (!inList || listType != newType) {
|
||||
closeList_(result, inList, listType)
|
||||
result.add(newType == "ul" ? "<ul>" : "<ol>")
|
||||
inList = true
|
||||
listType = newType
|
||||
}
|
||||
result.add("<li>" + processInline_(listItem["text"], safeMode) + "</li>")
|
||||
continue
|
||||
}
|
||||
|
||||
closeList_(result, inList, listType)
|
||||
inList = false
|
||||
result.add("<p>" + processInline_(line, safeMode) + "</p>")
|
||||
}
|
||||
|
||||
closeList_(result, inList, listType)
|
||||
if (inBlockquote) {
|
||||
result.add("</blockquote>")
|
||||
}
|
||||
if (inCodeBlock) {
|
||||
result.add("<pre><code>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
|
||||
}
|
||||
|
||||
return result.join("\n")
|
||||
}
|
||||
|
||||
static closeList_(result, inList, listType) {
|
||||
if (inList) {
|
||||
result.add(listType == "ul" ? "</ul>" : "</ol>")
|
||||
}
|
||||
}
|
||||
|
||||
static parseHeading_(line) {
|
||||
var level = 0
|
||||
for (c in line) {
|
||||
if (c == "#") {
|
||||
level = level + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (level > 0 && level <= 6 && line.count > level && line[level] == " ") {
|
||||
return {"level": level, "text": line[level + 1..-1]}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static parseListItem_(line) {
|
||||
var trimmed = line
|
||||
var indent = 0
|
||||
while (trimmed.count > 0 && trimmed[0] == " ") {
|
||||
trimmed = trimmed[1..-1]
|
||||
indent = indent + 1
|
||||
}
|
||||
|
||||
if (trimmed.count >= 2 && (trimmed[0] == "-" || trimmed[0] == "*" || trimmed[0] == "+") && trimmed[1] == " ") {
|
||||
return {"type": "ul", "text": trimmed[2..-1]}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
while (i < trimmed.count) {
|
||||
var c = trimmed[i].codePoints.toList[0]
|
||||
if (c < 48 || c > 57) break
|
||||
i = i + 1
|
||||
}
|
||||
if (i > 0 && i < trimmed.count - 1 && trimmed[i] == "." && trimmed[i + 1] == " ") {
|
||||
return {"type": "ol", "text": trimmed[i + 2..-1]}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
static processInline_(text, safeMode) {
|
||||
if (safeMode) text = Html.quote(text)
|
||||
text = processCode_(text)
|
||||
text = processBold_(text)
|
||||
text = processItalic_(text)
|
||||
text = processStrikethrough_(text)
|
||||
text = processImages_(text)
|
||||
text = processLinks_(text)
|
||||
return text
|
||||
}
|
||||
|
||||
static processCode_(text) {
|
||||
var result = ""
|
||||
var i = 0
|
||||
var chars = text.toList
|
||||
while (i < chars.count) {
|
||||
if (chars[i] == "`") {
|
||||
var end = i + 1
|
||||
while (end < chars.count && chars[end] != "`") {
|
||||
end = end + 1
|
||||
}
|
||||
if (end < chars.count) {
|
||||
var code = ""
|
||||
for (j in (i + 1)...end) {
|
||||
code = code + chars[j]
|
||||
}
|
||||
result = result + "<code>" + code + "</code>"
|
||||
i = end + 1
|
||||
} else {
|
||||
result = result + chars[i]
|
||||
i = i + 1
|
||||
}
|
||||
} else {
|
||||
result = result + chars[i]
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processBold_(text) {
|
||||
var result = text
|
||||
while (result.contains("**")) {
|
||||
var start = result.indexOf("**")
|
||||
var rest = result[start + 2..-1]
|
||||
var end = rest.indexOf("**")
|
||||
if (end >= 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 2..-1]
|
||||
result = before + "<strong>" + content + "</strong>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
while (result.contains("__")) {
|
||||
var start = result.indexOf("__")
|
||||
var rest = result[start + 2..-1]
|
||||
var end = rest.indexOf("__")
|
||||
if (end >= 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 2..-1]
|
||||
result = before + "<strong>" + content + "</strong>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processItalic_(text) {
|
||||
var result = text
|
||||
while (result.contains("*")) {
|
||||
var start = result.indexOf("*")
|
||||
if (start > 0 && result[start - 1] == "<") {
|
||||
break
|
||||
}
|
||||
var rest = result[start + 1..-1]
|
||||
var end = rest.indexOf("*")
|
||||
if (end >= 0 && end > 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 1..-1]
|
||||
result = before + "<em>" + content + "</em>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processStrikethrough_(text) {
|
||||
var result = text
|
||||
while (result.contains("~~")) {
|
||||
var start = result.indexOf("~~")
|
||||
var rest = result[start + 2..-1]
|
||||
var end = rest.indexOf("~~")
|
||||
if (end >= 0) {
|
||||
var before = result[0...start]
|
||||
var content = rest[0...end]
|
||||
var after = rest[end + 2..-1]
|
||||
result = before + "<del>" + content + "</del>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processLinks_(text) {
|
||||
var result = text
|
||||
while (result.contains("](")) {
|
||||
var linkEnd = result.indexOf("](")
|
||||
var textStart = linkEnd
|
||||
while (textStart > 0 && result[textStart - 1] != "[") {
|
||||
textStart = textStart - 1
|
||||
}
|
||||
if (textStart > 0 && result[textStart - 1] == "[") {
|
||||
var urlStart = linkEnd + 2
|
||||
var urlEnd = urlStart
|
||||
while (urlEnd < result.count && result[urlEnd] != ")") {
|
||||
urlEnd = urlEnd + 1
|
||||
}
|
||||
if (urlEnd < result.count) {
|
||||
var before = result[0...textStart - 1]
|
||||
var linkText = result[textStart...linkEnd]
|
||||
var url = result[urlStart...urlEnd]
|
||||
var after = result[urlEnd + 1..-1]
|
||||
result = before + "<a href=\"" + url + "\">" + linkText + "</a>" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static processImages_(text) {
|
||||
var result = text
|
||||
while (result.contains("![")) {
|
||||
var start = result.indexOf("![")
|
||||
var altEnd = start + 2
|
||||
while (altEnd < result.count && result[altEnd] != "]") {
|
||||
altEnd = altEnd + 1
|
||||
}
|
||||
if (altEnd < result.count && altEnd + 1 < result.count && result[altEnd + 1] == "(") {
|
||||
var urlStart = altEnd + 2
|
||||
var urlEnd = urlStart
|
||||
while (urlEnd < result.count && result[urlEnd] != ")") {
|
||||
urlEnd = urlEnd + 1
|
||||
}
|
||||
if (urlEnd < result.count) {
|
||||
var before = result[0...start]
|
||||
var alt = result[start + 2...altEnd]
|
||||
var url = result[urlStart...urlEnd]
|
||||
var after = result[urlEnd + 1..-1]
|
||||
result = before + "<img src=\"" + url + "\" alt=\"" + alt + "\">" + after
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/markdown.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* markdownModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"html\" for Html\n"
|
||||
"\n"
|
||||
"class Markdown {\n"
|
||||
" static toHtml(text) { toHtml(text, {}) }\n"
|
||||
"\n"
|
||||
" static toHtml(text, options) {\n"
|
||||
" var safeMode = options.containsKey(\"safeMode\") ? options[\"safeMode\"] : false\n"
|
||||
" var lines = text.split(\"\\n\")\n"
|
||||
" var result = []\n"
|
||||
" var inCodeBlock = false\n"
|
||||
" var codeBlockContent = []\n"
|
||||
" var inList = false\n"
|
||||
" var listType = null\n"
|
||||
" var inBlockquote = false\n"
|
||||
"\n"
|
||||
" for (i in 0...lines.count) {\n"
|
||||
" var line = lines[i]\n"
|
||||
"\n"
|
||||
" if (line.startsWith(\"```\")) {\n"
|
||||
" if (inCodeBlock) {\n"
|
||||
" result.add(\"<pre><code>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
|
||||
" codeBlockContent = []\n"
|
||||
" inCodeBlock = false\n"
|
||||
" } else {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" inCodeBlock = true\n"
|
||||
" }\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (inCodeBlock) {\n"
|
||||
" codeBlockContent.add(line)\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (line.count == 0) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" if (inBlockquote) {\n"
|
||||
" result.add(\"</blockquote>\")\n"
|
||||
" inBlockquote = false\n"
|
||||
" }\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (line.startsWith(\"> \")) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" if (!inBlockquote) {\n"
|
||||
" result.add(\"<blockquote>\")\n"
|
||||
" inBlockquote = true\n"
|
||||
" }\n"
|
||||
" result.add(\"<p>\" + processInline_(line[2..-1], safeMode) + \"</p>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (line.startsWith(\"---\") || line.startsWith(\"***\") || line.startsWith(\"___\")) {\n"
|
||||
" var isHr = true\n"
|
||||
" for (c in line) {\n"
|
||||
" if (c != \"-\" && c != \"*\" && c != \"_\" && c != \" \") {\n"
|
||||
" isHr = false\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (isHr && line.count >= 3) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" result.add(\"<hr>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var heading = parseHeading_(line)\n"
|
||||
" if (heading) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" result.add(\"<h%(heading[\"level\"])>\" + processInline_(heading[\"text\"], safeMode) + \"</h%(heading[\"level\"])>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var listItem = parseListItem_(line)\n"
|
||||
" if (listItem) {\n"
|
||||
" var newType = listItem[\"type\"]\n"
|
||||
" if (!inList || listType != newType) {\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" result.add(newType == \"ul\" ? \"<ul>\" : \"<ol>\")\n"
|
||||
" inList = true\n"
|
||||
" listType = newType\n"
|
||||
" }\n"
|
||||
" result.add(\"<li>\" + processInline_(listItem[\"text\"], safeMode) + \"</li>\")\n"
|
||||
" continue\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" inList = false\n"
|
||||
" result.add(\"<p>\" + processInline_(line, safeMode) + \"</p>\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" closeList_(result, inList, listType)\n"
|
||||
" if (inBlockquote) {\n"
|
||||
" result.add(\"</blockquote>\")\n"
|
||||
" }\n"
|
||||
" if (inCodeBlock) {\n"
|
||||
" result.add(\"<pre><code>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return result.join(\"\\n\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static closeList_(result, inList, listType) {\n"
|
||||
" if (inList) {\n"
|
||||
" result.add(listType == \"ul\" ? \"</ul>\" : \"</ol>\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseHeading_(line) {\n"
|
||||
" var level = 0\n"
|
||||
" for (c in line) {\n"
|
||||
" if (c == \"#\") {\n"
|
||||
" level = level + 1\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (level > 0 && level <= 6 && line.count > level && line[level] == \" \") {\n"
|
||||
" return {\"level\": level, \"text\": line[level + 1..-1]}\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseListItem_(line) {\n"
|
||||
" var trimmed = line\n"
|
||||
" var indent = 0\n"
|
||||
" while (trimmed.count > 0 && trimmed[0] == \" \") {\n"
|
||||
" trimmed = trimmed[1..-1]\n"
|
||||
" indent = indent + 1\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (trimmed.count >= 2 && (trimmed[0] == \"-\" || trimmed[0] == \"*\" || trimmed[0] == \"+\") && trimmed[1] == \" \") {\n"
|
||||
" return {\"type\": \"ul\", \"text\": trimmed[2..-1]}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var i = 0\n"
|
||||
" while (i < trimmed.count) {\n"
|
||||
" var c = trimmed[i].codePoints.toList[0]\n"
|
||||
" if (c < 48 || c > 57) break\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" if (i > 0 && i < trimmed.count - 1 && trimmed[i] == \".\" && trimmed[i + 1] == \" \") {\n"
|
||||
" return {\"type\": \"ol\", \"text\": trimmed[i + 2..-1]}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processInline_(text, safeMode) {\n"
|
||||
" if (safeMode) text = Html.quote(text)\n"
|
||||
" text = processCode_(text)\n"
|
||||
" text = processBold_(text)\n"
|
||||
" text = processItalic_(text)\n"
|
||||
" text = processStrikethrough_(text)\n"
|
||||
" text = processImages_(text)\n"
|
||||
" text = processLinks_(text)\n"
|
||||
" return text\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processCode_(text) {\n"
|
||||
" var result = \"\"\n"
|
||||
" var i = 0\n"
|
||||
" var chars = text.toList\n"
|
||||
" while (i < chars.count) {\n"
|
||||
" if (chars[i] == \"`\") {\n"
|
||||
" var end = i + 1\n"
|
||||
" while (end < chars.count && chars[end] != \"`\") {\n"
|
||||
" end = end + 1\n"
|
||||
" }\n"
|
||||
" if (end < chars.count) {\n"
|
||||
" var code = \"\"\n"
|
||||
" for (j in (i + 1)...end) {\n"
|
||||
" code = code + chars[j]\n"
|
||||
" }\n"
|
||||
" result = result + \"<code>\" + code + \"</code>\"\n"
|
||||
" i = end + 1\n"
|
||||
" } else {\n"
|
||||
" result = result + chars[i]\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" result = result + chars[i]\n"
|
||||
" i = i + 1\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processBold_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"**\")) {\n"
|
||||
" var start = result.indexOf(\"**\")\n"
|
||||
" var rest = result[start + 2..-1]\n"
|
||||
" var end = rest.indexOf(\"**\")\n"
|
||||
" if (end >= 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 2..-1]\n"
|
||||
" result = before + \"<strong>\" + content + \"</strong>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" while (result.contains(\"__\")) {\n"
|
||||
" var start = result.indexOf(\"__\")\n"
|
||||
" var rest = result[start + 2..-1]\n"
|
||||
" var end = rest.indexOf(\"__\")\n"
|
||||
" if (end >= 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 2..-1]\n"
|
||||
" result = before + \"<strong>\" + content + \"</strong>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processItalic_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"*\")) {\n"
|
||||
" var start = result.indexOf(\"*\")\n"
|
||||
" if (start > 0 && result[start - 1] == \"<\") {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" var rest = result[start + 1..-1]\n"
|
||||
" var end = rest.indexOf(\"*\")\n"
|
||||
" if (end >= 0 && end > 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 1..-1]\n"
|
||||
" result = before + \"<em>\" + content + \"</em>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processStrikethrough_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"~~\")) {\n"
|
||||
" var start = result.indexOf(\"~~\")\n"
|
||||
" var rest = result[start + 2..-1]\n"
|
||||
" var end = rest.indexOf(\"~~\")\n"
|
||||
" if (end >= 0) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var content = rest[0...end]\n"
|
||||
" var after = rest[end + 2..-1]\n"
|
||||
" result = before + \"<del>\" + content + \"</del>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processLinks_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"](\")) {\n"
|
||||
" var linkEnd = result.indexOf(\"](\")\n"
|
||||
" var textStart = linkEnd\n"
|
||||
" while (textStart > 0 && result[textStart - 1] != \"[\") {\n"
|
||||
" textStart = textStart - 1\n"
|
||||
" }\n"
|
||||
" if (textStart > 0 && result[textStart - 1] == \"[\") {\n"
|
||||
" var urlStart = linkEnd + 2\n"
|
||||
" var urlEnd = urlStart\n"
|
||||
" while (urlEnd < result.count && result[urlEnd] != \")\") {\n"
|
||||
" urlEnd = urlEnd + 1\n"
|
||||
" }\n"
|
||||
" if (urlEnd < result.count) {\n"
|
||||
" var before = result[0...textStart - 1]\n"
|
||||
" var linkText = result[textStart...linkEnd]\n"
|
||||
" var url = result[urlStart...urlEnd]\n"
|
||||
" var after = result[urlEnd + 1..-1]\n"
|
||||
" result = before + \"<a href=\\\"\" + url + \"\\\">\" + linkText + \"</a>\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static processImages_(text) {\n"
|
||||
" var result = text\n"
|
||||
" while (result.contains(\"![\")) {\n"
|
||||
" var start = result.indexOf(\"![\")\n"
|
||||
" var altEnd = start + 2\n"
|
||||
" while (altEnd < result.count && result[altEnd] != \"]\") {\n"
|
||||
" altEnd = altEnd + 1\n"
|
||||
" }\n"
|
||||
" if (altEnd < result.count && altEnd + 1 < result.count && result[altEnd + 1] == \"(\") {\n"
|
||||
" var urlStart = altEnd + 2\n"
|
||||
" var urlEnd = urlStart\n"
|
||||
" while (urlEnd < result.count && result[urlEnd] != \")\") {\n"
|
||||
" urlEnd = urlEnd + 1\n"
|
||||
" }\n"
|
||||
" if (urlEnd < result.count) {\n"
|
||||
" var before = result[0...start]\n"
|
||||
" var alt = result[start + 2...altEnd]\n"
|
||||
" var url = result[urlStart...urlEnd]\n"
|
||||
" var after = result[urlEnd + 1..-1]\n"
|
||||
" result = before + \"<img src=\\\"\" + url + \"\\\" alt=\\\"\" + alt + \"\\\">\" + after\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "crypto" for Crypto
|
||||
|
||||
class Uuid {
|
||||
static hexDigit_(n) {
|
||||
if (n < 10) return String.fromCodePoint(48 + n)
|
||||
return String.fromCodePoint(97 + n - 10)
|
||||
}
|
||||
|
||||
static toHex_(bytes) {
|
||||
var hex = ""
|
||||
for (b in bytes) {
|
||||
var hi = (b >> 4) & 0x0F
|
||||
var lo = b & 0x0F
|
||||
hex = hex + hexDigit_(hi) + hexDigit_(lo)
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
static v4() {
|
||||
var bytes = Crypto.randomBytes(16)
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80
|
||||
var hex = toHex_(bytes)
|
||||
return hex[0..7] + "-" + hex[8..11] + "-" + hex[12..15] + "-" + hex[16..19] + "-" + hex[20..31]
|
||||
}
|
||||
|
||||
static isValid(string) {
|
||||
if (!(string is String)) return false
|
||||
if (string.count != 36) return false
|
||||
if (string[8] != "-" || string[13] != "-" || string[18] != "-" || string[23] != "-") return false
|
||||
var hexChars = "0123456789abcdefABCDEF"
|
||||
var positions = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
|
||||
for (pos in positions) {
|
||||
if (!hexChars.contains(string[pos])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static isV4(string) {
|
||||
if (!isValid(string)) return false
|
||||
var version = string[14]
|
||||
if (version != "4") return false
|
||||
var variant = string[19]
|
||||
if (variant != "8" && variant != "9" && variant != "a" && variant != "b" && variant != "A" && variant != "B") return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/uuid.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* uuidModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"crypto\" for Crypto\n"
|
||||
"\n"
|
||||
"class Uuid {\n"
|
||||
" static hexDigit_(n) {\n"
|
||||
" if (n < 10) return String.fromCodePoint(48 + n)\n"
|
||||
" return String.fromCodePoint(97 + n - 10)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static toHex_(bytes) {\n"
|
||||
" var hex = \"\"\n"
|
||||
" for (b in bytes) {\n"
|
||||
" var hi = (b >> 4) & 0x0F\n"
|
||||
" var lo = b & 0x0F\n"
|
||||
" hex = hex + hexDigit_(hi) + hexDigit_(lo)\n"
|
||||
" }\n"
|
||||
" return hex\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static v4() {\n"
|
||||
" var bytes = Crypto.randomBytes(16)\n"
|
||||
" bytes[6] = (bytes[6] & 0x0F) | 0x40\n"
|
||||
" bytes[8] = (bytes[8] & 0x3F) | 0x80\n"
|
||||
" var hex = toHex_(bytes)\n"
|
||||
" return hex[0..7] + \"-\" + hex[8..11] + \"-\" + hex[12..15] + \"-\" + hex[16..19] + \"-\" + hex[20..31]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static isValid(string) {\n"
|
||||
" if (!(string is String)) return false\n"
|
||||
" if (string.count != 36) return false\n"
|
||||
" if (string[8] != \"-\" || string[13] != \"-\" || string[18] != \"-\" || string[23] != \"-\") return false\n"
|
||||
" var hexChars = \"0123456789abcdefABCDEF\"\n"
|
||||
" var positions = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]\n"
|
||||
" for (pos in positions) {\n"
|
||||
" if (!hexChars.contains(string[pos])) return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static isV4(string) {\n"
|
||||
" if (!isValid(string)) return false\n"
|
||||
" var version = string[14]\n"
|
||||
" if (version != \"4\") return false\n"
|
||||
" var variant = string[19]\n"
|
||||
" if (variant != \"8\" && variant != \"9\" && variant != \"a\" && variant != \"b\" && variant != \"A\" && variant != \"B\") return false\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+453
@@ -0,0 +1,453 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "regex" for Regex
|
||||
import "base64" for Base64
|
||||
import "json" for Json
|
||||
import "uuid" for Uuid
|
||||
|
||||
class Validator {
|
||||
static email(value) {
|
||||
if (!(value is String)) return false
|
||||
var at = value.indexOf("@")
|
||||
if (at < 1) return false
|
||||
var dot = value.indexOf(".", at)
|
||||
if (dot < at + 2) return false
|
||||
if (dot >= value.count - 1) return false
|
||||
for (c in value) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "@" || c == "." || c == "_" || c == "-" || c == "+") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static domain(value) {
|
||||
if (!(value is String)) return false
|
||||
if (value.count == 0) return false
|
||||
if (value[0] == "-" || value[-1] == "-") return false
|
||||
var parts = value.split(".")
|
||||
if (parts.count < 2) return false
|
||||
for (part in parts) {
|
||||
if (part.count == 0 || part.count > 63) return false
|
||||
if (part[0] == "-" || part[-1] == "-") return false
|
||||
for (c in part) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "-") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static safeStr(value) {
|
||||
if (!(value is String)) return false
|
||||
for (b in value.bytes) {
|
||||
if (b < 32 || b > 126) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static httpEncoded(value) {
|
||||
if (!(value is String)) return false
|
||||
for (c in value) {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "_" || c == "." || c == "~" || c == "-" || c == "+") continue
|
||||
if (c == "\%") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static base64(value) {
|
||||
if (!(value is String)) return false
|
||||
if (value.count % 4 != 0) return false
|
||||
var padCount = 0
|
||||
for (i in 0...value.count) {
|
||||
var c = value[i]
|
||||
if (c == "=") {
|
||||
padCount = padCount + 1
|
||||
if (i < value.count - 2) return false
|
||||
} else if (padCount > 0) {
|
||||
return false
|
||||
} else {
|
||||
var code = c.codePoints.toList[0]
|
||||
if (c == "+" || c == "/") continue
|
||||
if (code >= 48 && code <= 57) continue
|
||||
if (code >= 65 && code <= 90) continue
|
||||
if (code >= 97 && code <= 122) continue
|
||||
return false
|
||||
}
|
||||
}
|
||||
return padCount <= 2
|
||||
}
|
||||
|
||||
static json(value) {
|
||||
if (!(value is String)) return false
|
||||
var fiber = Fiber.new {
|
||||
Json.parse(value)
|
||||
}
|
||||
var result = fiber.try()
|
||||
return !fiber.error
|
||||
}
|
||||
|
||||
static regex(value, pattern) {
|
||||
if (!(value is String)) return false
|
||||
var re = Regex.new(pattern)
|
||||
return re.test(value)
|
||||
}
|
||||
|
||||
static url(value) {
|
||||
if (!(value is String)) return false
|
||||
if (!value.startsWith("http://") && !value.startsWith("https://")) return false
|
||||
var rest = value.startsWith("https://") ? value[8..-1] : value[7..-1]
|
||||
if (rest.count == 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
static uuid(value) {
|
||||
return Uuid.isValid(value)
|
||||
}
|
||||
|
||||
static minLength(value, min) {
|
||||
if (value is String) return value.count >= min
|
||||
if (value is List) return value.count >= min
|
||||
return false
|
||||
}
|
||||
|
||||
static maxLength(value, max) {
|
||||
if (value is String) return value.count <= max
|
||||
if (value is List) return value.count <= max
|
||||
return false
|
||||
}
|
||||
|
||||
static range(value, min, max) {
|
||||
if (!(value is Num)) return false
|
||||
return value >= min && value <= max
|
||||
}
|
||||
|
||||
static positive(value) {
|
||||
if (!(value is Num)) return false
|
||||
return value > 0
|
||||
}
|
||||
|
||||
static negative(value) {
|
||||
if (!(value is Num)) return false
|
||||
return value < 0
|
||||
}
|
||||
|
||||
static integer(value) {
|
||||
if (!(value is Num)) return false
|
||||
return value == value.floor
|
||||
}
|
||||
|
||||
static ipv4(value) {
|
||||
if (!(value is String)) return false
|
||||
var parts = value.split(".")
|
||||
if (parts.count != 4) return false
|
||||
for (part in parts) {
|
||||
if (part.count == 0 || part.count > 3) return false
|
||||
var n = Num.fromString(part)
|
||||
if (n == null || n < 0 || n > 255) return false
|
||||
if (part.count > 1 && part[0] == "0") return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class Field {
|
||||
static string() { StringField.new() }
|
||||
static string(options) { StringField.new(options) }
|
||||
static integer() { IntegerField.new() }
|
||||
static integer(options) { IntegerField.new(options) }
|
||||
static number() { NumberField.new() }
|
||||
static number(options) { NumberField.new(options) }
|
||||
static email() { EmailField.new() }
|
||||
static boolean() { BooleanField.new() }
|
||||
static list(itemType) { ListField.new(itemType) }
|
||||
static map() { MapField.new() }
|
||||
static optional(fieldDef) { OptionalField.new(fieldDef) }
|
||||
static required(fieldDef) {
|
||||
fieldDef.required = true
|
||||
return fieldDef
|
||||
}
|
||||
}
|
||||
|
||||
class BaseField {
|
||||
construct new() {
|
||||
_required = true
|
||||
_default = null
|
||||
_validators = []
|
||||
}
|
||||
|
||||
construct new(options) {
|
||||
_required = options.containsKey("required") ? options["required"] : true
|
||||
_default = options.containsKey("default") ? options["default"] : null
|
||||
_validators = options.containsKey("validators") ? options["validators"] : []
|
||||
}
|
||||
|
||||
required { _required }
|
||||
required=(value) { _required = value }
|
||||
default { _default }
|
||||
validators { _validators }
|
||||
|
||||
addValidator(fn) {
|
||||
_validators.add(fn)
|
||||
return this
|
||||
}
|
||||
|
||||
validate(value, path) {
|
||||
if (value == null) {
|
||||
if (_required) return ValidationError.new(path, "Field is required")
|
||||
return null
|
||||
}
|
||||
var typeError = validateType_(value, path)
|
||||
if (typeError) return typeError
|
||||
for (validator in _validators) {
|
||||
if (!validator.call(value)) {
|
||||
return ValidationError.new(path, "Custom validation failed")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
validateType_(value, path) { null }
|
||||
|
||||
coerce(value) { value }
|
||||
}
|
||||
|
||||
class StringField is BaseField {
|
||||
construct new() {
|
||||
super()
|
||||
_minLength = null
|
||||
_maxLength = null
|
||||
_pattern = null
|
||||
}
|
||||
construct new(options) {
|
||||
super(options)
|
||||
_minLength = options.containsKey("minLength") ? options["minLength"] : null
|
||||
_maxLength = options.containsKey("maxLength") ? options["maxLength"] : null
|
||||
_pattern = options.containsKey("pattern") ? options["pattern"] : null
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is String)) return ValidationError.new(path, "Expected string")
|
||||
if (_minLength && value.count < _minLength) {
|
||||
return ValidationError.new(path, "String too short")
|
||||
}
|
||||
if (_maxLength && value.count > _maxLength) {
|
||||
return ValidationError.new(path, "String too long")
|
||||
}
|
||||
if (_pattern) {
|
||||
var re = Regex.new(_pattern)
|
||||
if (!re.test(value)) {
|
||||
return ValidationError.new(path, "String does not match pattern")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
minLength { _minLength }
|
||||
maxLength { _maxLength }
|
||||
}
|
||||
|
||||
class IntegerField is BaseField {
|
||||
construct new() {
|
||||
super()
|
||||
_min = null
|
||||
_max = null
|
||||
}
|
||||
construct new(options) {
|
||||
super(options)
|
||||
_min = options.containsKey("min") ? options["min"] : null
|
||||
_max = options.containsKey("max") ? options["max"] : null
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Num)) return ValidationError.new(path, "Expected integer")
|
||||
if (value != value.floor) return ValidationError.new(path, "Expected integer, got float")
|
||||
if (_min && value < _min) return ValidationError.new(path, "Value below minimum")
|
||||
if (_max && value > _max) return ValidationError.new(path, "Value above maximum")
|
||||
return null
|
||||
}
|
||||
|
||||
min { _min }
|
||||
max { _max }
|
||||
|
||||
coerce(value) {
|
||||
if (value is String) return Num.fromString(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class NumberField is BaseField {
|
||||
construct new() {
|
||||
super()
|
||||
_min = null
|
||||
_max = null
|
||||
}
|
||||
construct new(options) {
|
||||
super(options)
|
||||
_min = options.containsKey("min") ? options["min"] : null
|
||||
_max = options.containsKey("max") ? options["max"] : null
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Num)) return ValidationError.new(path, "Expected number")
|
||||
if (_min && value < _min) return ValidationError.new(path, "Value below minimum")
|
||||
if (_max && value > _max) return ValidationError.new(path, "Value above maximum")
|
||||
return null
|
||||
}
|
||||
|
||||
min { _min }
|
||||
max { _max }
|
||||
|
||||
coerce(value) {
|
||||
if (value is String) return Num.fromString(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class EmailField is BaseField {
|
||||
construct new() { super() }
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is String)) return ValidationError.new(path, "Expected string")
|
||||
if (!Validator.email(value)) return ValidationError.new(path, "Invalid email format")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanField is BaseField {
|
||||
construct new() { super() }
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Bool)) return ValidationError.new(path, "Expected boolean")
|
||||
return null
|
||||
}
|
||||
|
||||
coerce(value) {
|
||||
if (value is String) {
|
||||
if (value == "true" || value == "1") return true
|
||||
if (value == "false" || value == "0") return false
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class ListField is BaseField {
|
||||
construct new(itemType) {
|
||||
super()
|
||||
_itemType = itemType
|
||||
}
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is List)) return ValidationError.new(path, "Expected list")
|
||||
for (i in 0...value.count) {
|
||||
var itemPath = path + "[" + i.toString + "]"
|
||||
var error = _itemType.validate(value[i], itemPath)
|
||||
if (error) return error
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
itemType { _itemType }
|
||||
}
|
||||
|
||||
class MapField is BaseField {
|
||||
construct new() { super() }
|
||||
|
||||
validateType_(value, path) {
|
||||
if (!(value is Map)) return ValidationError.new(path, "Expected map")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
class OptionalField is BaseField {
|
||||
construct new(fieldDef) {
|
||||
super()
|
||||
_field = fieldDef
|
||||
_required = false
|
||||
}
|
||||
|
||||
validate(value, path) {
|
||||
if (value == null) return null
|
||||
return _field.validate(value, path)
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError {
|
||||
construct new(path, message) {
|
||||
_path = path
|
||||
_message = message
|
||||
}
|
||||
|
||||
path { _path }
|
||||
message { _message }
|
||||
toString { _path + ": " + _message }
|
||||
}
|
||||
|
||||
class ValidationResult {
|
||||
construct new(isValid, errors, data) {
|
||||
_isValid = isValid
|
||||
_errors = errors
|
||||
_data = data
|
||||
}
|
||||
|
||||
isValid { _isValid }
|
||||
errors { _errors }
|
||||
data { _data }
|
||||
}
|
||||
|
||||
class Schema {
|
||||
construct new(definition) {
|
||||
_definition = definition
|
||||
}
|
||||
|
||||
validate(data) {
|
||||
if (!(data is Map)) {
|
||||
return ValidationResult.new(false, [ValidationError.new("", "Expected object")], null)
|
||||
}
|
||||
|
||||
var errors = []
|
||||
var validData = {}
|
||||
|
||||
for (key in _definition.keys) {
|
||||
var field = _definition[key]
|
||||
var value = data.containsKey(key) ? data[key] : null
|
||||
|
||||
if (value != null) {
|
||||
value = field.coerce(value)
|
||||
}
|
||||
|
||||
var error = field.validate(value, key)
|
||||
if (error) {
|
||||
errors.add(error)
|
||||
} else {
|
||||
validData[key] = value != null ? value : field.default
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.new(errors.count == 0, errors, errors.count == 0 ? validData : null)
|
||||
}
|
||||
|
||||
validateOrAbort(data) {
|
||||
var result = validate(data)
|
||||
if (!result.isValid) {
|
||||
var messages = []
|
||||
for (error in result.errors) {
|
||||
messages.add(error.toString)
|
||||
}
|
||||
Fiber.abort("Validation failed: " + messages.join(", "))
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/wdantic.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* wdanticModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"regex\" for Regex\n"
|
||||
"import \"base64\" for Base64\n"
|
||||
"import \"json\" for Json\n"
|
||||
"import \"uuid\" for Uuid\n"
|
||||
"\n"
|
||||
"class Validator {\n"
|
||||
" static email(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var at = value.indexOf(\"@\")\n"
|
||||
" if (at < 1) return false\n"
|
||||
" var dot = value.indexOf(\".\", at)\n"
|
||||
" if (dot < at + 2) return false\n"
|
||||
" if (dot >= value.count - 1) return false\n"
|
||||
" for (c in value) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"@\" || c == \".\" || c == \"_\" || c == \"-\" || c == \"+\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static domain(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" if (value.count == 0) return false\n"
|
||||
" if (value[0] == \"-\" || value[-1] == \"-\") return false\n"
|
||||
" var parts = value.split(\".\")\n"
|
||||
" if (parts.count < 2) return false\n"
|
||||
" for (part in parts) {\n"
|
||||
" if (part.count == 0 || part.count > 63) return false\n"
|
||||
" if (part[0] == \"-\" || part[-1] == \"-\") return false\n"
|
||||
" for (c in part) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"-\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static safeStr(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" for (b in value.bytes) {\n"
|
||||
" if (b < 32 || b > 126) return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static httpEncoded(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" for (c in value) {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"_\" || c == \".\" || c == \"~\" || c == \"-\" || c == \"+\") continue\n"
|
||||
" if (c == \"\\%\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static base64(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" if (value.count % 4 != 0) return false\n"
|
||||
" var padCount = 0\n"
|
||||
" for (i in 0...value.count) {\n"
|
||||
" var c = value[i]\n"
|
||||
" if (c == \"=\") {\n"
|
||||
" padCount = padCount + 1\n"
|
||||
" if (i < value.count - 2) return false\n"
|
||||
" } else if (padCount > 0) {\n"
|
||||
" return false\n"
|
||||
" } else {\n"
|
||||
" var code = c.codePoints.toList[0]\n"
|
||||
" if (c == \"+\" || c == \"/\") continue\n"
|
||||
" if (code >= 48 && code <= 57) continue\n"
|
||||
" if (code >= 65 && code <= 90) continue\n"
|
||||
" if (code >= 97 && code <= 122) continue\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return padCount <= 2\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static json(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var fiber = Fiber.new {\n"
|
||||
" Json.parse(value)\n"
|
||||
" }\n"
|
||||
" var result = fiber.try()\n"
|
||||
" return !fiber.error\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static regex(value, pattern) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var re = Regex.new(pattern)\n"
|
||||
" return re.test(value)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static url(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" if (!value.startsWith(\"http://\") && !value.startsWith(\"https://\")) return false\n"
|
||||
" var rest = value.startsWith(\"https://\") ? value[8..-1] : value[7..-1]\n"
|
||||
" if (rest.count == 0) return false\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static uuid(value) {\n"
|
||||
" return Uuid.isValid(value)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static minLength(value, min) {\n"
|
||||
" if (value is String) return value.count >= min\n"
|
||||
" if (value is List) return value.count >= min\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static maxLength(value, max) {\n"
|
||||
" if (value is String) return value.count <= max\n"
|
||||
" if (value is List) return value.count <= max\n"
|
||||
" return false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static range(value, min, max) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value >= min && value <= max\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static positive(value) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value > 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static negative(value) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value < 0\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static integer(value) {\n"
|
||||
" if (!(value is Num)) return false\n"
|
||||
" return value == value.floor\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static ipv4(value) {\n"
|
||||
" if (!(value is String)) return false\n"
|
||||
" var parts = value.split(\".\")\n"
|
||||
" if (parts.count != 4) return false\n"
|
||||
" for (part in parts) {\n"
|
||||
" if (part.count == 0 || part.count > 3) return false\n"
|
||||
" var n = Num.fromString(part)\n"
|
||||
" if (n == null || n < 0 || n > 255) return false\n"
|
||||
" if (part.count > 1 && part[0] == \"0\") return false\n"
|
||||
" }\n"
|
||||
" return true\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Field {\n"
|
||||
" static string() { StringField.new() }\n"
|
||||
" static string(options) { StringField.new(options) }\n"
|
||||
" static integer() { IntegerField.new() }\n"
|
||||
" static integer(options) { IntegerField.new(options) }\n"
|
||||
" static number() { NumberField.new() }\n"
|
||||
" static number(options) { NumberField.new(options) }\n"
|
||||
" static email() { EmailField.new() }\n"
|
||||
" static boolean() { BooleanField.new() }\n"
|
||||
" static list(itemType) { ListField.new(itemType) }\n"
|
||||
" static map() { MapField.new() }\n"
|
||||
" static optional(fieldDef) { OptionalField.new(fieldDef) }\n"
|
||||
" static required(fieldDef) {\n"
|
||||
" fieldDef.required = true\n"
|
||||
" return fieldDef\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" _required = true\n"
|
||||
" _default = null\n"
|
||||
" _validators = []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" construct new(options) {\n"
|
||||
" _required = options.containsKey(\"required\") ? options[\"required\"] : true\n"
|
||||
" _default = options.containsKey(\"default\") ? options[\"default\"] : null\n"
|
||||
" _validators = options.containsKey(\"validators\") ? options[\"validators\"] : []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" required { _required }\n"
|
||||
" required=(value) { _required = value }\n"
|
||||
" default { _default }\n"
|
||||
" validators { _validators }\n"
|
||||
"\n"
|
||||
" addValidator(fn) {\n"
|
||||
" _validators.add(fn)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validate(value, path) {\n"
|
||||
" if (value == null) {\n"
|
||||
" if (_required) return ValidationError.new(path, \"Field is required\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
" var typeError = validateType_(value, path)\n"
|
||||
" if (typeError) return typeError\n"
|
||||
" for (validator in _validators) {\n"
|
||||
" if (!validator.call(value)) {\n"
|
||||
" return ValidationError.new(path, \"Custom validation failed\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) { null }\n"
|
||||
"\n"
|
||||
" coerce(value) { value }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class StringField is BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" super()\n"
|
||||
" _minLength = null\n"
|
||||
" _maxLength = null\n"
|
||||
" _pattern = null\n"
|
||||
" }\n"
|
||||
" construct new(options) {\n"
|
||||
" super(options)\n"
|
||||
" _minLength = options.containsKey(\"minLength\") ? options[\"minLength\"] : null\n"
|
||||
" _maxLength = options.containsKey(\"maxLength\") ? options[\"maxLength\"] : null\n"
|
||||
" _pattern = options.containsKey(\"pattern\") ? options[\"pattern\"] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is String)) return ValidationError.new(path, \"Expected string\")\n"
|
||||
" if (_minLength && value.count < _minLength) {\n"
|
||||
" return ValidationError.new(path, \"String too short\")\n"
|
||||
" }\n"
|
||||
" if (_maxLength && value.count > _maxLength) {\n"
|
||||
" return ValidationError.new(path, \"String too long\")\n"
|
||||
" }\n"
|
||||
" if (_pattern) {\n"
|
||||
" var re = Regex.new(_pattern)\n"
|
||||
" if (!re.test(value)) {\n"
|
||||
" return ValidationError.new(path, \"String does not match pattern\")\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" minLength { _minLength }\n"
|
||||
" maxLength { _maxLength }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class IntegerField is BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" super()\n"
|
||||
" _min = null\n"
|
||||
" _max = null\n"
|
||||
" }\n"
|
||||
" construct new(options) {\n"
|
||||
" super(options)\n"
|
||||
" _min = options.containsKey(\"min\") ? options[\"min\"] : null\n"
|
||||
" _max = options.containsKey(\"max\") ? options[\"max\"] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Num)) return ValidationError.new(path, \"Expected integer\")\n"
|
||||
" if (value != value.floor) return ValidationError.new(path, \"Expected integer, got float\")\n"
|
||||
" if (_min && value < _min) return ValidationError.new(path, \"Value below minimum\")\n"
|
||||
" if (_max && value > _max) return ValidationError.new(path, \"Value above maximum\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" min { _min }\n"
|
||||
" max { _max }\n"
|
||||
"\n"
|
||||
" coerce(value) {\n"
|
||||
" if (value is String) return Num.fromString(value)\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class NumberField is BaseField {\n"
|
||||
" construct new() {\n"
|
||||
" super()\n"
|
||||
" _min = null\n"
|
||||
" _max = null\n"
|
||||
" }\n"
|
||||
" construct new(options) {\n"
|
||||
" super(options)\n"
|
||||
" _min = options.containsKey(\"min\") ? options[\"min\"] : null\n"
|
||||
" _max = options.containsKey(\"max\") ? options[\"max\"] : null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Num)) return ValidationError.new(path, \"Expected number\")\n"
|
||||
" if (_min && value < _min) return ValidationError.new(path, \"Value below minimum\")\n"
|
||||
" if (_max && value > _max) return ValidationError.new(path, \"Value above maximum\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" min { _min }\n"
|
||||
" max { _max }\n"
|
||||
"\n"
|
||||
" coerce(value) {\n"
|
||||
" if (value is String) return Num.fromString(value)\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class EmailField is BaseField {\n"
|
||||
" construct new() { super() }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is String)) return ValidationError.new(path, \"Expected string\")\n"
|
||||
" if (!Validator.email(value)) return ValidationError.new(path, \"Invalid email format\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class BooleanField is BaseField {\n"
|
||||
" construct new() { super() }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Bool)) return ValidationError.new(path, \"Expected boolean\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" coerce(value) {\n"
|
||||
" if (value is String) {\n"
|
||||
" if (value == \"true\" || value == \"1\") return true\n"
|
||||
" if (value == \"false\" || value == \"0\") return false\n"
|
||||
" }\n"
|
||||
" return value\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class ListField is BaseField {\n"
|
||||
" construct new(itemType) {\n"
|
||||
" super()\n"
|
||||
" _itemType = itemType\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is List)) return ValidationError.new(path, \"Expected list\")\n"
|
||||
" for (i in 0...value.count) {\n"
|
||||
" var itemPath = path + \"[\" + i.toString + \"]\"\n"
|
||||
" var error = _itemType.validate(value[i], itemPath)\n"
|
||||
" if (error) return error\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" itemType { _itemType }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class MapField is BaseField {\n"
|
||||
" construct new() { super() }\n"
|
||||
"\n"
|
||||
" validateType_(value, path) {\n"
|
||||
" if (!(value is Map)) return ValidationError.new(path, \"Expected map\")\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class OptionalField is BaseField {\n"
|
||||
" construct new(fieldDef) {\n"
|
||||
" super()\n"
|
||||
" _field = fieldDef\n"
|
||||
" _required = false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validate(value, path) {\n"
|
||||
" if (value == null) return null\n"
|
||||
" return _field.validate(value, path)\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class ValidationError {\n"
|
||||
" construct new(path, message) {\n"
|
||||
" _path = path\n"
|
||||
" _message = message\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" path { _path }\n"
|
||||
" message { _message }\n"
|
||||
" toString { _path + \": \" + _message }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class ValidationResult {\n"
|
||||
" construct new(isValid, errors, data) {\n"
|
||||
" _isValid = isValid\n"
|
||||
" _errors = errors\n"
|
||||
" _data = data\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" isValid { _isValid }\n"
|
||||
" errors { _errors }\n"
|
||||
" data { _data }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Schema {\n"
|
||||
" construct new(definition) {\n"
|
||||
" _definition = definition\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validate(data) {\n"
|
||||
" if (!(data is Map)) {\n"
|
||||
" return ValidationResult.new(false, [ValidationError.new(\"\", \"Expected object\")], null)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var errors = []\n"
|
||||
" var validData = {}\n"
|
||||
"\n"
|
||||
" for (key in _definition.keys) {\n"
|
||||
" var field = _definition[key]\n"
|
||||
" var value = data.containsKey(key) ? data[key] : null\n"
|
||||
"\n"
|
||||
" if (value != null) {\n"
|
||||
" value = field.coerce(value)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var error = field.validate(value, key)\n"
|
||||
" if (error) {\n"
|
||||
" errors.add(error)\n"
|
||||
" } else {\n"
|
||||
" validData[key] = value != null ? value : field.default\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return ValidationResult.new(errors.count == 0, errors, errors.count == 0 ? validData : null)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" validateOrAbort(data) {\n"
|
||||
" var result = validate(data)\n"
|
||||
" if (!result.isValid) {\n"
|
||||
" var messages = []\n"
|
||||
" for (error in result.errors) {\n"
|
||||
" messages.add(error.toString)\n"
|
||||
" }\n"
|
||||
" Fiber.abort(\"Validation failed: \" + messages.join(\", \"))\n"
|
||||
" }\n"
|
||||
" return result.data\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+707
@@ -0,0 +1,707 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import "net" for Socket, Server
|
||||
import "tls" for TlsSocket
|
||||
import "dns" for Dns
|
||||
import "websocket" for WebSocket, WebSocketServer, WebSocketMessage
|
||||
import "json" for Json
|
||||
import "html" for Html
|
||||
import "uuid" for Uuid
|
||||
import "datetime" for DateTime
|
||||
import "io" for File
|
||||
|
||||
class Request {
|
||||
construct new_(method, path, query, headers, body, params, socket) {
|
||||
_method = method
|
||||
_path = path
|
||||
_query = query
|
||||
_headers = headers
|
||||
_body = body
|
||||
_params = params
|
||||
_socket = socket
|
||||
_parsedJson = null
|
||||
_parsedForm = null
|
||||
_cookies = null
|
||||
_session = null
|
||||
}
|
||||
|
||||
method { _method }
|
||||
path { _path }
|
||||
query { _query }
|
||||
headers { _headers }
|
||||
body { _body }
|
||||
params { _params }
|
||||
socket { _socket }
|
||||
|
||||
header(name) {
|
||||
var lower = toLower_(name)
|
||||
for (key in _headers.keys) {
|
||||
if (toLower_(key) == lower) return _headers[key]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
json {
|
||||
if (_parsedJson == null && _body.count > 0) {
|
||||
_parsedJson = Json.parse(_body)
|
||||
}
|
||||
return _parsedJson
|
||||
}
|
||||
|
||||
form {
|
||||
if (_parsedForm == null && _body.count > 0) {
|
||||
_parsedForm = Html.decodeParams(_body)
|
||||
}
|
||||
return _parsedForm
|
||||
}
|
||||
|
||||
cookies {
|
||||
if (_cookies == null) {
|
||||
_cookies = {}
|
||||
var cookieHeader = header("Cookie")
|
||||
if (cookieHeader != null) {
|
||||
var pairs = cookieHeader.split("; ")
|
||||
for (pair in pairs) {
|
||||
var eq = pair.indexOf("=")
|
||||
if (eq > 0) {
|
||||
var name = pair[0...eq].trim()
|
||||
var value = pair[eq + 1..-1].trim()
|
||||
_cookies[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _cookies
|
||||
}
|
||||
|
||||
session { _session }
|
||||
session=(s) { _session = s }
|
||||
|
||||
toLower_(str) {
|
||||
var result = ""
|
||||
for (c in str) {
|
||||
var cp = c.codePoints.toList[0]
|
||||
if (cp >= 65 && cp <= 90) {
|
||||
result = result + String.fromCodePoint(cp + 32)
|
||||
} else {
|
||||
result = result + c
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Response {
|
||||
construct new() {
|
||||
_status = 200
|
||||
_statusText = "OK"
|
||||
_headers = {}
|
||||
_body = ""
|
||||
_cookies = []
|
||||
}
|
||||
|
||||
status { _status }
|
||||
status=(s) {
|
||||
_status = s
|
||||
_statusText = statusText_(s)
|
||||
}
|
||||
|
||||
headers { _headers }
|
||||
body { _body }
|
||||
body=(b) { _body = b }
|
||||
|
||||
header(name, value) {
|
||||
_headers[name] = value
|
||||
return this
|
||||
}
|
||||
|
||||
cookie(name, value) { cookie(name, value, {}) }
|
||||
|
||||
cookie(name, value, options) {
|
||||
var cookie = name + "=" + value
|
||||
if (options.containsKey("path")) cookie = cookie + "; Path=" + options["path"]
|
||||
if (options.containsKey("maxAge")) cookie = cookie + "; Max-Age=" + options["maxAge"].toString
|
||||
if (options.containsKey("httpOnly") && options["httpOnly"]) cookie = cookie + "; HttpOnly"
|
||||
if (options.containsKey("secure") && options["secure"]) cookie = cookie + "; Secure"
|
||||
_cookies.add(cookie)
|
||||
return this
|
||||
}
|
||||
|
||||
static text(content) {
|
||||
var r = Response.new()
|
||||
r.header("Content-Type", "text/plain; charset=utf-8")
|
||||
r.body = content
|
||||
return r
|
||||
}
|
||||
|
||||
static html(content) {
|
||||
var r = Response.new()
|
||||
r.header("Content-Type", "text/html; charset=utf-8")
|
||||
r.body = content
|
||||
return r
|
||||
}
|
||||
|
||||
static json(data) {
|
||||
var r = Response.new()
|
||||
r.header("Content-Type", "application/json; charset=utf-8")
|
||||
r.body = Json.stringify(data)
|
||||
return r
|
||||
}
|
||||
|
||||
static redirect(url) { redirect(url, 302) }
|
||||
|
||||
static redirect(url, status) {
|
||||
var r = Response.new()
|
||||
r.status = status
|
||||
r.header("Location", url)
|
||||
return r
|
||||
}
|
||||
|
||||
static file(path) { file(path, null) }
|
||||
|
||||
static file(path, contentType) {
|
||||
if (!File.exists(path)) {
|
||||
var r = Response.new()
|
||||
r.status = 404
|
||||
r.body = "Not Found"
|
||||
return r
|
||||
}
|
||||
var r = Response.new()
|
||||
var ct = contentType
|
||||
if (ct == null) ct = guessContentType_(path)
|
||||
r.header("Content-Type", ct)
|
||||
r.body = File.read(path)
|
||||
return r
|
||||
}
|
||||
|
||||
static guessContentType_(path) {
|
||||
if (path.endsWith(".html") || path.endsWith(".htm")) return "text/html; charset=utf-8"
|
||||
if (path.endsWith(".css")) return "text/css; charset=utf-8"
|
||||
if (path.endsWith(".js")) return "application/javascript; charset=utf-8"
|
||||
if (path.endsWith(".json")) return "application/json; charset=utf-8"
|
||||
if (path.endsWith(".png")) return "image/png"
|
||||
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg"
|
||||
if (path.endsWith(".gif")) return "image/gif"
|
||||
if (path.endsWith(".svg")) return "image/svg+xml"
|
||||
if (path.endsWith(".ico")) return "image/x-icon"
|
||||
if (path.endsWith(".txt")) return "text/plain; charset=utf-8"
|
||||
if (path.endsWith(".xml")) return "application/xml; charset=utf-8"
|
||||
if (path.endsWith(".pdf")) return "application/pdf"
|
||||
if (path.endsWith(".woff")) return "font/woff"
|
||||
if (path.endsWith(".woff2")) return "font/woff2"
|
||||
if (path.endsWith(".ttf")) return "font/ttf"
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
statusText_(code) {
|
||||
if (code == 200) return "OK"
|
||||
if (code == 201) return "Created"
|
||||
if (code == 204) return "No Content"
|
||||
if (code == 301) return "Moved Permanently"
|
||||
if (code == 302) return "Found"
|
||||
if (code == 304) return "Not Modified"
|
||||
if (code == 400) return "Bad Request"
|
||||
if (code == 401) return "Unauthorized"
|
||||
if (code == 403) return "Forbidden"
|
||||
if (code == 404) return "Not Found"
|
||||
if (code == 405) return "Method Not Allowed"
|
||||
if (code == 500) return "Internal Server Error"
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
build() {
|
||||
var response = "HTTP/1.1 %(_status) %(_statusText)\r\n"
|
||||
_headers["Content-Length"] = _body.bytes.count.toString
|
||||
for (name in _headers.keys) {
|
||||
response = response + name + ": " + _headers[name] + "\r\n"
|
||||
}
|
||||
for (cookie in _cookies) {
|
||||
response = response + "Set-Cookie: " + cookie + "\r\n"
|
||||
}
|
||||
response = response + "\r\n" + _body
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
class Session {
|
||||
construct new_(id, data) {
|
||||
_id = id
|
||||
_data = data
|
||||
_modified = false
|
||||
}
|
||||
|
||||
id { _id }
|
||||
|
||||
[key] { _data.containsKey(key) ? _data[key] : null }
|
||||
|
||||
[key]=(value) {
|
||||
_data[key] = value
|
||||
_modified = true
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
if (_data.containsKey(key)) {
|
||||
_data.remove(key)
|
||||
_modified = true
|
||||
}
|
||||
}
|
||||
|
||||
data { _data }
|
||||
isModified { _modified }
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
construct new() {
|
||||
_sessions = {}
|
||||
}
|
||||
|
||||
get(id) {
|
||||
if (_sessions.containsKey(id)) {
|
||||
return _sessions[id]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
create() {
|
||||
var id = Uuid.v4()
|
||||
_sessions[id] = {}
|
||||
return Session.new_(id, _sessions[id])
|
||||
}
|
||||
|
||||
save(session) {
|
||||
_sessions[session.id] = session.data
|
||||
}
|
||||
|
||||
destroy(id) {
|
||||
if (_sessions.containsKey(id)) {
|
||||
_sessions.remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Router {
|
||||
construct new() {
|
||||
_routes = []
|
||||
}
|
||||
|
||||
addRoute(method, path, handler) {
|
||||
_routes.add({"method": method, "path": path, "handler": handler, "pattern": compilePattern_(path)})
|
||||
}
|
||||
|
||||
get(path, handler) { addRoute("GET", path, handler) }
|
||||
post(path, handler) { addRoute("POST", path, handler) }
|
||||
put(path, handler) { addRoute("PUT", path, handler) }
|
||||
delete(path, handler) { addRoute("DELETE", path, handler) }
|
||||
patch(path, handler) { addRoute("PATCH", path, handler) }
|
||||
|
||||
match(method, path) {
|
||||
for (route in _routes) {
|
||||
if (route["method"] != method && route["method"] != "*") continue
|
||||
var match = matchPattern_(route["pattern"], path)
|
||||
if (match != null) {
|
||||
return {"handler": route["handler"], "params": match}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
compilePattern_(path) {
|
||||
var parts = path.split("/")
|
||||
var pattern = []
|
||||
for (part in parts) {
|
||||
if (part.count == 0) continue
|
||||
if (part.startsWith(":")) {
|
||||
pattern.add({"type": "param", "name": part[1..-1]})
|
||||
} else if (part == "*") {
|
||||
pattern.add({"type": "wildcard"})
|
||||
} else {
|
||||
pattern.add({"type": "literal", "value": part})
|
||||
}
|
||||
}
|
||||
return pattern
|
||||
}
|
||||
|
||||
matchPattern_(pattern, path) {
|
||||
var parts = path.split("/").where { |p| p.count > 0 }.toList
|
||||
if (parts.count != pattern.count) {
|
||||
var hasWildcard = pattern.any { |p| p["type"] == "wildcard" }
|
||||
if (!hasWildcard) return null
|
||||
}
|
||||
|
||||
var params = {}
|
||||
var pi = 0
|
||||
for (i in 0...pattern.count) {
|
||||
var p = pattern[i]
|
||||
if (p["type"] == "wildcard") {
|
||||
break
|
||||
}
|
||||
if (pi >= parts.count) return null
|
||||
if (p["type"] == "literal") {
|
||||
if (parts[pi] != p["value"]) return null
|
||||
} else if (p["type"] == "param") {
|
||||
params[p["name"]] = parts[pi]
|
||||
}
|
||||
pi = pi + 1
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
class View {
|
||||
get(request) { methodNotAllowed_() }
|
||||
post(request) { methodNotAllowed_() }
|
||||
put(request) { methodNotAllowed_() }
|
||||
delete(request) { methodNotAllowed_() }
|
||||
patch(request) { methodNotAllowed_() }
|
||||
|
||||
methodNotAllowed_() {
|
||||
var r = Response.new()
|
||||
r.status = 405
|
||||
r.body = "Method Not Allowed"
|
||||
return r
|
||||
}
|
||||
|
||||
dispatch(request) {
|
||||
if (request.method == "GET") return get(request)
|
||||
if (request.method == "POST") return post(request)
|
||||
if (request.method == "PUT") return put(request)
|
||||
if (request.method == "DELETE") return delete(request)
|
||||
if (request.method == "PATCH") return patch(request)
|
||||
return methodNotAllowed_()
|
||||
}
|
||||
}
|
||||
|
||||
class Application {
|
||||
construct new() {
|
||||
_router = Router.new()
|
||||
_staticPrefixes = []
|
||||
_sessionStore = SessionStore.new()
|
||||
_sessionCookieName = "session_id"
|
||||
_middleware = []
|
||||
_views = {}
|
||||
_wsHandlers = {}
|
||||
}
|
||||
|
||||
router { _router }
|
||||
sessionStore { _sessionStore }
|
||||
|
||||
addRoute(method, path, handler) {
|
||||
_router.addRoute(method, path, handler)
|
||||
return this
|
||||
}
|
||||
|
||||
get(path, handler) { addRoute("GET", path, handler) }
|
||||
post(path, handler) { addRoute("POST", path, handler) }
|
||||
put(path, handler) { addRoute("PUT", path, handler) }
|
||||
delete(path, handler) { addRoute("DELETE", path, handler) }
|
||||
patch(path, handler) { addRoute("PATCH", path, handler) }
|
||||
|
||||
addView(path, viewClass) {
|
||||
_views[path] = viewClass
|
||||
_router.addRoute("*", path, Fn.new { |req| handleView_(viewClass, req) })
|
||||
return this
|
||||
}
|
||||
|
||||
handleView_(viewClass, request) {
|
||||
var view = viewClass.new()
|
||||
return view.dispatch(request)
|
||||
}
|
||||
|
||||
static_(prefix, directory) {
|
||||
_staticPrefixes.add({"prefix": prefix, "directory": directory})
|
||||
return this
|
||||
}
|
||||
|
||||
websocket(path, handler) {
|
||||
_wsHandlers[path] = handler
|
||||
return this
|
||||
}
|
||||
|
||||
use(middleware) {
|
||||
_middleware.add(middleware)
|
||||
return this
|
||||
}
|
||||
|
||||
run(host, port) {
|
||||
var server = Server.bind(host, port)
|
||||
System.print("Server running on http://%(host):%(port)")
|
||||
|
||||
while (true) {
|
||||
var socket = server.accept()
|
||||
if (socket == null) continue
|
||||
handleConnection_(socket)
|
||||
}
|
||||
}
|
||||
|
||||
handleConnection_(socket) {
|
||||
var requestData = ""
|
||||
var contentLength = 0
|
||||
var headersComplete = false
|
||||
var body = ""
|
||||
|
||||
while (true) {
|
||||
var chunk = socket.read()
|
||||
if (chunk == null || chunk.count == 0) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
requestData = requestData + chunk
|
||||
|
||||
if (!headersComplete && requestData.contains("\r\n\r\n")) {
|
||||
headersComplete = true
|
||||
var headerEnd = requestData.indexOf("\r\n\r\n")
|
||||
var headerPart = requestData[0...headerEnd]
|
||||
body = requestData[headerEnd + 4..-1]
|
||||
|
||||
var headers = parseHeaders_(headerPart)
|
||||
if (headers.containsKey("Content-Length")) {
|
||||
contentLength = Num.fromString(headers["Content-Length"])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (headersComplete && body.bytes.count >= contentLength) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var headerEnd = requestData.indexOf("\r\n\r\n")
|
||||
var headerPart = requestData[0...headerEnd]
|
||||
body = requestData[headerEnd + 4..-1]
|
||||
if (contentLength > 0 && body.bytes.count > contentLength) {
|
||||
body = body[0...contentLength]
|
||||
}
|
||||
|
||||
var lines = headerPart.split("\r\n")
|
||||
if (lines.count == 0) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
var requestLine = lines[0].split(" ")
|
||||
if (requestLine.count < 2) {
|
||||
socket.close()
|
||||
return
|
||||
}
|
||||
|
||||
var method = requestLine[0]
|
||||
var fullPath = requestLine[1]
|
||||
var path = fullPath
|
||||
var query = {}
|
||||
|
||||
var queryStart = fullPath.indexOf("?")
|
||||
if (queryStart >= 0) {
|
||||
path = fullPath[0...queryStart]
|
||||
query = Html.decodeParams(fullPath[queryStart + 1..-1])
|
||||
}
|
||||
|
||||
var headers = {}
|
||||
for (i in 1...lines.count) {
|
||||
var colonPos = lines[i].indexOf(":")
|
||||
if (colonPos > 0) {
|
||||
var name = lines[i][0...colonPos].trim()
|
||||
var value = lines[i][colonPos + 1..-1].trim()
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
var request = Request.new_(method, path, query, headers, body, {}, socket)
|
||||
loadSession_(request)
|
||||
|
||||
var response = null
|
||||
|
||||
for (sp in _staticPrefixes) {
|
||||
if (path.startsWith(sp["prefix"])) {
|
||||
var filePath = sp["directory"] + path[sp["prefix"].count..-1]
|
||||
response = Response.file(filePath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
var route = _router.match(method, path)
|
||||
if (route != null) {
|
||||
request = Request.new_(method, path, query, headers, body, route["params"], socket)
|
||||
loadSession_(request)
|
||||
for (mw in _middleware) {
|
||||
var result = mw.call(request)
|
||||
if (result != null) {
|
||||
response = result
|
||||
break
|
||||
}
|
||||
}
|
||||
if (response == null) {
|
||||
response = route["handler"].call(request)
|
||||
}
|
||||
} else {
|
||||
response = Response.new()
|
||||
response.status = 404
|
||||
response.body = "Not Found"
|
||||
}
|
||||
}
|
||||
|
||||
saveSession_(request, response)
|
||||
socket.write(response.build())
|
||||
socket.close()
|
||||
}
|
||||
|
||||
loadSession_(request) {
|
||||
var sessionId = request.cookies[_sessionCookieName]
|
||||
if (sessionId != null) {
|
||||
var data = _sessionStore.get(sessionId)
|
||||
if (data != null) {
|
||||
request.session = Session.new_(sessionId, data)
|
||||
return
|
||||
}
|
||||
}
|
||||
request.session = _sessionStore.create()
|
||||
}
|
||||
|
||||
saveSession_(request, response) {
|
||||
if (request.session != null) {
|
||||
_sessionStore.save(request.session)
|
||||
response.cookie(_sessionCookieName, request.session.id, {"path": "/", "httpOnly": true})
|
||||
}
|
||||
}
|
||||
|
||||
parseHeaders_(headerPart) {
|
||||
var headers = {}
|
||||
var lines = headerPart.split("\r\n")
|
||||
for (i in 1...lines.count) {
|
||||
var colonPos = lines[i].indexOf(":")
|
||||
if (colonPos > 0) {
|
||||
var name = lines[i][0...colonPos].trim()
|
||||
var value = lines[i][colonPos + 1..-1].trim()
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
class Client {
|
||||
static get(url) { get(url, {}) }
|
||||
|
||||
static get(url, options) {
|
||||
return request_("GET", url, options)
|
||||
}
|
||||
|
||||
static post(url) { post(url, {}) }
|
||||
|
||||
static post(url, options) {
|
||||
return request_("POST", url, options)
|
||||
}
|
||||
|
||||
static put(url, options) {
|
||||
return request_("PUT", url, options)
|
||||
}
|
||||
|
||||
static delete(url, options) {
|
||||
return request_("DELETE", url, options)
|
||||
}
|
||||
|
||||
static request_(method, url, options) {
|
||||
var parsed = parseUrl_(url)
|
||||
var host = parsed["host"]
|
||||
var port = parsed["port"]
|
||||
var path = parsed["path"]
|
||||
var isSecure = parsed["scheme"] == "https"
|
||||
|
||||
var addresses = Dns.lookup(host, 4)
|
||||
if (addresses.count == 0) {
|
||||
Fiber.abort("Could not resolve host: " + host)
|
||||
}
|
||||
|
||||
var socket
|
||||
if (isSecure) {
|
||||
socket = TlsSocket.connect(addresses[0], port, host)
|
||||
} else {
|
||||
socket = Socket.connect(addresses[0], port)
|
||||
}
|
||||
|
||||
var body = options.containsKey("body") ? options["body"] : ""
|
||||
if (options.containsKey("json")) {
|
||||
body = Json.stringify(options["json"])
|
||||
if (!options.containsKey("headers")) options["headers"] = {}
|
||||
options["headers"]["Content-Type"] = "application/json"
|
||||
}
|
||||
|
||||
var request = method + " " + path + " HTTP/1.1\r\n"
|
||||
request = request + "Host: " + host + "\r\n"
|
||||
request = request + "Connection: close\r\n"
|
||||
|
||||
if (options.containsKey("headers")) {
|
||||
for (entry in options["headers"]) {
|
||||
request = request + entry.key + ": " + entry.value + "\r\n"
|
||||
}
|
||||
}
|
||||
|
||||
if (body.count > 0) {
|
||||
request = request + "Content-Length: " + body.bytes.count.toString + "\r\n"
|
||||
}
|
||||
request = request + "\r\n" + body
|
||||
|
||||
socket.write(request)
|
||||
|
||||
var response = ""
|
||||
while (true) {
|
||||
var chunk = socket.read()
|
||||
if (chunk == null || chunk.count == 0) break
|
||||
response = response + chunk
|
||||
}
|
||||
socket.close()
|
||||
|
||||
return parseResponse_(response)
|
||||
}
|
||||
|
||||
static parseUrl_(url) {
|
||||
var result = {"scheme": "http", "host": "", "port": 80, "path": "/"}
|
||||
var rest = url
|
||||
|
||||
var schemeEnd = rest.indexOf("://")
|
||||
if (schemeEnd >= 0) {
|
||||
result["scheme"] = rest[0...schemeEnd]
|
||||
rest = rest[schemeEnd + 3..-1]
|
||||
if (result["scheme"] == "https") result["port"] = 443
|
||||
}
|
||||
|
||||
var pathStart = rest.indexOf("/")
|
||||
var hostPart = pathStart >= 0 ? rest[0...pathStart] : rest
|
||||
result["path"] = pathStart >= 0 ? rest[pathStart..-1] : "/"
|
||||
|
||||
var portStart = hostPart.indexOf(":")
|
||||
if (portStart >= 0) {
|
||||
result["host"] = hostPart[0...portStart]
|
||||
result["port"] = Num.fromString(hostPart[portStart + 1..-1])
|
||||
} else {
|
||||
result["host"] = hostPart
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static parseResponse_(response) {
|
||||
var headerEnd = response.indexOf("\r\n\r\n")
|
||||
if (headerEnd < 0) return {"status": 0, "headers": {}, "body": response}
|
||||
|
||||
var headerPart = response[0...headerEnd]
|
||||
var body = response[headerEnd + 4..-1]
|
||||
|
||||
var lines = headerPart.split("\r\n")
|
||||
var statusLine = lines[0].split(" ")
|
||||
var status = statusLine.count > 1 ? Num.fromString(statusLine[1]) : 0
|
||||
|
||||
var headers = {}
|
||||
for (i in 1...lines.count) {
|
||||
var colonPos = lines[i].indexOf(":")
|
||||
if (colonPos > 0) {
|
||||
var name = lines[i][0...colonPos].trim()
|
||||
var value = lines[i][colonPos + 1..-1].trim()
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return {"status": status, "headers": headers, "body": body}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
// Please do not edit this file. It has been generated automatically
|
||||
// from `src/module/web.wren` using `util/wren_to_c_string.py`
|
||||
|
||||
static const char* webModuleSource =
|
||||
"// retoor <retoor@molodetz.nl>\n"
|
||||
"\n"
|
||||
"import \"net\" for Socket, Server\n"
|
||||
"import \"tls\" for TlsSocket\n"
|
||||
"import \"dns\" for Dns\n"
|
||||
"import \"websocket\" for WebSocket, WebSocketServer, WebSocketMessage\n"
|
||||
"import \"json\" for Json\n"
|
||||
"import \"html\" for Html\n"
|
||||
"import \"uuid\" for Uuid\n"
|
||||
"import \"datetime\" for DateTime\n"
|
||||
"import \"io\" for File\n"
|
||||
"\n"
|
||||
"class Request {\n"
|
||||
" construct new_(method, path, query, headers, body, params, socket) {\n"
|
||||
" _method = method\n"
|
||||
" _path = path\n"
|
||||
" _query = query\n"
|
||||
" _headers = headers\n"
|
||||
" _body = body\n"
|
||||
" _params = params\n"
|
||||
" _socket = socket\n"
|
||||
" _parsedJson = null\n"
|
||||
" _parsedForm = null\n"
|
||||
" _cookies = null\n"
|
||||
" _session = null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" method { _method }\n"
|
||||
" path { _path }\n"
|
||||
" query { _query }\n"
|
||||
" headers { _headers }\n"
|
||||
" body { _body }\n"
|
||||
" params { _params }\n"
|
||||
" socket { _socket }\n"
|
||||
"\n"
|
||||
" header(name) {\n"
|
||||
" var lower = toLower_(name)\n"
|
||||
" for (key in _headers.keys) {\n"
|
||||
" if (toLower_(key) == lower) return _headers[key]\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" json {\n"
|
||||
" if (_parsedJson == null && _body.count > 0) {\n"
|
||||
" _parsedJson = Json.parse(_body)\n"
|
||||
" }\n"
|
||||
" return _parsedJson\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" form {\n"
|
||||
" if (_parsedForm == null && _body.count > 0) {\n"
|
||||
" _parsedForm = Html.decodeParams(_body)\n"
|
||||
" }\n"
|
||||
" return _parsedForm\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" cookies {\n"
|
||||
" if (_cookies == null) {\n"
|
||||
" _cookies = {}\n"
|
||||
" var cookieHeader = header(\"Cookie\")\n"
|
||||
" if (cookieHeader != null) {\n"
|
||||
" var pairs = cookieHeader.split(\"; \")\n"
|
||||
" for (pair in pairs) {\n"
|
||||
" var eq = pair.indexOf(\"=\")\n"
|
||||
" if (eq > 0) {\n"
|
||||
" var name = pair[0...eq].trim()\n"
|
||||
" var value = pair[eq + 1..-1].trim()\n"
|
||||
" _cookies[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return _cookies\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" session { _session }\n"
|
||||
" session=(s) { _session = s }\n"
|
||||
"\n"
|
||||
" toLower_(str) {\n"
|
||||
" var result = \"\"\n"
|
||||
" for (c in str) {\n"
|
||||
" var cp = c.codePoints.toList[0]\n"
|
||||
" if (cp >= 65 && cp <= 90) {\n"
|
||||
" result = result + String.fromCodePoint(cp + 32)\n"
|
||||
" } else {\n"
|
||||
" result = result + c\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Response {\n"
|
||||
" construct new() {\n"
|
||||
" _status = 200\n"
|
||||
" _statusText = \"OK\"\n"
|
||||
" _headers = {}\n"
|
||||
" _body = \"\"\n"
|
||||
" _cookies = []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" status { _status }\n"
|
||||
" status=(s) {\n"
|
||||
" _status = s\n"
|
||||
" _statusText = statusText_(s)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" headers { _headers }\n"
|
||||
" body { _body }\n"
|
||||
" body=(b) { _body = b }\n"
|
||||
"\n"
|
||||
" header(name, value) {\n"
|
||||
" _headers[name] = value\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" cookie(name, value) { cookie(name, value, {}) }\n"
|
||||
"\n"
|
||||
" cookie(name, value, options) {\n"
|
||||
" var cookie = name + \"=\" + value\n"
|
||||
" if (options.containsKey(\"path\")) cookie = cookie + \"; Path=\" + options[\"path\"]\n"
|
||||
" if (options.containsKey(\"maxAge\")) cookie = cookie + \"; Max-Age=\" + options[\"maxAge\"].toString\n"
|
||||
" if (options.containsKey(\"httpOnly\") && options[\"httpOnly\"]) cookie = cookie + \"; HttpOnly\"\n"
|
||||
" if (options.containsKey(\"secure\") && options[\"secure\"]) cookie = cookie + \"; Secure\"\n"
|
||||
" _cookies.add(cookie)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static text(content) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.header(\"Content-Type\", \"text/plain; charset=utf-8\")\n"
|
||||
" r.body = content\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static html(content) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.header(\"Content-Type\", \"text/html; charset=utf-8\")\n"
|
||||
" r.body = content\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static json(data) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.header(\"Content-Type\", \"application/json; charset=utf-8\")\n"
|
||||
" r.body = Json.stringify(data)\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static redirect(url) { redirect(url, 302) }\n"
|
||||
"\n"
|
||||
" static redirect(url, status) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.status = status\n"
|
||||
" r.header(\"Location\", url)\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static file(path) { file(path, null) }\n"
|
||||
"\n"
|
||||
" static file(path, contentType) {\n"
|
||||
" if (!File.exists(path)) {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.status = 404\n"
|
||||
" r.body = \"Not Found\"\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
" var r = Response.new()\n"
|
||||
" var ct = contentType\n"
|
||||
" if (ct == null) ct = guessContentType_(path)\n"
|
||||
" r.header(\"Content-Type\", ct)\n"
|
||||
" r.body = File.read(path)\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static guessContentType_(path) {\n"
|
||||
" if (path.endsWith(\".html\") || path.endsWith(\".htm\")) return \"text/html; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".css\")) return \"text/css; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".js\")) return \"application/javascript; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".json\")) return \"application/json; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".png\")) return \"image/png\"\n"
|
||||
" if (path.endsWith(\".jpg\") || path.endsWith(\".jpeg\")) return \"image/jpeg\"\n"
|
||||
" if (path.endsWith(\".gif\")) return \"image/gif\"\n"
|
||||
" if (path.endsWith(\".svg\")) return \"image/svg+xml\"\n"
|
||||
" if (path.endsWith(\".ico\")) return \"image/x-icon\"\n"
|
||||
" if (path.endsWith(\".txt\")) return \"text/plain; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".xml\")) return \"application/xml; charset=utf-8\"\n"
|
||||
" if (path.endsWith(\".pdf\")) return \"application/pdf\"\n"
|
||||
" if (path.endsWith(\".woff\")) return \"font/woff\"\n"
|
||||
" if (path.endsWith(\".woff2\")) return \"font/woff2\"\n"
|
||||
" if (path.endsWith(\".ttf\")) return \"font/ttf\"\n"
|
||||
" return \"application/octet-stream\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" statusText_(code) {\n"
|
||||
" if (code == 200) return \"OK\"\n"
|
||||
" if (code == 201) return \"Created\"\n"
|
||||
" if (code == 204) return \"No Content\"\n"
|
||||
" if (code == 301) return \"Moved Permanently\"\n"
|
||||
" if (code == 302) return \"Found\"\n"
|
||||
" if (code == 304) return \"Not Modified\"\n"
|
||||
" if (code == 400) return \"Bad Request\"\n"
|
||||
" if (code == 401) return \"Unauthorized\"\n"
|
||||
" if (code == 403) return \"Forbidden\"\n"
|
||||
" if (code == 404) return \"Not Found\"\n"
|
||||
" if (code == 405) return \"Method Not Allowed\"\n"
|
||||
" if (code == 500) return \"Internal Server Error\"\n"
|
||||
" return \"Unknown\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" build() {\n"
|
||||
" var response = \"HTTP/1.1 %(_status) %(_statusText)\\r\\n\"\n"
|
||||
" _headers[\"Content-Length\"] = _body.bytes.count.toString\n"
|
||||
" for (name in _headers.keys) {\n"
|
||||
" response = response + name + \": \" + _headers[name] + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" for (cookie in _cookies) {\n"
|
||||
" response = response + \"Set-Cookie: \" + cookie + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" response = response + \"\\r\\n\" + _body\n"
|
||||
" return response\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Session {\n"
|
||||
" construct new_(id, data) {\n"
|
||||
" _id = id\n"
|
||||
" _data = data\n"
|
||||
" _modified = false\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" id { _id }\n"
|
||||
"\n"
|
||||
" [key] { _data.containsKey(key) ? _data[key] : null }\n"
|
||||
"\n"
|
||||
" [key]=(value) {\n"
|
||||
" _data[key] = value\n"
|
||||
" _modified = true\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" remove(key) {\n"
|
||||
" if (_data.containsKey(key)) {\n"
|
||||
" _data.remove(key)\n"
|
||||
" _modified = true\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" data { _data }\n"
|
||||
" isModified { _modified }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class SessionStore {\n"
|
||||
" construct new() {\n"
|
||||
" _sessions = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" get(id) {\n"
|
||||
" if (_sessions.containsKey(id)) {\n"
|
||||
" return _sessions[id]\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" create() {\n"
|
||||
" var id = Uuid.v4()\n"
|
||||
" _sessions[id] = {}\n"
|
||||
" return Session.new_(id, _sessions[id])\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" save(session) {\n"
|
||||
" _sessions[session.id] = session.data\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" destroy(id) {\n"
|
||||
" if (_sessions.containsKey(id)) {\n"
|
||||
" _sessions.remove(id)\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Router {\n"
|
||||
" construct new() {\n"
|
||||
" _routes = []\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" addRoute(method, path, handler) {\n"
|
||||
" _routes.add({\"method\": method, \"path\": path, \"handler\": handler, \"pattern\": compilePattern_(path)})\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" get(path, handler) { addRoute(\"GET\", path, handler) }\n"
|
||||
" post(path, handler) { addRoute(\"POST\", path, handler) }\n"
|
||||
" put(path, handler) { addRoute(\"PUT\", path, handler) }\n"
|
||||
" delete(path, handler) { addRoute(\"DELETE\", path, handler) }\n"
|
||||
" patch(path, handler) { addRoute(\"PATCH\", path, handler) }\n"
|
||||
"\n"
|
||||
" match(method, path) {\n"
|
||||
" for (route in _routes) {\n"
|
||||
" if (route[\"method\"] != method && route[\"method\"] != \"*\") continue\n"
|
||||
" var match = matchPattern_(route[\"pattern\"], path)\n"
|
||||
" if (match != null) {\n"
|
||||
" return {\"handler\": route[\"handler\"], \"params\": match}\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" compilePattern_(path) {\n"
|
||||
" var parts = path.split(\"/\")\n"
|
||||
" var pattern = []\n"
|
||||
" for (part in parts) {\n"
|
||||
" if (part.count == 0) continue\n"
|
||||
" if (part.startsWith(\":\")) {\n"
|
||||
" pattern.add({\"type\": \"param\", \"name\": part[1..-1]})\n"
|
||||
" } else if (part == \"*\") {\n"
|
||||
" pattern.add({\"type\": \"wildcard\"})\n"
|
||||
" } else {\n"
|
||||
" pattern.add({\"type\": \"literal\", \"value\": part})\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return pattern\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" matchPattern_(pattern, path) {\n"
|
||||
" var parts = path.split(\"/\").where { |p| p.count > 0 }.toList\n"
|
||||
" if (parts.count != pattern.count) {\n"
|
||||
" var hasWildcard = pattern.any { |p| p[\"type\"] == \"wildcard\" }\n"
|
||||
" if (!hasWildcard) return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var params = {}\n"
|
||||
" var pi = 0\n"
|
||||
" for (i in 0...pattern.count) {\n"
|
||||
" var p = pattern[i]\n"
|
||||
" if (p[\"type\"] == \"wildcard\") {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" if (pi >= parts.count) return null\n"
|
||||
" if (p[\"type\"] == \"literal\") {\n"
|
||||
" if (parts[pi] != p[\"value\"]) return null\n"
|
||||
" } else if (p[\"type\"] == \"param\") {\n"
|
||||
" params[p[\"name\"]] = parts[pi]\n"
|
||||
" }\n"
|
||||
" pi = pi + 1\n"
|
||||
" }\n"
|
||||
" return params\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class View {\n"
|
||||
" get(request) { methodNotAllowed_() }\n"
|
||||
" post(request) { methodNotAllowed_() }\n"
|
||||
" put(request) { methodNotAllowed_() }\n"
|
||||
" delete(request) { methodNotAllowed_() }\n"
|
||||
" patch(request) { methodNotAllowed_() }\n"
|
||||
"\n"
|
||||
" methodNotAllowed_() {\n"
|
||||
" var r = Response.new()\n"
|
||||
" r.status = 405\n"
|
||||
" r.body = \"Method Not Allowed\"\n"
|
||||
" return r\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" dispatch(request) {\n"
|
||||
" if (request.method == \"GET\") return get(request)\n"
|
||||
" if (request.method == \"POST\") return post(request)\n"
|
||||
" if (request.method == \"PUT\") return put(request)\n"
|
||||
" if (request.method == \"DELETE\") return delete(request)\n"
|
||||
" if (request.method == \"PATCH\") return patch(request)\n"
|
||||
" return methodNotAllowed_()\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Application {\n"
|
||||
" construct new() {\n"
|
||||
" _router = Router.new()\n"
|
||||
" _staticPrefixes = []\n"
|
||||
" _sessionStore = SessionStore.new()\n"
|
||||
" _sessionCookieName = \"session_id\"\n"
|
||||
" _middleware = []\n"
|
||||
" _views = {}\n"
|
||||
" _wsHandlers = {}\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" router { _router }\n"
|
||||
" sessionStore { _sessionStore }\n"
|
||||
"\n"
|
||||
" addRoute(method, path, handler) {\n"
|
||||
" _router.addRoute(method, path, handler)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" get(path, handler) { addRoute(\"GET\", path, handler) }\n"
|
||||
" post(path, handler) { addRoute(\"POST\", path, handler) }\n"
|
||||
" put(path, handler) { addRoute(\"PUT\", path, handler) }\n"
|
||||
" delete(path, handler) { addRoute(\"DELETE\", path, handler) }\n"
|
||||
" patch(path, handler) { addRoute(\"PATCH\", path, handler) }\n"
|
||||
"\n"
|
||||
" addView(path, viewClass) {\n"
|
||||
" _views[path] = viewClass\n"
|
||||
" _router.addRoute(\"*\", path, Fn.new { |req| handleView_(viewClass, req) })\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" handleView_(viewClass, request) {\n"
|
||||
" var view = viewClass.new()\n"
|
||||
" return view.dispatch(request)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static_(prefix, directory) {\n"
|
||||
" _staticPrefixes.add({\"prefix\": prefix, \"directory\": directory})\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" websocket(path, handler) {\n"
|
||||
" _wsHandlers[path] = handler\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" use(middleware) {\n"
|
||||
" _middleware.add(middleware)\n"
|
||||
" return this\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" run(host, port) {\n"
|
||||
" var server = Server.bind(host, port)\n"
|
||||
" System.print(\"Server running on http://%(host):%(port)\")\n"
|
||||
"\n"
|
||||
" while (true) {\n"
|
||||
" var socket = server.accept()\n"
|
||||
" if (socket == null) continue\n"
|
||||
" handleConnection_(socket)\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" handleConnection_(socket) {\n"
|
||||
" var requestData = \"\"\n"
|
||||
" var contentLength = 0\n"
|
||||
" var headersComplete = false\n"
|
||||
" var body = \"\"\n"
|
||||
"\n"
|
||||
" while (true) {\n"
|
||||
" var chunk = socket.read()\n"
|
||||
" if (chunk == null || chunk.count == 0) {\n"
|
||||
" socket.close()\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
" requestData = requestData + chunk\n"
|
||||
"\n"
|
||||
" if (!headersComplete && requestData.contains(\"\\r\\n\\r\\n\")) {\n"
|
||||
" headersComplete = true\n"
|
||||
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
|
||||
" var headerPart = requestData[0...headerEnd]\n"
|
||||
" body = requestData[headerEnd + 4..-1]\n"
|
||||
"\n"
|
||||
" var headers = parseHeaders_(headerPart)\n"
|
||||
" if (headers.containsKey(\"Content-Length\")) {\n"
|
||||
" contentLength = Num.fromString(headers[\"Content-Length\"])\n"
|
||||
" } else {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (headersComplete && body.bytes.count >= contentLength) {\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
|
||||
" var headerPart = requestData[0...headerEnd]\n"
|
||||
" body = requestData[headerEnd + 4..-1]\n"
|
||||
" if (contentLength > 0 && body.bytes.count > contentLength) {\n"
|
||||
" body = body[0...contentLength]\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var lines = headerPart.split(\"\\r\\n\")\n"
|
||||
" if (lines.count == 0) {\n"
|
||||
" socket.close()\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var requestLine = lines[0].split(\" \")\n"
|
||||
" if (requestLine.count < 2) {\n"
|
||||
" socket.close()\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var method = requestLine[0]\n"
|
||||
" var fullPath = requestLine[1]\n"
|
||||
" var path = fullPath\n"
|
||||
" var query = {}\n"
|
||||
"\n"
|
||||
" var queryStart = fullPath.indexOf(\"?\")\n"
|
||||
" if (queryStart >= 0) {\n"
|
||||
" path = fullPath[0...queryStart]\n"
|
||||
" query = Html.decodeParams(fullPath[queryStart + 1..-1])\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var headers = {}\n"
|
||||
" for (i in 1...lines.count) {\n"
|
||||
" var colonPos = lines[i].indexOf(\":\")\n"
|
||||
" if (colonPos > 0) {\n"
|
||||
" var name = lines[i][0...colonPos].trim()\n"
|
||||
" var value = lines[i][colonPos + 1..-1].trim()\n"
|
||||
" headers[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var request = Request.new_(method, path, query, headers, body, {}, socket)\n"
|
||||
" loadSession_(request)\n"
|
||||
"\n"
|
||||
" var response = null\n"
|
||||
"\n"
|
||||
" for (sp in _staticPrefixes) {\n"
|
||||
" if (path.startsWith(sp[\"prefix\"])) {\n"
|
||||
" var filePath = sp[\"directory\"] + path[sp[\"prefix\"].count..-1]\n"
|
||||
" response = Response.file(filePath)\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (response == null) {\n"
|
||||
" var route = _router.match(method, path)\n"
|
||||
" if (route != null) {\n"
|
||||
" request = Request.new_(method, path, query, headers, body, route[\"params\"], socket)\n"
|
||||
" loadSession_(request)\n"
|
||||
" for (mw in _middleware) {\n"
|
||||
" var result = mw.call(request)\n"
|
||||
" if (result != null) {\n"
|
||||
" response = result\n"
|
||||
" break\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" if (response == null) {\n"
|
||||
" response = route[\"handler\"].call(request)\n"
|
||||
" }\n"
|
||||
" } else {\n"
|
||||
" response = Response.new()\n"
|
||||
" response.status = 404\n"
|
||||
" response.body = \"Not Found\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" saveSession_(request, response)\n"
|
||||
" socket.write(response.build())\n"
|
||||
" socket.close()\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" loadSession_(request) {\n"
|
||||
" var sessionId = request.cookies[_sessionCookieName]\n"
|
||||
" if (sessionId != null) {\n"
|
||||
" var data = _sessionStore.get(sessionId)\n"
|
||||
" if (data != null) {\n"
|
||||
" request.session = Session.new_(sessionId, data)\n"
|
||||
" return\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" request.session = _sessionStore.create()\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" saveSession_(request, response) {\n"
|
||||
" if (request.session != null) {\n"
|
||||
" _sessionStore.save(request.session)\n"
|
||||
" response.cookie(_sessionCookieName, request.session.id, {\"path\": \"/\", \"httpOnly\": true})\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" parseHeaders_(headerPart) {\n"
|
||||
" var headers = {}\n"
|
||||
" var lines = headerPart.split(\"\\r\\n\")\n"
|
||||
" for (i in 1...lines.count) {\n"
|
||||
" var colonPos = lines[i].indexOf(\":\")\n"
|
||||
" if (colonPos > 0) {\n"
|
||||
" var name = lines[i][0...colonPos].trim()\n"
|
||||
" var value = lines[i][colonPos + 1..-1].trim()\n"
|
||||
" headers[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" return headers\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"class Client {\n"
|
||||
" static get(url) { get(url, {}) }\n"
|
||||
"\n"
|
||||
" static get(url, options) {\n"
|
||||
" return request_(\"GET\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static post(url) { post(url, {}) }\n"
|
||||
"\n"
|
||||
" static post(url, options) {\n"
|
||||
" return request_(\"POST\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static put(url, options) {\n"
|
||||
" return request_(\"PUT\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static delete(url, options) {\n"
|
||||
" return request_(\"DELETE\", url, options)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static request_(method, url, options) {\n"
|
||||
" var parsed = parseUrl_(url)\n"
|
||||
" var host = parsed[\"host\"]\n"
|
||||
" var port = parsed[\"port\"]\n"
|
||||
" var path = parsed[\"path\"]\n"
|
||||
" var isSecure = parsed[\"scheme\"] == \"https\"\n"
|
||||
"\n"
|
||||
" var addresses = Dns.lookup(host, 4)\n"
|
||||
" if (addresses.count == 0) {\n"
|
||||
" Fiber.abort(\"Could not resolve host: \" + host)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var socket\n"
|
||||
" if (isSecure) {\n"
|
||||
" socket = TlsSocket.connect(addresses[0], port, host)\n"
|
||||
" } else {\n"
|
||||
" socket = Socket.connect(addresses[0], port)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var body = options.containsKey(\"body\") ? options[\"body\"] : \"\"\n"
|
||||
" if (options.containsKey(\"json\")) {\n"
|
||||
" body = Json.stringify(options[\"json\"])\n"
|
||||
" if (!options.containsKey(\"headers\")) options[\"headers\"] = {}\n"
|
||||
" options[\"headers\"][\"Content-Type\"] = \"application/json\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var request = method + \" \" + path + \" HTTP/1.1\\r\\n\"\n"
|
||||
" request = request + \"Host: \" + host + \"\\r\\n\"\n"
|
||||
" request = request + \"Connection: close\\r\\n\"\n"
|
||||
"\n"
|
||||
" if (options.containsKey(\"headers\")) {\n"
|
||||
" for (entry in options[\"headers\"]) {\n"
|
||||
" request = request + entry.key + \": \" + entry.value + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (body.count > 0) {\n"
|
||||
" request = request + \"Content-Length: \" + body.bytes.count.toString + \"\\r\\n\"\n"
|
||||
" }\n"
|
||||
" request = request + \"\\r\\n\" + body\n"
|
||||
"\n"
|
||||
" socket.write(request)\n"
|
||||
"\n"
|
||||
" var response = \"\"\n"
|
||||
" while (true) {\n"
|
||||
" var chunk = socket.read()\n"
|
||||
" if (chunk == null || chunk.count == 0) break\n"
|
||||
" response = response + chunk\n"
|
||||
" }\n"
|
||||
" socket.close()\n"
|
||||
"\n"
|
||||
" return parseResponse_(response)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseUrl_(url) {\n"
|
||||
" var result = {\"scheme\": \"http\", \"host\": \"\", \"port\": 80, \"path\": \"/\"}\n"
|
||||
" var rest = url\n"
|
||||
"\n"
|
||||
" var schemeEnd = rest.indexOf(\"://\")\n"
|
||||
" if (schemeEnd >= 0) {\n"
|
||||
" result[\"scheme\"] = rest[0...schemeEnd]\n"
|
||||
" rest = rest[schemeEnd + 3..-1]\n"
|
||||
" if (result[\"scheme\"] == \"https\") result[\"port\"] = 443\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var pathStart = rest.indexOf(\"/\")\n"
|
||||
" var hostPart = pathStart >= 0 ? rest[0...pathStart] : rest\n"
|
||||
" result[\"path\"] = pathStart >= 0 ? rest[pathStart..-1] : \"/\"\n"
|
||||
"\n"
|
||||
" var portStart = hostPart.indexOf(\":\")\n"
|
||||
" if (portStart >= 0) {\n"
|
||||
" result[\"host\"] = hostPart[0...portStart]\n"
|
||||
" result[\"port\"] = Num.fromString(hostPart[portStart + 1..-1])\n"
|
||||
" } else {\n"
|
||||
" result[\"host\"] = hostPart\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static parseResponse_(response) {\n"
|
||||
" var headerEnd = response.indexOf(\"\\r\\n\\r\\n\")\n"
|
||||
" if (headerEnd < 0) return {\"status\": 0, \"headers\": {}, \"body\": response}\n"
|
||||
"\n"
|
||||
" var headerPart = response[0...headerEnd]\n"
|
||||
" var body = response[headerEnd + 4..-1]\n"
|
||||
"\n"
|
||||
" var lines = headerPart.split(\"\\r\\n\")\n"
|
||||
" var statusLine = lines[0].split(\" \")\n"
|
||||
" var status = statusLine.count > 1 ? Num.fromString(statusLine[1]) : 0\n"
|
||||
"\n"
|
||||
" var headers = {}\n"
|
||||
" for (i in 1...lines.count) {\n"
|
||||
" var colonPos = lines[i].indexOf(\":\")\n"
|
||||
" if (colonPos > 0) {\n"
|
||||
" var name = lines[i][0...colonPos].trim()\n"
|
||||
" var value = lines[i][colonPos + 1..-1].trim()\n"
|
||||
" headers[name] = value\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return {\"status\": status, \"headers\": headers, \"body\": body}\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
Vendored
+37
-47
@@ -5,6 +5,7 @@ import "tls" for TlsSocket
|
||||
import "dns" for Dns
|
||||
import "crypto" for Crypto, Hash
|
||||
import "base64" for Base64
|
||||
import "bytes" for Bytes
|
||||
|
||||
class WebSocketMessage {
|
||||
construct new_(opcode, payload, fin) {
|
||||
@@ -43,11 +44,11 @@ class WebSocketMessage {
|
||||
}
|
||||
|
||||
static bytesToString_(bytes) {
|
||||
var str = ""
|
||||
var parts = []
|
||||
for (b in bytes) {
|
||||
str = str + String.fromCodePoint(b)
|
||||
parts.add(String.fromCodePoint(b))
|
||||
}
|
||||
return str
|
||||
return parts.join("")
|
||||
}
|
||||
|
||||
bytesToString_(bytes) { WebSocketMessage.bytesToString_(bytes) }
|
||||
@@ -61,7 +62,7 @@ class WebSocket {
|
||||
_isOpen = true
|
||||
_fragmentBuffer = []
|
||||
_fragmentOpcode = null
|
||||
_readBuffer = []
|
||||
_readBuffer = ""
|
||||
}
|
||||
|
||||
static connect(url) { connect(url, {}) }
|
||||
@@ -226,11 +227,11 @@ class WebSocket {
|
||||
}
|
||||
|
||||
static bytesToString_(bytes) {
|
||||
var str = ""
|
||||
var parts = []
|
||||
for (b in bytes) {
|
||||
str = str + String.fromByte(b)
|
||||
parts.add(String.fromByte(b))
|
||||
}
|
||||
return str
|
||||
return parts.join("")
|
||||
}
|
||||
|
||||
url { _url }
|
||||
@@ -349,11 +350,7 @@ class WebSocket {
|
||||
|
||||
sendFrame_(opcode, payload) {
|
||||
var frame = encodeFrame_(opcode, payload, _isClient)
|
||||
var data = ""
|
||||
for (b in frame) {
|
||||
data = data + String.fromByte(b)
|
||||
}
|
||||
_socket.write(data)
|
||||
_socket.write(Bytes.fromList(frame))
|
||||
}
|
||||
|
||||
encodeFrame_(opcode, payload, masked) {
|
||||
@@ -382,9 +379,11 @@ class WebSocket {
|
||||
if (masked) {
|
||||
var mask = Crypto.randomBytes(4)
|
||||
for (b in mask) frame.add(b)
|
||||
for (i in 0...len) {
|
||||
frame.add(payload[i] ^ mask[i % 4])
|
||||
}
|
||||
var payloadBytes = Bytes.fromList(payload)
|
||||
var maskBytes = Bytes.fromList(mask)
|
||||
var maskedPayload = Bytes.xorMask(payloadBytes, maskBytes)
|
||||
var maskedList = Bytes.toList(maskedPayload)
|
||||
for (b in maskedList) frame.add(b)
|
||||
} else {
|
||||
for (b in payload) frame.add(b)
|
||||
}
|
||||
@@ -394,70 +393,61 @@ class WebSocket {
|
||||
|
||||
readFrame_() {
|
||||
var header = readBytes_(2)
|
||||
if (header == null || header.count < 2) return null
|
||||
if (header == null || Bytes.length(header) < 2) return null
|
||||
|
||||
var fin = (header[0] & 0x80) != 0
|
||||
var opcode = header[0] & 0x0F
|
||||
var masked = (header[1] & 0x80) != 0
|
||||
var len = header[1] & 0x7F
|
||||
var headerList = Bytes.toList(header)
|
||||
var fin = (headerList[0] & 0x80) != 0
|
||||
var opcode = headerList[0] & 0x0F
|
||||
var masked = (headerList[1] & 0x80) != 0
|
||||
var len = headerList[1] & 0x7F
|
||||
|
||||
if (len == 126) {
|
||||
var ext = readBytes_(2)
|
||||
if (ext == null || ext.count < 2) return null
|
||||
len = (ext[0] << 8) | ext[1]
|
||||
if (ext == null || Bytes.length(ext) < 2) return null
|
||||
var extList = Bytes.toList(ext)
|
||||
len = (extList[0] << 8) | extList[1]
|
||||
} else if (len == 127) {
|
||||
var ext = readBytes_(8)
|
||||
if (ext == null || ext.count < 8) return null
|
||||
if (ext == null || Bytes.length(ext) < 8) return null
|
||||
var extList = Bytes.toList(ext)
|
||||
len = 0
|
||||
for (i in 4...8) {
|
||||
len = (len << 8) | ext[i]
|
||||
len = (len << 8) | extList[i]
|
||||
}
|
||||
}
|
||||
|
||||
var mask = null
|
||||
if (masked) {
|
||||
mask = readBytes_(4)
|
||||
if (mask == null || mask.count < 4) return null
|
||||
if (mask == null || Bytes.length(mask) < 4) return null
|
||||
}
|
||||
|
||||
var payload = []
|
||||
var payload = ""
|
||||
if (len > 0) {
|
||||
payload = readBytes_(len)
|
||||
if (payload == null) return null
|
||||
}
|
||||
|
||||
if (masked && mask != null) {
|
||||
for (i in 0...payload.count) {
|
||||
payload[i] = payload[i] ^ mask[i % 4]
|
||||
}
|
||||
payload = Bytes.xorMask(payload, mask)
|
||||
}
|
||||
|
||||
return WebSocketMessage.new_(opcode, payload, fin)
|
||||
return WebSocketMessage.new_(opcode, Bytes.toList(payload), fin)
|
||||
}
|
||||
|
||||
readBytes_(count) {
|
||||
while (_readBuffer.count < count) {
|
||||
while (Bytes.length(_readBuffer) < count) {
|
||||
var chunk = _socket.read()
|
||||
if (chunk == null || chunk.count == 0) {
|
||||
if (_readBuffer.count == 0) return null
|
||||
var result = []
|
||||
for (b in _readBuffer) result.add(b)
|
||||
_readBuffer = []
|
||||
if (Bytes.length(_readBuffer) == 0) return null
|
||||
var result = _readBuffer
|
||||
_readBuffer = ""
|
||||
return result
|
||||
}
|
||||
for (b in chunk.bytes) {
|
||||
_readBuffer.add(b)
|
||||
}
|
||||
_readBuffer = Bytes.concat(_readBuffer, chunk)
|
||||
}
|
||||
var result = []
|
||||
for (i in 0...count) {
|
||||
result.add(_readBuffer[i])
|
||||
}
|
||||
var remaining = []
|
||||
for (i in count..._readBuffer.count) {
|
||||
remaining.add(_readBuffer[i])
|
||||
}
|
||||
_readBuffer = remaining
|
||||
var result = Bytes.slice(_readBuffer, 0, count)
|
||||
_readBuffer = Bytes.slice(_readBuffer, count, Bytes.length(_readBuffer))
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ static const char* websocketModuleSource =
|
||||
"import \"dns\" for Dns\n"
|
||||
"import \"crypto\" for Crypto, Hash\n"
|
||||
"import \"base64\" for Base64\n"
|
||||
"import \"bytes\" for Bytes\n"
|
||||
"\n"
|
||||
"class WebSocketMessage {\n"
|
||||
" construct new_(opcode, payload, fin) {\n"
|
||||
@@ -47,11 +48,11 @@ static const char* websocketModuleSource =
|
||||
" }\n"
|
||||
"\n"
|
||||
" static bytesToString_(bytes) {\n"
|
||||
" var str = \"\"\n"
|
||||
" var parts = []\n"
|
||||
" for (b in bytes) {\n"
|
||||
" str = str + String.fromCodePoint(b)\n"
|
||||
" parts.add(String.fromCodePoint(b))\n"
|
||||
" }\n"
|
||||
" return str\n"
|
||||
" return parts.join(\"\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" bytesToString_(bytes) { WebSocketMessage.bytesToString_(bytes) }\n"
|
||||
@@ -65,7 +66,7 @@ static const char* websocketModuleSource =
|
||||
" _isOpen = true\n"
|
||||
" _fragmentBuffer = []\n"
|
||||
" _fragmentOpcode = null\n"
|
||||
" _readBuffer = []\n"
|
||||
" _readBuffer = \"\"\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" static connect(url) { connect(url, {}) }\n"
|
||||
@@ -230,11 +231,11 @@ static const char* websocketModuleSource =
|
||||
" }\n"
|
||||
"\n"
|
||||
" static bytesToString_(bytes) {\n"
|
||||
" var str = \"\"\n"
|
||||
" var parts = []\n"
|
||||
" for (b in bytes) {\n"
|
||||
" str = str + String.fromByte(b)\n"
|
||||
" parts.add(String.fromByte(b))\n"
|
||||
" }\n"
|
||||
" return str\n"
|
||||
" return parts.join(\"\")\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" url { _url }\n"
|
||||
@@ -353,11 +354,7 @@ static const char* websocketModuleSource =
|
||||
"\n"
|
||||
" sendFrame_(opcode, payload) {\n"
|
||||
" var frame = encodeFrame_(opcode, payload, _isClient)\n"
|
||||
" var data = \"\"\n"
|
||||
" for (b in frame) {\n"
|
||||
" data = data + String.fromByte(b)\n"
|
||||
" }\n"
|
||||
" _socket.write(data)\n"
|
||||
" _socket.write(Bytes.fromList(frame))\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" encodeFrame_(opcode, payload, masked) {\n"
|
||||
@@ -386,9 +383,11 @@ static const char* websocketModuleSource =
|
||||
" if (masked) {\n"
|
||||
" var mask = Crypto.randomBytes(4)\n"
|
||||
" for (b in mask) frame.add(b)\n"
|
||||
" for (i in 0...len) {\n"
|
||||
" frame.add(payload[i] ^ mask[i % 4])\n"
|
||||
" }\n"
|
||||
" var payloadBytes = Bytes.fromList(payload)\n"
|
||||
" var maskBytes = Bytes.fromList(mask)\n"
|
||||
" var maskedPayload = Bytes.xorMask(payloadBytes, maskBytes)\n"
|
||||
" var maskedList = Bytes.toList(maskedPayload)\n"
|
||||
" for (b in maskedList) frame.add(b)\n"
|
||||
" } else {\n"
|
||||
" for (b in payload) frame.add(b)\n"
|
||||
" }\n"
|
||||
@@ -398,70 +397,61 @@ static const char* websocketModuleSource =
|
||||
"\n"
|
||||
" readFrame_() {\n"
|
||||
" var header = readBytes_(2)\n"
|
||||
" if (header == null || header.count < 2) return null\n"
|
||||
" if (header == null || Bytes.length(header) < 2) return null\n"
|
||||
"\n"
|
||||
" var fin = (header[0] & 0x80) != 0\n"
|
||||
" var opcode = header[0] & 0x0F\n"
|
||||
" var masked = (header[1] & 0x80) != 0\n"
|
||||
" var len = header[1] & 0x7F\n"
|
||||
" var headerList = Bytes.toList(header)\n"
|
||||
" var fin = (headerList[0] & 0x80) != 0\n"
|
||||
" var opcode = headerList[0] & 0x0F\n"
|
||||
" var masked = (headerList[1] & 0x80) != 0\n"
|
||||
" var len = headerList[1] & 0x7F\n"
|
||||
"\n"
|
||||
" if (len == 126) {\n"
|
||||
" var ext = readBytes_(2)\n"
|
||||
" if (ext == null || ext.count < 2) return null\n"
|
||||
" len = (ext[0] << 8) | ext[1]\n"
|
||||
" if (ext == null || Bytes.length(ext) < 2) return null\n"
|
||||
" var extList = Bytes.toList(ext)\n"
|
||||
" len = (extList[0] << 8) | extList[1]\n"
|
||||
" } else if (len == 127) {\n"
|
||||
" var ext = readBytes_(8)\n"
|
||||
" if (ext == null || ext.count < 8) return null\n"
|
||||
" if (ext == null || Bytes.length(ext) < 8) return null\n"
|
||||
" var extList = Bytes.toList(ext)\n"
|
||||
" len = 0\n"
|
||||
" for (i in 4...8) {\n"
|
||||
" len = (len << 8) | ext[i]\n"
|
||||
" len = (len << 8) | extList[i]\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var mask = null\n"
|
||||
" if (masked) {\n"
|
||||
" mask = readBytes_(4)\n"
|
||||
" if (mask == null || mask.count < 4) return null\n"
|
||||
" if (mask == null || Bytes.length(mask) < 4) return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" var payload = []\n"
|
||||
" var payload = \"\"\n"
|
||||
" if (len > 0) {\n"
|
||||
" payload = readBytes_(len)\n"
|
||||
" if (payload == null) return null\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" if (masked && mask != null) {\n"
|
||||
" for (i in 0...payload.count) {\n"
|
||||
" payload[i] = payload[i] ^ mask[i % 4]\n"
|
||||
" }\n"
|
||||
" payload = Bytes.xorMask(payload, mask)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" return WebSocketMessage.new_(opcode, payload, fin)\n"
|
||||
" return WebSocketMessage.new_(opcode, Bytes.toList(payload), fin)\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
" readBytes_(count) {\n"
|
||||
" while (_readBuffer.count < count) {\n"
|
||||
" while (Bytes.length(_readBuffer) < count) {\n"
|
||||
" var chunk = _socket.read()\n"
|
||||
" if (chunk == null || chunk.count == 0) {\n"
|
||||
" if (_readBuffer.count == 0) return null\n"
|
||||
" var result = []\n"
|
||||
" for (b in _readBuffer) result.add(b)\n"
|
||||
" _readBuffer = []\n"
|
||||
" if (Bytes.length(_readBuffer) == 0) return null\n"
|
||||
" var result = _readBuffer\n"
|
||||
" _readBuffer = \"\"\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
" for (b in chunk.bytes) {\n"
|
||||
" _readBuffer.add(b)\n"
|
||||
" }\n"
|
||||
" _readBuffer = Bytes.concat(_readBuffer, chunk)\n"
|
||||
" }\n"
|
||||
" var result = []\n"
|
||||
" for (i in 0...count) {\n"
|
||||
" result.add(_readBuffer[i])\n"
|
||||
" }\n"
|
||||
" var remaining = []\n"
|
||||
" for (i in count..._readBuffer.count) {\n"
|
||||
" remaining.add(_readBuffer[i])\n"
|
||||
" }\n"
|
||||
" _readBuffer = remaining\n"
|
||||
" var result = Bytes.slice(_readBuffer, 0, count)\n"
|
||||
" _readBuffer = Bytes.slice(_readBuffer, count, Bytes.length(_readBuffer))\n"
|
||||
" return result\n"
|
||||
" }\n"
|
||||
"\n"
|
||||
|
||||
Reference in New Issue
Block a user