rojo/plugin/Packages/json5/JSONEncoder.lua
2026-01-22 22:19:00 -06:00

565 lines
12 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--!strict
--!native
--!optimize 2
--[[
Copyright 2025 Joseph Melsha
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
-- Negative codes mean \u00XX
local StringCodes = {
[0x00] = -0x3030,
[0x01] = -0x3130,
[0x02] = -0x3230,
[0x03] = -0x3330,
[0x04] = -0x3430,
[0x05] = -0x3530,
[0x06] = -0x3630,
[0x07] = -0x3730,
[0x08] = 0x625C, -- \b
[0x09] = 0x745C, -- \t
[0x0A] = 0x6E5C, -- \n
[0x0B] = -0x6230,
[0x0C] = 0x665C, -- \f
[0x0D] = 0x725C, -- \r
[0x0E] = -0x6530,
[0x0F] = -0x6630,
[0x10] = -0x3031,
[0x11] = -0x3131,
[0x12] = -0x3231,
[0x13] = -0x3331,
[0x14] = -0x3431,
[0x15] = -0x3531,
[0x16] = -0x3631,
[0x17] = -0x3731,
[0x18] = -0x3831,
[0x19] = -0x3931,
[0x1A] = -0x6131,
[0x1B] = -0x6231,
[0x1C] = -0x6331,
[0x1D] = -0x6431,
[0x1E] = -0x6531,
[0x1F] = -0x6631,
}
local IdentifierChar1 = {
[36] = true, -- $
[95] = true, -- _
}
local IdentifierChar2 = {
[36] = true, -- $
[95] = true, -- _
}
-- A–Z
for b=65,90 do
IdentifierChar1[b] = true
IdentifierChar2[b] = true
end
-- a–z
for b=97,122 do
IdentifierChar1[b] = true
IdentifierChar2[b] = true
end
-- 0–9 (only valid in subsequent positions)
for b=48,57 do
IdentifierChar2[b] = true
end
type Stream = {
Buf: buffer,
Pos: number,
Cap: number,
Indent: number,
Pretty: boolean,
Encoders: {[string]: (out: Stream, v: any) -> ()},
Null: any?,
QuoteChar: number,
UnquoteIdent: boolean,
}
local function Stream(buf: buffer): Stream
return {
Buf = buf,
Pos = 0,
Cap = buffer.len(buf),
Indent = 0,
Pretty = false,
Encoders = {},
Null = nil,
QuoteChar = 0x22,
UnquoteIdent = false,
}
end
local function ToString(out: Stream): string
return buffer.readstring(out.Buf, 0, out.Pos)
end
local function AllocSize(
n: number
): number
return math.max(n, 2 ^ math.ceil(math.log(n, 2)))
end
local function Reserve(
out: Stream,
n: number
): number
local pos = out.Pos
local newPos = pos + n
if out.Cap < newPos then
local newCap = AllocSize(newPos)
local newBuf = buffer.create(newCap)
buffer.copy(newBuf, 0, out.Buf, 0, pos)
out.Buf, out.Cap = newBuf, newCap
end
out.Pos = newPos
return pos
end
local function WriteString(out: Stream, s: string)
local len = string.len(s)
local pos = Reserve(out, len)
buffer.writestring(out.Buf, pos, s, len)
end
local function WriteIndent(out: Stream)
local len = out.Indent * 2
local pos = Reserve(out, len)
local buf = out.Buf
for i=0,len-1 do
buffer.writeu16(buf, pos + i * 2, 0x2020)
end
end
local function IncrementIndent(out: Stream, n: number)
out.Indent += n
end
local function WriteU8(out: Stream, v: number)
local pos = Reserve(out, 1)
buffer.writeu8(out.Buf, pos, v)
end
local function WriteU16(out: Stream, v: number)
local pos = Reserve(out, 2)
buffer.writeu16(out.Buf, pos, v)
end
local function WriteU32(out: Stream, v: number)
local pos = Reserve(out, 4)
buffer.writeu32(out.Buf, pos, v)
end
local function EncodeNull(out: Stream, v: nil)
WriteU32(out, 0x6C6C756E) -- null
end
local function EncodeBoolean(out: Stream, v: boolean)
if v then
WriteU32(out, 0x65757274) -- true
else
local pos = Reserve(out, 5)
local buf = out.Buf
buffer.writeu32(buf, pos, 0x736C6166) -- fals
buffer.writeu8(buf, pos + 4, 0x65) -- e
end
end
local function EncodeNumber(out: Stream, n: number)
if n ~= n then
local pos = Reserve(out, 3)
local buf = out.Buf
buffer.writeu16(buf, pos, 0x614E) -- Na
buffer.writeu8(buf, pos + 2, 0x4E) -- N
elseif n == math.huge then
local pos = Reserve(out, 8)
local buf = out.Buf
buffer.writeu32(buf, pos, 0x69666E49) -- Infi
buffer.writeu32(buf, pos + 4, 0x7974696E) -- 'nity
elseif n == -math.huge then
local pos = Reserve(out, 9)
local buf = out.Buf
buffer.writeu8(buf, pos, 0x2D) -- '-'
buffer.writeu32(buf, pos + 1, 0x69666E49) -- Infi
buffer.writeu32(buf, pos + 5, 0x7974696E) -- 'nity
else
local s = tostring(n)
local i = string.find(s, "e%+")
if i then
s = string.sub(s, 1, i) .. string.sub(s, i + 2)
end
WriteString(out, s)
end
end
local function IsIdentifierName(
out: Stream,
s: string
): boolean
local len = string.len(s)
if len == 0 then
return false
end
local first = string.byte(s, 1)
if not IdentifierChar1[first] then
return false
end
local identifierChar2 = IdentifierChar2
for i=2,len do
if not identifierChar2[string.byte(s, i)] then
return false
end
end
return true
end
local function EncodeString(
out: Stream,
s: string,
unquoteIdent: boolean?
)
if unquoteIdent and not IsIdentifierName(out, s) then
unquoteIdent = false
end
local len = string.len(s)
local pos = Reserve(out, 2 + len * 6)
local buf = out.Buf
local quoteCode = out.QuoteChar
if not unquoteIdent then
buffer.writeu8(buf, pos, quoteCode) -- " or '
pos += 1
end
for i=1,len do
local b = string.byte(s, i)
if b > 31 then
if b ~= quoteCode and b ~= 0x5C then
buffer.writeu8(buf, pos, b)
pos += 1
else
buffer.writeu16(buf, pos, 0x5C + bit32.lshift(b, 8)) -- \" or \' or \\
pos += 2
end
else
local code = StringCodes[b]
if code < 0 then
code = -code
buffer.writeu32(buf, pos, 0x3030755C) -- \u00
pos += 4
end
buffer.writeu16(buf, pos, code)
pos += 2
end
end
if not unquoteIdent then
buffer.writeu8(buf, pos, quoteCode) -- " or '
pos += 1
end
out.Pos = pos
end
local function EncodeBuffer(out: Stream, data: buffer)
local len = buffer.len(data)
local pos = Reserve(out, 2 + len * 6)
local buf = out.Buf
local quoteCode = out.QuoteChar
buffer.writeu8(buf, pos, quoteCode) -- " or '
pos += 1
for i=0,len-1 do
local b = buffer.readu8(data, i)
if b > 31 then
if b ~= quoteCode and b ~= 0x5C then
buffer.writeu8(buf, pos, b)
pos += 1
else
buffer.writeu16(buf, pos, 0x5C + bit32.lshift(b, 8)) -- \" or \' or \\
pos += 2
end
else
local code = StringCodes[b]
if code < 0 then
code = -code
buffer.writeu32(buf, pos, 0x3030755C) -- \u00
pos += 4
end
buffer.writeu16(buf, pos, code)
pos += 2
end
end
buffer.writeu8(buf, pos, quoteCode) -- " or '
pos += 1
out.Pos = pos
end
local function EncodeAny(out: Stream, v: any)
if v == out.Null then
v = nil
end
out.Encoders[typeof(v)](out, v)
end
local function EncodeArray(out: Stream, o: {any})
local pretty = out.Pretty
WriteU8(out, 0x5B) -- [
if pretty then
IncrementIndent(out, 1)
end
for i, v in ipairs(o) do
if i > 1 then
WriteU8(out, 0x2C) -- ,
end
if pretty then
WriteU8(out, 0x0A) -- \n
WriteIndent(out)
end
EncodeAny(out, v)
end
if pretty then
IncrementIndent(out, -1)
if #o > 0 then
WriteU8(out, 0x0A) -- \n
WriteIndent(out)
end
end
WriteU8(out, 0x5D) -- ]
end
-- IDEA: encode keys as packed JSON, add to tuples, sort, encode
local function EncodeMap(out: Stream, o: {[string]: any})
local keys = {}
for k, _ in pairs(o) do
assert(type(k) == "string")
table.insert(keys, k)
end
table.sort(keys)
local pretty = out.Pretty
local unquoteIdent = out.UnquoteIdent
WriteU8(out, 0x7B) -- {
if pretty then
IncrementIndent(out, 1)
end
for i, k in ipairs(keys) do
if i > 1 then
WriteU8(out, 0x2C) -- ,
end
if pretty then
WriteU8(out, 0x0A) -- \n
WriteIndent(out)
end
EncodeString(out, k, unquoteIdent)
if pretty then
WriteU16(out, 0x203A) -- ': '
else
WriteU8(out, 0x3A) -- :
end
EncodeAny(out, o[k])
end
if pretty then
IncrementIndent(out, -1)
if #keys > 0 then
WriteU8(out, 0x0A) -- \n
WriteIndent(out)
end
end
WriteU8(out, 0x7D) -- }
end
local function EncodeTable(out: Stream, o: any)
-- assert(getmetatable(o) == nil)
if #o > 0 or next(o) == nil then
EncodeArray(out, o)
else
EncodeMap(out, o)
end
end
local function EncodeVector2(out: Stream, v: Vector2)
EncodeArray(out, { v.X, v.Y })
end
local function EncodeVector3(out: Stream, v: Vector3)
EncodeArray(out, { v.X, v.Y, v.Z })
end
local function EncodeVector2int16(out: Stream, v: Vector2int16)
EncodeArray(out, { v.X, v.Y })
end
local function EncodeVector3int16(out: Stream, v: Vector3int16)
EncodeArray(out, { v.X, v.Y, v.Z })
end
local function EncodeRegion3(out: Stream, v: Region3)
local min = v.CFrame * (v.Size * -0.5)
local max = v.CFrame * (v.Size * 0.5)
EncodeArray(out, { min.X, min.Y, min.Z, max.X, max.Y, max.Z })
end
local function EncodeRegion3int16(out: Stream, v: Region3int16)
EncodeArray(out, { v.Min.X, v.Min.Y, v.Min.Z, v.Max.X, v.Max.Y, v.Max.Z })
end
local function EncodeUDim(out: Stream, v: UDim)
EncodeArray(out, { v.Scale, v.Offset })
end
local function EncodeUDim2(out: Stream, v: UDim2)
EncodeArray(out, { v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset })
end
local function EncodeCFrame(out: Stream, v: CFrame)
EncodeArray(out, { v:GetComponents() })
end
local function EncodeColor3(out: Stream, v: Color3)
EncodeArray(out, {
math.round(v.R*255),
math.round(v.G*255),
math.round(v.B*255),
})
end
local function EncodeNumberRange(out: Stream, v: NumberRange)
EncodeArray(out, { v.Min, v.Max })
end
local function EncodeRect(out: Stream, v: Rect)
EncodeArray(out, { v.Min.X, v.Min.Y, v.Max.X, v.Max.Y })
end
local function EncodeEnumItem(out: Stream, v: EnumItem)
EncodeNumber(out, v.Value)
end
-- Encoders
local Encoders = {}
Encoders["nil"] = EncodeNull
Encoders["boolean"] = EncodeBoolean
Encoders["number"] = EncodeNumber
Encoders["string"] = EncodeString
Encoders["buffer"] = EncodeBuffer
Encoders["table"] = EncodeTable
-- Roblox Extensions
local EncodersExt = table.clone(Encoders)
EncodersExt["Vector2"] = EncodeVector2
EncodersExt["Vector3"] = EncodeVector3
EncodersExt["Vector2int16"] = EncodeVector2int16
EncodersExt["Vector3int16"] = EncodeVector3int16
EncodersExt["Region3"] = EncodeRegion3
EncodersExt["Region3int16"] = EncodeRegion3int16
EncodersExt["UDim"] = EncodeUDim
EncodersExt["UDim2"] = EncodeUDim2
EncodersExt["CFrame"] = EncodeCFrame
EncodersExt["Color3"] = EncodeColor3
EncodersExt["NumberRange"] = EncodeNumberRange
EncodersExt["Rect"] = EncodeRect
EncodersExt["EnumItem"] = EncodeEnumItem
local sharedBuffer = buffer.create(0x1000) -- 4k
local compactStream = Stream(sharedBuffer)
compactStream.Encoders = Encoders
local function Compact(v: any, nullValue: any?): string
local stream = compactStream
stream.Pos = 0
stream.Null = nullValue
EncodeAny(stream, v)
return ToString(stream)
end
local prettyStream = Stream(sharedBuffer)
prettyStream.Encoders = Encoders
prettyStream.Pretty = true
local function Pretty(v: any, nullValue: any?): string
local stream = prettyStream
stream.Pos = 0
stream.Indent = 0
stream.Null = nullValue
EncodeAny(stream, v)
return ToString(stream)
end
local compactExtStream = Stream(sharedBuffer)
compactExtStream.Encoders = EncodersExt
local function CompactExt(v: any, nullValue: any?): string
local stream = compactExtStream
stream.Pos = 0
stream.Null = nullValue
EncodeAny(stream, v)
return ToString(stream)
end
local prettyExtStream = Stream(sharedBuffer)
prettyExtStream.Encoders = EncodersExt
prettyExtStream.Pretty = true
local function PrettyExt(v: any, nullValue: any?): string
local stream = prettyExtStream
stream.Pos = 0
stream.Indent = 0
stream.Null = nullValue
EncodeAny(stream, v)
return ToString(stream)
end
-- JSON5
local compact5Stream = Stream(sharedBuffer)
compact5Stream.Encoders = Encoders
compact5Stream.UnquoteIdent = true
compact5Stream.QuoteChar = 0x27
local function Compact5(v: any, nullValue: any?): string
local stream = compact5Stream
stream.Pos = 0
stream.Null = nullValue
EncodeAny(stream, v)
return ToString(stream)
end
local pretty5Stream = Stream(sharedBuffer)
pretty5Stream.Encoders = Encoders
pretty5Stream.Pretty = true
pretty5Stream.UnquoteIdent = true
pretty5Stream.QuoteChar = 0x27
local function Pretty5(v: any, nullValue: any?): string
local stream = pretty5Stream
stream.Pos = 0
stream.Indent = 0
stream.Null = nullValue
EncodeAny(stream, v)
return ToString(stream)
end
local module = {
Compact = Compact,
Pretty = Pretty,
CompactExt = CompactExt,
PrettyExt = PrettyExt,
Compact5 = Compact5,
Pretty5 = Pretty5,
}
return table.freeze(module)