add thesis text

This commit is contained in:
2025-01-26 18:07:13 +02:00
parent e601749866
commit b190367681
13 changed files with 3311 additions and 0 deletions

728
doc/text/src/build.lua Normal file
View File

@@ -0,0 +1,728 @@
---@diagnostic disable: undefined-field
require "src.utils";
local json = require "src.json";
local function reader(fd, ...)
if type(fd) == "string" then
fd = assert(io.open(fd, ... or "r"));
elseif type(fd) == "function" then
return fd;
else
assert(fd, ...);
end
return function ()
if fd == nil then error "File is closed" end
local res = fd:read(1024);
if res == nil then
fd:close();
fd = nil;
return nil;
else
return res;
end
end;
end
local function read_to_str(fd, ...)
local res = {};
for val in reader(fd, ...) do
res[#res + 1] = val;
end
return table.concat(res);
end
local flag = {};
function flag.has(name, ...)
if name == ... then
return true;
elseif ... == nil then
return false;
else
return flag.has(name, select(2, ...));
end
end
function flag.add(name, ...)
if flag.has(name, ...) then
return ...;
else
return name, ...;
end
end
local converters = {};
local function sanitize(str)
return (str
:gsub("<", "&lt")
:gsub(">", "&gt")
);
end
local function get_indent(ctx, n)
return ("\t"):rep((ctx.indent or 0) + (n or 0));
end
local function indent(ctx)
ctx.indent = (ctx.indent or 0) + 1;
end
local function undent(ctx)
ctx.indent = (ctx.indent or 1) - 1;
end
local function convert(data, ctx, ...)
local func = converters[data.t];
if func == nil then
error(table.concat { "Unknown node '", data.t, "': ", to_readable(data) });
else
return func(data.c, ctx, ...);
end
end
local function convert_all(arr, ctx, ...)
local res = array {};
local plain = array {};
local inline = true;
for i = 1, #arr do
local a, b, c = convert(arr[i], ctx, ...);
res:append(a);
plain:append(b);
if not c then
inline = false;
res:append("\n", get_indent(ctx));
end
end
return res, plain, inline;
end
local function make_id(id, readable, ctx)
ctx.ids = ctx.ids or {};
local res;
if ctx.ids[id] ~= nil then
ctx.ids[id] = ctx.ids[id] + 1;
res = id .. "-" .. ctx.ids[id];
else
ctx.ids[id] = 0;
res = id;
end
if readable then
ctx.citables = ctx.citables or {};
ctx.citables[id] = readable;
end
return res;
end
local function last_id(id, ctx)
ctx.ids = ctx.ids or {};
if ctx.ids[id] ~= nil and ctx.ids[id] > 0 then
return id .. "-" .. ctx.ids[id];
else
return id;
end
end
local function count_headers(array)
local res = 0;
for i = 1, #array do
if not array[i].ignored then
res = res + 1;
end
end
return res;
end
local function read_attributes(data, readable, ctx, no_id)
local res = {};
if data[1] ~= "" then
res.id = data[1];
end
for i = 1, #data[2] do
res[data[2][i]] = true;
end
for i = 1, #data[3] do
local curr = data[3][i];
res[curr[1]] = curr[2];
end
if not no_id and res.id then
res.id = make_id(res.id, readable, ctx);
end
return res;
end
function converters.Table(data, ctx)
local options = data[3];
local header = data[4];
local body = data[5][1];
local text = array {};
local function convert_cell(cell, tag, options)
local align = options[1].t;
indent(ctx);
text:push("<", tag);
if align == "AlignRight" then
text:push [[ class="right"]];
elseif align == "AlignLeft" then
text:push [[ class="left"]];
elseif align == "AlignCenter" then
text:push [[ class="center"]];
end
text:push(">\n", get_indent(ctx));
text:append((convert_all(cell[5], ctx)));
undent(ctx);
text:push("\n", get_indent(ctx), "</" .. tag .. ">");
end
local function convert_row(row, tag)
text:push("<tr>");
for i = 1, #row[2] do
local cell = row[2][i];
indent(ctx);
text:push("\n", get_indent(ctx));
convert_cell(cell, tag, options[i]);
undent(ctx);
end
text:push("\n", get_indent(ctx), "</tr>");
end
text:push [[<table class="graphic-table">]];
indent(ctx);
indent(ctx);
text:push("\n", get_indent(ctx, -1), "<thead>");
text:push("\n", get_indent(ctx));
convert_row(header[2][1], "th");
text:push("\n", get_indent(ctx, -1), "</thead>");
text:push("\n", get_indent(ctx, -1), "<tbody>");
for i = 1, #body[4] do
text:push("\n", get_indent(ctx));
convert_row(body[4][i], "td");
end
text:push("\n", get_indent(ctx, -1), "</tbody>");
undent(ctx);
undent(ctx);
text:push("\n", get_indent(ctx), "</table>");
return text, array { "[table]" };
end
function converters.BulletList(data, ctx)
local text = array { "<ul class=\"bullet\">" };
local plain = array {};
indent(ctx);
for i = 1, #data do
local el_text, el_plain = convert_all(data[i], ctx);
text
:push("\n", get_indent(ctx), "<li>")
:append(el_text)
:push("</li>");
plain:push("* "):append(el_plain):push("\n");
end
undent(ctx);
text:push("\n", get_indent(ctx), "</ul>");
return text, plain;
end
function converters.OrderedList(data, ctx)
local text = array { "<ol>" };
local plain = array {};
indent(ctx);
for i = 1, #data[2] do
local el_text, el_plain = convert_all(data[2][i], ctx);
text
:push("\n", get_indent(ctx), "<li value=\"", i, "\">")
:append(el_text)
:push("</li>");
plain:push(i .. ") "):append(el_plain):push("\n");
end
undent(ctx);
text:push("\n", get_indent(ctx), "</ol>");
return text, plain;
end
function converters.Code(data, ctx)
local content = data[2];
return array { "<code>", sanitize(content), "</code>" }, array { content };
end
function converters.CodeBlock(data, ctx)
local attribs = read_attributes(data[1], nil, ctx);
local content = data[2];
local text = array { "<pre><code" };
local classes = array {};
for k, v in next, attribs do
if v == true then
classes:push("language-" .. k);
end
end
if #classes > 0 then
text:push(" class=\"", classes:join " ", "\"");
end
text:push(">", sanitize(content), "</code></pre>");
return text, array { content };
end
function converters.Image(data, ctx)
local alt, alt_plain = convert_all(data[2], ctx);
local url = data[3][1];
local title = data[3][2];
local attribs = read_attributes(data[1], title or alt_plain:join " ", ctx);
local classes = array {};
if attribs.small then classes:push("small") end
if attribs.big then classes:push("big") end
local text = array {}
:push("<img title=\"", title, "\" src=\"", url, "\" alt=\"")
:append(alt)
:push("\"");
if #classes > 0 then
text:push(" class=\"", classes:join " ", "\"");
end
text:push("/>");
return text, title and { title } or alt_plain or url or "[picture]";
end
function converters.Figure(data, ctx)
local chapter_i = #(ctx.headers or {});
ctx.figures_indices = ctx.figures_indices or {};
ctx.figures_indices[chapter_i] = (ctx.figures_indices[chapter_i] or 0) + 1;
local figure_i = ctx.figures_indices[chapter_i];
local prefix = ("%s.%s."):format(chapter_i, figure_i);
local attribs = read_attributes(data[1], prefix, ctx);
local id = attribs.id;
indent(ctx);
indent(ctx);
local name, name_plain = convert_all(data[2][1], ctx);
local content, plain = convert_all(data[3], ctx);
undent(ctx);
undent(ctx);
local text = array { "<figure" };
if id then text:push(" id=\"", id, "\"") end
text:push(">");
text
:push("\n", get_indent(ctx, 1), "<div class=\"fig-content\">", "\n", get_indent(ctx, 2))
:append(content)
:push("\n", get_indent(ctx, 1), "</div>", "\n", get_indent(ctx, 1), "<figcaption>", "\n", get_indent(ctx, 2))
:push(prefix, " ")
:append(name)
:push("\n", get_indent(ctx, 1), "</figcaption>")
:push("\n", get_indent(ctx), "</figure>");
plain
:push(" (", prefix, " ")
:append(name_plain)
:push(")");
ctx.citables = ctx.citables or {};
if id then ctx.citables[id] = prefix end
return text, plain;
end
function converters.Div(data, ctx)
local attribs = read_attributes(data[1], nil, ctx, true);
if attribs.figure then
local separator_i = data[2]:find_i(function (v) return v.t == "HorizontalRule" end);
local content_data, title_data;
if separator_i == nil then
content_data = array { data[2][1] };
title_data = data[2]:slice(2);
else
content_data = data[2]:slice(1, separator_i - 1);
title_data = data[2]:slice(separator_i + 1);
end
if #title_data == 1 and title_data[1].t == "Para" then
title_data = title_data[1].c
end
return converters.Figure(array {
data[1],
array { title_data },
content_data,
}, ctx);
else
error "Divs are not supported";
end
end
function converters.Cite(data, ctx)
local citation = data[1][1];
local id = last_id(citation.citationId, ctx);
local function target()
local res = (ctx.citables or {})[id];
if res == nil then
io.stderr:write("WARNING! Citation '" .. id .. "' doesn't exist!\n");
res = id;
end
return res;
end
return array { "<a href=\"#", id, "\">", target, "</a>" }, array { target }, true;
end
function converters.Header(data, ctx)
local level = data[1];
local attribs = read_attributes(data[2], nil, ctx);
ctx.headers = ctx.headers or array {};
ctx.header_stack = ctx.header_stack or array {};
local parent = ctx.header_stack[level - 1];
if level > 1 and parent == nil then error "Heading hierarchy inconsistent" end
local text, plain = convert_all(data[3], ctx);
local header = {
id = attribs.id,
level = level,
children = array {},
ignored = attribs.nonum or false,
};
local text_prefix, plain_prefix = "", "";
if level == 1 then
ctx.headers:push(header);
else
parent.children:push(header);
end
if not header.ignored then
local prefix = array { count_headers(ctx.headers) };
local text_format, plain_format;
for i = 1, level - 1 do
--- @diagnostic disable-next-line: undefined-field
prefix:push(count_headers(ctx.header_stack[i].children));
end
header.prefix = prefix:join ".";
if level == 1 then
text_format = ctx.chapter_format or "Chapter %s<br>";
plain_format = ctx.chapter_format_plain or "Chapter %s: ";
else
text_format = "%s. ";
plain_format = "%s. ";
end
text_prefix = text_format:format(header.prefix);
plain_prefix = plain_format:format(header.prefix);
end
ctx.header_stack = ctx.header_stack:fill(nil, level + 1, #ctx.header_stack);
ctx.header_stack[level] = header;
header.text = text_prefix .. text:join "";
header.plain = plain_prefix .. plain:join "";
if attribs.id then
ctx.citables = ctx.citables or {};
ctx.citables[attribs.id] = header.plain;
end
return
arrays.concat({ ("<h%d id=%q>"):format(level, attribs.id), text_prefix }, text, { ("</h%d>"):format(level) }),
array { plain_prefix }:append(plain);
end
function converters.Link(data, ctx)
local content, content_plain = convert_all(data[2], ctx);
local url = data[3][1];
local attribs = read_attributes(data[1], nil, ctx);
local id = attribs.id or data[3][2];
if id == "" then id = nil end
if id then
ctx.link_i = (ctx.link_i or 0) + 1;
local i = ctx.link_i;
ctx.citables = ctx.citables or {};
ctx.citables[id] = "[" .. i .. "]";
end
local plain = array {};
plain:push("<a href=\"", url, "\"");
if id ~= nil then
plain:push(" id=\"", id, "\"");
end
plain:push(">");
plain:append(content);
plain:push("</a>");
return plain, content_plain:push(" (", url, ")"), true;
end
function converters.Para(data, ctx)
indent(ctx);
local text, plain = convert_all(data, ctx);
undent(ctx);
return
array {}
:push "<p>"
:append(text)
:append "</p>",
plain;
end
function converters.Emph(data, ctx)
local text, plain = convert_all(data, ctx);
return arrays.concat({ "<i>" }, text, { "</i>" }), plain, true;
end
function converters.Strong(data, ctx)
local text, plain = convert_all(data, ctx);
return arrays.concat({ "<strong>" }, text, { "</strong>" }), plain, true;
end
function converters.Str(data)
return array { sanitize(data) }, array { data }, true;
end
function converters.Plain(data, ctx)
return convert_all(data, ctx);
end
function converters.RawBlock(data)
return array {}, array {}, true;
end
function converters.MetaInlines(data, ctx)
return convert_all(data, ctx);
end
function converters.LineBreak()
return array { "<br>" }, array { " " }, true;
end
function converters.SoftBreak()
return array { "&shy;" }, array { " " }, true;
end
function converters.HorizontalRule()
return array { "<br class=\"pg-break\"/>" }, array { " " };
end
function converters.Space()
return array { " " }, array { " " }, true;
end
local function parse_meta(meta)
local res = {};
for k, v in next, meta do
local text, plain = convert(v, {});
res[k] = text:concat "";
end
return res;
end
local function convert_headers(headers, ctx)
ctx = ctx or { indent = 0 };
indent(ctx);
local res = arrays.concat(
{ "<ul>" },
headers:flat_map(function (child)
return arrays.concat(
{
"\n", get_indent(ctx),
"<li><a href=\"#",
child.id,
"\"><span class=\"name\">",
child.plain,
"</span><span class=\"page\">Page</span></a>"
},
#child.children > 0 and convert_headers(child.children, ctx) or { "" },
{ "</li>" }
);
end),
{
"\n", get_indent(ctx, -1),
"</ul>"
}
);
undent(ctx);
return res;
end
local function parse(...)
local stream, err = io.popen("pandoc --highlight-style kate --from markdown-raw_html --to json -o - " .. array { ... }:join " ");
local str = read_to_str(stream, err);
local raw = json.parse(str);
return {
metadata = parse_meta(raw.meta),
content = raw.blocks or array {},
};
end
local function convert_page(data, ctx)
ctx = ctx or {};
ctx.headers = ctx.headers or array {};
local curr_page = array {};
local pages = array {};
for i = 1, #data.content do
local curr = data.content[i];
if curr.t == "Header" then
if curr.c[1] == 1 then
if #pages > 0 or #curr_page > 0 then
pages:append(curr_page);
end
curr_page = array {};
end
end
local text, _, inline = convert(curr, ctx);
curr_page:append(text);
if not inline then
curr_page:push("\n");
end
end
pages:append(curr_page);
local function mapper(v)
if type(v) == "function" then
return v();
else
return v;
end
end
return {
content = pages:map(mapper, true):join "",
toc = convert_headers(ctx.headers):map(mapper, true):join "",
};
end
local function subst(template, variables)
local replaces = array {};
for k, v in next, variables do
local i = 1;
local search = "{{" .. k .. "}}";
while true do
local b, e = string.find(template, search, i, true);
if b == nil then break end
replaces:push { b = b, e = e, v = v };
i = e + 1;
end
end
table.sort(replaces, function (a, b) return a.b < b.b end);
local parts = array {};
local prev = 1;
for i = 1, #replaces do
local curr = replaces[i];
parts:push(template:sub(prev, curr.b - 1), curr.v);
prev = curr.e + 1;
end
parts:push(template:sub(prev));
return parts:join "";
end
function build(options)
local toc = options.toc;
local content = options.content;
local ctx = options.ctx or {};
print(table.concat { "Building ", array(options):join ", ", "..." });
local res = convert_page(parse(unpack(options)), ctx);
if toc == nil and content == nil then
return res.content;
end
local obj = {};
if toc ~= nil then obj[toc] = res.toc end
if content ~= nil then obj[content] = res.content end
return obj;
end
function combine(objects)
local res = {};
for i = 1, #objects do
for k, v in next, objects[i] do
res[k] = v;
end
end
return res;
end
function template(options)
print("Templating into " .. options.template .. "...");
options = combine(array { options }:append(options));
return subst(read_to_str(io.open(options.template, "r")), options);
end
function emit(values)
local f = io.stdout;
if values.target then
f = assert(io.open(values.target, "w"));
print("Emitting to " .. values.target .. "...");
else
print("Emitting to stdout...");
end
for i = 1, #values do
assert(f:write(values[i]));
end
if f ~= io.stdout then
f:close();
end
end
-- return main;

244
doc/text/src/json.lua Normal file
View File

@@ -0,0 +1,244 @@
local json = { null = {} };
-- Internal functions.
local function kind_of(obj)
if type(obj) ~= "table" then return type(obj) 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
if check then
if string.sub(str, pos, pos) ~= "\"" then
return nil, pos;
else
pos = pos + 1;
end
else
pos = pos + 1;
end
local res = {};
while true do
local c = string.sub(str, pos, pos);
if c == "\"" then
return table.concat(res), pos + 1;
elseif c == "\\" then
c = string.sub(str, pos + 1, pos + 1);
res[#res + 1] = esc_map[c] or c;
pos = pos + 2;
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 json_end = { "eof" };
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 = pos + 1;
local key;
local obj = {};
c = string.sub(str, pos, pos);
if c == "}" then
return obj, pos
else
while true do
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 = array {};
local val;
local delim_found = true;
while true do
val, pos = parse_impl(str, pos, "]");
if val == json_end then return arr, pos end
if not delim_found then error("Comma missing between array items: " .. str:sub(pos, pos + 25)) end
arr[#arr + 1] = val;
pos, delim_found = skip_delim(str, pos, ",");
end
elseif 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 json_end, pos + 1, true;
elseif str:sub(pos, pos + 3) == "null" then
return nil, pos + 4;
elseif str:sub(pos, pos + 3) == "true" then
return true, pos + 4;
elseif str:sub(pos, pos + 4) == "false" then
return true, pos + 5;
else
error(table.concat { "Invalid json syntax starting at position ", pos, ": ", str:sub(pos, pos + 10) });
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
json.null = {}; -- This is a one-off table to represent the null value.
---@param str string
---@return unknown
function json.parse(str)
local obj = parse_impl(str, 1);
return obj;
end
return json

30
doc/text/src/perf.lua Normal file
View File

@@ -0,0 +1,30 @@
local ffi = require "ffi";
ffi.cdef [[
typedef struct timeval {
long tv_sec;
long tv_usec;
} timeval;
int gettimeofday(struct timeval* t, void* tzp);
]];
local function now()
local target = ffi.new "timeval";
ffi.C.gettimeofday(target, nil);
return tonumber(target.tv_sec) + tonumber(target.tv_usec) / 1000000;
end
local function measure(func, ...)
local start = now();
return (function(...)
io.stderr:write(("Took %s seconds\n"):format(now() - start));
return ...;
end)(func(...));
end
return {
now = now,
measure = measure,
}

429
doc/text/src/utils.lua Normal file
View File

@@ -0,0 +1,429 @@
-- TILL - TopchetoEU's "immaculate" lua libs
-- Some useful utilities every lua-er should use.
-- Might break shit, don't use in production for crying out loud
-- Reimplement this for lua <5.1
if table.move == nil then
--- @diagnostic disable: duplicate-set-field
function table.move(src, src_b, src_e, dst_b, dst)
if dst == nil then dst = src end
local offset = dst_b - src_b;
if dst_b < src_b then
for i = src_e, src_b, -1 do
dst[i + offset] = src[i];
end
else
for i = src_b, src_e do
dst[i + offset] = src[i];
end
end
return dst;
end
end
-- Why did we *remove* this from the spec again? Classical PUC
unpack = table.unpack or unpack;
table.unpack = unpack;
--- Prepares the object to be a class - puts an __index member in it pointing to the object itself
--- @generic T
--- @param obj T
--- @return T | { __index: T }
function class(obj)
--- @diagnostic disable-next-line: inject-field
obj.__index = obj;
return obj;
end
-- arrays
--- @alias array<T> T[] | arraylib
--- Converts the object to an array by putting "arrays" as its metatable
--- @generic T: unknown
--- @param obj T[]
--- @return array<T>
--- @overload fun(val: string): array<string>
function array(obj)
if type(obj) == "string" then
return obj:split "";
else
return arrays.mk(obj);
end
end
--- @class arraylib
arrays = class {};
--- Creates an array
function arrays.mk(obj)
return setmetatable(obj, arrays);
end
--- Adds every element of every passed array to the end of this array
function arrays:append(...)
local res = self;
local n = #res + 1;
for i = 1, select("#", ...) do
local curr = select(i, ...);
for j = 1, #curr do
res[n] = curr[j];
n = n + 1;
end
end
return res;
end
--- Returns all the given arrays, concatenated to one
function arrays.concat(...)
--- @diagnostic disable-next-line: missing-fields
return arrays.append(array {}, ...);
end
--- Adds all the given elements to the end of this array
function arrays:push(...)
return self:append({ ... });
end
--- Removes the last element of the array and returns it
--- @generic T
--- @param self array<T>
--- @return T val? The removed element, or nil if none
function arrays:pop()
local res = self[#self];
self[#self] = nil;
return res;
end
--- @generic T
--- @param self array<T>
--- @return T val? The last element of this array, or nil of none
function arrays:peek()
return self[#self];
end
--- Returns the result of mapping the values in table t through the function f
--- @param mutate boolean? If true, will operate directly on the given array
function arrays:map(f, mutate)
local out;
if mutate then
out = self;
else
out = array {};
end
for i = 1, #self do
out[i] = f(self[i], i, self);
end
return out;
end
--- Finds the index of the given element, or nil if it doesn't exist
function arrays:find_i(f)
for i = 1, #self do
if f(self[i], i, self) then
return i;
end
end
return nil;
end
--- Like arrays:map, but will expect the function to return arrays, and will :append them to the result array instead
function arrays:flat_map(f)
local out = array {};
for i = 1, #self do
out:append(f(self[i], i, self));
end
return out;
end
--- Sets each value from b to e to val in the given array
function arrays:fill(val, b, e)
if b == nil then b = 1 end
if e == nil then e = self end
if b < 0 then b = #self + 1 - b end
if e < 0 then e = #self + 1 - e end
for i = b, e do
self[i] = val;
end
return self;
end
--- Every element from start to stop is removed from this array and are replaced with the given elements
function arrays:splice(start, stop, ...)
-- TODO: optimize
if select("#") > 0 then
local n = stop - start + 1;
while n > 0 do
table.remove(self, start);
n = n - 1;
end
for i = 1, select("#") do
table.insert(self, start, (select(i, ...)));
end
return self;
else
local res = {};
for i = start, stop do
table.insert(res, self[i]);
end
return res;
end
end
--- Every element from start to stop is removed from this array and are replaced with the given elements
function arrays:slice(start, stop)
start = start or 1;
stop = stop or #self;
local res = array {};
for i = start, stop do
res:push(self[i]);
end
return res;
end
--- Equivalent of table.concat { table.unpack(self) }
--- @param sep string? Separator (defaults to empty)
--- @param b number? First element to take (defaults to beginning)
--- @param e number? Last element to take (defaults to end)
function arrays:join(sep, b, e)
return table.concat(self, sep, b, e);
end
function arrays:__concat(other)
return arrays.concat(self, other);
end
-- strings
--- Splits the text into an array of separate lines.
--- @param self string
function string:split(sep)
sep = sep or "";
local lines = arrays {};
local pos = 1;
if sep == "" then
for i = 1, #self do
lines:push(self:sub(1, 1));
end
else
while true do
local b, e = self:find(sep, pos);
if not b then
table.insert(lines, self:sub(pos));
break;
else
table.insert(lines, self:sub(pos, b - 1));
pos = e + 1;
end
end
end
return lines;
end
--- Gets the nth character
--- @param self string
--- @param i number
function string:at(i)
return self:sub(i, i);
end
--- Performs a plain string replace
--- @param self string
--- @param old string
--- @param new string
function string:replace(old, new)
local b, e = self:find(old, 1, true);
if b == nil then
return self;
else
return self:sub(1, b - 1) .. new .. self:sub(e + 1);
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
local function stringify_impl(obj, indent_str, n, passed)
local s = {}; -- We'll build the string as an array of strings to be concatenated.
local kind = type(obj); -- This is 'array' if it's an array or type(obj) otherwise.
if kind == "table" then
if passed[obj] then return "<circular>" end
passed[obj] = true;
local len = #obj;
for i = 1, len do
s[i] = stringify_impl(obj[i], indent_str, n + 1, passed) .. ",";
end
local keys = {};
for k, v in pairs(obj) do
if type(k) ~= "number" or k > len then
keys[#keys + 1] = { k, v };
end
end
table.sort(keys, function (a, b)
if type(a[1]) == "number" and type(b[1]) == "number" then
return a[1] < b[1];
elseif type(a[1]) == "string" and type(b[1]) == "string" then
return a[1] < b[1];
else
return type(a[1]) < type(b[1]) or tostring(a[1]) < tostring(b[1]);
end
end);
for i = 1, #keys do
local k = keys[i][1];
local v = keys[i][2];
local val = stringify_impl(v, indent_str, n + 1, passed);
if val ~= nil then
if type(k) == "string" then
s[#s + 1] = table.concat { k, ": ", val, "," };
else
s[#s + 1] = table.concat { "[", stringify_impl(k, indent_str, n + 1, passed), "]: ", val, "," };
end
end
end
local meta = getmetatable(obj);
if meta ~= nil and meta ~= arrays then
s[#s + 1] = "<meta> = " .. stringify_impl(meta, indent_str, n + 1, passed) .. ",";
end
passed[obj] = false;
if #s == 0 then
if meta == arrays then
return "[]";
else
return "{}";
end
end
local contents = table.concat(s, " "):sub(1, -2);
if #contents > 80 then
local indent = "\n" .. string.rep(indent_str, n + 1);
contents = table.concat {
indent,
table.concat(s, indent),
"\n", string.rep(indent_str, n)
};
else
contents = " " .. contents .. " ";
end
if meta == arrays then
return table.concat { "[", contents, "]" };
else
return table.concat { "{", contents, "}" };
end
elseif kind == "string" then
return "\"" .. escape_str(obj) .. "\"";
elseif kind == "function" then
local data = debug.getinfo(obj, "S");
return table.concat { tostring(obj), " @ ", data.short_src, ":", data.linedefined };
elseif kind == "nil" then
return "null";
else
return tostring(obj);
end
end
--- Turns the given value to a human-readable string.
--- Should be used only for debugging and display purposes
function to_readable(obj, indent_str)
return stringify_impl(obj, indent_str or " ", 0, {});
end
function print(...)
for i = 1, select("#", ...) do
if i > 1 then
io.stderr:write("\t");
end
io.stderr:write(tostring((select(i, ...))));
end
io.stderr:write("\n");
end
--- Prints the given values in a human-readable manner to stderr
--- Should be used only for debugging
function pprint(...)
for i = 1, select("#", ...) do
if i > 1 then
io.stderr:write("\t");
end
io.stderr:write(to_readable((select(i, ...))));
end
io.stderr:write("\n");
end
-- functions
--- @class functions
functions = class {};
--- Constructs a function, such that it calls the first function with the passed arguments,
--- the second function with the return of the first, and so on. The return value of the last function is returned
---
--- In short, does the following, if the passed functions are a, b and c: return c(b(a(...)))
---
--- Sometimes less cumbersome to write (a | b | c | d)(args...) than d(c(b(a(args...))))
--- @param self function
function functions:pipe(...)
if ... == nil then
return self;
else
local next = ...;
return functions.pipe(function (...)
return next(self(...));
end, select(2, ...));
end
end
--- Calls pipe with a and b; Alternative syntax for older Lua installation
function functions.__sub(a, b)
return functions.pipe(a, b);
end
--- Calls pipe with a and b
function functions.__bor(a, b)
return functions.pipe(a, b);
end
-- It's not vital to have this metatable, so we will just try our very best
if debug then
debug.setmetatable(load, functions);
end