Модуль:JsonLoader

Материал из Space Station 14 Вики

Для документации этого модуля может быть создана страница Модуль:JsonLoader/doc

local p = {}

local function skip_ws(text, pos)
	local len = #text
	while pos <= len do
		local ch = string.sub(text, pos, pos)
		if ch ~= " " and ch ~= "\n" and ch ~= "\r" and ch ~= "\t" then
			return pos
		end
		pos = pos + 1
	end
	return pos
end

local function skip_string(text, pos)
	pos = pos + 1
	local len = #text
	while pos <= len do
		local ch = string.sub(text, pos, pos)
		if ch == "\\" then
			pos = pos + 2
		elseif ch == "\"" then
			return pos
		else
			pos = pos + 1
		end
	end
	return nil
end

local function find_value_end(text, pos)
	local ch = string.sub(text, pos, pos)
	if ch == "\"" then
		return skip_string(text, pos)
	end

	if ch == "{" or ch == "[" then
		local depth = 0
		local len = #text
		while pos <= len do
			ch = string.sub(text, pos, pos)
			if ch == "\"" then
				pos = skip_string(text, pos)
				if not pos then
					return nil
				end
			elseif ch == "{" or ch == "[" then
				depth = depth + 1
			elseif ch == "}" or ch == "]" then
				depth = depth - 1
				if depth == 0 then
					return pos
				end
			end
			pos = pos + 1
		end
		return nil
	end

	local len = #text
	while pos <= len do
		ch = string.sub(text, pos, pos)
		if ch == "," or ch == "}" or ch == "]" or ch == "\n" or ch == "\r" then
			return pos - 1
		end
		pos = pos + 1
	end
	return len
end

local function decode_json(text)
	local ok, data = pcall(mw.text.jsonDecode, text)
	if ok then
		return data
	end
	return nil
end

local function find_member_range(text, objectStart, keyName)
	local pos = skip_ws(text, objectStart)
	if string.sub(text, pos, pos) ~= "{" then
		return nil, nil
	end

	pos = skip_ws(text, pos + 1)
	while pos <= #text and string.sub(text, pos, pos) ~= "}" do
		if string.sub(text, pos, pos) ~= "\"" then
			return nil, nil
		end

		local keyStart = pos
		local keyEnd = skip_string(text, pos)
		if not keyEnd then
			return nil, nil
		end

		local key = decode_json(string.sub(text, keyStart, keyEnd))
		pos = skip_ws(text, keyEnd + 1)
		if string.sub(text, pos, pos) ~= ":" then
			return nil, nil
		end

		local valueStart = skip_ws(text, pos + 1)
		local valueEnd = find_value_end(text, valueStart)
		if not valueEnd then
			return nil, nil
		end

		if key == keyName then
			return valueStart, valueEnd
		end

		pos = skip_ws(text, valueEnd + 1)
		local sep = string.sub(text, pos, pos)
		if sep == "," then
			pos = skip_ws(text, pos + 1)
		elseif sep == "}" then
			return nil, nil
		else
			return nil, nil
		end
	end

	return nil, nil
end

local function deep_copy(src)
	local dst = {}
	for k, v in pairs(src) do
		if type(v) == "table" then
			dst[k] = deep_copy(v)
		else
			dst[k] = v
		end
	end
	return dst
end

local function deep_merge(dst, src)
	for k, v in pairs(src) do
		if type(v) == "table" and type(dst[k]) == "table" then
			deep_merge(dst[k], v)
		elseif type(v) == "table" then
			dst[k] = deep_copy(v)
		else
			dst[k] = v
		end
	end
end

local function new_loader(titleName)
	local loader = {}
	local title = mw.title.new(titleName)
	local jsonData
	local fullData
	local defaultData
	local idDataCache = {}

	local function get_json()
		if jsonData == nil then
			jsonData = title and title:getContent() or ""
		end
		return jsonData
	end

	local function decode_root_member(keyName)
		local text = get_json()
		local rootStart = string.find(text, "{", 1, true)
		if not rootStart then
			return nil
		end

		local valueStart, valueEnd = find_member_range(text, rootStart, keyName)
		if not valueStart then
			return nil
		end

		return decode_json(string.sub(text, valueStart, valueEnd))
	end

	local function decode_id_member(id)
		if idDataCache[id] ~= nil then
			return idDataCache[id]
		end

		local text = get_json()
		local rootStart = string.find(text, "{", 1, true)
		if not rootStart then
			idDataCache[id] = false
			return nil
		end

		local idsStart = find_member_range(text, rootStart, "id")
		if not idsStart then
			idDataCache[id] = false
			return nil
		end

		local valueStart, valueEnd = find_member_range(text, idsStart, id)
		if not valueStart then
			idDataCache[id] = false
			return nil
		end

		local data = decode_json(string.sub(text, valueStart, valueEnd))
		idDataCache[id] = data or false
		return data
	end

	function loader.all()
		if fullData == nil then
			local data = decode_json(get_json())
			fullData = type(data) == "table" and data or {}
		end
		return fullData
	end

	function loader.get(id)
		id = tostring(id or "")

		if defaultData == nil then
			local data = decode_root_member("default")
			defaultData = type(data) == "table" and data or false
		end

		local base = defaultData ~= false and defaultData or nil
		if id == "" then
			return base
		end

		local specific = decode_id_member(id)
		if type(specific) ~= "table" then
			return base
		end

		if type(base) == "table" then
			local merged = deep_copy(base)
			deep_merge(merged, specific)
			return merged
		end

		return specific
	end

	return loader
end

function p.getFromTitle(titleName)
	return new_loader(titleName)
end

function p.getAllFromTitle(titleName)
	return new_loader(titleName).all()
end

function p.invoke(frame)
	local name = frame.args.title
	return p.getAllFromTitle(name)
end

return p