local function isArray(t) for k,v in pairs(t) do if type(k) ~= "number" then return false end if k%1 ~= 0 then return false end if k < 1 then return false end if k > #t then return false end end return true end local function toJsonArray(t) local r = {} for k,v in ipairs(t) do r[k] = toJson(v) end return '['..table.concat(r, ',')..']' end local function toJsonKey(t) if type(t) == "string" then return string.format("%q", t) end end local function toJsonObject(t) local r = {} for k,v in pairs(t) do local k2 = toJsonKey(k) if not k2 then error("invalid key type '"..type(k).."'") end r[#r+1] = k2..':'..toJson(v) end return '{'..table.concat(r, ',')..'}' end function toJson(t) if type(t) == "table" then if isArray(t) then return toJsonArray(t) else return toJsonObject(t) end elseif t == nil then return "null" else return toJsonKey(t) or tostring(t) end end local constants = { "true", true, "false", false, "null", nil, } local escapes = { ['"'] = '"', ['\\'] = '\\', ['/'] = '/', b = '\b', f = '\f', n = '\n', r = '\r', t = '\t', -- \uxxxx is missing } function fromJson(s) local pos = 0 local c local function next(amount) pos = pos + (amount or 1) c = pos <= #s and s:sub(pos, pos) end next() local function skipWhitespace() while c and c:match("^%s$") do next() end end local function assertCharacter(character, skipws) if skipws then skipWhitespace() end if c ~= character then error(string.format("Unexpected character (%q instead of %q)", c, character)) end end local function matchPattern(pattern) if not c then return false end local startpos,endpos = s:find(pattern, pos) if not startpos then return false end if startpos ~= pos then return false end next(endpos - pos + 1) return true end local function parseNumber() local startpos = pos -- optional - matchPattern("^-") -- int assert(matchPattern("^0") or matchPattern("^[1-9][0-9]*"), "digit expected") -- frac matchPattern("^%.[0-9]+") -- exp matchPattern("^[eE][+-]?[0-9]+") return tonumber(s:sub(startpos, pos-1)) or error("Invalid number format") end local function parseString() local ret = "" while true do next() if not c then error("Unterminated string") end if c == '"' then next() return ret end if c == '\\' then next() if c == 'u' then local codepoint = tonumber(s:sub(pos, pos + 3), 16) ret = ret..string.char(codepoint) next(4) else local escaped = escapes[c] if not escaped then error(string.format("Unknown escape sequence: '\%s'", c)) end ret = ret..escaped end else ret = ret..c end end end local parse local function parseObject() local ret = {} next() skipWhitespace() if c == '}' then next() return ret end while c do skipWhitespace() if c ~= '"' then error(string.format("Unexpected character ('%s' instead of '}' or '\"')", c)) end local k = parseString() assertCharacter(':', true) next() local v = parse() ret[k] = v skipWhitespace() if c == "," then next() else assertCharacter('}') next() return ret end end error("Unterminated object") end local function parseArray() local ret = {} next() skipWhitespace() if c == ']' then next() return ret end while c do ret[#ret + 1] = parse() skipWhitespace() if c == "," then next() else assertCharacter(']') next() return ret end end error("Unterminated object") end function parse() skipWhitespace() if c == '{' then return parseObject() end if c == '[' then return parseArray() end if c == '"' then return parseString() end if c:match("^[-0-9]$") then return parseNumber() end for i=1,#constants,2 do local k = constants[i] if s:sub(pos,pos+#k-1) == k then next(#k) return constants[i+1] end end error(string.format("Unrecognized character %q", c)) end return parse() end