246 lines
6.3 KiB
Lua
246 lines
6.3 KiB
Lua
local exports = {};
|
|
|
|
-- We can have arbitrary promises in the middle of the module
|
|
-- NO MORE ASYNC AWAIT MADNESS
|
|
|
|
promise.resolve():await();
|
|
|
|
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:push(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
|
|
|
|
function exports.stringify(obj, indent_str)
|
|
if indent_str == true then
|
|
indent_str = " ";
|
|
end
|
|
return stringify_impl(obj, false, indent_str, 0);
|
|
end
|
|
function exports.pretty(obj)
|
|
return stringify_impl(obj, true, " ", 0);
|
|
end
|
|
exports.null = {}; -- This is a one-off table to represent the null value.
|
|
|
|
---@param str string
|
|
---@return unknown
|
|
function exports.parse(str)
|
|
local obj = parse_impl(str, 1);
|
|
return obj;
|
|
end
|
|
|
|
return exports;
|