- Update AGENTS.md to encourage comments instead of forbidding them - Add docstrings to all public functions in src/ modules - Add module headers and function comments to lua/scl/ modules - Fix formatter to properly indent multi-line IF/ELSIF conditions - Fix FB call detection to not match IF statements with parentheses
257 lines
5.8 KiB
Lua
257 lines
5.8 KiB
Lua
-- JSON Encoder/Decoder for LSP server
|
|
-- Minimal implementation supporting basic JSON parsing and encoding
|
|
|
|
local json = {}
|
|
|
|
--- Check if a table is an array (sequential numeric keys starting from 1)
|
|
--- @param t table Table to check
|
|
--- @return boolean True if table is an array
|
|
local function is_array(t)
|
|
local i = 0
|
|
for _ in pairs(t) do
|
|
i = i + 1
|
|
if t[i] == nil then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- Escape special characters in string for JSON
|
|
--- @param s string String to escape
|
|
--- @return string Escaped string
|
|
local function escape(s)
|
|
s = s:gsub("\\", "\\\\")
|
|
s = s:gsub('"', '\\"')
|
|
s = s:gsub("\n", "\\n")
|
|
s = s:gsub("\r", "\\r")
|
|
s = s:gsub("\t", "\\t")
|
|
return s
|
|
end
|
|
|
|
--- Encode Lua value to JSON string
|
|
--- @param data any Value to encode
|
|
--- @return string JSON string
|
|
function json.encode(data)
|
|
local t = type(data)
|
|
if t == "nil" then
|
|
return "null"
|
|
elseif t == "boolean" then
|
|
return data and "true" or "false"
|
|
elseif t == "number" then
|
|
return tostring(data)
|
|
elseif t == "string" then
|
|
return '"' .. escape(data) .. '"'
|
|
elseif t == "table" then
|
|
local parts = {}
|
|
if is_array(data) then
|
|
for _, v in ipairs(data) do
|
|
table.insert(parts, json.encode(v))
|
|
end
|
|
return "[" .. table.concat(parts, ",") .. "]"
|
|
else
|
|
for k, v in pairs(data) do
|
|
table.insert(parts, '"' .. escape(k) .. '":' .. json.encode(v))
|
|
end
|
|
return "{" .. table.concat(parts, ",") .. "}"
|
|
end
|
|
else
|
|
return "null"
|
|
end
|
|
end
|
|
|
|
--- Skip whitespace in string
|
|
--- @param s string JSON string
|
|
--- @param i number Current position
|
|
--- @return number New position after whitespace
|
|
local function skip_whitespace(s, i)
|
|
while i <= #s and s:sub(i, i):match("%s") do
|
|
i = i + 1
|
|
end
|
|
return i
|
|
end
|
|
|
|
--- Parse JSON string literal
|
|
--- @param s string JSON string
|
|
--- @param i number Current position (at opening quote)
|
|
--- @return string|nil Parsed string
|
|
--- @return number New position
|
|
local function parse_string(s, i)
|
|
local result = {}
|
|
i = i + 1
|
|
while i <= #s do
|
|
local c = s:sub(i, i)
|
|
if c == '"' then
|
|
return table.concat(result), i + 1
|
|
elseif c == "\\" and i < #s then
|
|
local next_c = s:sub(i + 1, i + 1)
|
|
if next_c == "n" then
|
|
table.insert(result, "\n")
|
|
elseif next_c == "r" then
|
|
table.insert(result, "\r")
|
|
elseif next_c == "t" then
|
|
table.insert(result, "\t")
|
|
elseif next_c == '"' then
|
|
table.insert(result, '"')
|
|
elseif next_c == "\\" then
|
|
table.insert(result, "\\")
|
|
else
|
|
table.insert(result, next_c)
|
|
end
|
|
i = i + 2
|
|
else
|
|
table.insert(result, c)
|
|
i = i + 1
|
|
end
|
|
end
|
|
return nil, i
|
|
end
|
|
|
|
--- Parse JSON number
|
|
--- @param s string JSON string
|
|
--- @param i number Current position
|
|
--- @return number Parsed number
|
|
--- @return number New position
|
|
local function parse_number(s, i)
|
|
local start = i
|
|
if s:sub(i, i) == "-" then
|
|
i = i + 1
|
|
end
|
|
while i <= #s and s:sub(i, i):match("%d") do
|
|
i = i + 1
|
|
end
|
|
if s:sub(i, i) == "." then
|
|
i = i + 1
|
|
while i <= #s and s:sub(i, i):match("%d") do
|
|
i = i + 1
|
|
end
|
|
end
|
|
if s:sub(i, i):match("[eE]") then
|
|
i = i + 1
|
|
if s:sub(i, i):match("[+-]") then
|
|
i = i + 1
|
|
end
|
|
while i <= #s and s:sub(i, i):match("%d") do
|
|
i = i + 1
|
|
end
|
|
end
|
|
return tonumber(s:sub(start, i - 1)), i
|
|
end
|
|
|
|
local parse_value
|
|
|
|
--- Parse JSON array
|
|
--- @param s string JSON string
|
|
--- @param i number Current position (at opening bracket)
|
|
--- @return table|nil Parsed array
|
|
--- @return number New position
|
|
local function parse_array(s, i)
|
|
local arr = {}
|
|
i = i + 1
|
|
while i <= #s do
|
|
i = skip_whitespace(s, i)
|
|
if s:sub(i, i) == "]" then
|
|
return arr, i + 1
|
|
end
|
|
local val
|
|
val, i = parse_value(s, i)
|
|
table.insert(arr, val)
|
|
i = skip_whitespace(s, i)
|
|
if s:sub(i, i) == "]" then
|
|
return arr, i + 1
|
|
end
|
|
if s:sub(i, i) == "," then
|
|
i = i + 1
|
|
end
|
|
end
|
|
return nil, i
|
|
end
|
|
|
|
--- Parse JSON object
|
|
--- @param s string JSON string
|
|
--- @param i number Current position (at opening brace)
|
|
--- @return table|nil Parsed object
|
|
--- @return number New position
|
|
local function parse_object(s, i)
|
|
local obj = {}
|
|
i = i + 1
|
|
while i <= #s do
|
|
i = skip_whitespace(s, i)
|
|
if s:sub(i, i) == "}" then
|
|
return obj, i + 1
|
|
end
|
|
if s:sub(i, i) == '"' then
|
|
local key
|
|
key, i = parse_string(s, i)
|
|
i = skip_whitespace(s, i)
|
|
if s:sub(i, i) ~= ":" then
|
|
return nil, i
|
|
end
|
|
i = i + 1
|
|
local val
|
|
val, i = parse_value(s, i)
|
|
obj[key] = val
|
|
i = skip_whitespace(s, i)
|
|
if s:sub(i, i) == "}" then
|
|
return obj, i + 1
|
|
end
|
|
if s:sub(i, i) == "," then
|
|
i = i + 1
|
|
end
|
|
else
|
|
return nil, i
|
|
end
|
|
end
|
|
return nil, i
|
|
end
|
|
|
|
parse_value = function(s, i)
|
|
i = skip_whitespace(s, i)
|
|
if i > #s then
|
|
return nil, i
|
|
end
|
|
|
|
local c = s:sub(i, i)
|
|
|
|
if c == "{" then
|
|
return parse_object(s, i)
|
|
elseif c == "[" then
|
|
return parse_array(s, i)
|
|
elseif c == '"' then
|
|
return parse_string(s, i)
|
|
elseif c == "t" then
|
|
if s:sub(i, i + 3) == "true" then
|
|
return true, i + 4
|
|
end
|
|
return nil, i
|
|
elseif c == "f" then
|
|
if s:sub(i, i + 4) == "false" then
|
|
return false, i + 5
|
|
end
|
|
return nil, i
|
|
elseif c == "n" then
|
|
if s:sub(i, i + 3) == "null" then
|
|
return nil, i + 4
|
|
end
|
|
return nil, i
|
|
elseif c == "-" or c:match("%d") then
|
|
return parse_number(s, i)
|
|
else
|
|
return nil, i
|
|
end
|
|
end
|
|
|
|
--- Decode JSON string to Lua value
|
|
--- @param s string JSON string
|
|
--- @return any|nil Parsed value
|
|
--- @return number|nil Position after parsing or error
|
|
function json.decode(s)
|
|
if type(s) ~= "string" then
|
|
return nil, "invalid input"
|
|
end
|
|
local data, i = parse_value(s, 1)
|
|
return data, i
|
|
end
|
|
|
|
return json
|