--!pw caps= -- @pw/json: JSON written as Packwand writes its own (`canonical_json`): objects keep the order -- their members were set in, `pretty` indents by two spaces with `": "` and ends with a newline, -- `compact` has no spaces at all. Strings escape only `"`, `\` and control characters (`\u` in -- lowercase hex); everything else, `/` and DEL included, stays raw. Numbers are whole: a port -- that needs a fraction writes it some other way. A Luau table has no member order, so objects -- and arrays are built with `json.object` and `json.array`; any other table is refused. local json = {} local OBJECT = {} local ARRAY = {} export type Object = { kind: any, keys: { string }, values: { [string]: any } } export type Array = { kind: any, items: { any } } --- JSON's null, which a Luau table cannot hold as a value. json.null = table.freeze({}) --- An empty object; `json.set` adds its members in order. function json.object(): Object return { kind = OBJECT, keys = {}, values = {} } end --- An array of `items` (copied), in order. function json.array(items: { any }?): Array local copy = {} for _, item in ipairs(items or {}) do table.insert(copy, item) end return { kind = ARRAY, items = copy } end --- Sets `key` to `value`: a new key goes last, an existing one keeps its place. function json.set(object: Object, key: string, value: any) assert(object.kind == OBJECT, "json.set needs a json.object") if object.values[key] == nil then table.insert(object.keys, key) end object.values[key] = value end --- Appends `value` to `array`. function json.push(array: Array, value: any) assert(array.kind == ARRAY, "json.push needs a json.array") table.insert(array.items, value) end local escapes = { ['"'] = '\\"', ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t", } local function escape(byte: string): string return escapes[byte] or string.format("\\u%04x", string.byte(byte)) end local function quote(text: string): string return '"' .. string.gsub(text, '[%z\1-\31"\\]', escape) .. '"' end local function scalar(value: any): string local kind = type(value) if kind == "string" then return quote(value) elseif kind == "boolean" then return if value then "true" else "false" elseif value == json.null then return "null" elseif kind == "number" then assert(value == math.floor(value) and math.abs(value) < 2 ^ 53, "JSON numbers here are whole") return if value == 0 then "0" else string.format("%.0f", value) end error(`a {kind} is not JSON; build objects and arrays with json.object and json.array`) end local function write(out: { string }, value: any, depth: number?) local kind = if type(value) == "table" then value.kind else nil local inner = if depth then string.rep(" ", depth + 1) else "" local between = if depth then ",\n" else "," if kind == OBJECT then if #value.keys == 0 then table.insert(out, "{}") return end table.insert(out, if depth then "{\n" else "{") for i, key in ipairs(value.keys) do if i > 1 then table.insert(out, between) end table.insert(out, inner .. quote(key) .. (if depth then ": " else ":")) write(out, value.values[key], if depth then depth + 1 else nil) end table.insert(out, if depth then "\n" .. string.rep(" ", depth) .. "}" else "}") elseif kind == ARRAY then if #value.items == 0 then table.insert(out, "[]") return end table.insert(out, if depth then "[\n" else "[") for i, item in ipairs(value.items) do if i > 1 then table.insert(out, between) end table.insert(out, inner) write(out, item, if depth then depth + 1 else nil) end table.insert(out, if depth then "\n" .. string.rep(" ", depth) .. "]" else "]") else table.insert(out, scalar(value)) end end --- `value` as a pack file holds it: indented, with a final newline. function json.pretty(value: any): string local out = {} write(out, value, 0) table.insert(out, "\n") return table.concat(out) end --- `value` on one line, without spaces or a final newline. function json.compact(value: any): string local out = {} write(out, value, nil) return table.concat(out) end return json