Модуль:GetField

Материал из Space Station 14 Вики
Версия от 11:53, 25 марта 2026; Pok (обсуждение | вклад) (Замена текста — «getGenerator» на «searchStore»)

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

local p = {}

local JsonPaths = require('Module:JsonPaths')
local getArgs = require('Module:Arguments').getArgs

local function get_module_name(pagePath)
	return JsonPaths.get(pagePath)
end

local function load_cached_data(moduleName)
	local ok, loaded = pcall(mw.loadData, moduleName)
	if not ok or not loaded then
		return nil
	end
	return loaded
end

local function parse_indexed_part(part)
	local key, idx = string.match(part, "^(.-)%[(%d+)%]$")
	if key then
		return key, tonumber(idx)
	end

	local num = tonumber(part)
	if num then
		return nil, num
	end

	return part, nil
end

local function get_by_path(tbl, path)
	if not tbl or path == "" then
		return nil
	end

	local cur = tbl
	for part in string.gmatch(path, "([^%.]+)") do
		local key, idx = parse_indexed_part(part)

		if key and key ~= "" then
			if type(cur) ~= "table" then
				return nil
			end

			local nextCur = cur[key]
			if nextCur == nil then
				nextCur = cur["!type:" .. key]
			end
			cur = nextCur
		end

		if idx then
			if type(cur) ~= "table" then
				return nil
			end
			cur = cur[idx]
		end

		if cur == nil then
			return nil
		end
	end

	return cur
end

local function format_value(v)
	local okJson, json = pcall(mw.text.jsonEncode, v)
	if okJson and json == "null" then
		return "null"
	end

	if v == nil then
		return ""
	end

	local t = type(v)
	if t == "string" or t == "number" or t == "boolean" then
		return tostring(v)
	elseif t == "table" then
		local ok, json2 = pcall(mw.text.jsonEncode, v)
		if ok and json2 then
			return json2
		end
		return ""
	else
		return tostring(v)
	end
end

local function to_nowiki(v)
	return "<nowiki>" .. v .. "</nowiki>"
end

local function is_array(tbl)
	local max = 0
	local count = 0
	for k in pairs(tbl) do
		if type(k) ~= "number" then
			return false
		end
		if k > max then
			max = k
		end
		count = count + 1
	end
	return count > 0 and max == count
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 resolve_entry(data, id)
	if type(data) ~= "table" then
		return nil
	end

	if id and id ~= "" then
		local direct = data[id]
		if direct ~= nil then
			return direct
		end

		local idsTable = data.id
		if type(idsTable) == "table" then
			local specific = idsTable[id]
			if type(specific) == "table" then
				local base = data["default"]
				if type(base) == "table" then
					local merged = deep_copy(base)
					deep_merge(merged, specific)
					return merged
				end
				return deep_copy(specific)
			end
		end
	end

	local base = data["default"]
	if type(base) == "table" then
		return deep_copy(base)
	end

	return nil
end

local function collect_id_keys(data)
	if type(data) ~= "table" then
		return {}
	end

	local idsTable = data.id
	local ids = {}

	if type(idsTable) == "table" then
		for k in pairs(idsTable) do
			ids[#ids + 1] = k
		end
		return ids
	end

	for k in pairs(data) do
		if k ~= "default" and k ~= "id" then
			ids[#ids + 1] = k
		end
	end

	return ids
end

local function contains_target(v, target)
	if type(v) == "table" then
		if is_array(v) then
			for _, item in ipairs(v) do
				if tostring(item) == target then
					return true
				end
			end
			return false
		end

		for _, item in pairs(v) do
			if tostring(item) == target then
				return true
			end
		end
		return false
	end

	return tostring(v) == target
end

local function is_nonempty_value(v)
	if v == nil then
		return false
	end
	if type(v) == "table" then
		return next(v) ~= nil
	end
	return true
end

local function find_matching_ids(idsTable, keyPath, searchValue)
	local target = tostring(searchValue)
	local matches = {}

	for idKey, entry in pairs(idsTable) do
		if type(entry) == "table" then
			local v = get_by_path(entry, keyPath)
			if v ~= nil and contains_target(v, target) then
				matches[#matches + 1] = idKey
			end
		end
	end

	return matches
end

local function preprocess_or_return(frame, text)
	if type(frame) == "table" and type(frame.preprocess) == "function" then
		return frame:preprocess(text)
	end
	return text
end

local function get_field_loose(entry, fieldId)
	local value = entry[fieldId]
	if value ~= nil then
		return value
	end
	if fieldId == "" then
		return nil
	end

	local first = string.sub(fieldId, 1, 1)
	local tail = string.sub(fieldId, 2)

	value = entry[string.lower(first) .. tail]
	if value ~= nil then
		return value
	end

	return entry[string.upper(first) .. tail]
end

local function apply_pattern(s, pattern, repl)
	if not pattern or pattern == "" or not s then
		return s
	end

	local text = tostring(s)
	local replacement
	if repl and repl ~= "" then
		replacement = tostring(repl)
		replacement = replacement:gsub("\\(%d)", "%%%1")
	else
		replacement = "%1"
	end

	local patt = pattern
	if not patt:find("%^") and not patt:find("%$") then
		patt = "^" .. patt .. "$"
	end

	return (text:gsub(patt, replacement))
end

local function flatten_parts(entry)
	if type(entry) ~= "table" then
		return {}
	end

	local parts = {}

	local function append_table_json(key, value)
		local ok, json = pcall(mw.text.jsonEncode, value)
		if ok and json then
			parts[#parts + 1] = key .. "=" .. to_nowiki(json)
		end
	end

	local function walk(tbl, prefix)
		local keys = {}
		for k in pairs(tbl) do
			keys[#keys + 1] = k
		end
		table.sort(keys, function(a, b)
			return tostring(a) < tostring(b)
		end)

		for _, k in ipairs(keys) do
			local v = tbl[k]
			local kStr = tostring(k)
			local key = (prefix == "" and kStr or prefix .. "." .. kStr)

			if type(v) == "table" then
				if next(v) ~= nil then
					append_table_json(key, v)
					if is_array(v) then
						local first = v[1]
						if type(first) == "table" then
							walk(first, key)
						end
					else
						walk(v, key)
					end
				end
			else
				parts[#parts + 1] = key .. "=" .. tostring(v)
			end
		end
	end

	walk(entry, "")
	return parts
end

local function flatten_entry(entry)
	local parts = flatten_parts(entry)
	if #parts == 0 then
		return ""
	end
	return table.concat(parts, "|")
end

local function build_tpl(id, pagePath, tplPath, data)
	if id == "" or pagePath == "" or tplPath == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	if not data then
		data = load_cached_data(moduleName)
	end
	if not data then
		return ""
	end

	local entry = resolve_entry(data, id)
	local extra = flatten_entry(entry)

	local tplStr = "{{" .. tostring(tplPath) .. "|id=" .. tostring(id)
	if extra ~= "" then
		tplStr = tplStr .. "|" .. extra
	end
	tplStr = tplStr .. "}}"

	return tplStr
end

function p.findInGenerator(frame)
	local args = frame.args or {}
	local searchId = args[1] or ""
	local kind = (args[2] or ""):lower()
	local fieldId = args[3] or ""

	if searchId == "" or fieldId == "" then
		return ""
	end
	if kind ~= "prototype" and kind ~= "component" then
		return ""
	end

	local storeName = (kind == "prototype") and "prototype_store.json" or "component_store.json"
	local moduleName = get_module_name(storeName)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local entry = data[searchId]
	if type(entry) ~= "table" then
		return ""
	end

	local value = get_field_loose(entry, fieldId)
	if value == nil then
		return ""
	end

	local out = {}
	local t = type(value)
	if t == "table" then
		for _, v in ipairs(value) do
			out[#out + 1] = v
		end
	else
		out[1] = value
	end

	return mw.text.jsonEncode(out)
end

function p.flattenField(frame)
	local args = frame.args or {}
	local id = args[1] or ""
	local pagePath = args[2] or ""
	if id == "" or pagePath == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local entry = resolve_entry(data, id) or {}
	return flatten_entry(entry)
end

function p.get(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local id = args[1] or ""
	local pagePath = args[2] or ""
	local keyPath = args[3] or ""

	if pagePath == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local entry = resolve_entry(data, id)
	if entry == nil then
		return ""
	end

	if keyPath == "" then
		return format_value(entry)
	end

	local value = get_by_path(entry, keyPath)
	return format_value(value)
end

function p.searchId(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchValue = args[1] or ""
	local pagePath = args[2] or ""
	local keyPath = args[3] or ""
	local searchType = (args.searchType or ""):lower()

	if searchValue == "" or pagePath == "" or keyPath == "" then
		return ""
	end
	if searchType == "" then
		searchType = "value"
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return "[]"
	end

	local ids = collect_id_keys(data)
	if #ids == 0 then
		return ""
	end

	local matches
	if searchType == "key" then
		local target = tostring(searchValue)
		matches = {}
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry(data, idKey)
			if type(entry) == "table" then
				local v = get_by_path(entry, keyPath)
				if type(v) == "table" and v[target] ~= nil then
					matches[#matches + 1] = idKey
				end
			end
		end
	else
		local target = tostring(searchValue)
		matches = {}
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry(data, idKey)
			if type(entry) == "table" then
				local v = get_by_path(entry, keyPath)
				if v ~= nil and contains_target(v, target) then
					matches[#matches + 1] = idKey
				end
			end
		end
	end

	if #matches == 0 then
		return ""
	end

	local ok, json = pcall(mw.text.jsonEncode, matches)
	if ok and json then
		return json
	end

	return ""
end

function p.searchTplId(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchValue = args[1] or ""
	local pagePath = args[2] or ""
	local keyPath = args[3] or ""
	local tplPath = mw.text.unstripNoWiki(args[4] or "")
	local searchType = (args.searchType or ""):lower()

	if searchType == "" then
		searchType = "value"
	end

	if searchType == "path" then
		searchValue = ""
		pagePath = args[1] or ""
		keyPath = args[2] or ""
		tplPath = mw.text.unstripNoWiki(args[3] or "")
	end

	if pagePath == "" or keyPath == "" or tplPath == "" then
		return ""
	end
	if searchType ~= "path" and searchValue == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local ids = collect_id_keys(data)
	if #ids == 0 then
		return ""
	end

	local matches
	if searchType == "path" then
		matches = {}
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry(data, idKey)
			if type(entry) == "table" then
				local v = get_by_path(entry, keyPath)
				if is_nonempty_value(v) then
					matches[#matches + 1] = idKey
				end
			end
		end
	elseif searchType == "key" then
		local target = tostring(searchValue)
		matches = {}
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry(data, idKey)
			if type(entry) == "table" then
				local v = get_by_path(entry, keyPath)
				if type(v) == "table" and v[target] ~= nil then
					matches[#matches + 1] = idKey
				end
			end
		end
	else
		local target = tostring(searchValue)
		matches = {}
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry(data, idKey)
			if type(entry) == "table" then
				local v = get_by_path(entry, keyPath)
				if v ~= nil and contains_target(v, target) then
					matches[#matches + 1] = idKey
				end
			end
		end
	end

	if #matches == 0 then
		return ""
	end

	local out = {}
	for _, idKey in ipairs(matches) do
		local tpl = build_tpl(idKey, pagePath, tplPath, data)
		if tpl ~= "" then
			out[#out + 1] = tpl
		end
	end

	if #out == 0 then
		return ""
	end

	local result = table.concat(out, " ")
	return preprocess_or_return(frame, result)
end

function p.getTpl(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local id = args[1] or ""
	local pagePath = args[2] or ""
	local tplPath = mw.text.unstripNoWiki(args[3] or "")

	if id == "" or pagePath == "" or tplPath == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = frame.data
	if not data then
		data = load_cached_data(moduleName)
	end
	if not data then
		return ""
	end

	local tplStr = build_tpl(id, pagePath, tplPath, data)
	return preprocess_or_return(frame, tplStr)
end

function p.getTplGenerator(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchId = args[1] or ""
	local kind = (args[2] or ""):lower()
	local generatorId = args[3] or ""
	local tplPath = mw.text.unstripNoWiki(args[4] or "")

	if searchId == "" or generatorId == "" or tplPath == "" then
		return ""
	end
	if kind ~= "prototype" and kind ~= "component" then
		return ""
	end

	local dir = (kind == "prototype") and "prototype/" or "component/"
	local pagePath = dir .. generatorId .. ".json"

	local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
	local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
	if not ok or type(ids) ~= "table" or #ids == 0 then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local out = {}
	for _, id in ipairs(ids) do
		local tpl = build_tpl(id, pagePath, tplPath, data)
		if tpl ~= "" then
			out[#out + 1] = tpl
		end
	end

	local result = table.concat(out, " ")
	return preprocess_or_return(frame, result)
end

function p.flattenParams(entry)
	return flatten_parts(entry)
end

function p.searchStore(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchId = args[1] or ""
	local kind = (args[2] or ""):lower()
	local generatorId = args[3] or ""

	if searchId == "" or generatorId == "" then
		return ""
	end
	if kind ~= "prototype" and kind ~= "component" then
		return ""
	end

	local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
	local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
	if not ok or type(ids) ~= "table" or #ids == 0 then
		return ""
	end

	local okOut, outJson = pcall(mw.text.jsonEncode, ids)
	if okOut and outJson then
		return outJson
	end

	return ""
end

function p.hasComp(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local entityId = args[1] or ""
	local compName = args[2] or ""

	if entityId == "" or compName == "" then
		return "false"
	end

	local moduleName = get_module_name("component.json")
	local data = load_cached_data(moduleName)
	if not data then
		return "false"
	end

	if type(data) ~= "table" then
		return "false"
	end

	local entry = data[entityId]
	if type(entry) ~= "table" then
		return "false"
	end

	local target = tostring(compName)
	for _, v in ipairs(entry) do
		if tostring(v) == target then
			return "true"
		end
	end

	return "false"
end

function p.GeneratorId(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local pagePath = args[1] or ""
	local replace = mw.text.unstripNoWiki(args.replace or "")
	local pattern = mw.text.unstripNoWiki(args.pattern or "(.*)")

	if pagePath == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local idsTable = data.id
	if type(idsTable) ~= "table" then
		return ""
	end

	local ids = {}
	for k in pairs(idsTable) do
		ids[#ids + 1] = k
	end

	table.sort(ids)

	if replace ~= "" then
		local out = {}
		for _, id in ipairs(ids) do
			local text = apply_pattern(id, pattern, replace)
			if text ~= "" then
				out[#out + 1] = text
			end
		end
		if #out == 0 then
			return ""
		end
		return preprocess_or_return(frame, table.concat(out, "\n"))
	end

	local ok, json = pcall(mw.text.jsonEncode, ids)
	if ok and json then
		return json
	end

	return ""
end

function p.GeneratorTplId(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local pagePath = args[1] or ""
	local tplPath = args[2] or ""

	if pagePath == "" or tplPath == "" then
		return ""
	end

	local moduleName = get_module_name(pagePath)
	local data = load_cached_data(moduleName)
	if not data then
		return ""
	end

	local idsTable = data.id
	if type(idsTable) ~= "table" then
		return ""
	end

	local out = {}

	for idKey in pairs(idsTable) do
		local tpl = build_tpl(idKey, pagePath, tplPath, data)
		if tpl ~= "" then
			out[#out + 1] = tpl
		end
	end

	table.sort(out)

	local result = table.concat(out, " ")
	return preprocess_or_return(frame, result)
end

function p.json(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local jsonStr = mw.text.unstripNoWiki(args[1] or args.json or "")
	local tplPath = mw.text.unstripNoWiki(args[2] or args.template or "")
	if jsonStr == "" or tplPath == "" then
		return ""
	end

	local ok, data = pcall(mw.text.jsonDecode, jsonStr)
	if not ok or type(data) ~= "table" then
		return ""
	end

	local okDp, dp = pcall(require, "Module:GetField")

	local calls = {}

	local function makeCall(id, obj)
		if type(id) ~= "string" then
			return
		end

		local parts = { "{{" .. tplPath, "id=" .. id }

		if type(obj) == "table" then
			if okDp and dp and type(dp.flattenParams) == "function" then
				local extra = dp.flattenParams(obj)
				for i = 1, #extra do
					parts[#parts + 1] = extra[i]
				end
			else
				for k, v in pairs(obj) do
					if v ~= nil then
						parts[#parts + 1] = tostring(k) .. "=" .. tostring(v)
					end
				end
			end
		elseif obj ~= nil then
			parts[#parts + 1] = "value=" .. tostring(obj)
		end

		parts[#parts + 1] = "}}"
		calls[#calls + 1] = table.concat(parts, "|")
	end

	if is_array(data) then
		for _, item in ipairs(data) do
			if type(item) == "table" then
				for k, v in pairs(item) do
					makeCall(k, v)
				end
			end
		end
	else
		for k, v in pairs(data) do
			makeCall(k, v)
		end
	end

	if #calls == 0 then
		return ""
	end

	local rendered = table.concat(calls, " ")
	return frame:preprocess(rendered)
end

function p.jsonList(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local jsonStr = mw.text.unstripNoWiki(args[1] or args.json or "")
	if jsonStr == "" then
		return ""
	end

	local ok, data = pcall(mw.text.jsonDecode, jsonStr)
	if not ok or type(data) ~= "table" then
		return ""
	end

	local outputType = (args.type or "list"):lower()

	local bullet = mw.text.unstripNoWiki(args.prefix or "* ")
	local sep = mw.text.unstripNoWiki(args.sep or ": ")
	if outputType == "none" then
		bullet = ""
		sep = ""
	end

	local keyPattern = mw.text.unstripNoWiki(args.key_pattern or "(.*)")
	local keyReplace = mw.text.unstripNoWiki(args.key_replace or "\\1")
	local valuePattern = mw.text.unstripNoWiki(args.value_pattern or "(.*)")
	local valueReplace = mw.text.unstripNoWiki(args.value_replace or "\\1")

	local pairPattern = mw.text.unstripNoWiki(args.pattern or "(.*)")
	local pairReplace = mw.text.unstripNoWiki(args.replace or "\\1")

	local out = {}

	if is_array(data) then
		for _, v in ipairs(data) do
			local text = ""

			if type(v) == "table" then
				if is_array(v) then
					text = table.concat(v, ", ")
				else
					local okJson, jsonVal = pcall(mw.text.jsonEncode, v)
					if okJson and jsonVal then
						text = jsonVal
					end
				end
			else
				text = tostring(v)
			end

			if text ~= "" then
				local patt = valuePattern ~= "" and valuePattern or keyPattern
				local repl = valueReplace ~= "" and valueReplace or keyReplace
				text = apply_pattern(text, patt, repl)

				local line
				if outputType == "enum" then
					line = text
				else
					line = bullet .. text
				end

				if pairPattern ~= "" then
					line = apply_pattern(line, pairPattern, pairReplace)
				end

				table.insert(out, line)
			end
		end
	else
		local keys = {}
		for k in pairs(data) do
			keys[#keys + 1] = k
		end
		table.sort(keys, function(a, b)
			return tostring(a) < tostring(b)
		end)

		for _, k in ipairs(keys) do
			local v = data[k]
			local vStr

			if type(v) == "table" then
				local okJson, jsonVal = pcall(mw.text.jsonEncode, v)
				if okJson and jsonVal then
					vStr = jsonVal
				else
					vStr = ""
				end
			else
				vStr = tostring(v)
			end

			local baseKey = apply_pattern(tostring(k), keyPattern, "\\1")

			local MARK_KEY = "\31KEY\31"
			local vRepl = (valueReplace or "\\1"):gsub("\\2", MARK_KEY)
			local vStr0 = apply_pattern(vStr, valuePattern, vRepl)
			vStr0 = tostring(vStr0):gsub(MARK_KEY, baseKey)

			local MARK_VAL = "\31VAL\31"
			local kRepl = (keyReplace or "\\1"):gsub("\\2", MARK_VAL)
			local keyStr0 = apply_pattern(tostring(k), keyPattern, kRepl)
			local keyStr = tostring(keyStr0):gsub(MARK_VAL, vStr0)

			vStr = vStr0

			if vStr ~= "" then
				local line
				if outputType == "enum" then
					line = vStr .. " " .. keyStr
				else
					line = bullet .. keyStr .. sep .. vStr
				end

				if pairPattern ~= "" then
					line = apply_pattern(line, pairPattern, pairReplace)
				end

				table.insert(out, line)
			end
		end
	end

	if outputType == "enum" then
		return frame:preprocess(table.concat(out, ", "))
	elseif outputType == "list" then
		return frame:preprocess(table.concat(out, "\n"))
	else
		return frame:preprocess(table.concat(out, ""))
	end
end

return p