tal/mod/json.lua
2025-02-06 02:30:52 +02:00

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;