complete rework
This commit is contained in:
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
tab_width = 4
|
||||||
|
indent_style = tab
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,7 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
!/example
|
|
||||||
!/src
|
!/src
|
||||||
!/deno.json
|
!/lib
|
||||||
!/Dockerfile
|
/!decl
|
||||||
|
|
||||||
|
!/.editorconfig
|
||||||
!/.gitignore
|
!/.gitignore
|
||||||
!/README.md
|
|
||||||
|
!/Makefile
|
||||||
|
!/Dockerfile
|
||||||
|
|||||||
40
Dockerfile
40
Dockerfile
@@ -1,6 +1,21 @@
|
|||||||
FROM denoland/deno:alpine as prod
|
FROM alpine AS build
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
RUN apk add --no-cache lua5.4-dev lua5.4-luv make gcc pkgconf util-linux-misc
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY src ./src/
|
||||||
|
COPY lib ./lib/
|
||||||
|
COPY Makefile ./Makefile
|
||||||
|
COPY mklua ./mklua
|
||||||
|
# ADD https://git.topcheto.eu/topchetoeu/mklua/releases/download/latest/mklua ./mklua
|
||||||
|
|
||||||
|
RUN ln -s /usr/lib/liblua-5.4.so.0 /usr/lib/liblua.so.5.4
|
||||||
|
RUN ./mklua
|
||||||
|
RUN make MKLUA=./mklua
|
||||||
|
# ADD https://git.topcheto.eu/topchetoeu/mklua/releases/download/latest/mklua ./mklua
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
@@ -11,24 +26,11 @@ VOLUME /config
|
|||||||
|
|
||||||
STOPSIGNAL SIGINT
|
STOPSIGNAL SIGINT
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --start-period=1s CMD curl localhost:8080/.well-known/keepalive | grep "awake and alive"
|
HEALTHCHECK --interval=30s --start-period=1s CMD curl localhost:8080/.well-known/keepalive | grep "OK"
|
||||||
ENTRYPOINT deno run\
|
ENTRYPOINT ./bin/website
|
||||||
--allow-read=config.dev.yml\
|
|
||||||
--allow-read=config.test.yml\
|
|
||||||
--allow-net=0.0.0.0:8080\
|
|
||||||
--allow-sys=homedir\
|
|
||||||
--allow-read=./\
|
|
||||||
--allow-read=/plugins\
|
|
||||||
--allow-read=/views\
|
|
||||||
--allow-read=/static\
|
|
||||||
--allow-read=/config\
|
|
||||||
/app/src/main.ts\
|
|
||||||
/app/config.yml\
|
|
||||||
$(find /config -name "*.yml")
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY deno.json ./
|
RUN apk add --no-cache lua5.4 lua5.4-luv
|
||||||
RUN deno install
|
|
||||||
|
|
||||||
COPY src ./src/
|
COPY --from=build ./bin/website .
|
||||||
|
|||||||
50
Makefile
Normal file
50
Makefile
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Generated by mklua
|
||||||
|
MKLUA ?= mklua
|
||||||
|
CC ?= cc
|
||||||
|
OUTPUT ?= website
|
||||||
|
|
||||||
|
BINDIR := bin
|
||||||
|
LIBDIR := lib
|
||||||
|
SRCDIR := src
|
||||||
|
OUTPUT := $(BINDIR)/$(OUTPUT)
|
||||||
|
|
||||||
|
SOURCES := $(wildcard $(LIBDIR)/*.c)
|
||||||
|
OBJECTS := $(SOURCES:%.c=$(BINDIR)/%.o)
|
||||||
|
|
||||||
|
MKLUA_FLAGS += --path "$(SRCDIR)/?.lua;$(SRCDIR)/?/init.lua" --cpath ""
|
||||||
|
MKLUA_FLAGS += --entry
|
||||||
|
MKLUA_ENTRY ?= website-main
|
||||||
|
MKLUA_OUT := $(OUTPUT).c
|
||||||
|
|
||||||
|
LIBS := $(shell $(MKLUA) $(MKLUA_FLAGS) $(MKLUA_ENTRY) --libs)
|
||||||
|
DEPS := $(shell $(MKLUA) $(MKLUA_FLAGS) $(MKLUA_ENTRY) --deps)
|
||||||
|
|
||||||
|
CCARGS_LUA ?= $(shell pkg-config --cflags lua5.4)
|
||||||
|
LDARGS_LUA ?= $(shell pkg-config --libs lua5.4)
|
||||||
|
|
||||||
|
CCARGS += $(CCARGS_LUA) $(foreach x,$(LIBS),-I"$(dir $x)")
|
||||||
|
LDARGS += $(LDARGS_LUA)
|
||||||
|
|
||||||
|
ifeq ($(DEBUG), yes)
|
||||||
|
CCARGS += -g
|
||||||
|
MKLUA_FLAGS += -g
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(OUTPUT)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -fr $(BINDIR) $(MKLUA_OUT) deps/luv
|
||||||
|
|
||||||
|
compile_flags.txt:
|
||||||
|
printf -- '$(foreach v,$(CCARGS) $(LDARGS),\n$v)' > compile_flags.txt
|
||||||
|
|
||||||
|
$(OUTPUT): $(LIBS) $(MKLUA_OUT) $(SOURCES) | $(dir $(OUTPUT))
|
||||||
|
$(CC) $(CCARGS) $(LDARGS) $^ -o $@
|
||||||
|
|
||||||
|
$(MKLUA_OUT): $(DEPS) | $(dir $(MKLUA_OUT))
|
||||||
|
$(MKLUA) $(MKLUA_FLAGS) $(MKLUA_ENTRY) -o $@
|
||||||
|
|
||||||
|
%/:
|
||||||
|
mkdir -p $@
|
||||||
67
README.md
67
README.md
@@ -1,67 +0,0 @@
|
|||||||
# My website platform
|
|
||||||
|
|
||||||
I use this custom layer on top of PUG for my personal web page. It allows you to write full HTML pages, as well as simple markdown pages.
|
|
||||||
|
|
||||||
## How to use?
|
|
||||||
|
|
||||||
Using Deno, just running `src/main.ts` with an argument to the config file will start the server. The config consists of the following fields:
|
|
||||||
|
|
||||||
- ignore: consists of a list of paths that should be invisible to the public
|
|
||||||
- plugins: a list of plugins that should be loaded
|
|
||||||
- libs: a path to the folder which contains the plugins
|
|
||||||
- views: a path to the folder which contains the views
|
|
||||||
- maxCache: how many seconds should pass, before a cached page is discarded
|
|
||||||
- debug: If true, will output more information
|
|
||||||
- static: If provided, will be a path which contains statically-served resources
|
|
||||||
|
|
||||||
- notFound: The name of the not found page
|
|
||||||
- defaultPage: The name of the page template (the template for a file)
|
|
||||||
- defaultIndex: The name of the index template (the template for a folder)
|
|
||||||
- defaultTitle: If the page has no title, this will be used instead
|
|
||||||
|
|
||||||
- Everything else: Will become a default value for a global variable
|
|
||||||
|
|
||||||
### Structure
|
|
||||||
|
|
||||||
There is one folder - the views folder, which contains the files that will be rendered and served by the server. Resolving a view is not too different from resolving a NodeJS module - the server will search for these paths, in this order:
|
|
||||||
|
|
||||||
- ${viewsDir}/${path}/index.pug
|
|
||||||
- ${viewsDir}/${path}/index.md
|
|
||||||
- ${viewsDir}/${path}.pug
|
|
||||||
- ${viewsDir}/${path}.md
|
|
||||||
- ${viewsDir}/${notFound}.pug
|
|
||||||
- ${viewsDir}/${notFound}.md
|
|
||||||
- (built-in fallback error message)
|
|
||||||
|
|
||||||
The first path that is found will be used as the rendered file. If the found file is an index file, it will be rendered with the index base, otherwise, the page base will be used instead.
|
|
||||||
|
|
||||||
First things first, the server will resolve the metadata that the view provides (if any). That metadata will be overlaid over the config and will be used to resolve the base of the page. After that, our view will be rendered, and the output will be put in the metadata option "text". Of course, having a base page that is not `pug` would be useless, as markdown has no templating capabilities.
|
|
||||||
|
|
||||||
In a pug view, you will be able to access all metadata and config as global variables. The metadata will be overlaid over the global config.
|
|
||||||
|
|
||||||
In a JS view, you will be able to access the following relevant modules, which will allow you to get information about the current view:
|
|
||||||
- !lib
|
|
||||||
- View - the base class for all views
|
|
||||||
- config - the global config
|
|
||||||
- isIgnored - a function that checks whether the given path is ignored
|
|
||||||
- list - a utility function that converts an unknown value to a set of strings
|
|
||||||
- getPlugin - gets the function that calls a plugin (we will talk about them later)
|
|
||||||
- getLocals - gets an object, representing the current running metadata
|
|
||||||
- resolveView - from a given URL, returns the corresponding View object
|
|
||||||
- resolveViewFromUri - from a given filesystem path, returns the corresponding View object
|
|
||||||
- type Plugin
|
|
||||||
- type Locals
|
|
||||||
- default.View - same as View
|
|
||||||
- default.config - same as config
|
|
||||||
- default.isIgnored - same as isIgnored
|
|
||||||
- default.locals - a getter that calls getLocals
|
|
||||||
|
|
||||||
### Plugins
|
|
||||||
|
|
||||||
Plugins are used to provide additional reusable functionality for the views. They can be put in the configured plugins folder. Due to how the import system works, all plugins that will be used need to be listed in the config (they will be preloaded). A plugin must have its default export a function. That function will take a view as its first argument, and additional user-defined arguments. The function may return anything
|
|
||||||
|
|
||||||
Invoking plugins in Pug code can be done with the additionally exposed function `invoke(name, ...args)`. The current view will be passed as the first argument of the plugin.
|
|
||||||
|
|
||||||
## Deploying
|
|
||||||
|
|
||||||
It is as simple as building, using the included Dockerfile.
|
|
||||||
23
deno.json
23
deno.json
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"tasks": {
|
|
||||||
"dev": "deno run --watch main.ts"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"@std/http": "jsr:@std/http@^1.0.9",
|
|
||||||
"@std/path": "jsr:@std/path@^1.0.7",
|
|
||||||
"@std/yaml": "jsr:@std/yaml@^1.0.5",
|
|
||||||
"highlight.js": "npm:highlight.js@^11.10.0",
|
|
||||||
"markdown-it": "npm:markdown-it@^14.1.0",
|
|
||||||
"markdown-it-front-matter": "npm:markdown-it-front-matter@^0.2.4",
|
|
||||||
"pug": "npm:pug@^3.0.3",
|
|
||||||
"!lib": "./src/lib.ts"
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"rules": {
|
|
||||||
"exclude": [ "no-explicit-any", "no-cond-assign", "require-await", "no-regex-spaces", "no-empty" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fmt": {
|
|
||||||
"indentWidth": 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
plugins: []
|
|
||||||
libs: data/plugins
|
|
||||||
views: data/views
|
|
||||||
static: data/static
|
|
||||||
|
|
||||||
notFound: .templ/not-found
|
|
||||||
defaultPage: .templ/page
|
|
||||||
defaultIndex: .templ/index
|
|
||||||
|
|
||||||
debug: true
|
|
||||||
maxCache: 30
|
|
||||||
ignore:
|
|
||||||
- /.**
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
libs: /plugins
|
|
||||||
views: /views
|
|
||||||
static: /static
|
|
||||||
|
|
||||||
notFound: .templ/not-found
|
|
||||||
defaultPage: .templ/page
|
|
||||||
defaultIndex: .templ/index
|
|
||||||
plugins: []
|
|
||||||
|
|
||||||
debug: false
|
|
||||||
maxCache: 86400
|
|
||||||
ignore:
|
|
||||||
- /.**
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
plugins:
|
|
||||||
- breadcrumbs
|
|
||||||
- dir
|
|
||||||
- navbar
|
|
||||||
debug: false
|
|
||||||
15
lib/main.c
Normal file
15
lib/main.c
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#include <lauxlib.h>
|
||||||
|
#include <lua.h>
|
||||||
|
#include <lualib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
extern int mklua_entry(lua_State *ctx, int argc, const char **argv);
|
||||||
|
extern int luaopen_seek(lua_State *ctx);
|
||||||
|
|
||||||
|
int main(int argc, const char **argv) {
|
||||||
|
lua_State *ctx = luaL_newstate();
|
||||||
|
luaL_openlibs(ctx);
|
||||||
|
luaL_requiref(ctx, "seek", luaopen_seek, false);
|
||||||
|
|
||||||
|
return mklua_entry(ctx, argc, argv);
|
||||||
|
}
|
||||||
92
lib/seek.c
Normal file
92
lib/seek.c
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// For some fucking reason, libuv hasn't implemented lseek() (WHY??)
|
||||||
|
// Here, a sync version is implemented (as i cannot be bothered with callbacks)
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <lauxlib.h>
|
||||||
|
#include <lua.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <io.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#define off_t LONGLONG
|
||||||
|
#define _LARGEFILE_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int luafunc_seek(lua_State *ctx) {
|
||||||
|
|
||||||
|
int fd = lua_tointeger(ctx, 1);
|
||||||
|
off_t offset = lua_isnoneornil(ctx, 2) ? 0 : luaL_checkinteger(ctx, 2);
|
||||||
|
|
||||||
|
int whence = SEEK_CUR;
|
||||||
|
if (!lua_isnoneornil(ctx, 3)) {
|
||||||
|
const char *swhence = luaL_checkstring(ctx, 3);
|
||||||
|
if (!strcmp(swhence, "set")) {
|
||||||
|
whence = SEEK_SET;
|
||||||
|
}
|
||||||
|
else if (!strcmp(swhence, "cur")) {
|
||||||
|
whence = SEEK_CUR;
|
||||||
|
}
|
||||||
|
else if (!strcmp(swhence, "end")) {
|
||||||
|
whence = SEEK_END;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
luaL_typeerror(ctx, 3, "'set', 'cur' or 'end'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
HANDLE fh;
|
||||||
|
LARGE_INTEGER new_offset;
|
||||||
|
|
||||||
|
fh = (HANDLE)_get_osfhandle(fd);
|
||||||
|
if (fh == (HANDLE)-1) {
|
||||||
|
errno = EBADF;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD method;
|
||||||
|
switch (whence) {
|
||||||
|
case SEEK_SET:
|
||||||
|
method = FILE_BEGIN;
|
||||||
|
break;
|
||||||
|
case SEEK_CUR:
|
||||||
|
method = FILE_CURRENT;
|
||||||
|
break;
|
||||||
|
case SEEK_END:
|
||||||
|
method = FILE_END;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LARGE_INTEGER distance;
|
||||||
|
distance.QuadPart = offset;
|
||||||
|
|
||||||
|
if (!SetFilePointerEx(fh, distance, &new_offset, method)) {
|
||||||
|
lua_pushnil(ctx);
|
||||||
|
lua_pushstring(ctx, strerror(EINVAL));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
off_t offs = new_offset.QuadPart;
|
||||||
|
#else
|
||||||
|
off_t offs = lseek(fd, offset, whence);
|
||||||
|
if (offs < 0) {
|
||||||
|
lua_pushnil(ctx);
|
||||||
|
lua_pushstring(ctx, strerror(errno));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lua_pushinteger(ctx, offs);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int luaopen_seek(lua_State *ctx) {
|
||||||
|
lua_pushcfunction(ctx, luafunc_seek);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
102
src/config.ts
102
src/config.ts
@@ -1,102 +0,0 @@
|
|||||||
import { parse as parseYaml } from "@std/yaml";
|
|
||||||
import { list } from "./main.ts";
|
|
||||||
import { normalize } from "@std/path/posix";
|
|
||||||
|
|
||||||
interface Config {
|
|
||||||
ignore: Set<string>;
|
|
||||||
plugins: Set<string>;
|
|
||||||
|
|
||||||
maxCache?: number;
|
|
||||||
debug: boolean;
|
|
||||||
track?: boolean;
|
|
||||||
static?: string;
|
|
||||||
libs?: string;
|
|
||||||
views?: string;
|
|
||||||
notFound: string;
|
|
||||||
|
|
||||||
defaultPage?: string;
|
|
||||||
defaultIndex?: string;
|
|
||||||
defaultTitle?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function merge<T>(...objs: T[]): T {
|
|
||||||
if (objs.length === 0) throw new Error("Expected at least one object to merge");
|
|
||||||
let res = objs[0];
|
|
||||||
|
|
||||||
for (const obj of objs.slice(1)) {
|
|
||||||
if (obj == null) res = undefined as T;
|
|
||||||
else if (Array.isArray(obj)) {
|
|
||||||
if (Array.isArray(res)) res.push(...obj);
|
|
||||||
else res = obj;
|
|
||||||
}
|
|
||||||
else if (typeof obj === "object") {
|
|
||||||
if (typeof res !== "object" || res == null) {
|
|
||||||
res = obj;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (const key in obj) {
|
|
||||||
if (key in res) res[key] = merge(res[key], obj[key]);
|
|
||||||
else res[key] = obj[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else res = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readConfig() {
|
|
||||||
const objs = await Promise.all(Deno.args.map(async v => parseYaml(await Deno.readTextFile(v)) as Config));
|
|
||||||
const res = merge(...objs);
|
|
||||||
res.ignore = new Set(list(res.ignore).map(v => normalize(v)));
|
|
||||||
res.plugins = new Set(list(res.plugins).map(v => normalize(v)));
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await readConfig();
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
||||||
export let isIgnored = (path: string): boolean => {
|
|
||||||
const regexs: string[] = [];
|
|
||||||
|
|
||||||
for (let predicate of config.ignore) {
|
|
||||||
predicate = normalize(predicate);
|
|
||||||
let absolute;
|
|
||||||
|
|
||||||
if (predicate === "/") {
|
|
||||||
isIgnored = () => true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (predicate.startsWith("/")) {
|
|
||||||
absolute = true;
|
|
||||||
predicate = predicate.slice(1);
|
|
||||||
}
|
|
||||||
if (predicate.endsWith("/")) {
|
|
||||||
predicate = predicate.slice(0, -1);
|
|
||||||
}
|
|
||||||
if (predicate.startsWith("./") ||
|
|
||||||
predicate.startsWith("../") ||
|
|
||||||
predicate == ".." ||
|
|
||||||
predicate == ".") throw new Error(`'${predicate}' in ignore list includes relative specifiers`);
|
|
||||||
|
|
||||||
const body = predicate.split("/")
|
|
||||||
.filter(v => v !== "")
|
|
||||||
.map(v => v.replace(/\*\*|[.*+?^${}()|[\]\\]/g, val => {
|
|
||||||
if (val === "*") return "[^/]*";
|
|
||||||
else if (val === "**") return ".*";
|
|
||||||
else return "\\" + val;
|
|
||||||
}))
|
|
||||||
.join("/");
|
|
||||||
|
|
||||||
regexs.push(absolute ? "(^/" + body + "/)" : "(/" + body + "/)");
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = new RegExp(regexs.join("|"));
|
|
||||||
|
|
||||||
isIgnored = path => regex.test(normalize("/" + path + "/"));
|
|
||||||
return isIgnored(path);
|
|
||||||
};
|
|
||||||
11
src/debug-entry.lua
Normal file
11
src/debug-entry.lua
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
return function (...)
|
||||||
|
require "utils.printing";
|
||||||
|
pprint(require "lldebugger");
|
||||||
|
require "lldebugger".start();
|
||||||
|
|
||||||
|
local args = { ... };
|
||||||
|
|
||||||
|
return require "lldebugger".call(function ()
|
||||||
|
require "website-main" (table.unpack(args));
|
||||||
|
end);
|
||||||
|
end
|
||||||
61
src/highlight/init.lua
Normal file
61
src/highlight/init.lua
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
local slot = require "template".slot;
|
||||||
|
local e = require "template".elements;
|
||||||
|
|
||||||
|
require "highlight.lang.c";
|
||||||
|
require "highlight.lang.lua";
|
||||||
|
require "highlight.lang.python";
|
||||||
|
require "highlight.lang.py";
|
||||||
|
require "highlight.lang.javascript";
|
||||||
|
require "highlight.lang.js";
|
||||||
|
|
||||||
|
--- @param text string
|
||||||
|
--- @return template
|
||||||
|
return function (lang, text)
|
||||||
|
--- @type boolean, (fun(i: integer, src: string): integer?, string?, string?)?
|
||||||
|
local ok, tokens = pcall(require, "highlight.lang." .. lang);
|
||||||
|
if not ok then
|
||||||
|
return e.pre { class = "hl", e.code { text } }
|
||||||
|
end
|
||||||
|
|
||||||
|
local prefix = text:match "^([\t ]+)";
|
||||||
|
if prefix then
|
||||||
|
local lines = {};
|
||||||
|
|
||||||
|
for line in text:sub(#prefix + 1):gmatch "[^\n]*" do
|
||||||
|
if line:sub(1, #prefix) == prefix then
|
||||||
|
line = line:sub(#prefix + 1);
|
||||||
|
end
|
||||||
|
table.insert(lines, (line:gsub("%s+$", "")));
|
||||||
|
end
|
||||||
|
|
||||||
|
text = table.concat(lines, "\n");
|
||||||
|
end
|
||||||
|
|
||||||
|
return e.pre { class = "hl",
|
||||||
|
e.code {
|
||||||
|
slot(function (self, emit)
|
||||||
|
--- @type string?
|
||||||
|
local curr_tag;
|
||||||
|
local curr = {};
|
||||||
|
|
||||||
|
local function commit()
|
||||||
|
if #curr > 0 then
|
||||||
|
emit(e.span { class = "hl-" .. curr_tag, table.concat(curr) });
|
||||||
|
curr = {};
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, data, tag in tokens, text do
|
||||||
|
if tag ~= curr_tag then
|
||||||
|
commit();
|
||||||
|
curr_tag = tag;
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(curr, data);
|
||||||
|
end
|
||||||
|
|
||||||
|
commit();
|
||||||
|
end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
end
|
||||||
16
src/highlight/keywords.lua
Normal file
16
src/highlight/keywords.lua
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
--- @param list string[]
|
||||||
|
return function (list)
|
||||||
|
table.sort(list, function (a, b)
|
||||||
|
return #a > #b;
|
||||||
|
end);
|
||||||
|
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
return function (src, i)
|
||||||
|
for j = 1, #list do
|
||||||
|
if src:sub(i, i + #list[j] - 1) == list[j] then
|
||||||
|
return list[j];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
end
|
||||||
47
src/highlight/lang/c.lua
Normal file
47
src/highlight/lang/c.lua
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
local tokenizer = require "highlight.tokenizer";
|
||||||
|
local keywords = require "highlight.keywords";
|
||||||
|
local match_string = require "highlight.string";
|
||||||
|
|
||||||
|
return tokenizer {
|
||||||
|
{ "white", "%s+" },
|
||||||
|
|
||||||
|
{ "comment", "%/%*.-%*%/" },
|
||||||
|
{ "comment", "%/%/[^\n]*" },
|
||||||
|
{ "preproc", "#[^\n]*" },
|
||||||
|
|
||||||
|
{ "string", match_string("\"", "\\") },
|
||||||
|
{ "string", match_string("\'", "\\") },
|
||||||
|
|
||||||
|
{ "number", "0[xX][0-9a-fA-F]+" },
|
||||||
|
{ "number", "%.[0-9]+e[0-9]+" },
|
||||||
|
{ "number", "%.[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.[0-9]*e[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.?[0-9]*" },
|
||||||
|
|
||||||
|
{ "keyword", keywords {
|
||||||
|
"int", "short", "long", "char",
|
||||||
|
"unsigned", "signed",
|
||||||
|
|
||||||
|
"typedef", "struct", "union",
|
||||||
|
|
||||||
|
"return", "break", "continue", "goto", "case",
|
||||||
|
"switch", "do", "while", "for", "if", "else",
|
||||||
|
|
||||||
|
"static", "voilatile", "const", "inline",
|
||||||
|
}, "[a-zA-Z0-9_]" },
|
||||||
|
{ "operator", keywords {
|
||||||
|
"+", "-", "*", "/", "%", "^",
|
||||||
|
"&", "~", "|", "<<", ">>", "/",
|
||||||
|
"==", "!=", "<=", ">=", "<", ">", "=",
|
||||||
|
"+=", "-=", "*=", "/=", "&=", "|=", "^=", "<<=", ">>=",
|
||||||
|
"(", ")", "{", "}", "[", "]",
|
||||||
|
";", ":", ",", ".", "->",
|
||||||
|
"&&", "||", "!", "?",
|
||||||
|
"++", "--",
|
||||||
|
} },
|
||||||
|
{ "preproc", "[A-Z_][A-Z0-9_]*", "%a" },
|
||||||
|
{ "call", "([a-zA-Z_][a-zA-Z0-9_]*)%s*%(", "%a" },
|
||||||
|
{ "type", "[a-zA-Z_][a-zA-Z0-9_]*_t", "%a" },
|
||||||
|
{ "identifier", "[a-zA-Z_][a-zA-Z0-9_]*" },
|
||||||
|
{ "invalid", "." },
|
||||||
|
};
|
||||||
49
src/highlight/lang/javascript.lua
Normal file
49
src/highlight/lang/javascript.lua
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
local tokenizer = require "highlight.tokenizer";
|
||||||
|
local keywords = require "highlight.keywords";
|
||||||
|
local match_string = require "highlight.string";
|
||||||
|
|
||||||
|
return tokenizer {
|
||||||
|
{ "white", "%s+" },
|
||||||
|
|
||||||
|
{ "comment", "%/%*.-%*%/" },
|
||||||
|
{ "comment", "%/%/[^\n]*" },
|
||||||
|
|
||||||
|
{ "string", match_string("\"", "\\") },
|
||||||
|
{ "string", match_string("\'", "\\") },
|
||||||
|
{ "string", match_string("`", "\\") },
|
||||||
|
{ "string", match_string("/", "\\") },
|
||||||
|
|
||||||
|
{ "number", "0[xX][0-9a-fA-F]+" },
|
||||||
|
{ "number", "0[bB][01]+" },
|
||||||
|
{ "number", "%.[0-9]+e[0-9]+" },
|
||||||
|
{ "number", "%.[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.[0-9]*e[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.?[0-9]*" },
|
||||||
|
|
||||||
|
{ "keyword", keywords {
|
||||||
|
"var", "let", "const",
|
||||||
|
"return", "break", "continue",
|
||||||
|
|
||||||
|
"do", "while",
|
||||||
|
"switch", "case",
|
||||||
|
"for", "in", "of",
|
||||||
|
"if", "else",
|
||||||
|
"function", "class", "static",
|
||||||
|
"async", "yield", "await",
|
||||||
|
"true", "false", "null", "undefined", "this", "super",
|
||||||
|
}, "[a-zA-Z0-9_]" },
|
||||||
|
{ "operator", keywords {
|
||||||
|
"+", "-", "*", "/", "%", "^", "@",
|
||||||
|
"&", "~", "|", "<<", ">>", "//",
|
||||||
|
"==", "!=", "<=", ">=", "<", ">", "=",
|
||||||
|
"+=", "-=", "*=", "/=", "&=", "|=", "^=", "<<=", ">>=", "&&=", "||=",
|
||||||
|
"(", ")", "{", "}", "[", "]",
|
||||||
|
";", ":", ",", ".", "...", "=>",
|
||||||
|
"||", "&&", "!",
|
||||||
|
} },
|
||||||
|
{ "object", "([a-zA-Z_][a-zA-Z0-9_]*)%s*%." },
|
||||||
|
{ "type", "[A-Z][a-zA-Z0-9_]*" },
|
||||||
|
{ "call", "([a-zA-Z_][a-zA-Z0-9_]*)%s*%(" },
|
||||||
|
{ "identifier", "[a-zA-Z_][a-zA-Z0-9_]*" },
|
||||||
|
{ "invalid", "." },
|
||||||
|
};
|
||||||
1
src/highlight/lang/js.lua
Normal file
1
src/highlight/lang/js.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
return require "highlight.lang.javascript";
|
||||||
44
src/highlight/lang/lua.lua
Normal file
44
src/highlight/lang/lua.lua
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
local tokenizer = require "highlight.tokenizer";
|
||||||
|
local keywords = require "highlight.keywords";
|
||||||
|
local match_string = require "highlight.string";
|
||||||
|
|
||||||
|
return tokenizer {
|
||||||
|
{ "white", "%s+" },
|
||||||
|
|
||||||
|
{ "comment", "(%-%-%[(%=*)%[.-%]%2%])" },
|
||||||
|
{ "comment", "%-%-[^\n]*" },
|
||||||
|
|
||||||
|
{ "bigstring", "%[(%=*)%[.-%]%1%]" },
|
||||||
|
{ "string", match_string("\"", "\\") },
|
||||||
|
{ "string", match_string("\'", "\\") },
|
||||||
|
|
||||||
|
{ "number", "0[xX][0-9a-fA-F]+" },
|
||||||
|
{ "number", "%.[0-9]+e[0-9]+" },
|
||||||
|
{ "number", "%.[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.[0-9]*e[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.?[0-9]*" },
|
||||||
|
|
||||||
|
{ "keyword", keywords {
|
||||||
|
"local", "global",
|
||||||
|
"return", "break", "goto",
|
||||||
|
"and", "or", "not",
|
||||||
|
|
||||||
|
"while", "do",
|
||||||
|
"for", "in",
|
||||||
|
"if", "elseif", "else", "then",
|
||||||
|
"repeat", "until",
|
||||||
|
"function", "end",
|
||||||
|
"true", "false", "nil", "self",
|
||||||
|
}, "[a-zA-Z0-9_]" },
|
||||||
|
{ "operator", keywords {
|
||||||
|
"+", "-", "*", "/", "%", "^", "#",
|
||||||
|
"&", "~", "|", "<<", ">>", "//",
|
||||||
|
"==", "~=", "<=", ">=", "<", ">", "=",
|
||||||
|
"(", ")", "{", "}", "[", "]", "::",
|
||||||
|
";", ":", ",", ".", "..", "...",
|
||||||
|
} },
|
||||||
|
{ "object", "([a-zA-Z_][a-zA-Z0-9_]*)%s*[.:]" },
|
||||||
|
{ "call", "([a-zA-Z_][a-zA-Z0-9_]*)%s*[(\"']" },
|
||||||
|
{ "identifier", "[a-zA-Z_][a-zA-Z0-9_]*" },
|
||||||
|
{ "invalid", "." },
|
||||||
|
};
|
||||||
1
src/highlight/lang/py.lua
Normal file
1
src/highlight/lang/py.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
return require "highlight.lang.python";
|
||||||
47
src/highlight/lang/python.lua
Normal file
47
src/highlight/lang/python.lua
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
local tokenizer = require "highlight.tokenizer";
|
||||||
|
local keywords = require "highlight.keywords";
|
||||||
|
local match_string = require "highlight.string";
|
||||||
|
|
||||||
|
return tokenizer {
|
||||||
|
{ "white", "%s+" },
|
||||||
|
|
||||||
|
{ "comment", "%#[^\n]*" },
|
||||||
|
|
||||||
|
{ "string", "(f)[\"']" },
|
||||||
|
{ "string", match_string("\"", "\\") },
|
||||||
|
{ "string", match_string("\'", "\\") },
|
||||||
|
|
||||||
|
{ "number", "0[xX][0-9a-fA-F]+" },
|
||||||
|
{ "number", "%.[0-9]+e[0-9]+" },
|
||||||
|
{ "number", "%.[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.[0-9]*e[0-9]+" },
|
||||||
|
{ "number", "[0-9]+%.?[0-9]*" },
|
||||||
|
|
||||||
|
{ "keyword", keywords {
|
||||||
|
"async", "class", "def", "del", "global", "nonlocal", "lambda",
|
||||||
|
|
||||||
|
"return", "break", "continue", "goto", "yield", "await", "assert", "pass",
|
||||||
|
"try", "except", "finally", "raise",
|
||||||
|
"with", "while", "for", "if", "elif", "else",
|
||||||
|
|
||||||
|
"is", "in", "as",
|
||||||
|
|
||||||
|
"static", "voilatile", "const", "inline",
|
||||||
|
"and", "or", "not",
|
||||||
|
|
||||||
|
"True", "False", "None", "self",
|
||||||
|
}, "[a-zA-Z0-9_]" },
|
||||||
|
{ "operator", keywords {
|
||||||
|
"+", "-", "*", "/", "%", "^",
|
||||||
|
"&", "~", "|", "<<", ">>", "/",
|
||||||
|
"==", "!=", "<=", ">=", "<", ">", "=",
|
||||||
|
"+=", "-=", "*=", "/=", "&=", "|=", "^=", "<<=", ">>=",
|
||||||
|
"(", ")", "{", "}", "[", "]",
|
||||||
|
";", ":", ",", ".", "->",
|
||||||
|
"&&", "||", "!", "?",
|
||||||
|
} },
|
||||||
|
{ "type", "[A-Z][a-zA-Z0-9_]*" },
|
||||||
|
{ "call", "([a-zA-Z_][a-zA-Z0-9_]*)%s*%(", "%a" },
|
||||||
|
{ "identifier", "[a-zA-Z_][a-zA-Z0-9_]*" },
|
||||||
|
{ "invalid", "." },
|
||||||
|
};
|
||||||
21
src/highlight/string.lua
Normal file
21
src/highlight/string.lua
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
return function (quote, escape)
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
return function (src, i)
|
||||||
|
if src:sub(i, i) ~= quote then return false end
|
||||||
|
local start = i;
|
||||||
|
i = i + 1;
|
||||||
|
|
||||||
|
while i <= #src do
|
||||||
|
if src:sub(i, i) == escape then
|
||||||
|
i = i + 2;
|
||||||
|
elseif src:sub(i, i) == quote then
|
||||||
|
i = i + 1;
|
||||||
|
return src:sub(start, i - 1);
|
||||||
|
else
|
||||||
|
i = i + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
45
src/highlight/tokenizer.lua
Normal file
45
src/highlight/tokenizer.lua
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
--- @alias hl_pattern string | fun(src: string, i: integer): string? Either a lua pattern (automatically prefixed by ^), or a function that returns the size of the matched token at i
|
||||||
|
|
||||||
|
--- @param matchers { [1]: string, [2]: hl_pattern, [3]?: hl_pattern }[] 1 - tag, 2 - match pattern, 3 - end exclusion pattern
|
||||||
|
--- @return fun(src: string, i?: integer): integer?, string?, string?
|
||||||
|
return function (matchers)
|
||||||
|
return function (src, i)
|
||||||
|
if not i then i = 1 end
|
||||||
|
if not src then return nil end
|
||||||
|
if i > #src then return nil end
|
||||||
|
|
||||||
|
for j = 1, #matchers do
|
||||||
|
local tag = matchers[j][1];
|
||||||
|
local pat = matchers[j][2];
|
||||||
|
local end_pat = matchers[j][3];
|
||||||
|
|
||||||
|
local str;
|
||||||
|
|
||||||
|
if type(pat) == "string" then
|
||||||
|
str = src:match("^" .. pat, i);
|
||||||
|
else
|
||||||
|
str = pat(src, i);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @cast str string?
|
||||||
|
|
||||||
|
if str and i + #str <= #src and end_pat then
|
||||||
|
if type(end_pat) == "string" then
|
||||||
|
if src:find("^" .. end_pat, i + #str) then
|
||||||
|
str = nil;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if end_pat(src, i + #str) then
|
||||||
|
str = nil;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if str then
|
||||||
|
return i + #str, str, tag;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
end
|
||||||
509
src/http/init.lua
Normal file
509
src/http/init.lua
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
local tcp = require "sync.tcp";
|
||||||
|
local url = require "utils.url";
|
||||||
|
local sync = require "sync";
|
||||||
|
local uv = require "luv";
|
||||||
|
local stream = require "sync.stream"
|
||||||
|
|
||||||
|
local codes_msgs = {
|
||||||
|
[100] = "Continue",
|
||||||
|
[101] = "Switching Protocols",
|
||||||
|
[102] = "Processing",
|
||||||
|
[103] = "Early Hints",
|
||||||
|
|
||||||
|
[200] = "OK",
|
||||||
|
[201] = "Created",
|
||||||
|
[202] = "Accepted",
|
||||||
|
[203] = "Non-Authorative Information",
|
||||||
|
[204] = "No Content",
|
||||||
|
[205] = "Reset Content",
|
||||||
|
[206] = "Partial Content",
|
||||||
|
[207] = "Multi-Status",
|
||||||
|
[208] = "Already Reported",
|
||||||
|
[209] = "IM Used",
|
||||||
|
|
||||||
|
[300] = "Multiple Choices",
|
||||||
|
[301] = "Moved Permanently",
|
||||||
|
[302] = "Found",
|
||||||
|
[303] = "See Other",
|
||||||
|
[304] = "Not Modified",
|
||||||
|
[305] = "Use Proxy",
|
||||||
|
[306] = "Switch Proxy",
|
||||||
|
[307] = "Temporary Redirect",
|
||||||
|
[308] = "Premanent Redirect",
|
||||||
|
|
||||||
|
[400] = "Bad Request",
|
||||||
|
[401] = "Unauthorized",
|
||||||
|
[402] = "Payment Required",
|
||||||
|
[403] = "Forbidden",
|
||||||
|
[404] = "Not Found",
|
||||||
|
[405] = "Method Not Allowed",
|
||||||
|
[406] = "Not Acceptable",
|
||||||
|
[407] = "Proxy Authentication Required",
|
||||||
|
[408] = "Request Timeout",
|
||||||
|
[409] = "Conflict",
|
||||||
|
[410] = "Gone",
|
||||||
|
[411] = "Length Required",
|
||||||
|
[412] = "Precondition Failed",
|
||||||
|
[413] = "Payload Too Large",
|
||||||
|
[414] = "URI Too Long",
|
||||||
|
[415] = "Unsupported Media Type",
|
||||||
|
[416] = "Range Not Satisfiable",
|
||||||
|
[417] = "Expectation Failed",
|
||||||
|
[418] = "I'm a teapot",
|
||||||
|
[421] = "Misdirected Request",
|
||||||
|
[422] = "Unprocessable Content",
|
||||||
|
[423] = "Locked",
|
||||||
|
[424] = "Failed Dependency",
|
||||||
|
[425] = "Too Early",
|
||||||
|
[426] = "Upgrade Required",
|
||||||
|
[428] = "Precondition Required",
|
||||||
|
[429] = "Too Many Requests",
|
||||||
|
[431] = "Request Header Fields Too Large",
|
||||||
|
[451] = "Unavailable For Legal Reasons",
|
||||||
|
|
||||||
|
[500] = "Internal Server Error",
|
||||||
|
[501] = "Not Implemented",
|
||||||
|
[502] = "Bad Gateway",
|
||||||
|
[503] = "Service Unavailable",
|
||||||
|
[504] = "Gateway Timeout",
|
||||||
|
[505] = "HTTP Version Not Supported",
|
||||||
|
[506] = "Variant Also Negotiates (",
|
||||||
|
[507] = "Insufficient Storage",
|
||||||
|
[508] = "Loop Detected",
|
||||||
|
[510] = "Not Extended",
|
||||||
|
[511] = "Network Authentication Required",
|
||||||
|
};
|
||||||
|
|
||||||
|
local getaddrinfo = sync.wrap(uv.getaddrinfo);
|
||||||
|
|
||||||
|
--- @alias http_body (fun(): string?, string?) | stream | string | nil
|
||||||
|
|
||||||
|
--- @class http_headers
|
||||||
|
--- @field _map table<string, string[]>
|
||||||
|
local headers_index = {};
|
||||||
|
--- @return (fun(self: http_headers, prev?: string): string?, ...: string?), http_headers
|
||||||
|
function headers_index:iter()
|
||||||
|
--- @param prev string?
|
||||||
|
--- @param self http_headers
|
||||||
|
return function (self, prev)
|
||||||
|
local key, val = next(self._map, prev);
|
||||||
|
if not val then return nil end
|
||||||
|
return key, table.unpack(val);
|
||||||
|
end, self;
|
||||||
|
end
|
||||||
|
--- @param name string
|
||||||
|
function headers_index:get(name)
|
||||||
|
local val = self._map[name:lower()];
|
||||||
|
if not val then return end
|
||||||
|
return table.unpack(val);
|
||||||
|
end
|
||||||
|
--- @param name string
|
||||||
|
--- @return string ...
|
||||||
|
function headers_index:add(name, ...)
|
||||||
|
name = name:lower();
|
||||||
|
if not self._map[name] then self._map[name] = {} end
|
||||||
|
local val = self._map[name];
|
||||||
|
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
assert(select(i, ...), "element can't be nil");
|
||||||
|
table.insert(val, (select(i, ...)));
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.unpack(val, 1);
|
||||||
|
end
|
||||||
|
--- @param name string
|
||||||
|
--- @return string ...
|
||||||
|
function headers_index:set(name, ...)
|
||||||
|
self._map[name:lower()] = { ... };
|
||||||
|
return ...;
|
||||||
|
end
|
||||||
|
--- @param name string
|
||||||
|
--- @return string ...
|
||||||
|
function headers_index:del(name, ...)
|
||||||
|
name = name:lower();
|
||||||
|
local val = self._map[name];
|
||||||
|
self._map[name] = nil;
|
||||||
|
if val then return table.unpack(val) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local headers_meta = { __index = headers_index };
|
||||||
|
|
||||||
|
--- @class http_headers_lib
|
||||||
|
local headers = {};
|
||||||
|
--- @return http_headers
|
||||||
|
function headers.new()
|
||||||
|
return setmetatable({ _map = {} }, headers_meta);
|
||||||
|
end
|
||||||
|
--- @param init table<string, string | string[]>
|
||||||
|
function headers.of(init)
|
||||||
|
local res = headers.new();
|
||||||
|
|
||||||
|
for k, v in pairs(init) do
|
||||||
|
assert(type(k) == "string", "headers init key must be string");
|
||||||
|
if type(v) == "string" then
|
||||||
|
res:set(k, v);
|
||||||
|
else
|
||||||
|
res:set(k, table.unpack(v));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param stream stream
|
||||||
|
local function http_read_headers(stream)
|
||||||
|
local res = headers.new();
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local line, err = stream:read "L";
|
||||||
|
if not line then return nil, err or "EOF" end
|
||||||
|
|
||||||
|
if line == "\r\n" then return res end
|
||||||
|
|
||||||
|
local key, val = line:match "^(.-): ?(.*)\r\n$";
|
||||||
|
if not key then return nil, "unexpected header format" end
|
||||||
|
key = key:lower();
|
||||||
|
|
||||||
|
if val:find ", " then
|
||||||
|
local i = 1;
|
||||||
|
repeat
|
||||||
|
local l = val:find(", ", i);
|
||||||
|
res:add(key, val:sub(i, l and (l - 1) or -1));
|
||||||
|
if l then i = l + 1; end
|
||||||
|
until not l;
|
||||||
|
else
|
||||||
|
res:add(key, val);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- @param str stream
|
||||||
|
--- @param headers http_headers
|
||||||
|
--- @return stream?, string?
|
||||||
|
local function http_read_body(str, headers)
|
||||||
|
--- @type stream?
|
||||||
|
local str = str;
|
||||||
|
|
||||||
|
local len = tonumber((headers:get "content-length"));
|
||||||
|
local chunked = false;
|
||||||
|
|
||||||
|
local encoding = { headers:get "transfer-encoding" };
|
||||||
|
for i = 1, #encoding do
|
||||||
|
if encoding[i] == "chunked" then
|
||||||
|
chunked = true;
|
||||||
|
else
|
||||||
|
return nil, "unknown transfer encoding '" .. encoding[i] .. "'";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local _read;
|
||||||
|
|
||||||
|
if chunked then
|
||||||
|
function _read(cb)
|
||||||
|
if not str then return nil, "closed" end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local line, err;
|
||||||
|
|
||||||
|
line, err = str:read "L";
|
||||||
|
if not line then return nil, err or "EOF" end
|
||||||
|
|
||||||
|
local len = line:match "^([%da-zA-Z]+)\r\n$";
|
||||||
|
if not len then return nil, "malformed chunked encoding" end
|
||||||
|
len = tonumber(len, 16);
|
||||||
|
|
||||||
|
if len == 0 then break end
|
||||||
|
|
||||||
|
line, err = str:read(len);
|
||||||
|
if not line then return nil, err or "EOF" end
|
||||||
|
|
||||||
|
local term, err = str:read(2);
|
||||||
|
if not term then return nil, err or "EOF" end
|
||||||
|
if term ~= "\r\n" then return nil, "malformed chunked encoding" end
|
||||||
|
|
||||||
|
local ok, err = cb(line);
|
||||||
|
if ok == nil then return nil, err end
|
||||||
|
if not ok then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
elseif len then
|
||||||
|
function _read(cb)
|
||||||
|
if not str then return nil, "closed" end
|
||||||
|
|
||||||
|
local res, err = str:read(len);
|
||||||
|
if not res then
|
||||||
|
if not err then return true end
|
||||||
|
return nil, err;
|
||||||
|
end
|
||||||
|
len = 0;
|
||||||
|
|
||||||
|
local ok, err = cb(res);
|
||||||
|
if ok == nil then return nil, err end
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
return stream.new(
|
||||||
|
_read,
|
||||||
|
function (data)
|
||||||
|
return nil, "read-only";
|
||||||
|
end,
|
||||||
|
function ()
|
||||||
|
str = nil;
|
||||||
|
end,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @return string | "EOF" | nil method
|
||||||
|
--- @return string? path
|
||||||
|
--- @return http_headers? headers
|
||||||
|
local function http_read_req(stream)
|
||||||
|
local line, err = stream:read "L";
|
||||||
|
if not line then
|
||||||
|
if err then return nil, err end
|
||||||
|
return "EOF";
|
||||||
|
end
|
||||||
|
|
||||||
|
local type, path, version = line:match "^(%S-) (%S-) HTTP/(%S-)\r\n$";
|
||||||
|
if not type then return nil, "unexpected format" end
|
||||||
|
if version ~= "1.1" then return nil, "only HTTP 1.1 supported" end
|
||||||
|
|
||||||
|
local headers, err = http_read_headers(stream);
|
||||||
|
if not headers then return nil, err end
|
||||||
|
|
||||||
|
return type, path, headers;
|
||||||
|
end
|
||||||
|
--- @return integer | "EOF" | nil code
|
||||||
|
--- @return http_headers | string? headers
|
||||||
|
local function http_read_res(stream)
|
||||||
|
local line, err = stream:read "L";
|
||||||
|
if not line then
|
||||||
|
if err then return nil, err end
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
local version, code = line:match "^HTTP/(%S-) (%S-) (.-)\r\n$";
|
||||||
|
if not version then return nil, "unexpected format" end
|
||||||
|
if version ~= "1.1" then return nil, "only HTTP 1.1 supported" end
|
||||||
|
|
||||||
|
local headers, err = http_read_headers(stream);
|
||||||
|
if not headers then return nil, err end
|
||||||
|
|
||||||
|
return code, headers;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param stream stream
|
||||||
|
--- @param key? string
|
||||||
|
--- @param ... string
|
||||||
|
local function http_write_header(stream, key, ...)
|
||||||
|
if not key then
|
||||||
|
return stream:write "\r\n";
|
||||||
|
else
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local res, err = stream:write(("%s: %s\r\n"):format(key, (select(i, ...))));
|
||||||
|
if not res then return nil, err end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- @param stream stream
|
||||||
|
--- @param headers http_headers
|
||||||
|
local function http_write_headers(stream, headers)
|
||||||
|
local it = headers:iter();
|
||||||
|
local function iterator(stream, headers, it, k, ...)
|
||||||
|
if not k then
|
||||||
|
return http_write_header(stream);
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, err = http_write_header(stream, k, ...);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
return iterator(stream, headers, it, it(headers, k));
|
||||||
|
end
|
||||||
|
return iterator(stream, headers, it, it(headers, nil));
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param str stream
|
||||||
|
local function http_setup_body(str, body)
|
||||||
|
if not body then return true end
|
||||||
|
|
||||||
|
local _, err = str:write(("transfer-encoding: chunked\r\n"));
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
return stream.new(
|
||||||
|
function () return nil, "write-only" end,
|
||||||
|
function (data)
|
||||||
|
if str == nil then return nil, "closed" end
|
||||||
|
if #data ~= 0 then
|
||||||
|
return str:write(("%x\r\n"):format(#data), data, "\r\n");
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function ()
|
||||||
|
if str then
|
||||||
|
local ok, err = str:write("0\r\n\r\n");
|
||||||
|
--- @diagnostic disable-next-line
|
||||||
|
str = nil;
|
||||||
|
return ok, err;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param stream stream
|
||||||
|
--- @param method string
|
||||||
|
--- @param path string
|
||||||
|
--- @param headers http_headers
|
||||||
|
--- @param body? false
|
||||||
|
--- @return true?, string?
|
||||||
|
--- @overload fun(stream: stream, method: string, path: string, headers: http_headers, body: true): stream?, string?
|
||||||
|
local function http_write_req(stream, method, path, headers, body)
|
||||||
|
local _, err = stream:write(("%s %s HTTP/1.1\r\n"):format(method, path));
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
local body_str, err = http_setup_body(stream);
|
||||||
|
if not body_str then return nil, err end
|
||||||
|
|
||||||
|
local _, err = http_write_headers(stream, headers);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
return body_str;
|
||||||
|
end
|
||||||
|
--- @param stream stream
|
||||||
|
--- @param code integer
|
||||||
|
--- @param headers http_headers
|
||||||
|
--- @param body? false
|
||||||
|
--- @return true?, string?
|
||||||
|
--- @overload fun(stream: stream, code: integer, headers: http_headers, body: true): stream?, string?
|
||||||
|
local function http_write_res(stream, code, headers, body)
|
||||||
|
local _, err = stream:write(("HTTP/1.1 %d %s\r\n"):format(code, codes_msgs[code] or "Unknown"));
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
local body_str, err = http_setup_body(stream, body);
|
||||||
|
if not body_str then return nil, err end
|
||||||
|
|
||||||
|
local _, err = http_write_headers(stream, headers);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
return body_str;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function http_write_body(stream, body)
|
||||||
|
if type(body) == "function" then
|
||||||
|
local str <close> = stream;
|
||||||
|
--- @cast str stream
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local data, err = body();
|
||||||
|
if not data then
|
||||||
|
if err then return nil, err end
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, err = str:write(data);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
end
|
||||||
|
elseif type(body) == "string" then
|
||||||
|
local str <close> = stream;
|
||||||
|
--- @cast str stream
|
||||||
|
str:write(body);
|
||||||
|
elseif body then
|
||||||
|
local str <close> = stream;
|
||||||
|
--- @cast str stream
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local data, err = body:read "c";
|
||||||
|
|
||||||
|
if not data or #data == 0 then
|
||||||
|
if err then return nil, err end
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, err = str:write(data);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param arg { url: string, method?: string, headers?: http_headers, body?: http_body }
|
||||||
|
local function http_fetch(arg)
|
||||||
|
local parsed = assert(url.parse(arg.url));
|
||||||
|
if not parsed.scheme then return nil, "scheme must be specified" end
|
||||||
|
if parsed.scheme ~= "http" then return nil, "only http supported" end
|
||||||
|
if not parsed.host then return nil, "host must be specified" end
|
||||||
|
if parsed.username then return nil, "username and password not supported" end
|
||||||
|
|
||||||
|
--- @type uv.aliases.getaddrinfo_rtn?, string?
|
||||||
|
local dns_res, err = getaddrinfo(parsed.host, nil, nil);
|
||||||
|
if not dns_res then return nil, err or "unable to resolve host" end
|
||||||
|
|
||||||
|
local path = url.encode_url(parsed.path);
|
||||||
|
local params = url.encode_body(parsed.params);
|
||||||
|
if #params > 0 then path = path .. "?" .. params end
|
||||||
|
|
||||||
|
if not arg.headers then
|
||||||
|
arg.headers = headers.new();
|
||||||
|
end
|
||||||
|
|
||||||
|
if not arg.headers:get "host" then
|
||||||
|
arg.headers:add("host", parsed.host);
|
||||||
|
end
|
||||||
|
|
||||||
|
local conn;
|
||||||
|
for i = 1, #dns_res do
|
||||||
|
conn, err = tcp():connect(dns_res[i].addr, parsed.port or dns_res[i].port or 80);
|
||||||
|
if conn then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not conn then return nil, err end
|
||||||
|
|
||||||
|
local body_out, err = http_write_req(conn, arg.method or "GET", path, arg.headers, arg.body ~= nil);
|
||||||
|
if not body_out then return nil, err end
|
||||||
|
|
||||||
|
if arg.body then
|
||||||
|
local _, err = http_write_body(body_out, arg.body);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
end
|
||||||
|
|
||||||
|
local code, headers = http_read_res(conn);
|
||||||
|
if not code then return nil, headers end
|
||||||
|
|
||||||
|
local body, err = http_read_body(conn, headers --[[@as http_headers]]);
|
||||||
|
if not body and err then return nil, err end
|
||||||
|
|
||||||
|
return code, headers, body;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param stream stream
|
||||||
|
--- @param code? integer
|
||||||
|
--- @param hdrs? http_headers
|
||||||
|
--- @param body? http_body
|
||||||
|
local function http_respond(stream, code, hdrs, body)
|
||||||
|
local body_out, err = http_write_res(stream, code or 200, hdrs or headers.new(), body ~= nil);
|
||||||
|
if not body_out then return nil, err end
|
||||||
|
|
||||||
|
if body then
|
||||||
|
local _, err = http_write_body(body_out, body);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
end
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
read_req = http_read_req,
|
||||||
|
read_res = http_read_res,
|
||||||
|
read_body = http_read_body,
|
||||||
|
|
||||||
|
write_req = http_write_req,
|
||||||
|
write_res = http_write_res,
|
||||||
|
|
||||||
|
fetch = http_fetch,
|
||||||
|
respond = http_respond,
|
||||||
|
|
||||||
|
headers = headers,
|
||||||
|
};
|
||||||
14
src/lib.ts
14
src/lib.ts
@@ -1,14 +0,0 @@
|
|||||||
import View, { getLocals, type Locals } from "./views/View.ts";
|
|
||||||
import config, { isIgnored } from "./config.ts";
|
|
||||||
import { list } from "./main.ts";
|
|
||||||
import { getPlugin, type Plugin } from "./plugins.ts";
|
|
||||||
import { resolveViewFromUri, resolveView } from "./views/resolution.ts";
|
|
||||||
|
|
||||||
export { View, config, isIgnored, list, getPlugin, resolveView, resolveViewFromUri, getLocals, type Plugin, type Locals };
|
|
||||||
|
|
||||||
export default {
|
|
||||||
View,
|
|
||||||
config,
|
|
||||||
isIgnored,
|
|
||||||
get locals() { return getLocals(); }
|
|
||||||
}
|
|
||||||
114
src/main.ts
114
src/main.ts
@@ -1,114 +0,0 @@
|
|||||||
import "./plugins.ts";
|
|
||||||
|
|
||||||
import config from "./config.ts";
|
|
||||||
import { serveDir } from "@std/http";
|
|
||||||
import { normalize, resolve } from "@std/path/posix";
|
|
||||||
import { renderView } from "./views/resolution.ts";
|
|
||||||
import { loadPlugins } from "./plugins.ts";
|
|
||||||
|
|
||||||
export function list(data: string | Iterable<string> | null | undefined): string[] {
|
|
||||||
if (Array.isArray(data)) return data.flatMap(list);
|
|
||||||
else if (typeof data === "string") return String(data ?? "")
|
|
||||||
.split(/[,;]/)
|
|
||||||
.map(v => v.trim())
|
|
||||||
.filter(v => v !== "");
|
|
||||||
else return [...(data ?? [])].flatMap(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function main() {
|
|
||||||
await loadPlugins();
|
|
||||||
|
|
||||||
Deno.addSignalListener("SIGINT", async () => {
|
|
||||||
console.log("Stopping server");
|
|
||||||
server.shutdown();
|
|
||||||
Deno.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = Deno.serve({ port: 8080 }, async req => {
|
|
||||||
try {
|
|
||||||
const url = new URL(req.url);
|
|
||||||
|
|
||||||
if (url.pathname === "/.well-known/keepalive") {
|
|
||||||
return new Response("awake and alive");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.static != null) {
|
|
||||||
const res = await serveDir(req, {
|
|
||||||
showIndex: false,
|
|
||||||
fsRoot: resolve(config.static),
|
|
||||||
showDotfiles: false,
|
|
||||||
quiet: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 404) return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = normalize("/" + url.pathname);
|
|
||||||
const view = (renderView(path, { params: url.searchParams }, true))?.text;
|
|
||||||
if (view != null) return new Response(view, { headers: { "content-type": "text/html" } });
|
|
||||||
|
|
||||||
if (config.notFound != null && req.method === "GET") {
|
|
||||||
const view = (renderView(config.notFound, { params: url.searchParams }, true))?.text;
|
|
||||||
if (view != null) return new Response(view, { headers: { "content-type": "text/html" }, status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response("Not found", { status: 404 });
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(String(e));
|
|
||||||
if (config.debug) {
|
|
||||||
return new Response("Internal error: " + String(e), { status: 500 });
|
|
||||||
}
|
|
||||||
else return new Response("Internal error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.main) {
|
|
||||||
main();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// const app = express();
|
|
||||||
// export const lib = { View, PugView, MarkdownView, JSView, list, merge };
|
|
||||||
|
|
||||||
// app.use('/.well-known/keepalive', (req, res) => void res.send("awake and alive"));
|
|
||||||
|
|
||||||
// if (config.static != null) app.use(express.static(config.static));
|
|
||||||
|
|
||||||
// if (config.views != null) app.get("/*", (req, res, next) => {
|
|
||||||
// const path = normalize("/" + req.path);
|
|
||||||
|
|
||||||
// // for (const prefix of config.ignore) {
|
|
||||||
// // if (path.startsWith(prefix)) {
|
|
||||||
// // return next();
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// const view = View.render(path, { query: req.query, params: req.params }, true);
|
|
||||||
|
|
||||||
// if (view == null) next();
|
|
||||||
// // else if (view.cached) res.sendStatus(304);
|
|
||||||
// else res.send(view.text);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (config.views != null) app.use((req, res) => {
|
|
||||||
// res.status(404);
|
|
||||||
// if (req.method === "GET") {
|
|
||||||
// console.log(config.notFound);
|
|
||||||
// const view = View.render(config.notFound, { query: req.query, params: req.params }, true, true);
|
|
||||||
|
|
||||||
// if (view == null) res.send("Not found :/");
|
|
||||||
// // else if (view.cached) res.sendStatus(304);
|
|
||||||
// else res.send(view.text);
|
|
||||||
// }
|
|
||||||
// else res.send("Not found :/");
|
|
||||||
// });
|
|
||||||
// else app.use((req, res) => {
|
|
||||||
// res.sendStatus(404);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// app.listen(8080, () => console.log("Listening!"));
|
|
||||||
|
|
||||||
|
|
||||||
// // Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
|
|
||||||
488
src/markdown.lua
Normal file
488
src/markdown.lua
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
--- @diagnostic disable: cast-local-type
|
||||||
|
|
||||||
|
local e = require "template".elements;
|
||||||
|
local slot = require "template".slot;
|
||||||
|
local highlight = require "highlight";
|
||||||
|
|
||||||
|
local parse;
|
||||||
|
|
||||||
|
local PREC_TEXT = 3;
|
||||||
|
local PREC_ELEMENT = 2;
|
||||||
|
local PREC_PARAGRAPH = 1;
|
||||||
|
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
--- @param detect? boolean
|
||||||
|
local function skip_white(src, i, indent, detect)
|
||||||
|
local j = i;
|
||||||
|
local detected;
|
||||||
|
|
||||||
|
while true do
|
||||||
|
if j == 1 or src:sub(j - 1, j - 1) == "\n" then
|
||||||
|
if not src:find("^" .. indent, j) then return i end
|
||||||
|
j = j + #indent;
|
||||||
|
|
||||||
|
if detect then
|
||||||
|
detected = src:match("^[\t ]+", j);
|
||||||
|
if not detected then return i end
|
||||||
|
j = j + #detected;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, e;
|
||||||
|
|
||||||
|
_, e = src:find("^[ \t]*", j);
|
||||||
|
if not e then break end
|
||||||
|
j = e + 1;
|
||||||
|
|
||||||
|
_, e = src:find("^\n", j);
|
||||||
|
if not e then break end
|
||||||
|
j = e + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
return j, detected or indent;
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
--- @param eof string
|
||||||
|
local function parse_raw(src, i, indent, eof)
|
||||||
|
local j = i;
|
||||||
|
|
||||||
|
local parts = {};
|
||||||
|
|
||||||
|
while true do
|
||||||
|
if j == 1 or src:sub(j - 1, j - 1) == "\n" then
|
||||||
|
if not src:find("^" .. indent, j) then return i end
|
||||||
|
j = j + #indent;
|
||||||
|
end
|
||||||
|
|
||||||
|
local s, e = src:find(eof, j);
|
||||||
|
if not s or not e then return i end
|
||||||
|
|
||||||
|
local nl_i = src:find("\n", j) or (#src + 1);
|
||||||
|
|
||||||
|
if e <= nl_i then
|
||||||
|
local n = 0;
|
||||||
|
|
||||||
|
for i = s - 1, 1, -1 do
|
||||||
|
if src:sub(i, i) == "\\" then
|
||||||
|
n = n + 1;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if n >= 2 then
|
||||||
|
table.insert(parts, ("\\"):rep(n // 2));
|
||||||
|
end
|
||||||
|
|
||||||
|
if n % 2 ~= 0 then
|
||||||
|
table.insert(parts, src:sub(j, s - n - 1));
|
||||||
|
table.insert(parts, src:sub(s, e));
|
||||||
|
j = e + 1;
|
||||||
|
else
|
||||||
|
table.insert(parts, src:sub(j, s - n - 1));
|
||||||
|
j = e + 1;
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(parts, src:sub(j, nl_i - 1) .. "\n");
|
||||||
|
j = nl_i + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return j, table.concat(parts);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_paragraph(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data;
|
||||||
|
|
||||||
|
j, data = parse(src, j, indent, PREC_TEXT, "\n\n", true);
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
return j, e.p { data };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_heading(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data;
|
||||||
|
|
||||||
|
local prefix = src:match("^#+", j);
|
||||||
|
if not prefix then return i end
|
||||||
|
j = j + #prefix;
|
||||||
|
|
||||||
|
j, data = parse(src, j, indent, PREC_TEXT, "\n", true);
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
if #prefix == 1 then
|
||||||
|
return j, e.h1 { data };
|
||||||
|
elseif #prefix == 2 then
|
||||||
|
return j, e.h2 { data };
|
||||||
|
elseif #prefix == 3 then
|
||||||
|
return j, e.h3 { data };
|
||||||
|
elseif #prefix == 4 then
|
||||||
|
return j, e.h4 { data };
|
||||||
|
elseif #prefix == 5 then
|
||||||
|
return j, e.h5 { data };
|
||||||
|
else
|
||||||
|
return j, e.h6 { data };
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_bold(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data;
|
||||||
|
|
||||||
|
if not src:find("^%*%*", j) then return i end
|
||||||
|
j = j + 2;
|
||||||
|
|
||||||
|
j, data = parse(src, j, indent, PREC_TEXT, "%*%*");
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
return j, e.strong { data };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_italic(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data;
|
||||||
|
|
||||||
|
if not src:find("^%*", j) then return i end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
j, data = parse(src, j, indent, PREC_TEXT, "%*");
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
return j, e.em { data };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_strike(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data;
|
||||||
|
|
||||||
|
if not src:find("^~", j) then return i end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
j, data = parse(src, j, indent, PREC_TEXT, "~");
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
return j, e.s { data };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_link(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data, url;
|
||||||
|
|
||||||
|
if not src:find("^%[", j) then return i end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
j, data = parse(src, j, indent, PREC_TEXT, "%]");
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
j, indent = skip_white(src, j, indent);
|
||||||
|
if not indent then return i end
|
||||||
|
|
||||||
|
if not src:find("^%(", j) then return i end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
j, url = parse_raw(src, j, indent, "%)");
|
||||||
|
if not url then return i end
|
||||||
|
|
||||||
|
return j, e.a { href = url, data };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_img(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local alt, url;
|
||||||
|
|
||||||
|
if not src:find("^!%[", j) then return i end
|
||||||
|
j = j + 2;
|
||||||
|
|
||||||
|
j, alt = parse_raw(src, j, indent, "%]");
|
||||||
|
if not alt then return i end
|
||||||
|
|
||||||
|
j, indent = skip_white(src, j, indent);
|
||||||
|
if not indent then return i end
|
||||||
|
|
||||||
|
if not src:find("^%(", j) then return i end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
j, url = parse_raw(src, j, indent, "%)");
|
||||||
|
if not url then return i end
|
||||||
|
|
||||||
|
return j, e.img { src = url, alt = alt };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_code(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local data;
|
||||||
|
|
||||||
|
if not src:find("^`", j) then return i end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
j, data = parse_raw(src, j, indent, "`");
|
||||||
|
if not j then return i end
|
||||||
|
|
||||||
|
return j, e.code { data };
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_codeblock(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local lang, data;
|
||||||
|
|
||||||
|
if not src:find("^```", j) then return i end
|
||||||
|
j = j + 3;
|
||||||
|
|
||||||
|
j, lang = parse_raw(src, j, indent, "\n");
|
||||||
|
if not lang then return i end
|
||||||
|
|
||||||
|
j, data = parse_raw(src, j, indent, "```");
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
return j, highlight(lang, data);
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_bulletlist(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local parts = {};
|
||||||
|
|
||||||
|
while true do
|
||||||
|
if not src:find("^-[\t ]", j) then break end
|
||||||
|
j = j + 2;
|
||||||
|
|
||||||
|
local data;
|
||||||
|
j, data = parse(src, j, indent, PREC_ELEMENT, nil, true, true);
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
table.insert(parts, e.li { data });
|
||||||
|
|
||||||
|
j, indent = skip_white(src, j, indent);
|
||||||
|
if not indent then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #parts == 0 then return i end
|
||||||
|
|
||||||
|
return j, e.ul(parts);
|
||||||
|
end
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
local function parse_numlist(src, i, indent)
|
||||||
|
local j = i;
|
||||||
|
local parts = {};
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local num = src:match("^(%d+)%.[\t ]", j);
|
||||||
|
if not num then break end
|
||||||
|
j = j + #num + 2;
|
||||||
|
|
||||||
|
local data;
|
||||||
|
j, data = parse(src, j, indent, PREC_ELEMENT, nil, true, true);
|
||||||
|
if not data then return i end
|
||||||
|
|
||||||
|
table.insert(parts, e.li { data });
|
||||||
|
|
||||||
|
j, indent = skip_white(src, j, indent);
|
||||||
|
if not indent then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #parts == 0 then return i end
|
||||||
|
|
||||||
|
return j, e.ol(parts);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_one(src, i, indent, precedence)
|
||||||
|
local res;
|
||||||
|
if precedence <= PREC_ELEMENT and not res then i, res = parse_heading(src, i, indent) end
|
||||||
|
if precedence <= PREC_ELEMENT and not res then i, res = parse_bulletlist(src, i, indent) end
|
||||||
|
if precedence <= PREC_ELEMENT and not res then i, res = parse_numlist(src, i, indent) end
|
||||||
|
if precedence <= PREC_PARAGRAPH and not res then i, res = parse_paragraph(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_bold(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_italic(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_strike(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_img(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_link(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_codeblock(src, i, indent) end
|
||||||
|
if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_code(src, i, indent) end
|
||||||
|
-- if precedence ~= PREC_PARAGRAPH and not res then i, res = parse_word(src, i) end
|
||||||
|
|
||||||
|
return i, res;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param src string
|
||||||
|
--- @param i integer
|
||||||
|
--- @param indent string
|
||||||
|
--- @param precedence integer
|
||||||
|
--- @param eof? string
|
||||||
|
--- @param lax_eof? boolean
|
||||||
|
--- @return integer, template?
|
||||||
|
function parse(src, i, indent, precedence, eof, lax_eof, detect)
|
||||||
|
local j = i;
|
||||||
|
|
||||||
|
local elements = {};
|
||||||
|
local last_space = false;
|
||||||
|
|
||||||
|
while true do
|
||||||
|
if detect then
|
||||||
|
local _j, _indent = skip_white(src, j, indent, detect);
|
||||||
|
if not _indent then
|
||||||
|
break;
|
||||||
|
elseif _indent ~= indent then
|
||||||
|
indent = _indent;
|
||||||
|
detect = false;
|
||||||
|
end
|
||||||
|
|
||||||
|
if _j > j then
|
||||||
|
j = _j;
|
||||||
|
if not last_space then
|
||||||
|
table.insert(elements, " ");
|
||||||
|
end
|
||||||
|
last_space = false;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local _j, _indent = skip_white(src, j, indent, detect);
|
||||||
|
if not _indent then break end
|
||||||
|
indent = _indent;
|
||||||
|
if _j > j then
|
||||||
|
j = _j;
|
||||||
|
if not last_space then
|
||||||
|
table.insert(elements, " ");
|
||||||
|
end
|
||||||
|
last_space = false;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if j > #src then
|
||||||
|
if not lax_eof then return i end
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
|
||||||
|
local eof_s, eof_e;
|
||||||
|
if eof then
|
||||||
|
eof_s, eof_e = src:find(eof, j);
|
||||||
|
end
|
||||||
|
eof_s = eof_s or #src + 1;
|
||||||
|
eof_e = eof_e or #src + 1;
|
||||||
|
|
||||||
|
local word = {};
|
||||||
|
|
||||||
|
while j < eof_s do
|
||||||
|
local c = src:sub(j, j);
|
||||||
|
if c == "\\" then
|
||||||
|
j = j + 1;
|
||||||
|
c = src:sub(j, j);
|
||||||
|
table.insert(word, c);
|
||||||
|
|
||||||
|
if not c:match "%s" then
|
||||||
|
j = j + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
|
||||||
|
local res;
|
||||||
|
j, res = parse_one(src, j, indent, precedence);
|
||||||
|
if res then
|
||||||
|
if eof then
|
||||||
|
eof_s, eof_e = src:find(eof, j);
|
||||||
|
end
|
||||||
|
eof_s = eof_s or #src + 1;
|
||||||
|
eof_e = eof_e or #src + 1;
|
||||||
|
|
||||||
|
if #word > 0 then
|
||||||
|
table.insert(elements, table.concat(word));
|
||||||
|
word = {};
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(elements, res);
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
|
||||||
|
if c:match "%s" then
|
||||||
|
table.insert(word, " ");
|
||||||
|
last_space = true;
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
j = j + 1;
|
||||||
|
|
||||||
|
table.insert(word, c);
|
||||||
|
precedence = PREC_TEXT;
|
||||||
|
end
|
||||||
|
|
||||||
|
if #word > 0 then
|
||||||
|
table.insert(elements, table.concat(word));
|
||||||
|
end
|
||||||
|
|
||||||
|
if j >= eof_s then
|
||||||
|
j = eof_e + 1;
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return j, slot(elements);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param raw string
|
||||||
|
--- @return template
|
||||||
|
--- @return table meta
|
||||||
|
return function (raw)
|
||||||
|
local i = 1;
|
||||||
|
local res;
|
||||||
|
local meta = {};
|
||||||
|
|
||||||
|
i = skip_white(raw, i, "");
|
||||||
|
if raw:match "^%-%-%-" then
|
||||||
|
i = i + 4;
|
||||||
|
local raw_meta;
|
||||||
|
|
||||||
|
i, raw_meta = parse_raw(raw, i, "", "%-%-%-");
|
||||||
|
if not raw_meta then
|
||||||
|
i = 1;
|
||||||
|
else
|
||||||
|
assert(load(raw_meta, "=<md-meta>", "t", setmetatable({}, {
|
||||||
|
__index = function (_, k)
|
||||||
|
if meta[k] then
|
||||||
|
return meta[k];
|
||||||
|
else
|
||||||
|
return _ENV[k];
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__newindex = meta,
|
||||||
|
})))();
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return slot(function (self, emit)
|
||||||
|
|
||||||
|
while i <= #raw do
|
||||||
|
i = skip_white(raw, i, "");
|
||||||
|
|
||||||
|
i, res = parse_one(raw, i, "", PREC_PARAGRAPH);
|
||||||
|
if not res then error "invalid markdown syntax" end
|
||||||
|
emit(res);
|
||||||
|
end
|
||||||
|
end), meta;
|
||||||
|
end
|
||||||
81
src/mime-types.lua
Normal file
81
src/mime-types.lua
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
return {
|
||||||
|
aac = "audio/aac",
|
||||||
|
abw = "application/x-abiword",
|
||||||
|
apng = "image/apng",
|
||||||
|
arc = "application/x-freearc",
|
||||||
|
avif = "image/avif",
|
||||||
|
avi = "video/x-msvideo",
|
||||||
|
azw = "application/vnd.amazon.ebook",
|
||||||
|
bin = "application/octet-stream",
|
||||||
|
bmp = "image/bmp",
|
||||||
|
bz = "application/x-bzip",
|
||||||
|
bz2 = "application/x-bzip2",
|
||||||
|
cda = "application/x-cdf",
|
||||||
|
csh = "application/x-csh",
|
||||||
|
css = "text/css",
|
||||||
|
csv = "text/csv",
|
||||||
|
doc = "application/msword",
|
||||||
|
docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
eot = "application/vnd.ms-fontobject",
|
||||||
|
epub = "application/epub+zip",
|
||||||
|
gz = "application/gzip",
|
||||||
|
gif = "image/gif",
|
||||||
|
htm = "text/html",
|
||||||
|
html = "text/html",
|
||||||
|
ico = "image/vnd.microsoft.icon",
|
||||||
|
ics = "text/calendar",
|
||||||
|
jar = "application/java-archive",
|
||||||
|
jpg = "image/jpeg",
|
||||||
|
jpeg = "image/jpeg",
|
||||||
|
js = "text/javascript",
|
||||||
|
json = "application/json",
|
||||||
|
jsonld = "application/ld+json",
|
||||||
|
md = "text/markdown",
|
||||||
|
mid = "audio/midi",
|
||||||
|
midi = "audio/midi",
|
||||||
|
mjs = "text/javascript",
|
||||||
|
mp3 = "audio/mpeg",
|
||||||
|
mp4 = "video/mp4",
|
||||||
|
mpeg = "video/mpeg",
|
||||||
|
mpkg = "application/vnd.apple.installer+xml",
|
||||||
|
odp = "application/vnd.oasis.opendocument.presentation",
|
||||||
|
ods = "application/vnd.oasis.opendocument.spreadsheet",
|
||||||
|
odt = "application/vnd.oasis.opendocument.text",
|
||||||
|
oga = "audio/ogg",
|
||||||
|
ogv = "video/ogg",
|
||||||
|
ogx = "application/ogg",
|
||||||
|
opus = "audio/ogg",
|
||||||
|
otf = "font/otf",
|
||||||
|
png = "image/png",
|
||||||
|
pdf = "application/pdf",
|
||||||
|
php = "application/x-httpd-php",
|
||||||
|
ppt = "application/vnd.ms-powerpoint",
|
||||||
|
pptx = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
rar = "application/vnd.rar",
|
||||||
|
rtf = "application/rtf",
|
||||||
|
sh = "application/x-sh",
|
||||||
|
svg = "image/svg+xml",
|
||||||
|
tar = "application/x-tar",
|
||||||
|
tiff = "image/tiff",
|
||||||
|
tif = "image/tiff",
|
||||||
|
ts = "video/mp2t",
|
||||||
|
ttf = "font/ttf",
|
||||||
|
txt = "text/plain",
|
||||||
|
vsd = "application/vnd.visio",
|
||||||
|
wav = "audio/wav",
|
||||||
|
weba = "audio/webm",
|
||||||
|
webm = "video/webm",
|
||||||
|
webmanifest = "application/manifest+json",
|
||||||
|
webp = "image/webp",
|
||||||
|
woff = "font/woff",
|
||||||
|
woff2 = "font/woff2",
|
||||||
|
xhtml = "application/xhtml+xml",
|
||||||
|
xls = "application/vnd.ms-excel",
|
||||||
|
xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
xml = "application/xml",
|
||||||
|
xul = "application/vnd.mozilla.xul+xml",
|
||||||
|
zip = "application/zip",
|
||||||
|
["3gp"] = "video/3gpp",
|
||||||
|
["3g2"] = "video/3gpp2",
|
||||||
|
["7z"] = "application/x-7z-compressed",
|
||||||
|
};
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { resolve } from "@std/path/posix";
|
|
||||||
import type View from "./views/View.ts";
|
|
||||||
import config from "./config.ts";
|
|
||||||
|
|
||||||
export type Plugin = (this: View, ...args: unknown[]) => unknown;
|
|
||||||
|
|
||||||
const plugins = new Map<string, Plugin>();
|
|
||||||
let loaded = false;
|
|
||||||
|
|
||||||
export function getPlugin(name: string) {
|
|
||||||
const res = plugins.get(name);
|
|
||||||
if (res == null) throw new Error(`Plugin ${name} not loaded (maybe you forgot to include it in the config?)`);
|
|
||||||
else return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadPlugins() {
|
|
||||||
if (loaded) return;
|
|
||||||
loaded = true;
|
|
||||||
|
|
||||||
for (const plugin of config.plugins) {
|
|
||||||
const func = (await import(resolve(config.libs + "/" + plugin + ".ts"))).default;
|
|
||||||
plugins.set(plugin, func);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
77
src/resolve.lua
Normal file
77
src/resolve.lua
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
local fs = require "sync.fs";
|
||||||
|
local http = require "http";
|
||||||
|
local markdown = require "markdown";
|
||||||
|
|
||||||
|
--- @class page
|
||||||
|
--- @field template template
|
||||||
|
--- @field meta table
|
||||||
|
--- @field config table
|
||||||
|
--- @field path string
|
||||||
|
--- @field file string
|
||||||
|
|
||||||
|
--- @param config table
|
||||||
|
---@param path string
|
||||||
|
local function resolve(config, path)
|
||||||
|
local file = package.searchpath(path, config.luapages, "/");
|
||||||
|
if file then
|
||||||
|
--- @return page
|
||||||
|
return function ()
|
||||||
|
local f = assert(fs.open(file));
|
||||||
|
local data = assert(f:read "a");
|
||||||
|
assert(f:close());
|
||||||
|
|
||||||
|
--- @type page
|
||||||
|
local t = assert(load(data, "@" .. file, "t"))();
|
||||||
|
if not t then error(file .. " must export a function") end
|
||||||
|
|
||||||
|
if type(t) == "function" then
|
||||||
|
return {
|
||||||
|
meta = {},
|
||||||
|
template = t,
|
||||||
|
path = path,
|
||||||
|
file = file,
|
||||||
|
config = config,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return {
|
||||||
|
meta = t.meta,
|
||||||
|
template = t.template,
|
||||||
|
path = path,
|
||||||
|
file = file,
|
||||||
|
config = config,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
end, file;
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = package.searchpath(path, config.pages, "/");
|
||||||
|
if file then
|
||||||
|
--- @return page
|
||||||
|
return function ()
|
||||||
|
local f = assert(fs.open(file));
|
||||||
|
local data = assert(f:read "a");
|
||||||
|
assert(f:close());
|
||||||
|
|
||||||
|
local res, meta = markdown(data);
|
||||||
|
local base = require(meta.base or config.pages_base);
|
||||||
|
local args = { content = { res } };
|
||||||
|
|
||||||
|
if meta.args then
|
||||||
|
for k, v in pairs(meta.args) do
|
||||||
|
args[k] = v;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta = meta,
|
||||||
|
template = base(args),
|
||||||
|
path = path,
|
||||||
|
file = file,
|
||||||
|
config = config,
|
||||||
|
};
|
||||||
|
end, file;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return resolve;
|
||||||
5
src/sync/entry.lua
Normal file
5
src/sync/entry.lua
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
local uv = require "luv";
|
||||||
|
|
||||||
|
_ENV.uv = uv;
|
||||||
|
|
||||||
|
return require "sync".main_wrap;
|
||||||
203
src/sync/fs.lua
Normal file
203
src/sync/fs.lua
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
local uv = require "luv";
|
||||||
|
local sync = require "sync";
|
||||||
|
--- @type fun(fd: integer, pos?: integer, whence?: "set" | "cur" | "end"): integer?, string?
|
||||||
|
local seek = require "seek";
|
||||||
|
local stream = require "sync.stream"
|
||||||
|
|
||||||
|
--- @class file
|
||||||
|
--- @field _fd integer
|
||||||
|
local file_index = {};
|
||||||
|
|
||||||
|
local read = sync.wrap(uv.fs_read, "fromcb");
|
||||||
|
local write = sync.wrap(uv.fs_write, "dummytrue");
|
||||||
|
local open = sync.wrap(uv.fs_open, "fromcb");
|
||||||
|
local close = sync.wrap(uv.fs_close, "dummytrue");
|
||||||
|
local opendir = sync.wrap(uv.fs_opendir, "fromcb");
|
||||||
|
local readdir = sync.wrap(uv.fs_readdir, "fromcb");
|
||||||
|
local closedir = sync.wrap(uv.fs_closedir, "fromcb");
|
||||||
|
|
||||||
|
local stat = sync.wrap(uv.fs_stat, "fromcb");
|
||||||
|
|
||||||
|
--- @param fmt "c" | "a" | "l" | "L" | integer
|
||||||
|
function file_index:read(fmt)
|
||||||
|
if not self._fd then return nil, "closed" end
|
||||||
|
if fmt == "c" then fmt = 1024 end
|
||||||
|
|
||||||
|
if fmt == "a" then
|
||||||
|
local curr_pos, err = seek(self._fd);
|
||||||
|
if not curr_pos then return nil, err end
|
||||||
|
local end_pos = seek(self._fd, nil, "end");
|
||||||
|
if not end_pos then return nil, err end
|
||||||
|
|
||||||
|
return read(self._fd, end_pos - curr_pos, curr_pos);
|
||||||
|
elseif fmt == "l" or fmt == "L" then
|
||||||
|
local parts = {};
|
||||||
|
while true do
|
||||||
|
local data, err = read(self._fd, 1024, nil);
|
||||||
|
if not data then return nil, err end
|
||||||
|
|
||||||
|
local i = data:find "\n";
|
||||||
|
if i then
|
||||||
|
if fmt == "l" then
|
||||||
|
table.insert(parts, data:sub(1, i - 1));
|
||||||
|
else
|
||||||
|
table.insert(parts, data:sub(1, i));
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, err = seek(self._fd, -(#data - i) + 1);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
break;
|
||||||
|
else
|
||||||
|
table.insert(parts, data);
|
||||||
|
if #data == 0 then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(parts);
|
||||||
|
else
|
||||||
|
local data, err = read(self._fd, fmt, nil);
|
||||||
|
if not data then return nil, err end
|
||||||
|
if #data == 0 then return nil end
|
||||||
|
return data;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- @param ... string | integer
|
||||||
|
function file_index:write(...)
|
||||||
|
if not self._fd then return nil, "closed" end
|
||||||
|
return write(self._fd, table.concat { ... }, nil);
|
||||||
|
end
|
||||||
|
function file_index:close()
|
||||||
|
local fd = self._fd;
|
||||||
|
if fd then
|
||||||
|
self._fd = nil;
|
||||||
|
return close(fd);
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- @param self file
|
||||||
|
local function safe_close(self)
|
||||||
|
local fd = self._fd;
|
||||||
|
if fd then
|
||||||
|
self._fd = nil;
|
||||||
|
print("WARNING: file " .. fd .. " closed by GC. Please, close your files!");
|
||||||
|
uv.fs_close(fd);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function file_index:stream()
|
||||||
|
return stream.new(
|
||||||
|
function (cb)
|
||||||
|
while true do
|
||||||
|
if not self._fd then return nil, "closed" end
|
||||||
|
local data, err = read(self._fd, 1024, nil);
|
||||||
|
if not data then return nil, err end
|
||||||
|
|
||||||
|
local ok, err = cb(data);
|
||||||
|
if not ok and err then return nil, err end
|
||||||
|
if ok == false then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end,
|
||||||
|
function (data)
|
||||||
|
if not self._fd then return nil, "closed" end
|
||||||
|
return write(self._fd, data, nil);
|
||||||
|
end,
|
||||||
|
function ()
|
||||||
|
safe_close(self);
|
||||||
|
end,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local file_meta = {
|
||||||
|
__index = file_index,
|
||||||
|
__gc = safe_close,
|
||||||
|
__close = file_index.close,
|
||||||
|
};
|
||||||
|
|
||||||
|
--- @return file
|
||||||
|
local function wrap_file(fd)
|
||||||
|
return setmetatable({ _fd = fd }, file_meta)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @class fs_lib
|
||||||
|
local fs = {};
|
||||||
|
|
||||||
|
--- @param path string
|
||||||
|
--- @param flags? string
|
||||||
|
--- @param mode? integer
|
||||||
|
function fs.open(path, flags, mode)
|
||||||
|
--- @type integer?, string?
|
||||||
|
local fd, err = open(path, flags or "r", mode or 0);
|
||||||
|
if not fd then return nil, err end
|
||||||
|
|
||||||
|
return wrap_file(fd);
|
||||||
|
end
|
||||||
|
--- @param fd integer
|
||||||
|
function fs.from_fd(fd)
|
||||||
|
return wrap_file(fd);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param path string
|
||||||
|
function fs.readdir(path)
|
||||||
|
--- @type luv_dir_t?, string?
|
||||||
|
local fd, err = opendir(path);
|
||||||
|
if not fd then return nil, err end
|
||||||
|
-- local gc_tag = setmetatable({ fd = fd }, {
|
||||||
|
-- __gc = function (self)
|
||||||
|
-- uv.fs_closedir(self.fd);
|
||||||
|
-- end
|
||||||
|
-- });
|
||||||
|
|
||||||
|
--- @type uv.aliases.fs_readdir_entries[]?
|
||||||
|
local buff = nil;
|
||||||
|
--- @type integer?
|
||||||
|
local buff_i = 1;
|
||||||
|
|
||||||
|
return function ()
|
||||||
|
-- local gc_tag = gc_tag;
|
||||||
|
if not buff_i then return nil end
|
||||||
|
if buff and buff_i > #buff then
|
||||||
|
buff = nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
if not buff then
|
||||||
|
local err;
|
||||||
|
buff, err = readdir(fd);
|
||||||
|
--- @cast err string?
|
||||||
|
if not buff then
|
||||||
|
buff = nil;
|
||||||
|
buff_i = nil;
|
||||||
|
|
||||||
|
--- @type true?, string?
|
||||||
|
local _, err_close = closedir(fd);
|
||||||
|
if not _ then return nil, err_close end
|
||||||
|
|
||||||
|
if err then
|
||||||
|
return nil, err;
|
||||||
|
else
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
buff_i = 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @cast buff uv.aliases.fs_readdir_entries[]
|
||||||
|
local name, type = buff[buff_i].name, buff[buff_i].type;
|
||||||
|
buff_i = buff_i + 1;
|
||||||
|
return name, type;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
--- @return uv.aliases.fs_stat_table
|
||||||
|
function fs.stat(path)
|
||||||
|
return stat(path);
|
||||||
|
end
|
||||||
|
|
||||||
|
fs.stdout = fs.from_fd(0);
|
||||||
|
fs.stdin = fs.from_fd(1);
|
||||||
|
fs.stderr = fs.from_fd(2);
|
||||||
|
|
||||||
|
return fs;
|
||||||
204
src/sync/init.lua
Normal file
204
src/sync/init.lua
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
local tag = require "utils.tag"
|
||||||
|
local fork_traces = setmetatable({}, { __mode = "k" });
|
||||||
|
local close_tag = tag "close";
|
||||||
|
|
||||||
|
local function handle_resume(th, ok, ...)
|
||||||
|
if not ok then
|
||||||
|
print("error in thread " .. tostring(th) .. ": " .. ...);
|
||||||
|
uv.stop();
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function resume_safe(th, ...)
|
||||||
|
if coroutine.status(th) == "dead" then
|
||||||
|
print(debug.traceback("tried (somehow) to resume a dead thread", 1));
|
||||||
|
uv.stop();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
local ok, tag = handle_resume(th, coroutine.resume(th, ...));
|
||||||
|
if ok and close_tag == close_tag then
|
||||||
|
coroutine.close(th);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrap_gen_handle(res, ...)
|
||||||
|
if ... == nil then
|
||||||
|
return ...;
|
||||||
|
else
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param func function
|
||||||
|
--- @param flow? "fromcb" | "fromfunc" | "dummytrue"
|
||||||
|
local function wrap(func, flow)
|
||||||
|
flow = flow or "fromcb";
|
||||||
|
|
||||||
|
return function (...)
|
||||||
|
local args = table.pack(...);
|
||||||
|
local res;
|
||||||
|
local cb_thread;
|
||||||
|
|
||||||
|
local function clear_cb(...)
|
||||||
|
cb_thread = nil;
|
||||||
|
return ...;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function finish(...)
|
||||||
|
if ... == nil then return ... end
|
||||||
|
|
||||||
|
if res then
|
||||||
|
local _res = res;
|
||||||
|
res = nil;
|
||||||
|
return table.unpack(_res, 1, _res.n);
|
||||||
|
else
|
||||||
|
cb_thread = coroutine.running();
|
||||||
|
return clear_cb(coroutine.yield());
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function finish_fromfunc(...)
|
||||||
|
local err;
|
||||||
|
if res and res[1] ~= nil then
|
||||||
|
err = res[1];
|
||||||
|
res = nil;
|
||||||
|
else
|
||||||
|
cb_thread = coroutine.running();
|
||||||
|
err = coroutine.yield();
|
||||||
|
end
|
||||||
|
|
||||||
|
if err ~= nil then return clear_cb(nil, err) end
|
||||||
|
return clear_cb(...);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ret_cb(...)
|
||||||
|
if cb_thread then
|
||||||
|
resume_safe(cb_thread, ...);
|
||||||
|
else
|
||||||
|
res = table.pack(...);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if flow == "fromcb" then
|
||||||
|
args[args.n + 1] = function (err, ...)
|
||||||
|
if err then return ret_cb(nil, err) end
|
||||||
|
ret_cb(...);
|
||||||
|
end
|
||||||
|
return finish(func(table.unpack(args, 1, args.n + 1)));
|
||||||
|
elseif flow == "dummytrue" then
|
||||||
|
args[args.n + 1] = function (err, ...)
|
||||||
|
if err then return ret_cb(nil, err) end
|
||||||
|
ret_cb(true, ...);
|
||||||
|
end
|
||||||
|
return finish(func(table.unpack(args, 1, args.n + 1)));
|
||||||
|
elseif flow == "fromfunc" then
|
||||||
|
args[args.n + 1] = function (err)
|
||||||
|
return ret_cb(err);
|
||||||
|
end
|
||||||
|
return finish_fromfunc(func(table.unpack(args, 1, args.n + 1)));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- @param func function
|
||||||
|
--- @param dummy_true? boolean
|
||||||
|
--- @return function
|
||||||
|
local function wrap_gen(func, dummy_true)
|
||||||
|
return function (...)
|
||||||
|
local args = { ... };
|
||||||
|
|
||||||
|
local cb_thread;
|
||||||
|
local buff = nil;
|
||||||
|
|
||||||
|
local function clear_cb(...)
|
||||||
|
cb_thread = nil;
|
||||||
|
return ...;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function real_cb(...)
|
||||||
|
if not cb_thread then
|
||||||
|
buff = buff or {};
|
||||||
|
table.insert(buff, table.pack(...));
|
||||||
|
else
|
||||||
|
resume_safe(cb_thread, ...);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if dummy_true then
|
||||||
|
args[select("#", ...) + 1] = function (err, ...)
|
||||||
|
if err then
|
||||||
|
real_cb(nil, err);
|
||||||
|
else
|
||||||
|
real_cb(true, ...);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
args[select("#", ...) + 1] = function (err, ...)
|
||||||
|
if err then
|
||||||
|
real_cb(nil, err);
|
||||||
|
else
|
||||||
|
real_cb(...);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function res(...)
|
||||||
|
if buff and #buff > 0 then
|
||||||
|
local res = table.remove(buff, 1);
|
||||||
|
return table.unpack(res, 1, res.n);
|
||||||
|
else
|
||||||
|
cb_thread = coroutine.running();
|
||||||
|
return clear_cb(coroutine.yield());
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return wrap_gen_handle(res, func(table.unpack(args)));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sleep(timeout)
|
||||||
|
local timer = uv.new_timer();
|
||||||
|
assert(wrap(timer.start, "dummytrue")(timer, timeout, 0));
|
||||||
|
timer:stop();
|
||||||
|
timer:close();
|
||||||
|
end
|
||||||
|
|
||||||
|
local function int_fork(no_trace, func, ...)
|
||||||
|
local th;
|
||||||
|
th = coroutine.create(function (fork_at, entry, ...)
|
||||||
|
xpcall(entry, function (err)
|
||||||
|
print(debug.traceback(tostring(err), 2) .. fork_at);
|
||||||
|
end, ...);
|
||||||
|
|
||||||
|
return close_tag;
|
||||||
|
end);
|
||||||
|
|
||||||
|
if no_trace then
|
||||||
|
resume_safe(th, "", func, ...);
|
||||||
|
else
|
||||||
|
local prev_trace = fork_traces[coroutine.running()];
|
||||||
|
local curr_trace = "fork at\n" .. debug.traceback(nil, 2):match("\n(.*)");
|
||||||
|
if prev_trace then curr_trace = prev_trace .. "\n" .. curr_trace end
|
||||||
|
|
||||||
|
fork_traces[th] = curr_trace;
|
||||||
|
|
||||||
|
resume_safe(th, "\n" .. curr_trace, func, ...);
|
||||||
|
end
|
||||||
|
|
||||||
|
return th;
|
||||||
|
end
|
||||||
|
local function fork(func, ...)
|
||||||
|
return int_fork(false, func, ...);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function main_wrap(func)
|
||||||
|
return function (...)
|
||||||
|
int_fork(true, func, ...);
|
||||||
|
assert(uv.run());
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
sleep = sleep,
|
||||||
|
wrap = wrap,
|
||||||
|
wrap_gen = wrap_gen,
|
||||||
|
fork = fork,
|
||||||
|
main_wrap = main_wrap,
|
||||||
|
};
|
||||||
181
src/sync/stream.lua
Normal file
181
src/sync/stream.lua
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
local uv = require "luv";
|
||||||
|
local sync = require "sync";
|
||||||
|
|
||||||
|
--- @class stream
|
||||||
|
--- @field _read fun(cb: fun(data: string): boolean?, string?): true?, string?
|
||||||
|
--- @field _write fun(data: string): true?, string?
|
||||||
|
--- @field _close fun()
|
||||||
|
--- @field _cache string
|
||||||
|
--- @field _pos integer
|
||||||
|
--- @field _gc_safe boolean
|
||||||
|
local stream_index = {};
|
||||||
|
|
||||||
|
local read_start = sync.wrap_gen(uv.read_start, false);
|
||||||
|
local read_stop = uv.read_stop;
|
||||||
|
local write = sync.wrap(uv.write, "dummytrue");
|
||||||
|
local close = sync.wrap(uv.close, "dummytrue");
|
||||||
|
|
||||||
|
--- @param self stream
|
||||||
|
--- @param fmt "c" | "a" | "l" | "L" | integer
|
||||||
|
function stream_index:read(fmt)
|
||||||
|
local data = {};
|
||||||
|
|
||||||
|
local iter;
|
||||||
|
|
||||||
|
if fmt == "c" then
|
||||||
|
if self._cache then
|
||||||
|
local res = self._cache;
|
||||||
|
self._cache = nil;
|
||||||
|
return res;
|
||||||
|
else
|
||||||
|
local res;
|
||||||
|
local _, err = self._read(function (data)
|
||||||
|
res = data;
|
||||||
|
return false;
|
||||||
|
end);
|
||||||
|
|
||||||
|
if not _ then return nil, err end
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
elseif fmt == "a" then
|
||||||
|
function iter(chunk)
|
||||||
|
table.insert(data, chunk);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
elseif fmt == "l" or fmt == "L" then
|
||||||
|
function iter(chunk)
|
||||||
|
local i = chunk:find "\n";
|
||||||
|
if i then
|
||||||
|
if fmt == "L" then
|
||||||
|
table.insert(data, chunk:sub(1, i));
|
||||||
|
else
|
||||||
|
table.insert(data, chunk:sub(1, i - 1));
|
||||||
|
end
|
||||||
|
self._cache = chunk:sub(i + 1);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(data, chunk);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if fmt == 0 then return "" end
|
||||||
|
local n = 0;
|
||||||
|
function iter(chunk)
|
||||||
|
n = n + #chunk;
|
||||||
|
if n == fmt then
|
||||||
|
table.insert(data, chunk);
|
||||||
|
return false;
|
||||||
|
elseif n > fmt then
|
||||||
|
table.insert(data, chunk:sub(1, fmt - n - 1));
|
||||||
|
self._cache = chunk:sub(fmt - n);
|
||||||
|
n = n - #self._cache;
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(data, chunk);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local cache = self._cache;
|
||||||
|
self._cache = nil;
|
||||||
|
if not cache or iter(cache) then
|
||||||
|
local _, err = self._read(iter);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #data == 0 then return nil end
|
||||||
|
|
||||||
|
if self._cache == "" then self._cache = nil end
|
||||||
|
return table.concat(data);
|
||||||
|
end
|
||||||
|
--- @param self stream
|
||||||
|
--- @param ... string | integer
|
||||||
|
function stream_index:write(...)
|
||||||
|
return self._write(table.concat { ... });
|
||||||
|
end
|
||||||
|
function stream_index:close()
|
||||||
|
self._gc_safe = nil;
|
||||||
|
return self._close();
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param self stream
|
||||||
|
local function safe_close(self)
|
||||||
|
if self._gc_safe == nil then return end
|
||||||
|
if self._gc_safe then
|
||||||
|
self:_close();
|
||||||
|
else
|
||||||
|
print(debug.traceback("warning: stream leaked"));
|
||||||
|
end
|
||||||
|
self._gc_safe = nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
local stream_meta = {
|
||||||
|
__index = stream_index,
|
||||||
|
__gc = safe_close,
|
||||||
|
__close = stream_index.close,
|
||||||
|
};
|
||||||
|
|
||||||
|
--- @param read fun(cb: fun(data: string): boolean?, string?): true?, string?
|
||||||
|
--- @param write fun(data: string): true?, string?
|
||||||
|
--- @param close fun(sync?: boolean): true?, string?
|
||||||
|
--- @param gc_safe? boolean
|
||||||
|
--- @return stream
|
||||||
|
local function new(read, write, close, gc_safe)
|
||||||
|
return (setmetatable({
|
||||||
|
_read = read,
|
||||||
|
_write = write,
|
||||||
|
_close = close,
|
||||||
|
_cache = nil,
|
||||||
|
_gc_safe = gc_safe or false,
|
||||||
|
_pos = 0,
|
||||||
|
}, stream_meta));
|
||||||
|
end
|
||||||
|
--- @param read stream
|
||||||
|
--- @param write stream
|
||||||
|
local function combine(read, write)
|
||||||
|
return new(read._read, write._write, function ()
|
||||||
|
local ok_1, err_1 = read:close();
|
||||||
|
local ok_2, err_2 = write:close();
|
||||||
|
|
||||||
|
if not ok_1 then return nil, err_1 end
|
||||||
|
if not ok_2 then return nil, err_2 end
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end, read._gc_safe and write._gc_safe);
|
||||||
|
end
|
||||||
|
local function from_uv(hnd)
|
||||||
|
return new(
|
||||||
|
function (cb)
|
||||||
|
local it, err = read_start(hnd);
|
||||||
|
if not it then return nil, err end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local chunk, err = it();
|
||||||
|
if not chunk then
|
||||||
|
if not err then break end
|
||||||
|
return nil, err;
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = cb(chunk);
|
||||||
|
if ok == nil then return nil, err end
|
||||||
|
if not ok then break end
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, err = read_stop(hnd);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
return true;
|
||||||
|
end,
|
||||||
|
function (data)
|
||||||
|
return write(hnd, data);
|
||||||
|
end,
|
||||||
|
function ()
|
||||||
|
return close(hnd);
|
||||||
|
end,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
end
|
||||||
|
|
||||||
|
return { new = new, combine = combine, from_uv = from_uv, meta = stream_meta };
|
||||||
70
src/sync/tcp.lua
Normal file
70
src/sync/tcp.lua
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
local uv = require "luv";
|
||||||
|
local sync = require "sync";
|
||||||
|
local stream = require "sync.stream";
|
||||||
|
|
||||||
|
local tcp_bind = uv.tcp_bind;
|
||||||
|
local tcp_connect = sync.wrap(uv.tcp_connect, "fromfunc");
|
||||||
|
local listen = sync.wrap_gen(uv.listen, true);
|
||||||
|
local accept = uv.accept;
|
||||||
|
|
||||||
|
local wrap;
|
||||||
|
|
||||||
|
--- @class tcp_socket: stream
|
||||||
|
--- @field _hnd uv_tcp_t
|
||||||
|
local tcp_index = setmetatable({}, { __index = stream.meta.__index });
|
||||||
|
|
||||||
|
--- @param self tcp_socket
|
||||||
|
--- @param addr string
|
||||||
|
--- @param port integer
|
||||||
|
function tcp_index:bind(addr, port)
|
||||||
|
return tcp_bind(self._hnd, addr, port);
|
||||||
|
end
|
||||||
|
--- @param self tcp_socket
|
||||||
|
--- @param addr string
|
||||||
|
--- @param port integer
|
||||||
|
function tcp_index:connect(addr, port)
|
||||||
|
--- @type true?, string?
|
||||||
|
local _, err = tcp_connect(self._hnd, addr, port);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
return self;
|
||||||
|
end
|
||||||
|
--- @param self tcp_socket
|
||||||
|
--- @param backlog? integer
|
||||||
|
--- @return (fun(): stream?, string?)?
|
||||||
|
--- @return string? err
|
||||||
|
function tcp_index:listen(backlog)
|
||||||
|
local it, err = listen(self._hnd, backlog or 32);
|
||||||
|
if not it then return nil, err end
|
||||||
|
|
||||||
|
return function ()
|
||||||
|
local _, err = it();
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
local client = uv.new_tcp();
|
||||||
|
|
||||||
|
local _, err = accept(self._hnd, client);
|
||||||
|
if not _ then return nil, err end
|
||||||
|
|
||||||
|
return wrap(client);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local tcp_meta = {
|
||||||
|
__index = tcp_index,
|
||||||
|
__gc = tcp_index.close,
|
||||||
|
__close = tcp_index.close,
|
||||||
|
};
|
||||||
|
|
||||||
|
--- @return tcp_socket
|
||||||
|
function wrap(socket)
|
||||||
|
local res = stream.from_uv(socket);
|
||||||
|
setmetatable(res, tcp_meta);
|
||||||
|
--- @cast res tcp_socket
|
||||||
|
res._hnd = socket;
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
|
||||||
|
return function ()
|
||||||
|
return wrap(uv.new_tcp());
|
||||||
|
end
|
||||||
119
src/template.lua
Normal file
119
src/template.lua
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
--- @alias template fun(page: page, f: fun(...: string))
|
||||||
|
--- @alias emitable nil | string | emitable[] | template
|
||||||
|
--- @alias slotable nil | string | emitable[] | (fun(self: page, emit: fun(val?: emitable)): emitable?)
|
||||||
|
|
||||||
|
local html_entities = {
|
||||||
|
["&"] = "&",
|
||||||
|
["\""] = """,
|
||||||
|
["<"] = "<",
|
||||||
|
[">"] = ">",
|
||||||
|
}
|
||||||
|
|
||||||
|
--- @param f fun(...: string)
|
||||||
|
--- @param val? emitable
|
||||||
|
local function emit(ctx, f, val)
|
||||||
|
if type(val) == "function" then
|
||||||
|
val(ctx, f);
|
||||||
|
elseif type(val) == "string" then
|
||||||
|
f((val:gsub("[&\"<>]", html_entities)));
|
||||||
|
elseif type(val) == "table" then
|
||||||
|
for i = 1, #val do
|
||||||
|
emit(ctx, f, val[i]);
|
||||||
|
end
|
||||||
|
elseif val ~= nil then
|
||||||
|
error("invalid emit value " .. type(val));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param arg? slotable
|
||||||
|
--- @return template
|
||||||
|
local function slot(arg)
|
||||||
|
if type(arg) == "function" then
|
||||||
|
return function (ctx, f)
|
||||||
|
return emit(ctx, f, arg(ctx, function (t) return emit(ctx, f, t) end));
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return function (ctx, f)
|
||||||
|
return emit(ctx, f, arg);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param name string
|
||||||
|
--- @param no_body? boolean
|
||||||
|
--- @return fun(arg: string | { [integer]: emitable, [string]: string | integer }): template
|
||||||
|
local function element(name, no_body)
|
||||||
|
return function (arg)
|
||||||
|
local parts = { name };
|
||||||
|
local attribs = {};
|
||||||
|
|
||||||
|
if type(arg) == "string" then arg = { arg } end
|
||||||
|
|
||||||
|
for k in pairs(arg) do
|
||||||
|
if type(k) == "string" then
|
||||||
|
table.insert(attribs, k);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(attribs);
|
||||||
|
|
||||||
|
for i = 1, #attribs do
|
||||||
|
table.insert(parts, ("%s=%q"):format(attribs[i], tostring(arg[attribs[i]]):gsub("[&\"<>]", html_entities)));
|
||||||
|
end
|
||||||
|
|
||||||
|
local tag = table.concat(parts, " ");
|
||||||
|
|
||||||
|
if no_body then
|
||||||
|
return function (ctx, f)
|
||||||
|
f("<", tag, "/>");
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return function (ctx, f)
|
||||||
|
f("<", tag, ">");
|
||||||
|
emit(ctx, f, arg);
|
||||||
|
f("</", name, ">");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local elements = {
|
||||||
|
html = element "html",
|
||||||
|
head = element "head",
|
||||||
|
body = element "body",
|
||||||
|
|
||||||
|
style = element "style",
|
||||||
|
link = element "link",
|
||||||
|
script = element "script",
|
||||||
|
meta = element "meta",
|
||||||
|
title = element "title",
|
||||||
|
|
||||||
|
div = element "div",
|
||||||
|
pre = element "pre",
|
||||||
|
p = element "p",
|
||||||
|
|
||||||
|
span = element "span",
|
||||||
|
code = element "code",
|
||||||
|
|
||||||
|
ul = element "ul",
|
||||||
|
ol = element "ol",
|
||||||
|
li = element "li",
|
||||||
|
|
||||||
|
h1 = element "h1",
|
||||||
|
h2 = element "h2",
|
||||||
|
h3 = element "h3",
|
||||||
|
h4 = element "h4",
|
||||||
|
h5 = element "h5",
|
||||||
|
h6 = element "h6",
|
||||||
|
|
||||||
|
em = element "em",
|
||||||
|
strong = element "strong",
|
||||||
|
s = element "s",
|
||||||
|
|
||||||
|
a = element "a",
|
||||||
|
button = element "button",
|
||||||
|
input = element("input", true),
|
||||||
|
img = element("img", true),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { element = element, slot = slot, elements = elements };
|
||||||
185
src/utils/args.lua
Normal file
185
src/utils/args.lua
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
local args = {};
|
||||||
|
|
||||||
|
--- @alias consumers table<string, (fun(next: fun(): string?)) | string>
|
||||||
|
|
||||||
|
--- @param consumers consumers
|
||||||
|
--- @param name string
|
||||||
|
local function get_consumer(consumers, name)
|
||||||
|
local consumer = nil;
|
||||||
|
local path = {};
|
||||||
|
local n = 0;
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local curr = consumer or name;
|
||||||
|
if path[curr] then
|
||||||
|
local path_arr = {};
|
||||||
|
|
||||||
|
for k, v in next, path do
|
||||||
|
path_arr[v] = k;
|
||||||
|
end
|
||||||
|
|
||||||
|
error("Alias to '" .. curr .. "' is recursive: " .. table.concat(path_arr, " -> "));
|
||||||
|
end
|
||||||
|
|
||||||
|
consumer = consumers[curr];
|
||||||
|
if consumer == nil then
|
||||||
|
local prefix;
|
||||||
|
if n == 0 then
|
||||||
|
prefix = "Unknown flag";
|
||||||
|
else
|
||||||
|
prefix = "Unknown alias";
|
||||||
|
end
|
||||||
|
|
||||||
|
if #curr == 1 then
|
||||||
|
error(prefix .. " '-" .. curr .. "'");
|
||||||
|
else
|
||||||
|
error(prefix .. " '--" .. curr .. "'");
|
||||||
|
end
|
||||||
|
elseif type(consumer) == "function" then
|
||||||
|
return consumer;
|
||||||
|
end
|
||||||
|
|
||||||
|
path[curr] = n;
|
||||||
|
n = n + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local digest_next;
|
||||||
|
|
||||||
|
--- @param consumers consumers
|
||||||
|
--- @param name string
|
||||||
|
--- @param next fun(): string?
|
||||||
|
local function digest_arg(consumers, name, next)
|
||||||
|
get_consumer(consumers, name)(next);
|
||||||
|
return digest_next(consumers, next);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param consumers consumers
|
||||||
|
--- @param next fun(): string?
|
||||||
|
--- @param only_rest? boolean
|
||||||
|
local function digest_rest(consumers, next, only_rest)
|
||||||
|
if not consumers[1] then
|
||||||
|
local arg = next();
|
||||||
|
if not arg then return nil end
|
||||||
|
error("Invalid argument '" .. arg .. "'");
|
||||||
|
end
|
||||||
|
|
||||||
|
local consumed = false;
|
||||||
|
local function our_consumer()
|
||||||
|
consumed = true;
|
||||||
|
return next();
|
||||||
|
end
|
||||||
|
|
||||||
|
consumers[1](our_consumer);
|
||||||
|
|
||||||
|
if not consumed then
|
||||||
|
local arg = next();
|
||||||
|
if not arg then return nil end
|
||||||
|
error("Invalid argument '" .. arg .. "'");
|
||||||
|
end
|
||||||
|
|
||||||
|
if only_rest then
|
||||||
|
return digest_rest(consumers, next, true);
|
||||||
|
else
|
||||||
|
return digest_next(consumers, next);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param consumers consumers
|
||||||
|
--- @param next fun(): string?
|
||||||
|
function digest_next(consumers, next)
|
||||||
|
local arg = next();
|
||||||
|
if not arg then return nil end
|
||||||
|
|
||||||
|
if arg == "--" then
|
||||||
|
return digest_rest(consumers, next, true);
|
||||||
|
end
|
||||||
|
|
||||||
|
local name, val;
|
||||||
|
|
||||||
|
name, val = arg:match "^%-%-(.-)=(.+)$";
|
||||||
|
if name then
|
||||||
|
local function our_next()
|
||||||
|
our_next = next;
|
||||||
|
return val;
|
||||||
|
end
|
||||||
|
|
||||||
|
return digest_arg(consumers, name, function () return our_next() end);
|
||||||
|
end
|
||||||
|
|
||||||
|
name = arg:match "^%-%-(.-)$";
|
||||||
|
if name then
|
||||||
|
return digest_arg(consumers, name, next);
|
||||||
|
end
|
||||||
|
|
||||||
|
name = arg:match "^%-(.-)$";
|
||||||
|
if name then
|
||||||
|
for c in name:gmatch "." do
|
||||||
|
digest_arg(consumers, c, next);
|
||||||
|
end
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function our_next()
|
||||||
|
our_next = next;
|
||||||
|
return val;
|
||||||
|
end
|
||||||
|
|
||||||
|
return digest_rest(consumers, function () return our_next() end, false);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param consumers consumers
|
||||||
|
--- @returns fun(...: string)
|
||||||
|
function args.parser(consumers)
|
||||||
|
return function (...)
|
||||||
|
local args = { ... };
|
||||||
|
local i = 0;
|
||||||
|
|
||||||
|
local function next()
|
||||||
|
i = i + 1;
|
||||||
|
return args[i];
|
||||||
|
end
|
||||||
|
|
||||||
|
return digest_next(consumers, next);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function args.simple(settings)
|
||||||
|
return function (...)
|
||||||
|
local consumers = {};
|
||||||
|
local data = {};
|
||||||
|
|
||||||
|
for name, setting_val in pairs(settings) do
|
||||||
|
local real_name = name;
|
||||||
|
if name == 1 then
|
||||||
|
real_name = "<argument>";
|
||||||
|
end
|
||||||
|
|
||||||
|
if setting_val == "bool" or setting_val == "flag" then
|
||||||
|
consumers[name] = function ()
|
||||||
|
data[name] = true;
|
||||||
|
end
|
||||||
|
elseif setting_val == "string" then
|
||||||
|
consumers[name] = function (next)
|
||||||
|
data[name] = assert(next(), "Expected string for '" .. real_name .. "'");
|
||||||
|
end
|
||||||
|
elseif setting_val == "integer" then
|
||||||
|
consumers[name] = function (next)
|
||||||
|
local raw = assert(next(), "Expected integer for '" .. real_name .. "'");
|
||||||
|
local val = assert(tonumber(raw, 10), "Expected integer for '" .. real_name .. "'");
|
||||||
|
data[name] = val;
|
||||||
|
end
|
||||||
|
elseif type(setting_val) == "function" then
|
||||||
|
consumers[name] = function (next)
|
||||||
|
data[name] = setting_val(next);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
args.parser(consumers)(...);
|
||||||
|
return data;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return args;
|
||||||
17
src/utils/asserted.lua
Normal file
17
src/utils/asserted.lua
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
local function handle(...)
|
||||||
|
local ok, err = ...;
|
||||||
|
if not ok and err then error(err, 2) end
|
||||||
|
return ...;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @generic T
|
||||||
|
--- @param func? T
|
||||||
|
--- @param ... any
|
||||||
|
--- @return T
|
||||||
|
return function (func, ...)
|
||||||
|
func = assert(func, ...);
|
||||||
|
|
||||||
|
return function (...)
|
||||||
|
return handle(func(...));
|
||||||
|
end
|
||||||
|
end
|
||||||
39
src/utils/base64.lua
Normal file
39
src/utils/base64.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
local base64 = {};
|
||||||
|
local masks = { 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF };
|
||||||
|
local function extract(v, from, width)
|
||||||
|
return (v >> from) & masks[width];
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param str string
|
||||||
|
function base64.encode(str)
|
||||||
|
local alphabet = {
|
||||||
|
[0] = "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||||
|
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||||
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||||
|
"+", "/", "="
|
||||||
|
};
|
||||||
|
|
||||||
|
local parts = {};
|
||||||
|
local k, n = 1, #str;
|
||||||
|
local lastn = n % 3;
|
||||||
|
|
||||||
|
for i = 1, n-lastn, 3 do
|
||||||
|
local a, b, c = str:byte(i, i + 2);
|
||||||
|
local v = a * 0x10000 + b * 0x100 + c;
|
||||||
|
parts[k] = alphabet[extract(v, 18, 6)] .. alphabet[extract(v, 12, 6)] .. alphabet[extract(v, 6, 6)] .. alphabet[extract(v, 0, 6)];
|
||||||
|
k = k + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
if lastn == 2 then
|
||||||
|
local a, b = str:byte(n - 1, n);
|
||||||
|
local v = a * 0x10000 + b * 0x100;
|
||||||
|
parts[k] = alphabet[extract(v, 18, 6)] .. alphabet[extract(v, 12, 6)] .. alphabet[extract(v, 6, 6)] .. alphabet[64];
|
||||||
|
elseif lastn == 1 then
|
||||||
|
local v = str:byte(n) * 0x10000;
|
||||||
|
parts[k] = alphabet[extract(v, 18, 6)] .. alphabet[extract(v, 12, 6)] .. alphabet[64] .. alphabet[64];
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(parts);
|
||||||
|
end
|
||||||
|
|
||||||
|
return base64;
|
||||||
260
src/utils/json.lua
Normal file
260
src/utils/json.lua
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
local tag = require "utils.tag";
|
||||||
|
local json = {};
|
||||||
|
|
||||||
|
-- Internal functions.
|
||||||
|
|
||||||
|
local function kind_of(obj)
|
||||||
|
if type(obj) ~= "table" then return type(obj) end
|
||||||
|
if obj[json.array] then return "array" end
|
||||||
|
|
||||||
|
local i = 1;
|
||||||
|
for _ in pairs(obj) do
|
||||||
|
if obj[i] ~= nil then
|
||||||
|
i = i + 1;
|
||||||
|
else
|
||||||
|
return "table";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if i == 1 then
|
||||||
|
return "table";
|
||||||
|
else
|
||||||
|
return "array";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function escape_str(s)
|
||||||
|
local in_char = { "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" };
|
||||||
|
local out_char = { "\\", "\"", "/", "b", "f", "n", "r", "t" };
|
||||||
|
for i, c in ipairs(in_char) do
|
||||||
|
s = s:gsub(c, "\\" .. out_char[i]);
|
||||||
|
end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns pos, did_find; there are two cases:
|
||||||
|
-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
|
||||||
|
-- 2. Delimiter not found: pos = pos after leading space; did_find = false.
|
||||||
|
-- This throws an error if err_if_missing is true and the delim is not found.
|
||||||
|
local function skip_delim(str, pos, delim, err_if_missing)
|
||||||
|
pos = string.find(str, "%S", pos) or pos;
|
||||||
|
|
||||||
|
if string.sub(str, pos, pos) ~= delim then
|
||||||
|
if err_if_missing then
|
||||||
|
error(table.concat { "Expected ", delim, " near position ", pos });
|
||||||
|
end
|
||||||
|
|
||||||
|
return pos, false;
|
||||||
|
end
|
||||||
|
|
||||||
|
return pos + 1, true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local esc_map = { b = "\b", f = "\f", n = "\n", r = "\r", t = "\t", v = "\v" };
|
||||||
|
|
||||||
|
-- Expects the given pos to be the first character after the opening quote.
|
||||||
|
-- Returns val, pos; the returned pos is after the closing quote character.
|
||||||
|
local function parse_str_val(str, pos, check)
|
||||||
|
if pos > #str then error("End of input found while parsing string") end
|
||||||
|
|
||||||
|
local quote = str:sub(pos, pos);
|
||||||
|
|
||||||
|
if check then
|
||||||
|
if quote ~= "\"" and quote ~= "'" then
|
||||||
|
return nil, pos;
|
||||||
|
else
|
||||||
|
pos = pos + 1;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
pos = pos + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
local res = {};
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local c = str:sub(pos, pos);
|
||||||
|
if c == quote then
|
||||||
|
return table.concat(res), pos + 1;
|
||||||
|
elseif c == "\\" then
|
||||||
|
local unicode = str:match("^\\u([%da-zA-Z][%da-zA-Z][%da-zA-Z][%da-zA-Z])", pos);
|
||||||
|
|
||||||
|
if unicode then
|
||||||
|
table.insert(res, require "utf8".char(tonumber(unicode, 16)));
|
||||||
|
pos = pos + 6;
|
||||||
|
else
|
||||||
|
c = str:sub(pos + 1, pos + 1);
|
||||||
|
res[#res + 1] = esc_map[c] or c;
|
||||||
|
pos = pos + 2;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
res[#res + 1] = c;
|
||||||
|
pos = pos + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns val, pos; the returned pos is after the number's final character.
|
||||||
|
local function parse_num_val(str, pos)
|
||||||
|
local num_str = string.match(str, "^-?%d+%.?%d*[eE]?[+-]?%d*", pos);
|
||||||
|
local val = tonumber(num_str);
|
||||||
|
|
||||||
|
if not val then error(table.concat { "Error parsing number at position ", pos, "." }) end
|
||||||
|
return val, pos + #num_str;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_impl(str, pos, end_delim)
|
||||||
|
pos = pos or 1;
|
||||||
|
if pos > #str then error("Reached unexpected end of input") end
|
||||||
|
|
||||||
|
pos = str:find("%S", pos) or pos;
|
||||||
|
local c = str:sub(pos, pos);
|
||||||
|
local delim_found;
|
||||||
|
|
||||||
|
if c == "{" then
|
||||||
|
pos = str:find("%S", pos + 1) or pos + 1;
|
||||||
|
|
||||||
|
local key;
|
||||||
|
local obj = {};
|
||||||
|
|
||||||
|
c = str:sub(pos, pos);
|
||||||
|
if c == "}" then
|
||||||
|
return obj, pos + 1;
|
||||||
|
else
|
||||||
|
while true do
|
||||||
|
pos = str:find("%S", pos) or pos;
|
||||||
|
key, pos = parse_str_val(str, pos, true);
|
||||||
|
if key == nil then error("Expected a string key") end
|
||||||
|
|
||||||
|
pos = skip_delim(str, pos, ":", true); -- true -> error if missing.
|
||||||
|
obj[key], pos = parse_impl(str, pos);
|
||||||
|
|
||||||
|
pos, delim_found = skip_delim(str, pos, "}");
|
||||||
|
if delim_found then return obj, pos end
|
||||||
|
|
||||||
|
pos, delim_found = skip_delim(str, pos, ",");
|
||||||
|
if not delim_found then error("Expected semicolon or comma") end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif c == "[" then
|
||||||
|
pos = pos + 1
|
||||||
|
|
||||||
|
local arr = {};
|
||||||
|
arr[json.array] = true;
|
||||||
|
|
||||||
|
local val;
|
||||||
|
delim_found = true;
|
||||||
|
|
||||||
|
while true do
|
||||||
|
val, pos = parse_impl(str, pos, "]");
|
||||||
|
if val == nil then return arr, pos end
|
||||||
|
if not delim_found then error "Comma missing between array items" end
|
||||||
|
|
||||||
|
arr[#arr + 1] = val;
|
||||||
|
pos, delim_found = skip_delim(str, pos, ",");
|
||||||
|
end
|
||||||
|
elseif c == "\"" or c == "\'" then -- Parse a string.
|
||||||
|
return parse_str_val(str, pos, false);
|
||||||
|
elseif c == "-" or c:find("%d") then -- Parse a number.
|
||||||
|
return parse_num_val(str, pos);
|
||||||
|
elseif c == end_delim then -- End of an object or array.
|
||||||
|
return nil, pos + 1;
|
||||||
|
elseif str:sub(pos, pos + 3) == "null" then
|
||||||
|
return json.null, pos + 4;
|
||||||
|
elseif str:sub(pos, pos + 3) == "true" then
|
||||||
|
return true, pos + 4;
|
||||||
|
elseif str:sub(pos, pos + 4) == "false" then
|
||||||
|
return false, pos + 5;
|
||||||
|
else
|
||||||
|
error(table.concat { "Invalid json syntax starting at position ", pos, ": ", str:sub(pos, pos + 25) });
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stringify_impl(obj, all, indent_str, n)
|
||||||
|
local s = {}; -- We'll build the string as an array of strings to be concatenated.
|
||||||
|
local kind = kind_of(obj); -- This is 'array' if it's an array or type(obj) otherwise.
|
||||||
|
|
||||||
|
if kind == "array" then
|
||||||
|
for i, val in ipairs(obj) do
|
||||||
|
s[i] = stringify_impl(val, all, indent_str, n + 1);
|
||||||
|
end
|
||||||
|
|
||||||
|
if not indent_str then
|
||||||
|
return "[" .. table.concat(s, ",") .. "]";
|
||||||
|
elseif #s == 0 then
|
||||||
|
return "[]";
|
||||||
|
else
|
||||||
|
local indent = "\n" .. string.rep(indent_str, n + 1);
|
||||||
|
return table.concat {
|
||||||
|
"[",
|
||||||
|
indent, table.concat(s, "," .. indent),
|
||||||
|
"\n", string.rep(indent_str, n), "]"
|
||||||
|
};
|
||||||
|
end
|
||||||
|
elseif kind == "table" then
|
||||||
|
for k, v in pairs(obj) do
|
||||||
|
local sep = indent_str and ": " or ":";
|
||||||
|
local val = stringify_impl(v, all, indent_str, n + 1);
|
||||||
|
if val ~= nil then
|
||||||
|
if type(k) == "string" then
|
||||||
|
s[#s + 1] = stringify_impl(k) .. sep .. val;
|
||||||
|
elseif type(k) == "number" then
|
||||||
|
s[#s + 1] = "\"" .. k .. "\"" .. sep .. val;
|
||||||
|
elseif type(k) == "boolean" then
|
||||||
|
s[#s + 1] = "\"" .. k .. "\"" .. sep .. val;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not indent_str then
|
||||||
|
return "{" .. table.concat(s, ",") .. "}";
|
||||||
|
elseif #s == 0 then
|
||||||
|
return "{}";
|
||||||
|
else
|
||||||
|
local indent = "\n" .. string.rep(indent_str, n + 1);
|
||||||
|
return table.concat {
|
||||||
|
"{",
|
||||||
|
indent, table.concat(s, "," .. indent),
|
||||||
|
"\n", string.rep(indent_str, n), "}"
|
||||||
|
};
|
||||||
|
end
|
||||||
|
return "{" .. table.concat(s, ",") .. "}";
|
||||||
|
elseif kind == "string" then
|
||||||
|
return "\"" .. escape_str(obj) .. "\"";
|
||||||
|
elseif kind == "number" then
|
||||||
|
return tostring(obj);
|
||||||
|
elseif kind == 'boolean' then
|
||||||
|
return tostring(obj);
|
||||||
|
elseif kind == "nil" then
|
||||||
|
return "null";
|
||||||
|
elseif all then
|
||||||
|
return tostring(obj);
|
||||||
|
else
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- local indent_str = " ";
|
||||||
|
|
||||||
|
function json.stringify(obj, indent_str)
|
||||||
|
if indent_str == true then
|
||||||
|
indent_str = " ";
|
||||||
|
end
|
||||||
|
return stringify_impl(obj, false, indent_str, 0);
|
||||||
|
end
|
||||||
|
function json.pretty(obj)
|
||||||
|
return stringify_impl(obj, true, " ", 0);
|
||||||
|
end
|
||||||
|
|
||||||
|
--- This is a one-off table to represent the null value.
|
||||||
|
json.null = tag "null";
|
||||||
|
--- Used to differentiate JSON arrays and object
|
||||||
|
json.array = tag "array";
|
||||||
|
|
||||||
|
---@param str string
|
||||||
|
---@return unknown
|
||||||
|
function json.parse(str)
|
||||||
|
local obj = parse_impl(str, 1);
|
||||||
|
return obj;
|
||||||
|
end
|
||||||
|
|
||||||
|
return json;
|
||||||
447
src/utils/mqtt.lua
Normal file
447
src/utils/mqtt.lua
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
---@diagnostic disable: cast-local-type
|
||||||
|
|
||||||
|
local ack_codes = {
|
||||||
|
"Connection Refused: unacceptable protocol version",
|
||||||
|
"Connection Refused: identifier rejected",
|
||||||
|
"Connection Refused: server unavailable",
|
||||||
|
"Connection Refused: bad user name or password",
|
||||||
|
"Connection Refused: not authorized",
|
||||||
|
};
|
||||||
|
|
||||||
|
local function read_uint8(data, i)
|
||||||
|
return string.unpack("!1> I1", data, i);
|
||||||
|
end
|
||||||
|
local function read_uint16(data, i)
|
||||||
|
return string.unpack("!1> I2", data, i);
|
||||||
|
end
|
||||||
|
local function read_utf8(data, i)
|
||||||
|
return string.unpack("!1> s2", data, i);
|
||||||
|
end
|
||||||
|
|
||||||
|
local function to_read(next_chunk)
|
||||||
|
local remaining = nil;
|
||||||
|
|
||||||
|
return function (n)
|
||||||
|
if n == 0 then return "" end
|
||||||
|
if next_chunk == nil then return nil end
|
||||||
|
|
||||||
|
local res = {};
|
||||||
|
local i;
|
||||||
|
|
||||||
|
if remaining ~= nil then
|
||||||
|
i = #remaining;
|
||||||
|
res[1] = remaining;
|
||||||
|
else
|
||||||
|
i = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
while i < n do
|
||||||
|
local curr = next_chunk();
|
||||||
|
if curr == nil then
|
||||||
|
remaining = nil;
|
||||||
|
next_chunk = nil;
|
||||||
|
local str = table.concat(res, "");
|
||||||
|
if str == "" then
|
||||||
|
return nil;
|
||||||
|
else
|
||||||
|
return "";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
i = i + #curr;
|
||||||
|
res[#res + 1] = curr;
|
||||||
|
end
|
||||||
|
|
||||||
|
local last = table.remove(res);
|
||||||
|
|
||||||
|
local overshot = i - n;
|
||||||
|
local used = #last - overshot;
|
||||||
|
|
||||||
|
remaining = last:sub(used + 1);
|
||||||
|
res[#res + 1] = last:sub(1, used);
|
||||||
|
local val = table.concat(res, "");
|
||||||
|
assert(#val == n, "Incorrect len");
|
||||||
|
return val;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function raw_packet_reader(next_chunk)
|
||||||
|
local reader = to_read(next_chunk);
|
||||||
|
|
||||||
|
local function read_byte()
|
||||||
|
local char = reader(1);
|
||||||
|
if char == nil then return nil end
|
||||||
|
return (string.unpack("I1", char, 1));
|
||||||
|
end
|
||||||
|
|
||||||
|
return function()
|
||||||
|
if reader == nil then return nil end
|
||||||
|
|
||||||
|
local byte = read_byte();
|
||||||
|
if byte == nil then
|
||||||
|
reader = nil;
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
local type, flags = byte >> 4, byte & 0xF;
|
||||||
|
|
||||||
|
local len, n = 0, 0;
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local curr = read_byte();
|
||||||
|
if not curr then error "Unexpected EOF" end
|
||||||
|
len = len | (curr & 0x7F) << n;
|
||||||
|
n = n + 7
|
||||||
|
until (curr & 0x80) == 0;
|
||||||
|
|
||||||
|
local body = reader(len);
|
||||||
|
|
||||||
|
return type, flags, body;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_pk_connect(buff)
|
||||||
|
local i, name, version, flags, keepalive_max, client_id, will_topic, will_msg, username, password;
|
||||||
|
|
||||||
|
name, i = read_utf8(buff, 1);
|
||||||
|
if name ~= "MQIsdp" then
|
||||||
|
error(table.concat { "Protocol name is not 'MQIsdp', found '", name, "' instead" });
|
||||||
|
end
|
||||||
|
version, i = read_uint8(buff, i);
|
||||||
|
if version ~= 3 then
|
||||||
|
error(table.concat { "Protocol version is not '3', found '", version, "' instead" });
|
||||||
|
end
|
||||||
|
|
||||||
|
flags, i = read_uint8(buff, i);
|
||||||
|
|
||||||
|
local clean_session = (flags & 2) ~= 0;
|
||||||
|
local will = (flags & 4) ~= 0;
|
||||||
|
local will_qos = (flags & 24) >> 3;
|
||||||
|
local will_retain = (flags & 32) ~= 0;
|
||||||
|
local has_password = (flags & 64) ~= 0;
|
||||||
|
local has_username = (flags & 128) ~= 0;
|
||||||
|
|
||||||
|
keepalive_max, i = read_uint16(buff, i);
|
||||||
|
|
||||||
|
client_id, i = read_utf8(buff, i);
|
||||||
|
if will then
|
||||||
|
will_topic, i = read_utf8(buff, i);
|
||||||
|
will_msg, i = read_utf8(buff, i);
|
||||||
|
end
|
||||||
|
if has_username then
|
||||||
|
username, i = read_utf8(buff, i);
|
||||||
|
end
|
||||||
|
if has_password then
|
||||||
|
password, i = read_utf8(buff, i);
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "connect",
|
||||||
|
clean_session = clean_session,
|
||||||
|
|
||||||
|
keepalive_max = keepalive_max,
|
||||||
|
client_id = client_id,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
|
||||||
|
will = will and {
|
||||||
|
topic = will_topic,
|
||||||
|
msg = will_msg,
|
||||||
|
qos = will_qos,
|
||||||
|
retain = will_retain
|
||||||
|
} or nil,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_conack(buff)
|
||||||
|
local _, code = string.unpack("!1> I1 I1", buff);
|
||||||
|
local msg;
|
||||||
|
|
||||||
|
if code == 0 then
|
||||||
|
msg = "Success";
|
||||||
|
else
|
||||||
|
msg = ack_codes[code] or "Reserved code given";
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "ack",
|
||||||
|
code = code,
|
||||||
|
msg = msg,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_publish(buff, flags)
|
||||||
|
local retain = (flags & 1) ~= 0;
|
||||||
|
local qos = (flags >> 1) & 3;
|
||||||
|
local dup = (flags & 8) ~= 0;
|
||||||
|
|
||||||
|
local topic, msg_id, i = string.unpack("!1> s2 I2", buff);
|
||||||
|
local data = buff:sub(i);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "publish",
|
||||||
|
retain = retain,
|
||||||
|
qos = qos,
|
||||||
|
dup = dup,
|
||||||
|
topic = topic,
|
||||||
|
id = msg_id,
|
||||||
|
data = data,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_puback(buff, flags)
|
||||||
|
return {
|
||||||
|
type = "puback",
|
||||||
|
id = string.unpack("!1> I2", buff),
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_pubrec(buff)
|
||||||
|
return {
|
||||||
|
type = "pubrec",
|
||||||
|
id = string.unpack("!1> I2", buff),
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_pubrel(buff)
|
||||||
|
return {
|
||||||
|
type = "pubrel",
|
||||||
|
id = string.unpack("!1> I2", buff),
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_pubcomp(buff)
|
||||||
|
return {
|
||||||
|
type = "pubcomp",
|
||||||
|
id = string.unpack("!1> I2", buff),
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_subscribe(buff)
|
||||||
|
local id, i = string.unpack("!1> I2", buff);
|
||||||
|
local topics = {};
|
||||||
|
|
||||||
|
while i < #buff do
|
||||||
|
local name, qos;
|
||||||
|
name, qos, i = string.unpack("!1> s2 I1", buff, i);
|
||||||
|
topics[#topics + 1] = { name = name, qos = qos };
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "subscribe",
|
||||||
|
id = id,
|
||||||
|
topics = topics,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_suback(buff)
|
||||||
|
local id, i = string.unpack("!1> I2", buff);
|
||||||
|
local granted_qos = {};
|
||||||
|
|
||||||
|
while i < #buff do
|
||||||
|
local qos;
|
||||||
|
qos, i = string.unpack("!1> I1", buff, i);
|
||||||
|
granted_qos[#granted_qos + 1] = qos;
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "suback",
|
||||||
|
id = id,
|
||||||
|
granted_qos = granted_qos,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_unsubscribe(buff)
|
||||||
|
local id, i = string.unpack("!1> I2", buff);
|
||||||
|
local topics = {};
|
||||||
|
|
||||||
|
while i < #buff do
|
||||||
|
local name, qos;
|
||||||
|
name, i = string.unpack("!1> s2", buff, i);
|
||||||
|
topics[#topics + 1] = name;
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "unsubscribe",
|
||||||
|
id = id,
|
||||||
|
topics = topics,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_unsuback(buff)
|
||||||
|
local id, i = string.unpack("!1> I2", buff);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type = "unsuback",
|
||||||
|
id = id,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
local function read_pk_pingreq()
|
||||||
|
return { type = "pingreq" };
|
||||||
|
end
|
||||||
|
local function read_pk_pingresp()
|
||||||
|
return { type = "pingresp" };
|
||||||
|
end
|
||||||
|
local function read_pk_disconnect()
|
||||||
|
return { type = "disconnect" };
|
||||||
|
end
|
||||||
|
|
||||||
|
local readers = {
|
||||||
|
read_pk_connect,
|
||||||
|
read_pk_conack,
|
||||||
|
read_pk_publish,
|
||||||
|
read_pk_puback,
|
||||||
|
read_pk_pubrec,
|
||||||
|
read_pk_pubrel,
|
||||||
|
read_pk_pubcomp,
|
||||||
|
read_pk_subscribe,
|
||||||
|
read_pk_suback,
|
||||||
|
read_pk_unsubscribe,
|
||||||
|
read_pk_unsuback,
|
||||||
|
read_pk_pingreq,
|
||||||
|
read_pk_pingresp,
|
||||||
|
read_pk_disconnect,
|
||||||
|
};
|
||||||
|
|
||||||
|
local function packet_reader(next_chunk)
|
||||||
|
local raw_packets = raw_packet_reader(next_chunk);
|
||||||
|
|
||||||
|
return function ()
|
||||||
|
if raw_packets == nil then return nil end
|
||||||
|
|
||||||
|
local type, flags, buff = raw_packets();
|
||||||
|
if type == nil then
|
||||||
|
raw_packets = nil;
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
local reader = readers[type];
|
||||||
|
if reader == nil then
|
||||||
|
error(table.concat { "Unknown packet type '", type, "'" });
|
||||||
|
end
|
||||||
|
|
||||||
|
return reader(buff, flags);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pk_wrap(type, flags, parts)
|
||||||
|
local res = { string.pack("!1> I1", (type << 4) | (flags & 0xF)) };
|
||||||
|
|
||||||
|
local len = 0;
|
||||||
|
|
||||||
|
for i = 1, #parts do
|
||||||
|
len = len + #parts[i];
|
||||||
|
end
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local byte = len & 0x7F;
|
||||||
|
len = len >> 7;
|
||||||
|
if len ~= 0 then
|
||||||
|
byte = byte | 0x80;
|
||||||
|
end
|
||||||
|
|
||||||
|
res[#res + 1] = string.pack("!1> I1", byte);
|
||||||
|
until len == 0;
|
||||||
|
|
||||||
|
for i = 1, #parts do
|
||||||
|
res[#res + 1] = parts[i];
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(res);
|
||||||
|
end
|
||||||
|
|
||||||
|
local writers = {};
|
||||||
|
|
||||||
|
function writers.connect(pk)
|
||||||
|
local flags = 0;
|
||||||
|
|
||||||
|
if pk.clean_session then flags = flags | 2 end
|
||||||
|
if pk.password then flags = flags | 64 end
|
||||||
|
if pk.username then flags = flags | 128 end
|
||||||
|
|
||||||
|
if pk.will then
|
||||||
|
flags = flags | 4;
|
||||||
|
if pk.will.retian then flags = flags | 32 end
|
||||||
|
flags = flags | (pk.will.qos & 3) << 3;
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {
|
||||||
|
string.pack("!1> s2 I1 I1 I2 s2", "MQIsdp", 3, flags, pk.keepalive_max, pk.client_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
if pk.will then
|
||||||
|
parts[#parts + 1] = string.pack("!1> s2 s2", pk.will.topic, pk.will.msg);
|
||||||
|
end
|
||||||
|
if pk.username then
|
||||||
|
parts[#parts + 1] = string.pack("!1> s2", pk.username);
|
||||||
|
end
|
||||||
|
if pk.password then
|
||||||
|
parts[#parts + 1] = string.pack("!1> s2", pk.password);
|
||||||
|
end
|
||||||
|
|
||||||
|
return pk_wrap(1, 0, parts);
|
||||||
|
end
|
||||||
|
function writers.ack(pk)
|
||||||
|
return pk_wrap(2, 0, { string.pack("!1> I1 I1", 0, pk.code or 0) });
|
||||||
|
end
|
||||||
|
function writers.publish(pk)
|
||||||
|
local flags = ((pk.qos or 0) & 3) << 1;
|
||||||
|
if pk.retain then flags = flags | 1 end
|
||||||
|
if pk.dup then flags = flags | 8 end
|
||||||
|
|
||||||
|
return pk_wrap(3, flags, { string.pack("!1> s2 I2", pk.topic, pk.id), pk.data });
|
||||||
|
end
|
||||||
|
function writers.puback(pk)
|
||||||
|
return pk_wrap(4, 2, { string.pack("!1> I2", pk.id) });
|
||||||
|
end
|
||||||
|
function writers.pubrec(pk)
|
||||||
|
return pk_wrap(5, 0, { string.pack("!1> I2", pk.id) });
|
||||||
|
end
|
||||||
|
function writers.pubrel(pk)
|
||||||
|
return pk_wrap(6, 0, { string.pack("!1> I2", pk.id) });
|
||||||
|
end
|
||||||
|
function writers.pubcomp(pk)
|
||||||
|
return pk_wrap(7, 0, { string.pack("!1> I2", pk.id) });
|
||||||
|
end
|
||||||
|
function writers.subscribe(pk)
|
||||||
|
local parts = { string.pack("!1> I2", pk.id) };
|
||||||
|
|
||||||
|
for i = 1, #pk.topics do
|
||||||
|
parts[i + 1] = string.pack("!1> s2 I1", pk.topics[i].name, pk.topics[i].qos);
|
||||||
|
end
|
||||||
|
|
||||||
|
return pk_wrap(8, 2, parts);
|
||||||
|
end
|
||||||
|
function writers.suback(pk)
|
||||||
|
local parts = { string.pack("!1> I2", pk.id) };
|
||||||
|
|
||||||
|
for i = 1, #pk.topics do
|
||||||
|
parts[i + 1] = string.pack("!1> I1", pk.granted_qos[i]);
|
||||||
|
end
|
||||||
|
|
||||||
|
return pk_wrap(9, 0, parts);
|
||||||
|
end
|
||||||
|
function writers.unsubscribe(pk)
|
||||||
|
local parts = { string.pack("!1> I2", pk.id) };
|
||||||
|
|
||||||
|
for i = 1, #pk.topics do
|
||||||
|
parts[i + 1] = string.pack("!1> s2", pk.topics[i]);
|
||||||
|
end
|
||||||
|
|
||||||
|
return pk_wrap(10, 2, parts);
|
||||||
|
end
|
||||||
|
function writers.unsuback(pk)
|
||||||
|
return pk_wrap(11, 0, { string.pack("!1> I2", pk.id) });
|
||||||
|
end
|
||||||
|
function writers.pingreq()
|
||||||
|
return pk_wrap(12, 0, {});
|
||||||
|
end
|
||||||
|
function writers.pingresp()
|
||||||
|
return pk_wrap(13, 0, {});
|
||||||
|
end
|
||||||
|
function writers.disconnect()
|
||||||
|
return pk_wrap(14, 0, {});
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_packet(pk)
|
||||||
|
local writer = writers[pk.type];
|
||||||
|
if writer == nil then
|
||||||
|
error(table.concat { "Unknown packet type '", pk.type or "(nil)", "'" });
|
||||||
|
end
|
||||||
|
|
||||||
|
return writer(pk);
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
read = packet_reader,
|
||||||
|
write = write_packet,
|
||||||
|
};
|
||||||
160
src/utils/path.lua
Normal file
160
src/utils/path.lua
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
---@diagnostic disable: cast-local-type
|
||||||
|
local exports = {};
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string[] parts
|
||||||
|
---@return integer? i
|
||||||
|
---@return boolean dir
|
||||||
|
function exports.split(...)
|
||||||
|
--- @type integer | nil
|
||||||
|
local i = 0;
|
||||||
|
--- @type string[]
|
||||||
|
local res = {};
|
||||||
|
local dir = false;
|
||||||
|
|
||||||
|
local _, start = (... or ""):find("^/+");
|
||||||
|
if start == nil then
|
||||||
|
start = 1;
|
||||||
|
else
|
||||||
|
i = nil;
|
||||||
|
dir = true;
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
local p = select(i, ...);
|
||||||
|
|
||||||
|
if p ~= nil then
|
||||||
|
for part, slashes in p:gmatch("([^/]+)(/*)", start) do
|
||||||
|
dir = #slashes > 0;
|
||||||
|
if part == ".." then
|
||||||
|
if #res == 0 then
|
||||||
|
if i ~= nil then
|
||||||
|
i = i + 1;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
res[#res] = nil;
|
||||||
|
end
|
||||||
|
elseif part ~= "." and part ~= "" then
|
||||||
|
res[#res + 1] = part;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res, i, dir;
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param parts string[]
|
||||||
|
---@param i integer?
|
||||||
|
---@param dir boolean | "all"
|
||||||
|
function exports.stringify(parts, i, dir)
|
||||||
|
local a = table.concat(parts, "/");
|
||||||
|
|
||||||
|
if #parts == 0 then
|
||||||
|
if i == nil then
|
||||||
|
return dir and "/" or "/.";
|
||||||
|
elseif i == 0 then
|
||||||
|
return dir and "./" or "";
|
||||||
|
else
|
||||||
|
local res = (".."):rep(i, "/");
|
||||||
|
return dir and res .. "/" or res;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local res = i and ("../"):rep(i) or "/";
|
||||||
|
|
||||||
|
if dir then
|
||||||
|
return res .. a .. "/";
|
||||||
|
else
|
||||||
|
return res .. a;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string path
|
||||||
|
---@return boolean dir
|
||||||
|
function exports.join(...)
|
||||||
|
local parts, i, dir = exports.split(...);
|
||||||
|
return exports.stringify(parts, i, dir), dir;
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string
|
||||||
|
function exports.join_dir(...)
|
||||||
|
local p, i = exports.split(...);
|
||||||
|
return exports.stringify(p, i, true);
|
||||||
|
end
|
||||||
|
---@param ... string
|
||||||
|
---@return string
|
||||||
|
function exports.join_file(...)
|
||||||
|
local p, i = exports.split(...);
|
||||||
|
return exports.stringify(p, i, false);
|
||||||
|
end
|
||||||
|
---@param p string
|
||||||
|
---@return boolean
|
||||||
|
function exports.is_dir(p)
|
||||||
|
return p:match "/$" ~= nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string
|
||||||
|
---@return boolean dir
|
||||||
|
function exports.chroot(...)
|
||||||
|
local parts, i, dir = exports.split((...));
|
||||||
|
|
||||||
|
for i = 2, select("#", ...) do
|
||||||
|
local new_parts, _, new_dir = exports.split((select(i, ...)));
|
||||||
|
dir = new_dir;
|
||||||
|
|
||||||
|
for j = 1, #new_parts do
|
||||||
|
table.insert(parts, new_parts[j]);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return exports.stringify(parts, i, dir), dir;
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string
|
||||||
|
function exports.cwd(...)
|
||||||
|
local parts, i, dir = exports.split((...));
|
||||||
|
|
||||||
|
for i = 2, select("#", ...) do
|
||||||
|
local new_parts, new_i, new_dir = exports.split((select(i, ...)));
|
||||||
|
dir = new_dir;
|
||||||
|
|
||||||
|
if new_i == nil then
|
||||||
|
parts = new_parts;
|
||||||
|
i = nil;
|
||||||
|
else
|
||||||
|
for _ = 1, new_i do
|
||||||
|
parts[#parts] = nil;
|
||||||
|
end
|
||||||
|
|
||||||
|
if new_parts ~= nil then
|
||||||
|
local offset = parts and #parts or 0;
|
||||||
|
for i = 1, #new_parts do
|
||||||
|
parts[i + offset] = new_parts[i];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return exports.stringify(parts, i, dir);
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string
|
||||||
|
function exports.dirname(...)
|
||||||
|
local parts, i = exports.split(...);
|
||||||
|
parts[#parts] = nil;
|
||||||
|
return exports.stringify(parts, i, true);
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ... string
|
||||||
|
---@return string
|
||||||
|
function exports.filename(...)
|
||||||
|
return table.remove(exports.split(...)) or "";
|
||||||
|
end
|
||||||
|
|
||||||
|
return exports;
|
||||||
263
src/utils/printing.lua
Normal file
263
src/utils/printing.lua
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
local default_colors = {
|
||||||
|
func_kw = "\x1B[34m",
|
||||||
|
func_name = "\x1B[93m",
|
||||||
|
str = "\x1B[32m",
|
||||||
|
num = "\x1B[33m",
|
||||||
|
bool = "\x1B[34m",
|
||||||
|
["nil"] = "\x1B[34m",
|
||||||
|
meta = "\x1B[90m",
|
||||||
|
ref = "\x1B[91m",
|
||||||
|
reset = "\x1B[0m",
|
||||||
|
thread = "\x1B[34m",
|
||||||
|
udata = "\x1B[34m",
|
||||||
|
};
|
||||||
|
local str_escape_codes = {
|
||||||
|
["\x00"] = "\\0",
|
||||||
|
["\x01"] = "\\x01",
|
||||||
|
["\x02"] = "\\x02",
|
||||||
|
["\x03"] = "\\x03",
|
||||||
|
["\x04"] = "\\x04",
|
||||||
|
["\x05"] = "\\x05",
|
||||||
|
["\x06"] = "\\x06",
|
||||||
|
["\x07"] = "\\x07",
|
||||||
|
["\x08"] = "\\x08",
|
||||||
|
["\x09"] = "\\t",
|
||||||
|
["\x0A"] = "\\n",
|
||||||
|
["\x0B"] = "\\x0B",
|
||||||
|
["\x0C"] = "\\x0C",
|
||||||
|
["\x0D"] = "\\r",
|
||||||
|
["\x0E"] = "\\x0E",
|
||||||
|
["\x0F"] = "\\x0F",
|
||||||
|
|
||||||
|
["\x10"] = "\\x10",
|
||||||
|
["\x11"] = "\\x11",
|
||||||
|
["\x12"] = "\\x12",
|
||||||
|
["\x13"] = "\\x13",
|
||||||
|
["\x14"] = "\\x14",
|
||||||
|
["\x15"] = "\\x15",
|
||||||
|
["\x16"] = "\\x16",
|
||||||
|
["\x17"] = "\\x17",
|
||||||
|
["\x18"] = "\\x18",
|
||||||
|
["\x19"] = "\\x19",
|
||||||
|
["\x1A"] = "\\x1A",
|
||||||
|
["\x1B"] = "\\x1B",
|
||||||
|
["\x1C"] = "\\x1C",
|
||||||
|
["\x1D"] = "\\x1D",
|
||||||
|
["\x1E"] = "\\x1E",
|
||||||
|
["\x1F"] = "\\x1F",
|
||||||
|
|
||||||
|
["\\"] = "\\\\",
|
||||||
|
["\""] = "\\\"",
|
||||||
|
|
||||||
|
["\x7F"] = "\\x7F",
|
||||||
|
["\xFF"] = "\\xFF",
|
||||||
|
};
|
||||||
|
|
||||||
|
--- @alias tal.printing.color fun(color: string): fun(str: string): string, integer
|
||||||
|
|
||||||
|
--- @param tab table<string, string> | false
|
||||||
|
--- @return tal.printing.color
|
||||||
|
local function mkcolors(tab)
|
||||||
|
local function noop(v)
|
||||||
|
return v, #v;
|
||||||
|
end
|
||||||
|
|
||||||
|
if not tab then
|
||||||
|
return function ()
|
||||||
|
return noop;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local reset = tab.reset;
|
||||||
|
|
||||||
|
return function (color)
|
||||||
|
if tab[color] then
|
||||||
|
local fmt = tab[color];
|
||||||
|
return function (text)
|
||||||
|
return fmt .. text .. reset, #text;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return noop;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param color tal.printing.color
|
||||||
|
--- @return string
|
||||||
|
--- @return integer text_len
|
||||||
|
local function stringify_int(obj, n, color, passed, hit)
|
||||||
|
local kind = type(obj);
|
||||||
|
|
||||||
|
if kind == "table" then
|
||||||
|
if passed[obj] then
|
||||||
|
hit[obj] = true;
|
||||||
|
return color "ref" ("<circular " .. passed[obj] .. ">");
|
||||||
|
end
|
||||||
|
|
||||||
|
passed[obj] = passed.next;
|
||||||
|
passed.next = passed.next + 1;
|
||||||
|
|
||||||
|
local tablen = #obj;
|
||||||
|
local parts = {};
|
||||||
|
local res_len = 0;
|
||||||
|
|
||||||
|
for i = 1, tablen do
|
||||||
|
parts[i] = stringify_int(obj[i], n .. "\t", color, passed, hit) .. ",";
|
||||||
|
end
|
||||||
|
|
||||||
|
local keys = {};
|
||||||
|
|
||||||
|
for k in pairs(obj) do
|
||||||
|
if type(k) ~= "number" or k < 1 or k > tablen then
|
||||||
|
table.insert(keys, k);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(keys, function (a, b)
|
||||||
|
if type(a) ~= type(b) then
|
||||||
|
return type(a) < type(b);
|
||||||
|
else
|
||||||
|
local ok, res = pcall(function (a, b) return a < b end);
|
||||||
|
if ok then return res end
|
||||||
|
|
||||||
|
return tostring(a) < tostring(b);
|
||||||
|
end
|
||||||
|
end);
|
||||||
|
|
||||||
|
for i = 1, #keys do
|
||||||
|
local k = keys[i];
|
||||||
|
local v = obj[k];
|
||||||
|
|
||||||
|
local val, val_len = stringify_int(v, n .. "\t", color, passed, hit);
|
||||||
|
if val ~= nil then
|
||||||
|
if type(k) == "string" and k:find "^[a-zA-Z_][a-zA-Z0-9_]*$" then
|
||||||
|
res_len = res_len + #k + 3 + val_len + 1;
|
||||||
|
table.insert(parts, k .. " = " .. val .. ",");
|
||||||
|
else
|
||||||
|
local key, key_len = stringify_int(k, n .. "\t", color, passed, hit);
|
||||||
|
res_len = res_len + 1 + key_len + 4 + val_len + 1;
|
||||||
|
table.insert(parts, "[" .. key .. "] = " .. val .. ",");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local meta = getmetatable(obj);
|
||||||
|
if meta ~= nil then
|
||||||
|
local meta_str, meta_len = stringify_int(meta, n .. "\t", color, passed, hit);
|
||||||
|
res_len = res_len + 6 + 3 + meta_len + 1;
|
||||||
|
table.insert(parts, color "meta" ("<meta>") .. " = " .. meta_str .. ",");
|
||||||
|
end
|
||||||
|
|
||||||
|
if #parts == 0 then
|
||||||
|
if hit[obj] then
|
||||||
|
return color "ref" ("<ref " .. passed[obj] .. ">") .. " {}", #("<ref " .. passed[obj] .. "> {}");
|
||||||
|
else
|
||||||
|
return "{}", 2;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local contents;
|
||||||
|
if res_len > 160 then
|
||||||
|
local indent = "\n" .. n .. "\t";
|
||||||
|
|
||||||
|
contents = indent .. table.concat(parts, indent) .. "\n" .. n;
|
||||||
|
else
|
||||||
|
contents = " " .. table.concat(parts, " "):sub(1, -2) .. " ";
|
||||||
|
end
|
||||||
|
|
||||||
|
if hit[obj] then
|
||||||
|
return color "ref" ("<ref " .. passed[obj] .. ">") .. " {" .. contents .. "}", #("<ref " .. passed[obj] .. ">") + 3 + res_len;
|
||||||
|
else
|
||||||
|
return "{" .. contents .. "}", 2 + res_len;
|
||||||
|
end
|
||||||
|
elseif kind == "function" then
|
||||||
|
local data = debug.getinfo(obj, "Sn");
|
||||||
|
local res = color "func_kw" "function";
|
||||||
|
local res_len = 8;
|
||||||
|
|
||||||
|
if data.name then
|
||||||
|
res = res .. " " .. color "func_name" (data.name);
|
||||||
|
res_len = res_len + 1 + #data.name;
|
||||||
|
end
|
||||||
|
|
||||||
|
if data.source ~= "=?" and data.source ~= "=[C]" then
|
||||||
|
res = res .. " @ " .. data.short_src;
|
||||||
|
res_len = 3 + #data.short_src;
|
||||||
|
|
||||||
|
if data.linedefined then
|
||||||
|
res = res .. ":" .. data.linedefined;
|
||||||
|
res_len = 1 + #tostring(data.linedefined);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res, res_len;
|
||||||
|
elseif kind == "string" then
|
||||||
|
local escaped, n = obj:gsub("[\x00-\x1F\"\\\x7F\xFF]", str_escape_codes);
|
||||||
|
if n > 4 and n > #obj / 80 then
|
||||||
|
local marker = obj:match "%](%=*)%]";
|
||||||
|
|
||||||
|
for curr in obj:gmatch "%](%=*)%]" do
|
||||||
|
if not marker or #marker < #curr then
|
||||||
|
marker = curr;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not marker then
|
||||||
|
marker = "";
|
||||||
|
else
|
||||||
|
marker = marker .. "=";
|
||||||
|
end
|
||||||
|
return color "str" ("[" .. marker .. "[" .. obj .. "]" .. marker .. "]");
|
||||||
|
else
|
||||||
|
return color "str" ("\"" .. escaped .. "\"");
|
||||||
|
end
|
||||||
|
elseif kind == "nil" then
|
||||||
|
return color "nil" ("nil");
|
||||||
|
elseif kind == "boolean" then
|
||||||
|
return color "bool" (tostring(obj));
|
||||||
|
elseif kind == "number" then
|
||||||
|
return color "num" (tostring(obj));
|
||||||
|
elseif kind == "thread" then
|
||||||
|
return color "thread" (tostring(obj));
|
||||||
|
elseif kind == "userdata" then
|
||||||
|
return color "udata" (tostring(obj));
|
||||||
|
else
|
||||||
|
error "unknown type";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param colors true | false | table
|
||||||
|
local function stringify(obj, colors)
|
||||||
|
if colors == true then colors = default_colors end
|
||||||
|
return stringify_int(obj, "", mkcolors(colors), { next = 0 }, {});
|
||||||
|
end
|
||||||
|
|
||||||
|
function pprint(...)
|
||||||
|
if select("#", ...) == 0 then return end
|
||||||
|
|
||||||
|
local function fix(...)
|
||||||
|
if select("#", ...) == 0 then
|
||||||
|
return;
|
||||||
|
else
|
||||||
|
return stringify((...), true), fix(select(2, ...));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print(fix(...));
|
||||||
|
end
|
||||||
|
function print(...)
|
||||||
|
if select("#", ...) == 0 then
|
||||||
|
return;
|
||||||
|
elseif select("#", ...) == 1 then
|
||||||
|
io.stderr:write(tostring(...), "\n");
|
||||||
|
else
|
||||||
|
io.stderr:write(tostring(...), "\t");
|
||||||
|
print(select(2, ...));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
print = print,
|
||||||
|
pprint = pprint,
|
||||||
|
stringify = stringify,
|
||||||
|
};
|
||||||
11
src/utils/tag.lua
Normal file
11
src/utils/tag.lua
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
return function (name)
|
||||||
|
return setmetatable({}, {
|
||||||
|
__newindex = function ()
|
||||||
|
error "Tags are read-only";
|
||||||
|
end,
|
||||||
|
__metatable = false,
|
||||||
|
__tostring = function ()
|
||||||
|
return "@tag(" .. name .. ")";
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
122
src/utils/url.lua
Normal file
122
src/utils/url.lua
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
local url = {};
|
||||||
|
|
||||||
|
function url.encode(data)
|
||||||
|
data = tostring(data);
|
||||||
|
return data:gsub("[^a-zA-Z0-9%-_]", function (c)
|
||||||
|
return "%" .. ("%.2X"):format(string.byte(c));
|
||||||
|
end);
|
||||||
|
end
|
||||||
|
function url.encode_url(data)
|
||||||
|
data = tostring(data);
|
||||||
|
return data:gsub("[^%?/a-zA-Z0-9%-_]", function (c)
|
||||||
|
return "%" .. ("%.2X"):format(string.byte(c));
|
||||||
|
end);
|
||||||
|
end
|
||||||
|
|
||||||
|
function url.decode(data)
|
||||||
|
return data:gsub("%%(%d%d)", function (val)
|
||||||
|
return string.char(assert(tonumber(val, 16)));
|
||||||
|
end);
|
||||||
|
end
|
||||||
|
|
||||||
|
function url.encode_body(body)
|
||||||
|
local parts = {};
|
||||||
|
|
||||||
|
for i = 1, #body do
|
||||||
|
table.insert(parts, url.encode(body[i][1]) .. "=" .. url.encode(body[i][2]));
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(body) do
|
||||||
|
if type(k) == "string" then
|
||||||
|
table.insert(parts, url.encode(k) .. "=" .. url.encode(v));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(parts, "&");
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param raw string
|
||||||
|
--- @return { scheme: string?, host?: string, port?: integer, path: string, params: table<string, string | true>, username?: string, password?: string }?
|
||||||
|
--- @return string?
|
||||||
|
function url.parse(raw)
|
||||||
|
local scheme, username, password, host, port, path, params;
|
||||||
|
|
||||||
|
local l;
|
||||||
|
local i = 1;
|
||||||
|
|
||||||
|
params = {};
|
||||||
|
|
||||||
|
scheme, l = raw:match("^([%w%d_%-]+):()", i);
|
||||||
|
i = l or i;
|
||||||
|
|
||||||
|
if raw:match("^//", i) then
|
||||||
|
i = i + 2;
|
||||||
|
username, password, l = raw:match("^(.-):(.-)@()", i);
|
||||||
|
i = l or i;
|
||||||
|
|
||||||
|
if username then
|
||||||
|
username = url.decode(username);
|
||||||
|
password = url.decode(password);
|
||||||
|
end
|
||||||
|
|
||||||
|
host, i = raw:match("^([^:/]*)()", i);
|
||||||
|
port, l = raw:match("^:(%d+)()", i);
|
||||||
|
i = l or i;
|
||||||
|
|
||||||
|
if host then
|
||||||
|
host = url.decode(host);
|
||||||
|
end
|
||||||
|
if port then
|
||||||
|
port = tonumber(port);
|
||||||
|
end
|
||||||
|
|
||||||
|
if i > #raw then
|
||||||
|
return {
|
||||||
|
scheme = scheme,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
host = host,
|
||||||
|
port = tonumber(port),
|
||||||
|
path = "/",
|
||||||
|
params = {}
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
i = raw:match("^()/", i);
|
||||||
|
end
|
||||||
|
|
||||||
|
if not i then return nil, "invalid URL syntax" end
|
||||||
|
|
||||||
|
path, i = raw:match("^([^?]*)()", i);
|
||||||
|
path = url.decode(path);
|
||||||
|
|
||||||
|
if i <= #raw then
|
||||||
|
i = raw:match("^%?()", i);
|
||||||
|
if not i then return nil, "invalid URL syntax" end
|
||||||
|
|
||||||
|
for part in raw:gmatch("[^&]*", i) do
|
||||||
|
local key, val = part:match "^(.-)=(.*)$";
|
||||||
|
if not key then
|
||||||
|
key = url.decode(part);
|
||||||
|
val = true;
|
||||||
|
else
|
||||||
|
key = url.decode(key);
|
||||||
|
val = url.decode(val);
|
||||||
|
end
|
||||||
|
|
||||||
|
params[key] = val;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
scheme = scheme,
|
||||||
|
username = username,
|
||||||
|
password = password,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
path = path,
|
||||||
|
params = params,
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
return url;
|
||||||
569
src/utils/xml.lua
Normal file
569
src/utils/xml.lua
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
--- @alias xml_node_raw { tag: string, attribs: { [string]: string }, [integer]: xml_element }
|
||||||
|
--- @alias xml_element string | xml_node
|
||||||
|
|
||||||
|
--- @class xml_node
|
||||||
|
--- @field tag string
|
||||||
|
--- @field attribs { [string]: string }
|
||||||
|
--- @field [integer] xml_element
|
||||||
|
local xml_node = {};
|
||||||
|
xml_node.__index = xml_node;
|
||||||
|
|
||||||
|
--- @param name string?
|
||||||
|
function xml_node:get_all(name)
|
||||||
|
--- @type xml_node[]
|
||||||
|
local res = {};
|
||||||
|
|
||||||
|
for _, el in ipairs(self) do
|
||||||
|
if type(el) ~= "string" and (not name or el.tag == name) then
|
||||||
|
res[#res + 1] = el;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param name string
|
||||||
|
function xml_node:get(name)
|
||||||
|
local res = self:get_all(name);
|
||||||
|
if #res == 0 then
|
||||||
|
error("node '" .. name .. "' not found");
|
||||||
|
elseif #res > 1 then
|
||||||
|
error("multiple nodes '" .. name .. "' exist");
|
||||||
|
else
|
||||||
|
return res[1];
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param name string
|
||||||
|
--- @return xml_node[]
|
||||||
|
function xml_node:query_all(name)
|
||||||
|
local tag, id, classes, attribs;
|
||||||
|
local rem = name;
|
||||||
|
|
||||||
|
local function get_tag(rem)
|
||||||
|
local tag, res_rem = rem:match "^([^%s#%.%[%]]+)%s*(.*)$";
|
||||||
|
if tag then return res_rem, tag end
|
||||||
|
return rem;
|
||||||
|
end
|
||||||
|
local function get_class(rem)
|
||||||
|
local class, res_rem = rem:match "^%.([^%s#%.%[%]]+)%s*(.*)$";
|
||||||
|
if class then return res_rem, class end
|
||||||
|
return rem;
|
||||||
|
end
|
||||||
|
local function get_id(rem)
|
||||||
|
local id, res_rem = rem:match "^#([^%s#%.%[%]]+)%s*(.*)$";
|
||||||
|
if id then return res_rem, id end
|
||||||
|
return rem;
|
||||||
|
end
|
||||||
|
local function get_attrib(rem)
|
||||||
|
local inner, res_rem = rem:match "^%[%s*([^%]]+)%s*%]%s*(.*)$";
|
||||||
|
if not inner then return rem end
|
||||||
|
|
||||||
|
local prop, val = inner:match "^(.-)%s*=%s*(.-)$";
|
||||||
|
if prop then
|
||||||
|
return res_rem, prop, val;
|
||||||
|
else
|
||||||
|
return res_rem, inner;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rem, tag = get_tag(rem);
|
||||||
|
rem, id = get_id(rem);
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local class, key, val;
|
||||||
|
rem, class = get_class(rem);
|
||||||
|
if class then
|
||||||
|
classes = classes or {};
|
||||||
|
classes[class] = 0;
|
||||||
|
else
|
||||||
|
rem, key, val = get_attrib(rem);
|
||||||
|
if key then
|
||||||
|
attribs = attribs or {};
|
||||||
|
attribs[key] = val or true;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #rem > 0 then error "invalid query" end
|
||||||
|
|
||||||
|
local res = {};
|
||||||
|
|
||||||
|
local function match(el)
|
||||||
|
if type(el) == "string" then return false end
|
||||||
|
if tag and el.tag ~= tag then return false end
|
||||||
|
if id and el.attribs.id ~= id then return false end
|
||||||
|
if classes then
|
||||||
|
if not el.attribs.class then return false end
|
||||||
|
|
||||||
|
for name in el.attribs.class:gmatch "[^%s]+" do
|
||||||
|
if classes[name] then
|
||||||
|
classes[name] = classes[name] + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok = true;
|
||||||
|
|
||||||
|
for k, v in pairs(classes) do
|
||||||
|
if classes[k] == 0 then
|
||||||
|
ok = false;
|
||||||
|
end
|
||||||
|
|
||||||
|
classes[k] = 0;
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ok then return false end
|
||||||
|
end
|
||||||
|
|
||||||
|
if attribs then
|
||||||
|
for k, v in pairs(attribs) do
|
||||||
|
if v == true then
|
||||||
|
if not el.attribs[k] then return false end
|
||||||
|
else
|
||||||
|
if el.attribs[k] ~= v then return false end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #self do
|
||||||
|
if match(self[i]) then
|
||||||
|
table.insert(res, self[i]);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param name string
|
||||||
|
--- @return xml_node
|
||||||
|
function xml_node:query(name)
|
||||||
|
local res = self:query_all(name);
|
||||||
|
if #res == 1 then
|
||||||
|
return res[1];
|
||||||
|
else
|
||||||
|
error("no single element with the query found", 2);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param name string
|
||||||
|
--- @return xml_node
|
||||||
|
function xml_node:query_first(name)
|
||||||
|
local res = self:query_all(name);
|
||||||
|
if #res >= 1 then
|
||||||
|
return res[1];
|
||||||
|
else
|
||||||
|
return nil;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @return string
|
||||||
|
function xml_node:text()
|
||||||
|
if #self == 0 then
|
||||||
|
return "";
|
||||||
|
elseif #self == 1 and type(self[1]) == "string" then
|
||||||
|
--- @diagnostic disable-next-line: return-type-mismatch
|
||||||
|
return self[1];
|
||||||
|
else
|
||||||
|
error "not a text-only node";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function xml_node:to_string(level)
|
||||||
|
level = level or 0;
|
||||||
|
local indent = ("\t"):rep(level);
|
||||||
|
|
||||||
|
local lines = { self.tag .. " {" };
|
||||||
|
|
||||||
|
for k, v in pairs(self.attribs) do
|
||||||
|
table.insert(lines, indent .. "\t" .. k .. " = " .. v);
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #self do
|
||||||
|
local child = self[i];
|
||||||
|
if type(child) == "string" then
|
||||||
|
table.insert(lines, indent .. "\t" .. child);
|
||||||
|
else
|
||||||
|
table.insert(lines, indent .. "\t" .. child:to_string(level + 1));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #lines == 1 then
|
||||||
|
return self.tag .. "{ }";
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(lines, indent .. "}");
|
||||||
|
|
||||||
|
return table.concat(lines, "\n");
|
||||||
|
end
|
||||||
|
|
||||||
|
function xml_node:__tostring()
|
||||||
|
return self:to_string();
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param raw xml_node_raw
|
||||||
|
--- @return xml_node
|
||||||
|
function xml_node.new(raw)
|
||||||
|
local res = setmetatable({
|
||||||
|
tag = raw.tag,
|
||||||
|
attribs = raw.attribs or {},
|
||||||
|
}, xml_node);
|
||||||
|
|
||||||
|
table.move(raw, 1, #raw, 1, res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function skip_spaces(raw, i)
|
||||||
|
local next_i = raw:find("[^%s]", i);
|
||||||
|
if next_i then return next_i end
|
||||||
|
|
||||||
|
if raw:find("^%s", i) then
|
||||||
|
local match = raw:match("^%s*", i);
|
||||||
|
if match then return i + #match end
|
||||||
|
else
|
||||||
|
return i;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_tag(raw, i, state)
|
||||||
|
i = skip_spaces(raw, i);
|
||||||
|
|
||||||
|
local tag = raw:match("^[%w0-9%-_:]+", i);
|
||||||
|
if tag == nil then
|
||||||
|
if state.relaxed then
|
||||||
|
return nil, i;
|
||||||
|
else
|
||||||
|
error("expected tag name near '" .. raw:sub(i, i + 25) .. "'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
i = i + #tag;
|
||||||
|
|
||||||
|
local attribs = {};
|
||||||
|
|
||||||
|
while true do
|
||||||
|
i = skip_spaces(raw, i);
|
||||||
|
|
||||||
|
local all, key, _, val = raw:match("^(([%w0-9%-_:]-)%s*=%s*(['\"])(.-)%3%s*)", i);
|
||||||
|
if all then
|
||||||
|
attribs[key] = val;
|
||||||
|
i = i + #all;
|
||||||
|
elseif state.relaxed then
|
||||||
|
all, key, val = raw:match("^(([%w0-9%-_:]+)%s*=%s*([^/<>%s]+))", i);
|
||||||
|
if all then
|
||||||
|
attribs[key] = val;
|
||||||
|
i = i + #all;
|
||||||
|
else
|
||||||
|
all, key = raw:match("^(([%w0-9%-_:]+)%s*)", i);
|
||||||
|
if all then
|
||||||
|
attribs[key] = "";
|
||||||
|
i = i + #all;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { tag = tag, attribs = attribs }, i;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_part(raw, i, state)
|
||||||
|
i = skip_spaces(raw, i);
|
||||||
|
local j = i;
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local comment_end;
|
||||||
|
|
||||||
|
if raw:sub(i, i + 3) == "<!--" then
|
||||||
|
i = i + 4;
|
||||||
|
|
||||||
|
comment_end = raw:find("-->", i);
|
||||||
|
if comment_end then
|
||||||
|
i = comment_end + 3;
|
||||||
|
else
|
||||||
|
i = #raw + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
i = skip_spaces(raw, i);
|
||||||
|
end
|
||||||
|
until not comment_end;
|
||||||
|
|
||||||
|
local fallback = false;
|
||||||
|
|
||||||
|
if i > #raw then
|
||||||
|
return { type = "eof" }, i;
|
||||||
|
elseif (state.start or state.relaxed) and raw:sub(i, i + 1) == "<?" then
|
||||||
|
i = i + 2;
|
||||||
|
|
||||||
|
local tag;
|
||||||
|
tag, i = parse_tag(raw, i, state);
|
||||||
|
|
||||||
|
if tag then
|
||||||
|
if raw:sub(i, i + 1) == "!>" then
|
||||||
|
i = i + 2;
|
||||||
|
|
||||||
|
return { type = "version", tag = tag.tag, attribs = tag.attribs }, i;
|
||||||
|
end
|
||||||
|
elseif not state.relaxed then
|
||||||
|
error("malformed XML near '" .. raw:sub(i, i + 25) .. "'");
|
||||||
|
end
|
||||||
|
|
||||||
|
fallback = true;
|
||||||
|
elseif (state.start or state.relaxed) and raw:sub(i, i + 1) == "<!" then
|
||||||
|
i = i + 2;
|
||||||
|
|
||||||
|
local tag;
|
||||||
|
tag, i = parse_tag(raw, i, state);
|
||||||
|
if tag then
|
||||||
|
if raw:sub(i, i) == ">" then
|
||||||
|
i = i + 1;
|
||||||
|
return { type = "version", tag = tag.tag, attribs = tag.attribs }, i;
|
||||||
|
elseif not state.relaxed then
|
||||||
|
error("malformed XML near '" .. raw:sub(i, i + 25) .. "'");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fallback = true;
|
||||||
|
elseif raw:sub(i, i + 1) == "</" then
|
||||||
|
i = i + 2;
|
||||||
|
i = skip_spaces(raw, i);
|
||||||
|
|
||||||
|
local tag = raw:match("[%w0-9%-_:]+", i);
|
||||||
|
if tag then
|
||||||
|
i = i + #tag;
|
||||||
|
|
||||||
|
if raw:sub(i, i) == ">" then
|
||||||
|
i = i + 1;
|
||||||
|
return { type = "end", tag = tag }, i;
|
||||||
|
elseif not state.relaxed then
|
||||||
|
error("malformed closing tag near '" .. raw:sub(i, i + 25) .. "'");
|
||||||
|
end
|
||||||
|
elseif not state.relaxed then
|
||||||
|
error("expected closing tag name near '" .. raw:sub(i, i + 25) .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
fallback = true;
|
||||||
|
elseif raw:sub(i, i) == "<" then
|
||||||
|
i = i + 1;
|
||||||
|
|
||||||
|
local tag;
|
||||||
|
tag, i = parse_tag(raw, i, state);
|
||||||
|
if tag then
|
||||||
|
if raw:sub(i, i + 1) == "/>" then
|
||||||
|
i = i + 2;
|
||||||
|
return { type = "small", tag = tag.tag, attribs = tag.attribs }, i;
|
||||||
|
elseif raw:sub(i, i) == ">" then
|
||||||
|
i = i + 1;
|
||||||
|
return { type = "begin", tag = tag.tag, attribs = tag.attribs }, i;
|
||||||
|
elseif not state.relaxed then
|
||||||
|
error("malformed opening tag near '" .. raw:sub(i, i + 25) .. "'");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fallback = true;
|
||||||
|
end
|
||||||
|
|
||||||
|
i = j;
|
||||||
|
|
||||||
|
local text_parts = {};
|
||||||
|
|
||||||
|
if fallback then
|
||||||
|
table.insert(text_parts, raw:sub(i, i));
|
||||||
|
i = i + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
while i <= #raw do
|
||||||
|
local text_end = raw:find("<", i);
|
||||||
|
|
||||||
|
local text_part = raw:sub(i, text_end and text_end - 1 or #raw)
|
||||||
|
:match "^%s*(.-)%s*$"
|
||||||
|
:gsub("%s+", " ");
|
||||||
|
|
||||||
|
if text_part ~= "" then
|
||||||
|
text_parts[#text_parts + 1] = text_part;
|
||||||
|
end
|
||||||
|
|
||||||
|
if not text_end then i = #raw + 1 break end
|
||||||
|
|
||||||
|
i = text_end or #raw;
|
||||||
|
|
||||||
|
local comment_end;
|
||||||
|
|
||||||
|
if raw:sub(i, i + 3) == "<!--" then
|
||||||
|
i = i + 4;
|
||||||
|
|
||||||
|
comment_end = raw:find("-->", i);
|
||||||
|
if comment_end then
|
||||||
|
i = comment_end + 3;
|
||||||
|
else
|
||||||
|
i = #raw + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
i = skip_spaces(raw, i);
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #text_parts > 0 then
|
||||||
|
return { type = "text", text = table.concat(text_parts, " ") }, i;
|
||||||
|
elseif i > #raw then
|
||||||
|
return { type = "eof" }, i;
|
||||||
|
else
|
||||||
|
error("malformed XML near '" .. raw:sub(i, i + 25) .. "'");
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_in_list(tag, attribs, list)
|
||||||
|
if not list then return false end
|
||||||
|
|
||||||
|
if list[tag] then
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
for i = 1, #list do
|
||||||
|
if list[i](tag, attribs) then
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
local function fix_list(list)
|
||||||
|
local res = {};
|
||||||
|
if list then
|
||||||
|
for k, v in pairs(list) do
|
||||||
|
if type(k) == "number" then
|
||||||
|
if type(v) == "function" then
|
||||||
|
table.insert(res, v);
|
||||||
|
else
|
||||||
|
res[v] = true;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
res[k] = true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return res;
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param raw string
|
||||||
|
--- @param settings? { relaxed?: boolean, self_closing?: { [integer]: string | (fun(tag: string, attribs: table<string, string>): boolean), [string]: true } } | "html"
|
||||||
|
--- @return xml_node
|
||||||
|
local function parse(raw, settings)
|
||||||
|
if settings == "html" then
|
||||||
|
settings = {
|
||||||
|
relaxed = true,
|
||||||
|
self_closing = {
|
||||||
|
"area", "base", "br", "col", "embed", "hr", "img", "input", "meta", "param", "source", "track", "wbr",
|
||||||
|
function (tag, attribs) return tag == "script" and attribs.src end,
|
||||||
|
function (tag, attribs) return tag == "link" and attribs.href end,
|
||||||
|
-- function (tag, attribs) end,
|
||||||
|
},
|
||||||
|
no_children = { "script", "style" },
|
||||||
|
};
|
||||||
|
end
|
||||||
|
|
||||||
|
settings = settings or {};
|
||||||
|
|
||||||
|
local state = {
|
||||||
|
start = true,
|
||||||
|
relaxed = settings.relaxed or false,
|
||||||
|
self_closing = fix_list(settings.self_closing),
|
||||||
|
no_children = fix_list(settings.no_children),
|
||||||
|
}
|
||||||
|
|
||||||
|
--- @type xml_node
|
||||||
|
local document = xml_node.new { tag = "document", attribs = {} };
|
||||||
|
local curr_node = document;
|
||||||
|
local stack = {};
|
||||||
|
local i = 1;
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local part;
|
||||||
|
part, i = parse_part(raw, i, state);
|
||||||
|
if part.type == "eof" then
|
||||||
|
break;
|
||||||
|
elseif part.type == "text" then
|
||||||
|
if not state.relaxed and #stack == 0 then
|
||||||
|
error("text may not appear outside a tag (near '" .. raw:sub(i, i + 25) .. "')");
|
||||||
|
else
|
||||||
|
if type(curr_node[#curr_node]) == "string" then
|
||||||
|
curr_node[#curr_node] = curr_node[#curr_node] .. part.text;
|
||||||
|
else
|
||||||
|
table.insert(curr_node, part.text);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif part.type == "version" then
|
||||||
|
curr_node.attribs.type = part.tag;
|
||||||
|
curr_node.attribs.version = part.attribs.version;
|
||||||
|
elseif part.type == "begin" then
|
||||||
|
local new_node = xml_node.new { tag = part.tag, attribs = part.attribs };
|
||||||
|
table.insert(curr_node, new_node);
|
||||||
|
|
||||||
|
if is_in_list(part.tag, part.attribs, state.no_children) then
|
||||||
|
local start_i = i;
|
||||||
|
local end_part;
|
||||||
|
while true do
|
||||||
|
local end_i = i - 1;
|
||||||
|
end_part, i = parse_part(raw, i, state);
|
||||||
|
if end_part.type == "end" and end_part.tag == part.tag or end_part.type == "eof" then
|
||||||
|
table.insert(new_node, raw:sub(start_i, end_i));
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif not is_in_list(part.tag, part.attribs, state.self_closing) then
|
||||||
|
curr_node = new_node;
|
||||||
|
table.insert(stack, new_node);
|
||||||
|
end
|
||||||
|
elseif part.type == "end" then
|
||||||
|
if not state.relaxed then
|
||||||
|
if part.tag ~= curr_node.tag then
|
||||||
|
error("closing tag '" .. part.tag .. "' doesn't match most recent opening tag '" .. curr_node.tag .. "'");
|
||||||
|
else
|
||||||
|
table.remove(stack);
|
||||||
|
curr_node = stack[#stack];
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local found_i;
|
||||||
|
|
||||||
|
for i = #stack, 1, -1 do
|
||||||
|
if stack[i].tag == part.tag then
|
||||||
|
found_i = i;
|
||||||
|
break;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if found_i then
|
||||||
|
for _ = #stack, found_i, -1 do
|
||||||
|
table.remove(stack);
|
||||||
|
end
|
||||||
|
|
||||||
|
curr_node = stack[#stack] or document;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif part.type == "small" then
|
||||||
|
curr_node[#curr_node + 1] = xml_node.new { tag = part.tag, attribs = part.attribs };
|
||||||
|
else
|
||||||
|
error "wtf";
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not state.relaxed and #stack > 0 then
|
||||||
|
error("tag '" .. curr_node.tag .. "' was left open");
|
||||||
|
end
|
||||||
|
|
||||||
|
return document;
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
parse = parse,
|
||||||
|
node = xml_node.new,
|
||||||
|
};
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { invoke, resolveView } from "./resolution.ts";
|
|
||||||
import View, { popLocals, pushLocals, type Locals } from "./View.ts";
|
|
||||||
|
|
||||||
export default class JSView extends View {
|
|
||||||
public override onMetadata() {
|
|
||||||
const res = [];
|
|
||||||
|
|
||||||
for (const match of this.src.matchAll(/^(?:#!.+?\n)?\s*\/\*---\n(.+)\n---\*\/$/g)) {
|
|
||||||
res.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override onRender(locals: Locals = {}, full = false): string {
|
|
||||||
locals = this.locals(locals);
|
|
||||||
let res: any, text;
|
|
||||||
|
|
||||||
try {
|
|
||||||
pushLocals(locals);
|
|
||||||
res = invoke(this, this.path);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
popLocals();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof res === "object" && res != null && typeof res[Symbol.iterator] === "function") {
|
|
||||||
res = res[Symbol.iterator]();
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const el = res.next();
|
|
||||||
if (el.value != null) parts.push(el.value);
|
|
||||||
if (el.done) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = parts.join("");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
text = String(res ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (full && locals.page) {
|
|
||||||
const parent = resolveView(String(locals.page), false, true);
|
|
||||||
return parent?.render({ ...locals, text }, full) ?? text;
|
|
||||||
}
|
|
||||||
else return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import frontMatter from "markdown-it-front-matter";
|
|
||||||
import markdown from "markdown-it";
|
|
||||||
import hljs from "highlight.js";
|
|
||||||
import View, { type Locals, type Metadata } from "./View.ts";
|
|
||||||
import { resolveView } from "./resolution.ts";
|
|
||||||
import { parse as parseYaml } from "@std/yaml";
|
|
||||||
|
|
||||||
export default class MarkdownView extends View {
|
|
||||||
public static metadata: Metadata;
|
|
||||||
public static readonly renderer = markdown({
|
|
||||||
html: true,
|
|
||||||
xhtmlOut: false,
|
|
||||||
breaks: false,
|
|
||||||
langPrefix: 'language-',
|
|
||||||
linkify: true,
|
|
||||||
typographer: false,
|
|
||||||
quotes: '“”‘’',
|
|
||||||
highlight: (str: string, lang: string) => {
|
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
|
||||||
try {
|
|
||||||
return hljs.highlight(str, { language: lang }).value;
|
|
||||||
}
|
|
||||||
catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}).use(frontMatter, (v: Metadata) => this.metadata = v);
|
|
||||||
|
|
||||||
public override onMetadata() {
|
|
||||||
MarkdownView.renderMarkdown(this.src, { metadata: true }, false);
|
|
||||||
return MarkdownView.metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override onRender(locals: Locals, full = false) {
|
|
||||||
locals = this.locals(locals);
|
|
||||||
|
|
||||||
const text = MarkdownView.renderMarkdown(this.src, locals, full);
|
|
||||||
|
|
||||||
if (full && locals.page) {
|
|
||||||
const parent = resolveView(String(locals.page), false, true);
|
|
||||||
return parent?.render({ ...locals, text }, full) ?? text;
|
|
||||||
}
|
|
||||||
else return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static renderMarkdown(text: string, _locals: object, _full: boolean): string {
|
|
||||||
this.metadata = "{}" as any;
|
|
||||||
const res = this.renderer.render(text);
|
|
||||||
this.metadata = parseYaml(this.metadata as any) as Metadata;
|
|
||||||
return res;
|
|
||||||
// return (await MarkdownView.renderer.render(text, { metadata: true })).value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import View, { type Locals } from "./View.ts";
|
|
||||||
import config from "../config.ts";
|
|
||||||
import { compile } from "pug";
|
|
||||||
import MarkdownView from "./MarkdownView.ts";
|
|
||||||
import { invoke, renderView } from "./resolution.ts";
|
|
||||||
|
|
||||||
export default class PugView extends View {
|
|
||||||
public override onMetadata() {
|
|
||||||
const src = this.src;
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
for (const match of src.matchAll(/\/\/---\n((?: .+?\n)*)\/\/---/g)) {
|
|
||||||
lines.push(...match[1].split("\n").map(v => v.slice(4)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join("\n") ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override onRender(locals: Locals, full: boolean): string {
|
|
||||||
locals = this.locals(locals);
|
|
||||||
const src = this.src;
|
|
||||||
|
|
||||||
const func = compile(src, {
|
|
||||||
basedir: config.views,
|
|
||||||
compileDebug: false,
|
|
||||||
debug: config.debug,
|
|
||||||
pretty: config.debug,
|
|
||||||
doctype: 'HTML',
|
|
||||||
filters: {
|
|
||||||
md: (src: string) => MarkdownView.renderMarkdown(src, {}, false),
|
|
||||||
},
|
|
||||||
globals: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = func({
|
|
||||||
...locals,
|
|
||||||
invoke: invoke.bind(View, this),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (full && !src.match(/^extends\s/g) && !src.match(/^doctype\s/g)) {
|
|
||||||
if (locals.page != null) return renderView(String(locals.page), { ...locals, text: res }, false, true, true)?.text ?? res;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { parse as parseYaml } from "@std/yaml";
|
|
||||||
import config from "../config.ts";
|
|
||||||
|
|
||||||
export type Locals = Record<string, unknown>;
|
|
||||||
export type Metadata = Record<string, unknown>;
|
|
||||||
export type ViewConstr = new (mtime: number, path: string, uri: string, dirMtime: number | undefined, dirPath: string, dirUri: string, cached: boolean) => View;
|
|
||||||
|
|
||||||
const stack: Locals[] = [];
|
|
||||||
|
|
||||||
export function pushLocals(locals: Locals) {
|
|
||||||
stack.push(locals);
|
|
||||||
}
|
|
||||||
export function popLocals() {
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
export function getLocals() {
|
|
||||||
const res = stack.at(-1);
|
|
||||||
if (res == null) throw new Error("getLocals() not called in a render context");
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default abstract class View {
|
|
||||||
public static cache = new Map<string, View>();
|
|
||||||
|
|
||||||
private _cache?: string;
|
|
||||||
private _metadata?: Metadata;
|
|
||||||
private _src?: string;
|
|
||||||
private _timer?: number;
|
|
||||||
|
|
||||||
public get key() {
|
|
||||||
return this.path;
|
|
||||||
}
|
|
||||||
public get isIndex() {
|
|
||||||
return this.dirMtime != null;
|
|
||||||
}
|
|
||||||
public get hasCache() {
|
|
||||||
return this._cache != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get metadata() {
|
|
||||||
if (this._metadata != null) return this._metadata;
|
|
||||||
|
|
||||||
let res = this.onMetadata();
|
|
||||||
if (typeof res === "string") res = parseYaml(res) as Metadata ?? {};
|
|
||||||
if (typeof res !== "object") res = { data: res };
|
|
||||||
if (this.cached) this._metadata = res as Metadata;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get src() {
|
|
||||||
return this._src ??= Deno.readTextFileSync(this.uri);
|
|
||||||
}
|
|
||||||
public get title() {
|
|
||||||
return String(this.metadata.title ?? this.path.split("/").at(-1));
|
|
||||||
}
|
|
||||||
public get date() {
|
|
||||||
const rawDate = this.metadata.date as any;
|
|
||||||
if (rawDate == null) return undefined;
|
|
||||||
|
|
||||||
const res = new Date(rawDate);
|
|
||||||
if (res == null) return undefined;
|
|
||||||
else return res.toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public setTimeout() {
|
|
||||||
if (this._timer != null) clearTimeout(this._timer);
|
|
||||||
if (config.maxCache != null) this._timer = setTimeout(() => {
|
|
||||||
View.cache.delete(this.key);
|
|
||||||
if (config.debug) console.log("FREE " + this.path);
|
|
||||||
}, config.maxCache * 1000);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public clearTimeout() {
|
|
||||||
if (this._timer != null) clearTimeout(this._timer);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract onMetadata(): Metadata | string;
|
|
||||||
protected abstract onRender(locals: Locals, full: boolean): string;
|
|
||||||
|
|
||||||
protected locals(locals: Locals) {
|
|
||||||
return {
|
|
||||||
title: config.defaultTitle,
|
|
||||||
page: this.isIndex ? config.defaultIndex : config.defaultPage,
|
|
||||||
...locals,
|
|
||||||
...this.metadata
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(locals: Locals, full: boolean) {
|
|
||||||
if (this._cache != null) return this._cache;
|
|
||||||
|
|
||||||
pushLocals(locals);
|
|
||||||
try {
|
|
||||||
const res = this.onRender(locals, full);
|
|
||||||
if (this.cached) this._cache = res;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
popLocals();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public readonly mtime: number,
|
|
||||||
public readonly uri: string,
|
|
||||||
public readonly path: string,
|
|
||||||
public readonly dirMtime: number | undefined,
|
|
||||||
public readonly dirUri: string,
|
|
||||||
public readonly dirPath: string,
|
|
||||||
public readonly cached = true
|
|
||||||
) { }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import config, { isIgnored } from "../config.ts";
|
|
||||||
import type { ViewConstr } from "./View.ts";
|
|
||||||
import { dirname, normalize, resolve } from "@std/path/posix";
|
|
||||||
import MarkdownView from "./MarkdownView.ts";
|
|
||||||
import PugView from "./PugView.ts";
|
|
||||||
import JSView from "./JSView.ts";
|
|
||||||
import { getPlugin } from "../plugins.ts";
|
|
||||||
import View from "./View.ts";
|
|
||||||
|
|
||||||
let getType = (ext: string) => {
|
|
||||||
const types = {
|
|
||||||
".md": MarkdownView,
|
|
||||||
".pug": PugView,
|
|
||||||
".js": JSView,
|
|
||||||
".ts": JSView,
|
|
||||||
};
|
|
||||||
|
|
||||||
getType = ext => types[ext as keyof typeof types];
|
|
||||||
return types[ext as keyof typeof types];
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryStatFile(path: string) {
|
|
||||||
try {
|
|
||||||
return Deno.statSync(path);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryMtime(path: string | Deno.FileInfo | undefined) {
|
|
||||||
if (typeof path === "string") {
|
|
||||||
const stat = tryStatFile(path);
|
|
||||||
if (stat?.mtime != null) return Number(stat.mtime);
|
|
||||||
else return undefined;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (path?.mtime != null) return Number(path.mtime);
|
|
||||||
else return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function of(
|
|
||||||
constr: ViewConstr,
|
|
||||||
uri: string, path: string, stat: Deno.FileInfo | undefined,
|
|
||||||
dirUri: string, dirPath: string, dirStat: Deno.FileInfo | undefined | boolean,
|
|
||||||
cached = true, force = false
|
|
||||||
) {
|
|
||||||
stat ??= tryStatFile(uri);
|
|
||||||
const mtime = tryMtime(stat);
|
|
||||||
|
|
||||||
if (stat?.isFile) {
|
|
||||||
if (!force && isIgnored(path)) return undefined;
|
|
||||||
if (dirStat === true) {
|
|
||||||
dirStat = tryStatFile(dirUri) ?? undefined;
|
|
||||||
if (dirStat == null) return undefined;
|
|
||||||
}
|
|
||||||
else if (dirStat === false) dirStat = undefined;
|
|
||||||
if (typeof dirStat === "boolean") throw "fuck typescript";
|
|
||||||
|
|
||||||
const index = dirStat != null;
|
|
||||||
|
|
||||||
if (cached && View.cache.has(path)) {
|
|
||||||
const res = View.cache.get(path)!;
|
|
||||||
if (res.path === path && res.isIndex === index && res.mtime >= mtime!) {
|
|
||||||
if (dirStat != null) {
|
|
||||||
if (res.dirMtime! >= Number(dirStat.mtime)) return res.setTimeout();
|
|
||||||
}
|
|
||||||
else return res.setTimeout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = new constr(mtime!, uri, path, tryMtime(dirStat), normalize(dirUri + "/"), normalize(dirPath + "/"), cached);
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
const old = View.cache.get(path);
|
|
||||||
if (old != null) old.clearTimeout();
|
|
||||||
|
|
||||||
View.cache.set(path, res);
|
|
||||||
return res.setTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invoke(view: View, name: string, ...args: unknown[]) {
|
|
||||||
if (config.libs == null) throw new Error("No 'lib' folder specified");
|
|
||||||
return getPlugin(name).apply(view, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveView(path: string, cached = true, force = false): View | undefined {
|
|
||||||
path = resolve("/" + path);
|
|
||||||
|
|
||||||
const uri = resolve(config.views + path);
|
|
||||||
let res;
|
|
||||||
|
|
||||||
const dirStat = tryStatFile(uri);
|
|
||||||
|
|
||||||
if (dirStat?.isDirectory) {
|
|
||||||
res = [
|
|
||||||
of(MarkdownView, uri + "/index.md", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(PugView, uri + "/index.pug", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(JSView, uri + "/index.js", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(JSView, uri + "/index.ts", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(MarkdownView, uri + ".md", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(PugView, uri + ".pug", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(JSView, uri + ".js", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(JSView, uri + ".ts", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
].find(v => v != null);
|
|
||||||
if (res != null) return res;
|
|
||||||
|
|
||||||
if (config.defaultIndex != null) {
|
|
||||||
const templUri = resolve(config.views!, config.defaultIndex);
|
|
||||||
return [
|
|
||||||
of(MarkdownView, templUri + ".md", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(PugView, templUri + ".pug", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(JSView, templUri + ".js", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
of(JSView, templUri + ".ts", path, undefined, uri + "/", path + "/", dirStat, cached, force),
|
|
||||||
].find(v => v != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return [
|
|
||||||
of(MarkdownView, uri + ".md", path, undefined, dirname(uri) + "/", dirname(path) + "/", undefined, cached, force),
|
|
||||||
of(PugView, uri + ".pug", path, undefined, dirname(uri) + "/", dirname(path) + "/", undefined, cached, force),
|
|
||||||
of(JSView, uri + ".js", path, undefined, dirname(uri) + "/", dirname(path) + "/", undefined, cached, force),
|
|
||||||
of(JSView, uri + ".ts", path, undefined, dirname(uri) + "/", dirname(path) + "/", undefined, cached, force),
|
|
||||||
].find(v => v != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
export function resolveViewFromUri(uri: string, cached = true, index = true) {
|
|
||||||
uri = resolve(uri);
|
|
||||||
|
|
||||||
const views = resolve(config.views!);
|
|
||||||
if (!uri.startsWith(views + "/")) return undefined;
|
|
||||||
|
|
||||||
const getFile = (ext: string) => {
|
|
||||||
const type = getType(ext);
|
|
||||||
if (type == null) return undefined;
|
|
||||||
|
|
||||||
const path = uri.slice(views.length, -ext.length);
|
|
||||||
const dirPath = dirname(path);
|
|
||||||
const dirUri = resolve(views + "/" + dirPath);
|
|
||||||
|
|
||||||
const dirStat = tryStatFile(dirUri);
|
|
||||||
|
|
||||||
if (dirStat?.isDirectory) {
|
|
||||||
return of(type, uri, path, stat, resolve(views + "/" + path), path, dirStat, cached);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return of(type, uri, path, stat, dirUri, dirPath, undefined, cached);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getDir = (ext: string) => {
|
|
||||||
const type = getType(ext);
|
|
||||||
if (type == null) return undefined;
|
|
||||||
|
|
||||||
let newUri = uri + "/index" + ext;
|
|
||||||
|
|
||||||
let fileStat = tryStatFile(newUri);
|
|
||||||
if (fileStat == null) {
|
|
||||||
if (config.defaultIndex != null) {
|
|
||||||
newUri = resolve(config.views + "/" + config.defaultIndex + ext);
|
|
||||||
fileStat = tryStatFile(newUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileStat == null) return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = uri.slice(views.length);
|
|
||||||
const dirUri = uri;
|
|
||||||
|
|
||||||
return of(type, newUri, path, fileStat, dirUri, path, stat ?? undefined, cached);
|
|
||||||
};
|
|
||||||
const getIndex = (ext: string) => {
|
|
||||||
if (!index) return undefined;
|
|
||||||
if (!uri.endsWith("/index" + ext)) return undefined;
|
|
||||||
|
|
||||||
const type = getType(ext);
|
|
||||||
if (type == null) return undefined;
|
|
||||||
|
|
||||||
const dirUri = dirname(uri);
|
|
||||||
const dirPath = dirUri.slice(views.length);
|
|
||||||
const dirStat = Deno.statSync(dirUri);
|
|
||||||
|
|
||||||
return of(type, uri, dirPath, stat, dirUri, dirPath, dirStat, cached);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stat = tryStatFile(uri);
|
|
||||||
let match;
|
|
||||||
|
|
||||||
if (stat == null) return undefined;
|
|
||||||
else if (match = uri.match(/index(\.(?:md|pug|js))$/)) return getIndex(match![1]);
|
|
||||||
else if ((match = uri.match(/\.(?:md|pug|js)$/)) && stat.isFile) return getFile(match![0]);
|
|
||||||
else if (stat.isDirectory) return getDir(".md") ?? getDir(".pug") ?? getDir(".js") ?? getDir(".ts");
|
|
||||||
else return undefined;
|
|
||||||
}
|
|
||||||
export function renderView(view: string, locals = {}, full = false, force = false, passthroughLocals = false) {
|
|
||||||
const res = resolveView(decodeURIComponent(view), true, force);
|
|
||||||
if (res == null) return undefined;
|
|
||||||
const cached = res.hasCache;
|
|
||||||
return {
|
|
||||||
text: res?.render({
|
|
||||||
...locals,
|
|
||||||
...(passthroughLocals ? {} : {
|
|
||||||
dir: res.dirPath,
|
|
||||||
dirUri: res.dirUri,
|
|
||||||
path: res.path,
|
|
||||||
uri: res.uri,
|
|
||||||
isIndex: res.isIndex,
|
|
||||||
})
|
|
||||||
}, full),
|
|
||||||
cached,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
80
src/website-main.lua
Normal file
80
src/website-main.lua
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
require "utils.printing";
|
||||||
|
require "template";
|
||||||
|
|
||||||
|
local sync = require "sync";
|
||||||
|
local tcp = require "sync.tcp";
|
||||||
|
local http = require "http";
|
||||||
|
local asserted = require "utils.asserted";
|
||||||
|
local fs = require "sync.fs";
|
||||||
|
local p = require "utils.path";
|
||||||
|
local resolve = require "resolve";
|
||||||
|
|
||||||
|
local mime_types = require "mime-types";
|
||||||
|
|
||||||
|
local function try_static(conn, path, config)
|
||||||
|
local f_path = package.searchpath(path, config.static, "/");
|
||||||
|
if not f_path then return false end
|
||||||
|
if fs.stat(f_path).type ~= "file" then return false end
|
||||||
|
|
||||||
|
local f <close> = assert(fs.open(f_path));
|
||||||
|
assert(http.respond(
|
||||||
|
conn, 200,
|
||||||
|
http.headers.of { ["content-type"] = mime_types[path:match "%.(.-)$"] },
|
||||||
|
f:stream()
|
||||||
|
));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
return require "sync.entry" (function (...)
|
||||||
|
-- Why a bad pipe manages to topple a whole process in unix is beyond me
|
||||||
|
local sigpipe = assert(uv.new_signal());
|
||||||
|
uv.signal_start(sigpipe, "sigpipe", function () end);
|
||||||
|
|
||||||
|
local conf_file = ...;
|
||||||
|
local config = assert(loadfile(conf_file))();
|
||||||
|
|
||||||
|
package.loaded.config = config;
|
||||||
|
|
||||||
|
if config.path and #config.path > 0 then
|
||||||
|
package.path = package.path .. ";" .. config.path;
|
||||||
|
end
|
||||||
|
|
||||||
|
local server = tcp();
|
||||||
|
assert(server:bind("0.0.0.0", config.port));
|
||||||
|
print("Listening on :" .. config.port);
|
||||||
|
|
||||||
|
for conn in asserted(server:listen()) do
|
||||||
|
sync.fork(function ()
|
||||||
|
--- @type string, string, http_headers
|
||||||
|
local method, path, headers = assert(http.read_req(conn));
|
||||||
|
if method == "EOF" then return end
|
||||||
|
|
||||||
|
if method ~= "GET" then
|
||||||
|
assert(http.respond(conn, 405, nil, "Bad method\n"));
|
||||||
|
return conn:close();
|
||||||
|
end
|
||||||
|
|
||||||
|
if path == "/.well-known/keepalive" then
|
||||||
|
assert(http.respond(conn, 200, nil, "OK"));
|
||||||
|
return conn:close();
|
||||||
|
end
|
||||||
|
|
||||||
|
local fixed_path = p.chroot("/", path):sub(2);
|
||||||
|
|
||||||
|
if not try_static(conn, fixed_path, config) then
|
||||||
|
local get_page = resolve(config, fixed_path);
|
||||||
|
if get_page then
|
||||||
|
local page = get_page();
|
||||||
|
local body_res = assert(http.write_res(conn, 200, http.headers.of { ["content-type"] = "text/html" }, true));
|
||||||
|
page:template(function (...) body_res:write(...) end);
|
||||||
|
body_res:close();
|
||||||
|
else
|
||||||
|
assert(http.respond(conn, 404, nil, "Not found\n"));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
conn:close();
|
||||||
|
end);
|
||||||
|
end
|
||||||
|
end);
|
||||||
Reference in New Issue
Block a user