This commit is contained in:
2026-01-24 23:40:22 +01:00
parent 1a0f7f51a1
commit 8c1a721c4c
105 changed files with 23275 additions and 94 deletions
+38
View File
@@ -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
+277
View File
@@ -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
}
}
+281
View File
@@ -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";
+180
View File
@@ -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);
}
+15
View File
@@ -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
+10
View File
@@ -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)
}
+14
View File
@@ -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";
+263
View File
@@ -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}
}
}
+267
View File
@@ -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";
+177
View File
@@ -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 + "&amp;"
} else if (c == "<") {
result = result + "&lt;"
} else if (c == ">") {
result = result + "&gt;"
} else if (c == "\"") {
result = result + "&quot;"
} else if (c == "'") {
result = result + "&#39;"
} else {
result = result + c
}
}
return result
}
static unquote(string) {
if (!(string is String)) Fiber.abort("Argument must be a string.")
return string
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&#39;", "'")
}
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
}
}
+181
View File
@@ -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 + \"&amp;\"\n"
" } else if (c == \"<\") {\n"
" result = result + \"&lt;\"\n"
" } else if (c == \">\") {\n"
" result = result + \"&gt;\"\n"
" } else if (c == \"\\\"\") {\n"
" result = result + \"&quot;\"\n"
" } else if (c == \"'\") {\n"
" result = result + \"&#39;\"\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(\"&amp;\", \"&\")\n"
" .replace(\"&lt;\", \"<\")\n"
" .replace(\"&gt;\", \">\")\n"
" .replace(\"&quot;\", \"\\\"\")\n"
" .replace(\"&#39;\", \"'\")\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";
+328
View File
@@ -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
}
}
+332
View File
@@ -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";
+49
View File
@@ -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
}
}
+53
View File
@@ -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";
+453
View File
@@ -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
}
}
+457
View File
@@ -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";
+707
View File
@@ -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}
}
}
+711
View File
@@ -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";
+37 -47
View File
@@ -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
}
+37 -47
View File
@@ -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"