Модуль:Песочница/Pok: различия между версиями

Материал из Space Station 14 Вики
Нет описания правки
Нет описания правки
 
(не показано 17 промежуточных версий этого же участника)
Строка 1: Строка 1:
local p = {}
local p = {}
local BASE_USER = "IanComradeBot/"
local getArgs = require('Module:Arguments').getArgs
local CURRENT_PROJECT
local JsonPaths = require('Module:JsonPaths')


local function try_call_expand(node)
local dpOk, dpModule = pcall(require, "Module:GetField")
    if type(node) ~= "table" then return nil end
local dp = dpOk and dpModule or nil
    local f = node.expand
 
    if type(f) ~= "function" then return nil end
local switchModeRegistry = {}
    local ok, res = pcall(function() return f(node) end)
local switchModeOrder = {}
     if ok and res and res ~= "" then return res end
 
     ok, res = pcall(function() return f() end)
local function trim(s)
    if ok and res and res ~= "" then return res end
     if not s then return s end
    ok, res = pcall(f, node)
     return (s:gsub("^%s*(.-)%s*$", "%1"))
    if ok and res and res ~= "" then return res end
    return nil
end
end


local function gather_text(x, out, seen)
local function each_csv_value(str, fn)
     out = out or {}
     if not str or str == "" then
    seen = seen or {}
         return
    if type(x) == "string" then
     end
        table.insert(out, x)
    for item in string.gmatch(str, "[^,]+") do
         return out
         local value = trim(item)
     elseif type(x) == "number" or type(x) == "boolean" then
         if value ~= "" then
        table.insert(out, tostring(x))
            fn(value)
        return out
    elseif type(x) == "table" then
        if seen[x] then return out end
        seen[x] = true
        local s = try_call_expand(x)
        if s and s ~= "" then
            table.insert(out, s)
            return out
        end
        if x.text and type(x.text) == "string" and x.text ~= "" then
            table.insert(out, x.text)
        end
         local i = 1
        while x[i] ~= nil do
            gather_text(x[i], out, seen)
            i = i + 1
        end
         for k, v in pairs(x) do
            if type(k) ~= "number" then
                if type(v) == "string" or type(v) == "number" or type(v) == "boolean" then
                    table.insert(out, tostring(v))
                elseif type(v) == "table" then
                    gather_text(v, out, seen)
                end
            end
         end
         end
        return out
     end
     end
    return out
end
end


local function extract_string(v)
local function load_module_data(page)
     if v == nil then return nil end
     local moduleName = JsonPaths.get(page)
    if type(v) == "string" then if v == "" then return nil end; return v end
     local ok, data = pcall(mw.loadData, moduleName)
    if type(v) == "number" or type(v) == "boolean" then return tostring(v) end
    if not ok then
     if type(v) == "table" then
         return nil
        local parts = gather_text(v)
        if #parts == 0 then return nil end
        local s = table.concat(parts, "")
        if s == "" then return nil end
         return s
     end
     end
     return nil
     return data
end
end


local function short_serialize(obj, depth, seen)
local function load_template_content(path)
     depth = depth or 0
     local title = mw.title.new("Template:" .. path)
    seen = seen or {}
     if not title then
    if type(obj) ~= "table" then return tostring(obj) end
        return nil
     if seen[obj] then return "<cycle>" end
    if depth > 2 then return "{...}" end
    seen[obj] = true
    local parts = {}
    local i = 1
    while obj[i] ~= nil do
        table.insert(parts, short_serialize(obj[i], depth+1, seen))
        i = i + 1
     end
     end
     for k,v in pairs(obj) do
     local ok, content = pcall(title.getContent, title)
        if type(k) ~= "number" then
    if not ok then
            table.insert(parts, tostring(k) .. "=" .. short_serialize(v, depth+1, seen))
         return nil
         end
     end
     end
     return "{" .. table.concat(parts, ", ") .. "}"
     return content
end
end


function p.set_path(frame)
local function lcfirst(s)
    local raw
     if not s or s == "" then return s end
     if type(frame) == "table" and frame.args then
    return string.lower(s:sub(1, 1)) .. (s:sub(2) or "")
        raw = frame.args[1] or nil
end
        if not raw then
            local ok, a = pcall(function() return frame:getArgument(1) end)
            if ok then raw = a end
        end
        if not raw then
            local ok2, a2 = pcall(function() return frame:getArgument("GetFieldPath") end)
            if ok2 then raw = a2 end
        end
    elseif type(frame) == "string" then
        raw = frame
    end


    local val = extract_string(raw)
local function load_entity_data(entityId)
     if val and val ~= "" then
     if not entityId or entityId == "" then
        CURRENT_PROJECT = val
         return nil
         return "OK:" .. tostring(CURRENT_PROJECT)
     end
     end


     local diag = "NOT_SET"
     local page = "prototype/Entity/" .. entityId .. ".json"
    if type(frame) == "table" then
    local moduleName = JsonPaths.get(page)
        diag = diag .. "; raw_args=" .. short_serialize(frame.args or {})
    local ok, data = pcall(mw.loadData, moduleName)
        local ok1, a1 = pcall(function() return frame:getArgument(1) end)
    if not ok or type(data) ~= "table" then
        diag = diag .. "; getArgument(1)=" .. (ok1 and short_serialize(a1) or "(err)")
         return nil
        local ok2, a2 = pcall(function() return frame:getArgument("GetFieldPath") end)
        diag = diag .. "; getArgument('GetFieldPath')=" .. (ok2 and short_serialize(a2) or "(err)")
    else
         diag = diag .. "; frame_type=" .. type(frame) .. "; frame_val=" .. tostring(frame)
     end
     end
     return diag
     return data
end
end


function p._debug_get_cached()
local function normalize_component_name(name)
     if not CURRENT_PROJECT then return "(nil)" end
     if type(name) ~= "string" then
    return tostring(CURRENT_PROJECT)
        return nil
end
    end


function p.inspect(frame)
     name = trim(name)
     local f = frame or mw.getCurrentFrame()
     if name == "" then
     if not f then return "NO_FRAME" end
         return nil
    local out = {}
    local depth = 0
    while f and depth < 10 do
        table.insert(out, ("FRAME depth=" .. depth))
        table.insert(out, ("  args = " .. short_serialize(f.args or {})))
        local ok1, a1 = pcall(function() return f:getArgument(1) end)
         table.insert(out, ("  getArgument('1') = " .. (ok1 and short_serialize(a1) or "(err)")))
        local okN, aN = pcall(function() return f:getArgument("GetFieldPath") end)
        table.insert(out, ("  getArgument('GetFieldPath') = " .. (okN and short_serialize(aN) or "(err)")))
        table.insert(out, ("  has:getParent = " .. tostring(type(f.getParent) == "function")))
        table.insert(out, ("  has:callParserFunction = " .. tostring(type(f.callParserFunction) == "function")))
        f = (type(f.getParent) == "function") and f:getParent() or nil
        depth = depth + 1
     end
     end
    table.insert(out, ("CURRENT_PROJECT = " .. tostring(CURRENT_PROJECT)))
    return table.concat(out, "\n")
end


function p.get_module_name(frame, pagePath)
    if name:sub(1, 5) == "type:" then
    local project = pagePath and extract_string(pagePath) or CURRENT_PROJECT
         return name:sub(6)
    if (not project or project == "") and frame then
         local ok, res = pcall(function() return frame:callParserFunction{ name = "var", args = { "GetFieldPath" } } end)
        if ok and res and res ~= "" then project = tostring(res) end
     end
     end
     if not project or project == "" then project = "default" end
     if name:sub(1, 6) == "!type:" then
     return "Module:" .. BASE_USER .. project .. "/data"
        return name:sub(7)
    end
 
     return name
end
end


local function load_cached_data(moduleName)
local function collect_entity_components(entity)
     local ok, loaded = pcall(mw.loadData, moduleName)
     local out = {}
     if not ok or not loaded then
    local seen = {}
         return nil
 
     if type(entity) ~= "table" then
         return out
     end
     end
    return loaded
end


local function parse_indexed_part(part)
     local comps = entity.components
     local key, idx = string.match(part, "^(.-)%[(%d+)%]$")
    if type(comps) ~= "table" then
    if key then
         return out
         return key, tonumber(idx)
     end
     end
    local num = tonumber(part)
    if num then
        return nil, num
    end
    return part, nil
end


local function get_by_path(tbl, path)
     if #comps > 0 then
     if not tbl or path == "" then return nil end
        for _, v in ipairs(comps) do
    local cur = tbl
            local name = normalize_component_name(v)
    for part in string.gmatch(path, "([^%.]+)") do
            if name and not seen[name] then
        local key, idx = parse_indexed_part(part)
                seen[name] = true
        if key and key ~= "" then
                 out[#out + 1] = name
            if type(cur) ~= "table" then return nil end
            local nextCur = cur[key]
            if nextCur == nil then
                 nextCur = cur["!type:" .. key]
             end
             end
            cur = nextCur
         end
         end
         if idx then
    else
             if type(cur) ~= "table" then return nil end
         for k in pairs(comps) do
            cur = cur[idx]
             local name = normalize_component_name(k)
            if name and not seen[name] then
                seen[name] = true
                out[#out + 1] = name
            end
         end
         end
        if cur == nil then return nil end
     end
     end
     return cur
 
    table.sort(out)
     return out
end
end


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


     if v == nil then return "" end
    local getter = dp.getEntityComponents or dp.collectEntityComponents or dp.getComp
     if type(getter) ~= "function" then
        return nil
    end


     local t = type(v)
     local ok, result = pcall(getter, { args = { entityId } })
     if t == "string" or t == "number" or t == "boolean" then
     if not ok or result == nil or result == "" then
         return tostring(v)
         return nil
    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
end


local function to_nowiki(v)
    if type(result) == "table" then
    return "<nowiki>" .. v .. "</nowiki>"
        return result
end
    end


local function is_array(tbl)
    if type(result) == "string" then
    local max = 0
        local okJson, decoded = pcall(mw.text.jsonDecode, result)
    local count = 0
         if okJson and type(decoded) == "table" then
    for k in pairs(tbl) do
             return decoded
         if type(k) ~= "number" then
             return false
         end
         end
        if k > max then max = k end
        count = count + 1
     end
     end
     return count > 0 and max == count
 
     return nil
end
end


local function deep_copy(src)
local function load_entity_components(entityId)
     local dst = {}
     local viaDp = load_entity_components_from_dp(entityId)
    for k, v in pairs(src) do
    if type(viaDp) == "table" and next(viaDp) ~= nil then
        if type(v) == "table" then
        local out = {}
            dst[k] = deep_copy(v)
        local seen = {}
        if #viaDp > 0 then
            for _, v in ipairs(viaDp) do
                local name = normalize_component_name(v)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
         else
         else
             dst[k] = v
             for k in pairs(viaDp) do
                local name = normalize_component_name(k)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
         end
         end
        table.sort(out)
        return out
     end
     end
    return dst
end


local function deep_merge(dst, src)
    local entity = load_entity_data(entityId)
     for k, v in pairs(src) do
     return collect_entity_components(entity)
        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
end


local function resolve_entry(data, id)
p.loadEntityData = load_entity_data
     if type(data) ~= "table" then
p.collectEntityComponents = collect_entity_components
         return nil
p.loadEntityComponents = load_entity_components
p.entityHasComponent = function(entityOrId, compName)
     if not compName or compName == "" then
         return false
     end
     end


     if id and id ~= "" then
     if type(entityOrId) == "string" then
         local direct = data[id]
         local entity = load_entity_data(entityOrId)
         if direct ~= nil then
         if not entity then
             return direct
             return false
         end
         end
 
         local comps = collect_entity_components(entity)
         local idsTable = data.id
         for _, v in ipairs(comps) do
         if type(idsTable) == "table" then
             if v == compName then
            local specific = idsTable[id]
                 return true
             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
         end
        return false
     end
     end


     local base = data["default"]
     local comps = collect_entity_components(entityOrId)
     if type(base) == "table" then
     for _, v in ipairs(comps) do
         return deep_copy(base)
        if v == compName then
            return true
         end
     end
     end
     return nil
     return false
end
 
local function makeTplCall(tplPath, sw, key, id, extra)
    local tplStr = "{{" .. tplPath .. "|" .. sw .. "|" .. key
    tplStr = tplStr .. "|id=" .. tostring(id)
    if extra and extra ~= "" then tplStr = tplStr .. "|" .. extra end
    tplStr = tplStr .. "}}"
    return tplStr
end
end


local function collect_id_keys(data)
local function add_template_param(params, seen, raw)
     if type(data) ~= "table" then
     local param = trim(raw or "")
         return {}
    if param == "" or param == "id" or param:match("^%d+$") then
         return
     end
     end
    if not seen[param] then
        seen[param] = true
        params[#params + 1] = param
    end
end


     local idsTable = data.id
local function collect_template_params(content)
     local ids = {}
     local params = {}
     local seen = {}


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


     for k in pairs(data) do
     for param in content:gmatch("{{{%s*([^|}]+)%s*|") do
         if k ~= "default" and k ~= "id" then
         add_template_param(params, seen, param)
            ids[#ids + 1] = k
    end
         end
    for param in content:gmatch("{{{%s*([^|}]+)%s*}}") do
         add_template_param(params, seen, param)
     end
     end
     return ids
 
     return params
end
 
local function get_template_params(tplPath, content)
    return collect_template_params(content)
end
end


local function contains_target(v, target)
local function sort_entries_by_priority(entries)
     if type(v) == "table" then
     table.sort(entries, function(a, b)
        if is_array(v) then
        if a.priority == b.priority then return a.idx < b.idx end
            for _, item in ipairs(v) do
        return a.priority > b.priority
                if tostring(item) == target then
    end)
                    return true
end
                end
            end
            return false
        end


        for _, item in pairs(v) do
local function make_source(kind, name, pathName, tplPath)
            if tostring(item) == target then
    return { kind = kind, name = name, pathName = pathName, tplPath = tplPath }
                return true
end
            end
        end
        return false
    end


    return tostring(v) == target
local function register_switch_mode(name, cfg)
    switchModeRegistry[name] = cfg or {}
    switchModeOrder[#switchModeOrder + 1] = name
end
end


local function is_nonempty_value(v)
local function new_switch_state()
     if v == nil then return false end
     local state = { keyOrder = {}, keyToTemplates = {}, keySources = {} }
     if type(v) == "table" then
     for _, sw in ipairs(switchModeOrder) do
         return next(v) ~= nil
        state.keyOrder[sw] = {}
        state.keyToTemplates[sw] = {}
         state.keySources[sw] = {}
     end
     end
     return true
     return state
end
end


local function find_matching_ids(idsTable, keyPath, searchValue)
local function ensure_switch_key(state, sw, key)
     local target = tostring(searchValue)
     local byKey = state.keyToTemplates[sw]
     local matches = {}
     if not byKey[key] then
 
        byKey[key] = {}
    for idKey, entry in pairs(idsTable) do
         state.keyOrder[sw][#state.keyOrder[sw] + 1] = key
         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
    return byKey[key]
end


     return matches
local function add_switch_entry(state, sw, key, entry)
     local bucket = ensure_switch_key(state, sw, key)
    entry.idx = #bucket + 1
    bucket[#bucket + 1] = entry
end
end


local function preprocess_or_return(frame, text)
local function collect_tpl_calls(entries)
     if type(frame.preprocess) == "function" then
    local tplCalls = {}
         return frame:preprocess(text)
    local sources = {}
     if #entries > 0 then
        sort_entries_by_priority(entries)
        for _, e in ipairs(entries) do
            tplCalls[#tplCalls + 1] = e.tpl
            sources[#sources + 1] = e.source
         end
     end
     end
     return text
     return tplCalls, sources
end
end


local function get_field_loose(entry, fieldId)
local function makeSourceLink(s)
     local value = entry[fieldId]
     local className =
    if value ~= nil then return value end
        (s.name:sub(1, 1):upper() .. s.name:sub(2)) ..
    if fieldId == "" then return nil end
        (s.kind and (s.kind:sub(1, 1):upper() .. s.kind:sub(2)) or "")


     local first = string.sub(fieldId, 1, 1)
     local tplLabel = "Template:" .. s.tplPath
    local tail = string.sub(fieldId, 2)
     return "[[" .. tplLabel .. "|" .. className .. "]]"
     value = entry[string.lower(first) .. tail]
end
    if value ~= nil then return value end


     return entry[string.upper(first) .. tail]
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
    local parts = {}
     if tplCalls and #tplCalls > 0 then
        for i, tpl in ipairs(tplCalls) do
            local add = true
            if frame then
                local expanded = frame:preprocess(tpl)
                add = expanded and trim(expanded) ~= ""
            end
            if add then
                local src = sources and sources[i]
                local line = '<div>' .. tpl .. '</div>'
                if showSource and src then
                    line = line .. '<div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
                end
                parts[#parts + 1] = '<div class="ts-Сущность">' .. line .. '</div>'
            end
        end
        if #parts == 0 then
            return ""
        end
        if includeHeader then
            table.insert(parts, 1, "<h2>" .. mw.text.encode(key) .. "</h2>")
        end
    end
    return table.concat(parts, "\n")
end
end


local function apply_pattern(s, pattern, repl)
local function split_title_key(key)
     if not pattern or pattern == "" or not s then
     local main, sub = (key or ""):match("^([^_]+)_(.+)$")
         return s
    if main and sub then
         return main, sub
     end
     end
    return key, nil
end


    local text = tostring(s)
local function renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
     local replacement
     local groups = {}
     if repl and repl ~= "" then
     local groupOrder = {}
         replacement = tostring(repl)
 
         replacement = replacement:gsub("\\(%d)", "%%%1")
    for _, key in ipairs(keyOrder or {}) do
    else
         local mainTitle, subTitle = split_title_key(key)
        replacement = "%1"
         if mainTitle and mainTitle ~= "" then
    end
            local group = groups[mainTitle]
            if not group then
                group = { blocks = {} }
                groups[mainTitle] = group
                groupOrder[#groupOrder + 1] = mainTitle
            end


    local patt = pattern
            group.blocks[#group.blocks + 1] = {
    if not patt:find("%^") and not patt:find("%$") then
                subTitle = subTitle,
         patt = "^" .. patt .. "$"
                entries = keyToTemplates[key] or {}
            }
         end
     end
     end


     return (text:gsub(patt, replacement))
     local out = {}
end
    for _, mainTitle in ipairs(groupOrder) do
        local group = groups[mainTitle]
        local parts = {}


local function flatten_parts(entry)
         if not noHeaders then
    if type(entry) ~= "table" then return {} end
             parts[#parts + 1] = "<h2>" .. mw.text.encode(mainTitle) .. "</h2>"
 
    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
    end


    local function walk(tbl, prefix)
         for _, block in ipairs(group.blocks or {}) do
        local keys = {}
             local tplCalls, sources = collect_tpl_calls(block.entries or {})
        for k in pairs(tbl) do keys[#keys + 1] = k end
             local blockText = renderTitleBlock(block.subTitle or mainTitle, tplCalls, sources, false, frame, showSource)
        table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
             if blockText ~= "" then
         for _, k in ipairs(keys) do
                 if block.subTitle and not noHeaders then
             local v = tbl[k]
                     parts[#parts + 1] = "<h3>" .. mw.text.encode(block.subTitle) .. "</h3>"
            local kStr = tostring(k)
             local key = (prefix == "" and kStr or prefix .. "." .. kStr)
             if type(v) == "table" then
                 if next(v) == nil then
                else
                     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
                 end
            else
                 parts[#parts + 1] = blockText
                 parts[#parts + 1] = key .. "=" .. tostring(v)
             end
             end
        end
        if #parts > 0 then
            out[#out + 1] = table.concat(parts, "\n")
         end
         end
     end
     end


     walk(entry, "")
     return table.concat(out, "\n")
end


     return parts
local function normalizeFilterKey(s)
    s = trim(s or "")
    s = s:gsub("%s*_%s*", "_")
     return s
end
end


local function flatten_entry(entry)
local function matches_card_list(list, callKey, compositeKey)
    local parts = flatten_parts(entry)
     if not list then
     if #parts == 0 then
         return false
         return ""
     end
     end
     return table.concat(parts, "|")
     callKey = normalizeFilterKey(callKey)
    compositeKey = normalizeFilterKey(compositeKey)
    return list[callKey] or list[compositeKey] or false
end
end


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


     if searchId == "" or fieldId == "" then
     if entityId and entityId ~= "" then
         return ""
         parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
     end
     end
     if kind ~= "prototype" and kind ~= "component" then
 
        return ""
     if merged.tags and #merged.tags > 0 then
        table.sort(merged.tags)
        parts[#parts + 1] = "тип=" .. table.concat(merged.tags, ", ")
     end
     end


     local storeName = (kind == "prototype") and "prototype_store.json" or "component_store.json"
     if merged.sections and #merged.sections > 0 then
    local moduleName = get_module_name(storeName)
        table.sort(merged.sections, function(a, b)
    local data = load_cached_data(moduleName)
            if a == "Сущность" then return true end
    if not data then
            if b == "Сущность" then return false end
         return ""
            return a < b
        end)
        parts[#parts + 1] = "sections=" .. table.concat(merged.sections, ", ")
        for _, section in ipairs(merged.sections) do
            local labels = merged.labelLists[section]
            if labels and #labels > 0 then
                local enc = {}
                for i = 1, #labels do
                    enc[i] = mw.text.encode(labels[i])
                end
                parts[#parts + 1] = mw.text.encode(section) .. "=" .. table.concat(enc, ", ")
            end
         end
     end
     end


     local entry = data[searchId]
     for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
     if type(entry) ~= "table" then
        if displayLabel and displayLabel ~= "" then
        return ""
            parts[#parts + 1] = mw.text.encode(compositeKey .. "_label") .. "=" .. displayLabel
        end
     end
    for compositeKey, content in pairs(merged.contentByKey or {}) do
        if content and content ~= "" then
            parts[#parts + 1] = mw.text.encode(compositeKey) .. "=" .. content
        end
     end
     end


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


     local out = {}
     return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
    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
end


function p.flattenField(frame)
local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
     local args = frame.args or {}
     local merged = {
    local id = args[1] or ""
        sections = {},
    local pagePath = args[2] or ""
        sectionsMap = {},
    if id == "" or pagePath == "" then return "" end
        labelLists = {},
        labelSets = {},
        labelOverrides = {},
        contentByKey = {},
        tags = {},
        tagSet = {}
    }
    local rawContentParts = {}
    for _, callKey in ipairs(keyOrder or {}) do
        local entries = keyToTemplates[callKey] or {}
        if #entries > 0 then
            sort_entries_by_priority(entries)
            for _, e in ipairs(entries) do
                local displayLabel = trim(frame:preprocess(e.tplLabel or "") or "")
                local content = trim(frame:preprocess(e.tplContent or "") or "")
                local tagText = ""
                if e.tplTag then
                    tagText = trim(frame:preprocess(e.tplTag or "") or "")
                end
                local compositeKey = (callKey:find("_", 1, true)) and callKey or ("Сущность_" .. callKey)
                local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность"


    local moduleName = get_module_name(pagePath)
                local isWhitelisted = cardFilter and matches_card_list(cardFilter.whitelist, callKey, compositeKey) or
    local data = load_cached_data(moduleName)
                    false
    if not data then return "" end
                local isBlacklisted = cardFilter and matches_card_list(cardFilter.blacklist, callKey, compositeKey) or
                    false
                if isWhitelisted and content ~= "" then
                    rawContentParts[#rawContentParts + 1] = content
                end


    local entry = resolve_entry(data, id) or {}
                local allowCardEntry = not isWhitelisted and not isBlacklisted and
    return flatten_entry(entry)
                    ((not cardFilter) or (not cardFilter.hasWhitelist) or
end
                        matches_card_list(cardFilter.cardWhitelist, callKey, compositeKey))


function p.get(frame)
                if allowCardEntry and (displayLabel ~= "" or content ~= "") then
    local args = frame.args or {}
                    if not merged.sectionsMap[section] then
    local id = args[1] or ""
                        merged.sectionsMap[section] = true
    local pagePath = args[2] or ""
                        merged.sections[#merged.sections + 1] = section
    local keyPath = args[3] or ""
                    end
                    if displayLabel ~= "" and (not merged.labelOverrides[compositeKey] or merged.labelOverrides[compositeKey] == "") then
                        merged.labelOverrides[compositeKey] = displayLabel
                    end
                    if content ~= "" then
                        local prev = merged.contentByKey[compositeKey]
                        if prev and prev ~= "" then
                            merged.contentByKey[compositeKey] = prev .. "\n" .. content
                        else
                            merged.contentByKey[compositeKey] = content
                        end
                    end


    if pagePath == "" then return "" end
                    merged.labelSets[section] = merged.labelSets[section] or {}
                    if not merged.labelSets[section][compositeKey] then
                        merged.labelSets[section][compositeKey] = true
                        local cur = merged.labelLists[section] or {}
                        cur[#cur + 1] = compositeKey
                        merged.labelLists[section] = cur
                    end
                end


    local moduleName = get_module_name(pagePath)
                if allowCardEntry and tagText ~= "" then
    local data = load_cached_data(moduleName)
                    if not merged.tagSet[tagText] then
    if not data then return "" end
                        merged.tagSet[tagText] = true
 
                        merged.tags[#merged.tags + 1] = tagText
    local entry = resolve_entry(data, id)
                    end
    if entry == nil then return "" end
                end
 
            end
    if keyPath == "" then
         end
         return format_value(entry)
     end
     end


     local value = get_by_path(entry, keyPath)
     each_csv_value(frame.args.cardTag or "", function(extraTag)
     return format_value(value)
        if not merged.tagSet[extraTag] then
end
            merged.tagSet[extraTag] = true
            merged.tags[#merged.tags + 1] = extraTag
        end
     end)


function p.getId(frame)
     local out = {}
     local args = frame.args or {}
     if #rawContentParts > 0 then
     local searchValue = args[1] or ""
        out[#out + 1] = table.concat(rawContentParts, "\n")
    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
     end


     local moduleName = get_module_name(pagePath)
     local cardCall = buildCardCall(merged, entityId)
    local data = load_cached_data(moduleName)
    if not data then return "[]" end


    local ids = collect_id_keys(data)
     if noHeaders then
     if #ids == 0 then
         local hasLabel = false
         return ""
         for _, v in pairs(merged.labelOverrides or {}) do
    end
             if v and v ~= "" then
 
                 hasLabel = true
    local matches
                break
    if searchType == "key" then
            end
        local target = tostring(searchValue)
        end
        matches = {}
        if not hasLabel then
         for _, idKey in ipairs(ids) do
            for _, lst in pairs(merged.labelLists or {}) do
            local entry = resolve_entry(data, idKey)
                 if #lst > 0 then
             if type(entry) == "table" then
                     hasLabel = true
                 local v = get_by_path(entry, keyPath)
                    break
                 if type(v) == "table" and v[target] ~= nil then
                     matches[#matches + 1] = idKey
                 end
                 end
             end
             end
         end
         end
    else
 
         local target = tostring(searchValue)
         local hasContent = false
        matches = {}
         for _, v in pairs(merged.contentByKey or {}) do
         for _, idKey in ipairs(ids) do
             if v and v ~= "" then
            local entry = resolve_entry(data, idKey)
                 hasContent = true
             if type(entry) == "table" then
                 break
                 local v = get_by_path(entry, keyPath)
                if v ~= nil and contains_target(v, target) then
                    matches[#matches + 1] = idKey
                 end
             end
             end
         end
         end
    end


    if #matches == 0 then
        if not hasLabel and not hasContent then
        return ""
            return table.concat(out, "\n")
        end
     end
     end


     local ok, json = pcall(mw.text.jsonEncode, matches)
     if cardCall ~= "" then
    if ok and json then
         out[#out + 1] = cardCall
         return json
     end
     end


     return ""
     return table.concat(out, "\n")
end
end


function p.getTplId(frame)
register_switch_mode("card", {
    local args = frame.args or {}
    full = true,
     local searchValue = args[1] or ""
    build_entry = function(ctx, key)
    local pagePath = args[2] or ""
        return {
     local keyPath = args[3] or ""
            tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ctx.id, ctx.extra),
    local tplPath = mw.text.unstripNoWiki(args[4] or "")
            tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ctx.id, ctx.extra),
    local searchType = (args.searchType or ""):lower()
            tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
            source = ctx.source,
            priority = ctx.priority
        }
     end,
    build_preview_entry = function(ctx, key)
        return {
            tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ""),
            tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ""),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
     add_entity_extras = function(state, parsed, ctx)
        if type(parsed) == "table" and parsed.cardTag and parsed.cardTag ~= "" then
            add_switch_entry(state, "card", "cardTag", {
                tplLabel = "",
                tplContent = "",
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                source = ctx.source,
                priority = ctx.priority
            })
        end
    end,
    render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource, cardFilter)
        return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
    end
})


    if searchType == "" then
register_switch_mode("title", {
        searchType = "value"
    full = true,
     end
     build_entry = function(ctx, key)
    if searchType == "path" then
         return {
         searchValue = ""
            tpl = makeTplCall(ctx.tplPath, "title", key, ctx.id, ctx.extra),
        pagePath = args[1] or ""
            source = ctx.source,
        keyPath = args[2] or ""
            priority = ctx.priority
         tplPath = mw.text.unstripNoWiki(args[3] or "")
         }
     end
     end,
     if pagePath == "" or keyPath == "" or tplPath == "" then
     build_preview_entry = function(ctx, key)
         return ""
        return {
     end
            tpl = makeTplCall(ctx.tplPath, "title", key, ""),
     if searchType ~= "path" and searchValue == "" then
            source = ctx.source,
         return ""
            priority = ctx.priority
         }
     end,
     render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource)
         return renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
     end
     end
})


    local moduleName = get_module_name(pagePath)
local function getTemplateMeta(frame, tplPath)
     local data = load_cached_data(moduleName)
     local expanded = frame:expandTemplate {
    if not data then return "" end
        title = tplPath,
        args = { "json" }
    }


     local ids = collect_id_keys(data)
     local ok, data = pcall(mw.text.jsonDecode, expanded)
     if #ids == 0 then
     if not ok or type(data) ~= "table" then
         return ""
         return ""
     end
     end


    local matches
     if data.card == nil then
     if searchType == "path" then
         local cardKeys = {}
         matches = {}
        local seen = {}
         for _, idKey in ipairs(ids) do
         for base, labels in pairs(data) do
             local entry = resolve_entry(data, idKey)
             if type(base) == "string" and base ~= "card" and base:sub(1, 4) == "card" and type(labels) == "table" then
            if type(entry) == "table" then
                 for _, lab in ipairs(labels) do
                 local v = get_by_path(entry, keyPath)
                    if not seen[lab] then
                if is_nonempty_value(v) then
                        seen[lab] = true
                    matches[#matches + 1] = idKey
                        cardKeys[#cardKeys + 1] = lab
                    end
                 end
                 end
             end
             end
         end
         end
     elseif searchType == "key" then
        data.card = cardKeys
        local target = tostring(searchValue)
    end
        matches = {}
 
        for _, idKey in ipairs(ids) do
     return data
            local entry = resolve_entry(data, idKey)
end
            if type(entry) == "table" then
 
                local v = get_by_path(entry, keyPath)
local function parseListArg(str)
                if type(v) == "table" and v[target] ~= nil then
    local res = {}
                    matches[#matches + 1] = idKey
    if not str or str == "" then return res end
                end
    for item in string.gmatch(str, "[^,]+") do
        local s = normalizeFilterKey(item)
        if s ~= "" then
            local a, b = s:match("^([^_]+)_(.+)$")
            if a and b then
                res[a] = res[a] or {}
                res[a][b] = true
             end
             end
         end
         end
     else
     end
        local target = tostring(searchValue)
    return res
        matches = {}
end
        for _, idKey in ipairs(ids) do
 
            local entry = resolve_entry(data, idKey)
local function build_key_filter(args)
            if type(entry) == "table" then
    local filter = {}
                local v = get_by_path(entry, keyPath)
    filter.blacklist = parseListArg(args.blacklist or "")
                if v ~= nil and contains_target(v, target) then
    filter.whitelist = parseListArg(args.whitelist or "")
                    matches[#matches + 1] = idKey
    filter.hasWhitelist = false
                end
    for _, sw in ipairs(switchModeOrder) do
             end
        if filter.whitelist[sw] and next(filter.whitelist[sw]) ~= nil then
            filter.hasWhitelist = true
             break
         end
         end
     end
     end
    if not filter.hasWhitelist and filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil then
        filter.hasWhitelist = true
    end
    return filter
end
local function build_render_options(filter)
    return {
        noHeaders = false,
        cardFilter = {
            hasWhitelist = filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil or false,
            cardWhitelist = filter.whitelist.card or {},
            blacklist = filter.blacklist.cardContent or {},
            whitelist = filter.whitelist.cardContent or {}
        }
    }
end


     if #matches == 0 then
local function should_include_key(filter, sw, key)
         return ""
     if filter.hasWhitelist then
        if filter.whitelist[sw] and filter.whitelist[sw][key] then
            return true
        end
        if sw == "card" and filter.whitelist.cardContent and filter.whitelist.cardContent[key] then
            return true
        end
         return false
     end
     end
    return not (filter.blacklist[sw] and filter.blacklist[sw][key])
end


     local out = {}
local function parse_csv_set(str)
     for _, idKey in ipairs(matches) do
     local res = {}
         local tpl = p.getTpl({ args = { idKey, pagePath, tplPath }, data = data })
     each_csv_value(str, function(name)
         if tpl ~= "" then
         res[name] = true
            out[#out + 1] = tpl
    end)
    return res
end
 
local function apply_entity_set_filters(foundSet, whitelistSet, blacklistSet)
    local hasWhitelist = next(whitelistSet or {}) ~= nil
 
    if hasWhitelist then
         for name in pairs(foundSet) do
            if not whitelistSet[name] then
                foundSet[name] = nil
            end
         end
         end
     end
     end


     if #out == 0 then
     for name in pairs(blacklistSet or {}) do
         return ""
         foundSet[name] = nil
     end
     end
    local result = table.concat(out, " ")
    return preprocess_or_return(frame, result)
end
end


function p.getTpl(frame)
local function collect_entity_sets(id, prototypeStoreDefs, componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
     local args = frame.args or {}
     local foundComponents, foundPrototypes = {}, {}
    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
    local compList = load_entity_components(id)
         return ""
     if type(compList) == "table" then
        for _, v in ipairs(compList) do
            if type(v) == "string" and v ~= "" then
                foundComponents[v] = true
            end
         end
     end
     end


     local moduleName = get_module_name(pagePath)
     local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
    local data = frame.data
    if type(protoStore) == "table" then
    if not data then
         for protoName in pairs(protoStore) do
         data = load_cached_data(moduleName)
            if type(protoName) == "string" and protoName ~= "" then
    end
                foundPrototypes[protoName] = true
    if not data then
            end
         return ""
         end
     end
     end


     local entry = resolve_entry(data, id)
     apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
    local extra = flatten_entry(entry)
     apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
     local tplStr = "{{" .. tostring(tplPath) .. "|id=" .. tostring(id)
    if extra ~= "" then
        tplStr = tplStr .. "|" .. extra
    end
    tplStr = tplStr .. "}}"


     return preprocess_or_return(frame, tplStr)
     return foundComponents, foundPrototypes
end
end


function p.getTplGenerator(frame)
local function resolve_priority(parsed)
     local args = frame.args or {}
     local basePriority = 1
     local searchId = args[1] or ""
     if type(parsed) == "table" and parsed.priority ~= nil then
    local kind = (args[2] or ""):lower()
        if type(parsed.priority) == "number" then
    local generatorId = args[3] or ""
            basePriority = parsed.priority
    local tplPath = mw.text.unstripNoWiki(args[4] or "")
        else
            local pnum = tonumber(parsed.priority)
            if pnum then
                basePriority = pnum
            end
        end
    end
    return basePriority
end


     if searchId == "" or generatorId == "" or tplPath == "" then
local function get_selective_extra(id, dataPage, paramNames)
     if not dp or type(dp.flattenFieldSelectiveDirect) ~= "function" then
         return ""
         return ""
     end
     end
     if kind ~= "prototype" and kind ~= "component" then
     if type(paramNames) ~= "table" or #paramNames == 0 then
         return ""
         return ""
     end
     end


     local dir = (kind == "prototype") and "prototype/" or "component/"
     return dp.flattenFieldSelectiveDirect(id, dataPage, paramNames) or ""
    local pagePath = dir .. generatorId .. ".json"
end


    local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
local function add_card_tag_value(tags, seen, value)
     local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
     value = trim(value or "")
     if not ok or type(ids) ~= "table" or #ids == 0 then
     if value == "" or seen[value] then
         return ""
         return
     end
     end
    seen[value] = true
    tags[#tags + 1] = value
end


     local moduleName = get_module_name(pagePath)
local function merge_card_tag_text(...)
     local data = load_cached_data(moduleName)
     local tags = {}
    if not data then
     local seen = {}
         return ""
 
    for i = 1, select("#", ...) do
        each_csv_value(select(i, ...), function(value)
            add_card_tag_value(tags, seen, value)
         end)
     end
     end


     local out = {}
     return table.concat(tags, ", ")
     for _, id in ipairs(ids) do
end
         local tpl = p.getTpl({ args = { id, pagePath, tplPath }, data = data })
 
         if tpl ~= "" then
local function add_entries_from_meta(state, parsed, ctx, filter, isPreview)
             out[#out + 1] = tpl
     for _, sw in ipairs(switchModeOrder) do
         local mode = switchModeRegistry[sw] or {}
        local keys
         if type(mode.get_keys) == "function" then
             keys = mode.get_keys(parsed)
        else
            keys = (type(parsed) == "table" and parsed[sw]) or {}
         end
         end
    end


    local result = table.concat(out, " ")
        if type(keys) == "table" then
    return preprocess_or_return(frame, result)
            for _, key in ipairs(keys) do
end
                if (not filter) or should_include_key(filter, sw, key) then
                    local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry
                    if type(buildFn) == "function" then
                        local entry = buildFn(ctx, key)
                        if entry then
                            add_switch_entry(state, sw, key, entry)
                        end
                    end
                end
            end
        end


function p.flattenParams(entry)
        if (not isPreview) and type(mode.add_entity_extras) == "function" then
     return flatten_parts(entry)
            mode.add_entity_extras(state, parsed, ctx)
        end
     end
end
end


function p.getGenerator(frame)
local function extract_whitelist_search_strings(keyFilter)
     local args = frame.args or {}
     if not keyFilter or not keyFilter.hasWhitelist then
    local searchId = args[1] or ""
        return nil
    local kind = (args[2] or ""):lower()
     end
     local generatorId = args[3] or ""


     if searchId == "" or generatorId == "" then
     local strings = {}
         return ""
    for sw, keys in pairs(keyFilter.whitelist) do
        if type(keys) == "table" then
            for key in pairs(keys) do
                strings[#strings + 1] = key
            end
         end
     end
     end
     if kind ~= "prototype" and kind ~= "component" then
 
         return ""
     if #strings == 0 then
         return nil
     end
     end


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


     local okOut, outJson = pcall(mw.text.jsonEncode, ids)
     for _, s in ipairs(searchStrings) do
    if okOut and outJson then
        if string.find(content, s, 1, true) then
         return outJson
            return true
         end
     end
     end


     return ""
     return false
end
end


function p.hasComp(frame)
local function each_entity_data(frame, id, onEntity, onMissing, keyFilter)
     local args = frame.args or {}
     local componentWhitelist = frame.args.componentWhitelist or frame.args.componentwhitelist or ""
     local entityId = args[1] or ""
    local componentBlacklist = frame.args.componentBlacklist or frame.args.componentblacklist or ""
     local compName = args[2] or ""
     local prototypeWhitelist = frame.args.prototypeWhitelist or frame.args.prototypewhitelist or ""
     local prototypeBlacklist = frame.args.prototypeBlacklist or frame.args.prototypeblacklist or ""


     if entityId == "" or compName == "" then
     local prototypeStoreDefs = load_module_data("prototype_store.json")
         return "false"
    if not prototypeStoreDefs then
         return false
     end
     end


     local moduleName = get_module_name("component.json")
     local foundComponents, foundPrototypes = collect_entity_sets(id, prototypeStoreDefs,
    local data = load_cached_data(moduleName)
        componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
    if not data then
 
         return "false"
    local compWhitelistSet = parse_csv_set(componentWhitelist)
    local compBlacklistSet = parse_csv_set(componentBlacklist)
    local protoWhitelistSet = parse_csv_set(prototypeWhitelist)
    local protoBlacklistSet = parse_csv_set(prototypeBlacklist)
 
    local compHasWhitelist = next(compWhitelistSet) ~= nil
    local protoHasWhitelist = next(protoWhitelistSet) ~= nil
    local anyEntityWhitelist = compHasWhitelist or protoHasWhitelist
 
    local whitelistSearchStrings = extract_whitelist_search_strings(keyFilter)
 
    local function processEntity(kind, name, isStore)
        local pathName = lcfirst(name)
        local tplPath = kind .. "/" .. pathName
        if isStore then
            tplPath = tplPath .. "/store"
        end
 
        local content = load_template_content(tplPath)
        if not content then
            if onMissing then
                onMissing(kind, name, isStore, tplPath)
            end
            return
         end
 
        if not content_matches_whitelist(content, whitelistSearchStrings) then
            return
        end
 
        local parsed = getTemplateMeta(frame, tplPath)
        if type(parsed) ~= "table" then
            parsed = {}
        end
 
        local extra = ""
        local paramNames = get_template_params(tplPath, content)
        if dp then
            local dataPage = tplPath .. ".json"
            extra = get_selective_extra(id, dataPage, paramNames)
        end
 
        onEntity(parsed, {
            tplPath = tplPath,
            id = id,
            extra = extra,
            source = make_source(kind, name, pathName, tplPath),
            priority = resolve_priority(parsed)
        })
     end
     end


     if type(data) ~= "table" then
     for compName in pairs(foundComponents) do
        return "false"
        if not anyEntityWhitelist or compHasWhitelist then
            processEntity("component", compName, false)
        end
    end
    for protoName in pairs(foundPrototypes) do
        if not anyEntityWhitelist or protoHasWhitelist then
            processEntity("prototype", protoName, false)
        end
     end
     end


     local entry = data[entityId]
     local componentStoreDefs = load_module_data("component_store.json")
     if type(entry) ~= "table" then
     if type(componentStoreDefs) == "table" and (not anyEntityWhitelist or compHasWhitelist) then
         return "false"
         local compStore = componentStoreDefs[id]
        if type(compStore) == "table" then
            for compName in pairs(compStore) do
                local allowed = true
                if compBlacklistSet[compName] then
                    allowed = false
                elseif compHasWhitelist and not compWhitelistSet[compName] then
                    allowed = false
                end
                if allowed then
                    processEntity("component", compName, true)
                end
            end
        end
     end
     end


     local target = tostring(compName)
     if type(prototypeStoreDefs) == "table" and (not anyEntityWhitelist or protoHasWhitelist) then
    for _, v in ipairs(entry) do
        local protoStore = prototypeStoreDefs[id]
        if tostring(v) == target then
        if type(protoStore) == "table" then
            return "true"
            for protoName in pairs(protoStore) do
                local allowed = true
                if protoBlacklistSet[protoName] then
                    allowed = false
                elseif protoHasWhitelist and not protoWhitelistSet[protoName] then
                    allowed = false
                end
                if allowed then
                    processEntity("prototype", protoName, true)
                end
            end
         end
         end
     end
     end


     return "false"
     return true
end
end


function p.GeneratorId(frame)
local function collect_card_tag_text(frame, args, id)
     local args = frame.args or {}
     local filter = build_key_filter(args)
     local pagePath = args[1] or ""
    local cardFilter = build_render_options(filter).cardFilter
    local replace = mw.text.unstripNoWiki(args.replace or "")
    local entries = {}
    local pattern = mw.text.unstripNoWiki(args.pattern or "(.*)")
 
     local ok = each_entity_data(frame, id, function(parsed, ctx)
        local keys = parsed.card or {}
 
        if type(keys) == "table" then
            for _, key in ipairs(keys) do
                local compositeKey = (key:find("_", 1, true)) and key or ("Сущность_" .. key)
                local isWhitelisted = matches_card_list(cardFilter.whitelist, key, compositeKey)
                local isBlacklisted = matches_card_list(cardFilter.blacklist, key, compositeKey)
                local allowCardEntry = not isWhitelisted and not isBlacklisted and
                    ((not cardFilter.hasWhitelist) or matches_card_list(cardFilter.cardWhitelist, key, compositeKey))
 
                if allowCardEntry then
                    entries[#entries + 1] = {
                        tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
                        priority = ctx.priority,
                        idx = #entries + 1
                    }
                end
            end
        end


    if pagePath == "" then
        if parsed.cardTag and parsed.cardTag ~= "" then
         return ""
            entries[#entries + 1] = {
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                priority = ctx.priority,
                idx = #entries + 1
            }
        end
    end)
 
    if not ok then
         return trim(args.tag or "")
     end
     end


     local moduleName = get_module_name(pagePath)
    sort_entries_by_priority(entries)
     local data = load_cached_data(moduleName)
 
    if not data then
     local tags = {}
         return ""
     local seen = {}
    for _, entry in ipairs(entries) do
         local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
        add_card_tag_value(tags, seen, tagText)
     end
     end


     local idsTable = data.id
     return merge_card_tag_text(table.concat(tags, ", "), args.tag)
     if type(idsTable) ~= "table" then
end
         return ""
 
p.mergeCardTagText = merge_card_tag_text
p.collectCardTagText = collect_card_tag_text
 
local function build_missing_template_error(kind, name, isStore, tplPath)
     local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "")
    local classType = baseType
    if isStore then
         classType = classType .. "Store"
     end
     end
    local className = name .. baseType
    local tplLabel = "Template:" .. tplPath
    return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}"
end


     local ids = {}
local function renderBlocks(frame, state, renderOptions, entityId, showSource)
     for k in pairs(idsTable) do
     local outLocal = {}
         ids[#ids + 1] = k
    local noHeaders = renderOptions and renderOptions.noHeaders
    local cardFilter = renderOptions and renderOptions.cardFilter
     for _, sw in ipairs(switchModeOrder) do
         local mode = switchModeRegistry[sw] or {}
        if mode.full then
            local outStr = ""
            if type(mode.render_full) == "function" then
                outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
                    entityId, noHeaders, showSource, cardFilter)
            end
            if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
        else
            for _, key in ipairs(state.keyOrder[sw] or {}) do
                local entries = state.keyToTemplates[sw][key] or {}
                if type(mode.render_key) == "function" then
                    local outStr = mode.render_key(frame, key, entries, noHeaders, showSource)
                    if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
                end
            end
        end
     end
     end
    return outLocal
end


     table.sort(ids)
function p.get(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local id = args[1] or ""
    if id == "" then return "" end
 
     local showSource = trim(args.showSource or "") == ""
 
    local filter = build_key_filter(args)
    local renderOptions = build_render_options(filter)


     if replace ~= "" then
     local state = new_switch_state()
        local out = {}
    local errors = {}
        for _, id in ipairs(ids) do
    local ok = each_entity_data(frame, id, function(parsed, ctx)
            local text = apply_pattern(id, pattern, replace)
        add_entries_from_meta(state, parsed, ctx, filter, false)
            if text ~= "" then
    end, function(kind, name, isStore, tplPath)
                out[#out + 1] = text
        if not filter.hasWhitelist then
            end
            errors[#errors + 1] = build_missing_template_error(kind, name, isStore, tplPath)
         end
         end
        if #out == 0 then
    end, filter)
            return ""
    if not ok then return "" end
        end
 
         return preprocess_or_return(frame, table.concat(out, "\n"))
    local out = {}
 
    if #errors > 0 then
         out[#out + 1] = '{{сущность/infobox|' .. table.concat(errors, "\n") .. '}}'
     end
     end


     local ok, json = pcall(mw.text.jsonEncode, ids)
    renderOptions.noHeaders = filter.hasWhitelist
     if ok and json then
 
         return json
     local blocks = renderBlocks(frame, state, renderOptions, id, showSource)
     for _, b in ipairs(blocks) do
         out[#out + 1] = b
     end
     end


     return ""
     return frame:preprocess(table.concat(out, "\n"))
end
end


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


     if pagePath == "" or tplPath == "" then
     local showSource = trim(args.nosource or "") == ""
    local previewFilter = build_key_filter(args)
    local renderOptions = build_render_options(previewFilter)
 
    local content = load_template_content(tplPath)
    if not content then
         return ""
         return ""
     end
     end


     local moduleName = get_module_name(pagePath)
     local parsed = getTemplateMeta(frame, tplPath) or {}
     local data = load_cached_data(moduleName)
     if type(parsed) ~= "table" then
    if not data then
         parsed = {}
         return ""
     end
     end


     local idsTable = data.id
     local state = new_switch_state()
     if type(idsTable) ~= "table" then
     add_entries_from_meta(state, parsed, {
         return ""
        tplPath = tplPath,
     end
        id = "",
        extra = "",
         source = make_source("", tplPath, tplPath, tplPath),
        priority = 1
    }, nil, true)
 
    local hasWhitelist = previewFilter.hasWhitelist
     renderOptions.noHeaders = hasWhitelist


     local out = {}
     local out = {}
 
     local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
     for idKey in pairs(idsTable) do
    for _, b in ipairs(blocks) do
        local tpl = p.getTpl({ args = { idKey, pagePath, tplPath }, data = data })
         out[#out + 1] = b
         if tpl ~= "" then
            out[#out + 1] = tpl
        end
     end
     end


     table.sort(out)
     return frame:preprocess(table.concat(out, "\n"))
 
    local result = table.concat(out, " ")
    return preprocess_or_return(frame, result)
end
end


return p
return p

Текущая версия от 16:16, 21 июня 2026

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

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

local dpOk, dpModule = pcall(require, "Module:GetField")
local dp = dpOk and dpModule or nil

local switchModeRegistry = {}
local switchModeOrder = {}

local function trim(s)
    if not s then return s end
    return (s:gsub("^%s*(.-)%s*$", "%1"))
end

local function each_csv_value(str, fn)
    if not str or str == "" then
        return
    end
    for item in string.gmatch(str, "[^,]+") do
        local value = trim(item)
        if value ~= "" then
            fn(value)
        end
    end
end

local function load_module_data(page)
    local moduleName = JsonPaths.get(page)
    local ok, data = pcall(mw.loadData, moduleName)
    if not ok then
        return nil
    end
    return data
end

local function load_template_content(path)
    local title = mw.title.new("Template:" .. path)
    if not title then
        return nil
    end
    local ok, content = pcall(title.getContent, title)
    if not ok then
        return nil
    end
    return content
end

local function lcfirst(s)
    if not s or s == "" then return s end
    return string.lower(s:sub(1, 1)) .. (s:sub(2) or "")
end

local function load_entity_data(entityId)
    if not entityId or entityId == "" then
        return nil
    end

    local page = "prototype/Entity/" .. entityId .. ".json"
    local moduleName = JsonPaths.get(page)
    local ok, data = pcall(mw.loadData, moduleName)
    if not ok or type(data) ~= "table" then
        return nil
    end
    return data
end

local function normalize_component_name(name)
    if type(name) ~= "string" then
        return nil
    end

    name = trim(name)
    if name == "" then
        return nil
    end

    if name:sub(1, 5) == "type:" then
        return name:sub(6)
    end
    if name:sub(1, 6) == "!type:" then
        return name:sub(7)
    end

    return name
end

local function collect_entity_components(entity)
    local out = {}
    local seen = {}

    if type(entity) ~= "table" then
        return out
    end

    local comps = entity.components
    if type(comps) ~= "table" then
        return out
    end

    if #comps > 0 then
        for _, v in ipairs(comps) do
            local name = normalize_component_name(v)
            if name and not seen[name] then
                seen[name] = true
                out[#out + 1] = name
            end
        end
    else
        for k in pairs(comps) do
            local name = normalize_component_name(k)
            if name and not seen[name] then
                seen[name] = true
                out[#out + 1] = name
            end
        end
    end

    table.sort(out)
    return out
end

local function load_entity_components_from_dp(entityId)
    if not dp then
        return nil
    end

    local getter = dp.getEntityComponents or dp.collectEntityComponents or dp.getComp
    if type(getter) ~= "function" then
        return nil
    end

    local ok, result = pcall(getter, { args = { entityId } })
    if not ok or result == nil or result == "" then
        return nil
    end

    if type(result) == "table" then
        return result
    end

    if type(result) == "string" then
        local okJson, decoded = pcall(mw.text.jsonDecode, result)
        if okJson and type(decoded) == "table" then
            return decoded
        end
    end

    return nil
end

local function load_entity_components(entityId)
    local viaDp = load_entity_components_from_dp(entityId)
    if type(viaDp) == "table" and next(viaDp) ~= nil then
        local out = {}
        local seen = {}
        if #viaDp > 0 then
            for _, v in ipairs(viaDp) do
                local name = normalize_component_name(v)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        else
            for k in pairs(viaDp) do
                local name = normalize_component_name(k)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        end
        table.sort(out)
        return out
    end

    local entity = load_entity_data(entityId)
    return collect_entity_components(entity)
end

p.loadEntityData = load_entity_data
p.collectEntityComponents = collect_entity_components
p.loadEntityComponents = load_entity_components
p.entityHasComponent = function(entityOrId, compName)
    if not compName or compName == "" then
        return false
    end

    if type(entityOrId) == "string" then
        local entity = load_entity_data(entityOrId)
        if not entity then
            return false
        end
        local comps = collect_entity_components(entity)
        for _, v in ipairs(comps) do
            if v == compName then
                return true
            end
        end
        return false
    end

    local comps = collect_entity_components(entityOrId)
    for _, v in ipairs(comps) do
        if v == compName then
            return true
        end
    end
    return false
end

local function makeTplCall(tplPath, sw, key, id, extra)
    local tplStr = "{{" .. tplPath .. "|" .. sw .. "|" .. key
    tplStr = tplStr .. "|id=" .. tostring(id)
    if extra and extra ~= "" then tplStr = tplStr .. "|" .. extra end
    tplStr = tplStr .. "}}"
    return tplStr
end

local function add_template_param(params, seen, raw)
    local param = trim(raw or "")
    if param == "" or param == "id" or param:match("^%d+$") then
        return
    end
    if not seen[param] then
        seen[param] = true
        params[#params + 1] = param
    end
end

local function collect_template_params(content)
    local params = {}
    local seen = {}

    if not content or content == "" then
        return params
    end

    for param in content:gmatch("{{{%s*([^|}]+)%s*|") do
        add_template_param(params, seen, param)
    end
    for param in content:gmatch("{{{%s*([^|}]+)%s*}}") do
        add_template_param(params, seen, param)
    end

    return params
end

local function get_template_params(tplPath, content)
    return collect_template_params(content)
end

local function sort_entries_by_priority(entries)
    table.sort(entries, function(a, b)
        if a.priority == b.priority then return a.idx < b.idx end
        return a.priority > b.priority
    end)
end

local function make_source(kind, name, pathName, tplPath)
    return { kind = kind, name = name, pathName = pathName, tplPath = tplPath }
end

local function register_switch_mode(name, cfg)
    switchModeRegistry[name] = cfg or {}
    switchModeOrder[#switchModeOrder + 1] = name
end

local function new_switch_state()
    local state = { keyOrder = {}, keyToTemplates = {}, keySources = {} }
    for _, sw in ipairs(switchModeOrder) do
        state.keyOrder[sw] = {}
        state.keyToTemplates[sw] = {}
        state.keySources[sw] = {}
    end
    return state
end

local function ensure_switch_key(state, sw, key)
    local byKey = state.keyToTemplates[sw]
    if not byKey[key] then
        byKey[key] = {}
        state.keyOrder[sw][#state.keyOrder[sw] + 1] = key
    end
    return byKey[key]
end

local function add_switch_entry(state, sw, key, entry)
    local bucket = ensure_switch_key(state, sw, key)
    entry.idx = #bucket + 1
    bucket[#bucket + 1] = entry
end

local function collect_tpl_calls(entries)
    local tplCalls = {}
    local sources = {}
    if #entries > 0 then
        sort_entries_by_priority(entries)
        for _, e in ipairs(entries) do
            tplCalls[#tplCalls + 1] = e.tpl
            sources[#sources + 1] = e.source
        end
    end
    return tplCalls, sources
end

local function makeSourceLink(s)
    local className =
        (s.name:sub(1, 1):upper() .. s.name:sub(2)) ..
        (s.kind and (s.kind:sub(1, 1):upper() .. s.kind:sub(2)) or "")

    local tplLabel = "Template:" .. s.tplPath
    return "[[" .. tplLabel .. "|" .. className .. "]]"
end

local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
    local parts = {}
    if tplCalls and #tplCalls > 0 then
        for i, tpl in ipairs(tplCalls) do
            local add = true
            if frame then
                local expanded = frame:preprocess(tpl)
                add = expanded and trim(expanded) ~= ""
            end
            if add then
                local src = sources and sources[i]
                local line = '<div>' .. tpl .. '</div>'
                if showSource and src then
                    line = line .. '<div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
                end
                parts[#parts + 1] = '<div class="ts-Сущность">' .. line .. '</div>'
            end
        end
        if #parts == 0 then
            return ""
        end
        if includeHeader then
            table.insert(parts, 1, "<h2>" .. mw.text.encode(key) .. "</h2>")
        end
    end
    return table.concat(parts, "\n")
end

local function split_title_key(key)
    local main, sub = (key or ""):match("^([^_]+)_(.+)$")
    if main and sub then
        return main, sub
    end
    return key, nil
end

local function renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
    local groups = {}
    local groupOrder = {}

    for _, key in ipairs(keyOrder or {}) do
        local mainTitle, subTitle = split_title_key(key)
        if mainTitle and mainTitle ~= "" then
            local group = groups[mainTitle]
            if not group then
                group = { blocks = {} }
                groups[mainTitle] = group
                groupOrder[#groupOrder + 1] = mainTitle
            end

            group.blocks[#group.blocks + 1] = {
                subTitle = subTitle,
                entries = keyToTemplates[key] or {}
            }
        end
    end

    local out = {}
    for _, mainTitle in ipairs(groupOrder) do
        local group = groups[mainTitle]
        local parts = {}

        if not noHeaders then
            parts[#parts + 1] = "<h2>" .. mw.text.encode(mainTitle) .. "</h2>"
        end

        for _, block in ipairs(group.blocks or {}) do
            local tplCalls, sources = collect_tpl_calls(block.entries or {})
            local blockText = renderTitleBlock(block.subTitle or mainTitle, tplCalls, sources, false, frame, showSource)
            if blockText ~= "" then
                if block.subTitle and not noHeaders then
                    parts[#parts + 1] = "<h3>" .. mw.text.encode(block.subTitle) .. "</h3>"
                end
                parts[#parts + 1] = blockText
            end
        end

        if #parts > 0 then
            out[#out + 1] = table.concat(parts, "\n")
        end
    end

    return table.concat(out, "\n")
end

local function normalizeFilterKey(s)
    s = trim(s or "")
    s = s:gsub("%s*_%s*", "_")
    return s
end

local function matches_card_list(list, callKey, compositeKey)
    if not list then
        return false
    end
    callKey = normalizeFilterKey(callKey)
    compositeKey = normalizeFilterKey(compositeKey)
    return list[callKey] or list[compositeKey] or false
end

local function buildCardCall(merged, entityId)
    local parts = {}

    if entityId and entityId ~= "" then
        parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
    end

    if merged.tags and #merged.tags > 0 then
        table.sort(merged.tags)
        parts[#parts + 1] = "тип=" .. table.concat(merged.tags, ", ")
    end

    if merged.sections and #merged.sections > 0 then
        table.sort(merged.sections, function(a, b)
            if a == "Сущность" then return true end
            if b == "Сущность" then return false end
            return a < b
        end)
        parts[#parts + 1] = "sections=" .. table.concat(merged.sections, ", ")
        for _, section in ipairs(merged.sections) do
            local labels = merged.labelLists[section]
            if labels and #labels > 0 then
                local enc = {}
                for i = 1, #labels do
                    enc[i] = mw.text.encode(labels[i])
                end
                parts[#parts + 1] = mw.text.encode(section) .. "=" .. table.concat(enc, ", ")
            end
        end
    end

    for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
        if displayLabel and displayLabel ~= "" then
            parts[#parts + 1] = mw.text.encode(compositeKey .. "_label") .. "=" .. displayLabel
        end
    end
    for compositeKey, content in pairs(merged.contentByKey or {}) do
        if content and content ~= "" then
            parts[#parts + 1] = mw.text.encode(compositeKey) .. "=" .. content
        end
    end

    if #parts == 0 then
        return ""
    end

    return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
end

local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
    local merged = {
        sections = {},
        sectionsMap = {},
        labelLists = {},
        labelSets = {},
        labelOverrides = {},
        contentByKey = {},
        tags = {},
        tagSet = {}
    }
    local rawContentParts = {}
    for _, callKey in ipairs(keyOrder or {}) do
        local entries = keyToTemplates[callKey] or {}
        if #entries > 0 then
            sort_entries_by_priority(entries)
            for _, e in ipairs(entries) do
                local displayLabel = trim(frame:preprocess(e.tplLabel or "") or "")
                local content = trim(frame:preprocess(e.tplContent or "") or "")
                local tagText = ""
                if e.tplTag then
                    tagText = trim(frame:preprocess(e.tplTag or "") or "")
                end
                local compositeKey = (callKey:find("_", 1, true)) and callKey or ("Сущность_" .. callKey)
                local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность"

                local isWhitelisted = cardFilter and matches_card_list(cardFilter.whitelist, callKey, compositeKey) or
                    false
                local isBlacklisted = cardFilter and matches_card_list(cardFilter.blacklist, callKey, compositeKey) or
                    false
                if isWhitelisted and content ~= "" then
                    rawContentParts[#rawContentParts + 1] = content
                end

                local allowCardEntry = not isWhitelisted and not isBlacklisted and
                    ((not cardFilter) or (not cardFilter.hasWhitelist) or
                        matches_card_list(cardFilter.cardWhitelist, callKey, compositeKey))

                if allowCardEntry and (displayLabel ~= "" or content ~= "") then
                    if not merged.sectionsMap[section] then
                        merged.sectionsMap[section] = true
                        merged.sections[#merged.sections + 1] = section
                    end
                    if displayLabel ~= "" and (not merged.labelOverrides[compositeKey] or merged.labelOverrides[compositeKey] == "") then
                        merged.labelOverrides[compositeKey] = displayLabel
                    end
                    if content ~= "" then
                        local prev = merged.contentByKey[compositeKey]
                        if prev and prev ~= "" then
                            merged.contentByKey[compositeKey] = prev .. "\n" .. content
                        else
                            merged.contentByKey[compositeKey] = content
                        end
                    end

                    merged.labelSets[section] = merged.labelSets[section] or {}
                    if not merged.labelSets[section][compositeKey] then
                        merged.labelSets[section][compositeKey] = true
                        local cur = merged.labelLists[section] or {}
                        cur[#cur + 1] = compositeKey
                        merged.labelLists[section] = cur
                    end
                end

                if allowCardEntry and tagText ~= "" then
                    if not merged.tagSet[tagText] then
                        merged.tagSet[tagText] = true
                        merged.tags[#merged.tags + 1] = tagText
                    end
                end
            end
        end
    end

    each_csv_value(frame.args.cardTag or "", function(extraTag)
        if not merged.tagSet[extraTag] then
            merged.tagSet[extraTag] = true
            merged.tags[#merged.tags + 1] = extraTag
        end
    end)

    local out = {}
    if #rawContentParts > 0 then
        out[#out + 1] = table.concat(rawContentParts, "\n")
    end

    local cardCall = buildCardCall(merged, entityId)

    if noHeaders then
        local hasLabel = false
        for _, v in pairs(merged.labelOverrides or {}) do
            if v and v ~= "" then
                hasLabel = true
                break
            end
        end
        if not hasLabel then
            for _, lst in pairs(merged.labelLists or {}) do
                if #lst > 0 then
                    hasLabel = true
                    break
                end
            end
        end

        local hasContent = false
        for _, v in pairs(merged.contentByKey or {}) do
            if v and v ~= "" then
                hasContent = true
                break
            end
        end

        if not hasLabel and not hasContent then
            return table.concat(out, "\n")
        end
    end

    if cardCall ~= "" then
        out[#out + 1] = cardCall
    end

    return table.concat(out, "\n")
end

register_switch_mode("card", {
    full = true,
    build_entry = function(ctx, key)
        return {
            tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ctx.id, ctx.extra),
            tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ctx.id, ctx.extra),
            tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    build_preview_entry = function(ctx, key)
        return {
            tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ""),
            tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ""),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    add_entity_extras = function(state, parsed, ctx)
        if type(parsed) == "table" and parsed.cardTag and parsed.cardTag ~= "" then
            add_switch_entry(state, "card", "cardTag", {
                tplLabel = "",
                tplContent = "",
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                source = ctx.source,
                priority = ctx.priority
            })
        end
    end,
    render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource, cardFilter)
        return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
    end
})

register_switch_mode("title", {
    full = true,
    build_entry = function(ctx, key)
        return {
            tpl = makeTplCall(ctx.tplPath, "title", key, ctx.id, ctx.extra),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    build_preview_entry = function(ctx, key)
        return {
            tpl = makeTplCall(ctx.tplPath, "title", key, ""),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource)
        return renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
    end
})

local function getTemplateMeta(frame, tplPath)
    local expanded = frame:expandTemplate {
        title = tplPath,
        args = { "json" }
    }

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

    if data.card == nil then
        local cardKeys = {}
        local seen = {}
        for base, labels in pairs(data) do
            if type(base) == "string" and base ~= "card" and base:sub(1, 4) == "card" and type(labels) == "table" then
                for _, lab in ipairs(labels) do
                    if not seen[lab] then
                        seen[lab] = true
                        cardKeys[#cardKeys + 1] = lab
                    end
                end
            end
        end
        data.card = cardKeys
    end

    return data
end

local function parseListArg(str)
    local res = {}
    if not str or str == "" then return res end
    for item in string.gmatch(str, "[^,]+") do
        local s = normalizeFilterKey(item)
        if s ~= "" then
            local a, b = s:match("^([^_]+)_(.+)$")
            if a and b then
                res[a] = res[a] or {}
                res[a][b] = true
            end
        end
    end
    return res
end

local function build_key_filter(args)
    local filter = {}
    filter.blacklist = parseListArg(args.blacklist or "")
    filter.whitelist = parseListArg(args.whitelist or "")
    filter.hasWhitelist = false
    for _, sw in ipairs(switchModeOrder) do
        if filter.whitelist[sw] and next(filter.whitelist[sw]) ~= nil then
            filter.hasWhitelist = true
            break
        end
    end
    if not filter.hasWhitelist and filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil then
        filter.hasWhitelist = true
    end
    return filter
end

local function build_render_options(filter)
    return {
        noHeaders = false,
        cardFilter = {
            hasWhitelist = filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil or false,
            cardWhitelist = filter.whitelist.card or {},
            blacklist = filter.blacklist.cardContent or {},
            whitelist = filter.whitelist.cardContent or {}
        }
    }
end

local function should_include_key(filter, sw, key)
    if filter.hasWhitelist then
        if filter.whitelist[sw] and filter.whitelist[sw][key] then
            return true
        end
        if sw == "card" and filter.whitelist.cardContent and filter.whitelist.cardContent[key] then
            return true
        end
        return false
    end
    return not (filter.blacklist[sw] and filter.blacklist[sw][key])
end

local function parse_csv_set(str)
    local res = {}
    each_csv_value(str, function(name)
        res[name] = true
    end)
    return res
end

local function apply_entity_set_filters(foundSet, whitelistSet, blacklistSet)
    local hasWhitelist = next(whitelistSet or {}) ~= nil

    if hasWhitelist then
        for name in pairs(foundSet) do
            if not whitelistSet[name] then
                foundSet[name] = nil
            end
        end
    end

    for name in pairs(blacklistSet or {}) do
        foundSet[name] = nil
    end
end

local function collect_entity_sets(id, prototypeStoreDefs, componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
    local foundComponents, foundPrototypes = {}, {}

    local compList = load_entity_components(id)
    if type(compList) == "table" then
        for _, v in ipairs(compList) do
            if type(v) == "string" and v ~= "" then
                foundComponents[v] = true
            end
        end
    end

    local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
    if type(protoStore) == "table" then
        for protoName in pairs(protoStore) do
            if type(protoName) == "string" and protoName ~= "" then
                foundPrototypes[protoName] = true
            end
        end
    end

    apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
    apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))

    return foundComponents, foundPrototypes
end

local function resolve_priority(parsed)
    local basePriority = 1
    if type(parsed) == "table" and parsed.priority ~= nil then
        if type(parsed.priority) == "number" then
            basePriority = parsed.priority
        else
            local pnum = tonumber(parsed.priority)
            if pnum then
                basePriority = pnum
            end
        end
    end
    return basePriority
end

local function get_selective_extra(id, dataPage, paramNames)
    if not dp or type(dp.flattenFieldSelectiveDirect) ~= "function" then
        return ""
    end
    if type(paramNames) ~= "table" or #paramNames == 0 then
        return ""
    end

    return dp.flattenFieldSelectiveDirect(id, dataPage, paramNames) or ""
end

local function add_card_tag_value(tags, seen, value)
    value = trim(value or "")
    if value == "" or seen[value] then
        return
    end
    seen[value] = true
    tags[#tags + 1] = value
end

local function merge_card_tag_text(...)
    local tags = {}
    local seen = {}

    for i = 1, select("#", ...) do
        each_csv_value(select(i, ...), function(value)
            add_card_tag_value(tags, seen, value)
        end)
    end

    return table.concat(tags, ", ")
end

local function add_entries_from_meta(state, parsed, ctx, filter, isPreview)
    for _, sw in ipairs(switchModeOrder) do
        local mode = switchModeRegistry[sw] or {}
        local keys
        if type(mode.get_keys) == "function" then
            keys = mode.get_keys(parsed)
        else
            keys = (type(parsed) == "table" and parsed[sw]) or {}
        end

        if type(keys) == "table" then
            for _, key in ipairs(keys) do
                if (not filter) or should_include_key(filter, sw, key) then
                    local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry
                    if type(buildFn) == "function" then
                        local entry = buildFn(ctx, key)
                        if entry then
                            add_switch_entry(state, sw, key, entry)
                        end
                    end
                end
            end
        end

        if (not isPreview) and type(mode.add_entity_extras) == "function" then
            mode.add_entity_extras(state, parsed, ctx)
        end
    end
end

local function extract_whitelist_search_strings(keyFilter)
    if not keyFilter or not keyFilter.hasWhitelist then
        return nil
    end

    local strings = {}
    for sw, keys in pairs(keyFilter.whitelist) do
        if type(keys) == "table" then
            for key in pairs(keys) do
                strings[#strings + 1] = key
            end
        end
    end

    if #strings == 0 then
        return nil
    end

    return strings
end

local function content_matches_whitelist(content, searchStrings)
    if not searchStrings then
        return true
    end
    if not content then
        return false
    end

    for _, s in ipairs(searchStrings) do
        if string.find(content, s, 1, true) then
            return true
        end
    end

    return false
end

local function each_entity_data(frame, id, onEntity, onMissing, keyFilter)
    local componentWhitelist = frame.args.componentWhitelist or frame.args.componentwhitelist or ""
    local componentBlacklist = frame.args.componentBlacklist or frame.args.componentblacklist or ""
    local prototypeWhitelist = frame.args.prototypeWhitelist or frame.args.prototypewhitelist or ""
    local prototypeBlacklist = frame.args.prototypeBlacklist or frame.args.prototypeblacklist or ""

    local prototypeStoreDefs = load_module_data("prototype_store.json")
    if not prototypeStoreDefs then
        return false
    end

    local foundComponents, foundPrototypes = collect_entity_sets(id, prototypeStoreDefs,
        componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)

    local compWhitelistSet = parse_csv_set(componentWhitelist)
    local compBlacklistSet = parse_csv_set(componentBlacklist)
    local protoWhitelistSet = parse_csv_set(prototypeWhitelist)
    local protoBlacklistSet = parse_csv_set(prototypeBlacklist)

    local compHasWhitelist = next(compWhitelistSet) ~= nil
    local protoHasWhitelist = next(protoWhitelistSet) ~= nil
    local anyEntityWhitelist = compHasWhitelist or protoHasWhitelist

    local whitelistSearchStrings = extract_whitelist_search_strings(keyFilter)

    local function processEntity(kind, name, isStore)
        local pathName = lcfirst(name)
        local tplPath = kind .. "/" .. pathName
        if isStore then
            tplPath = tplPath .. "/store"
        end

        local content = load_template_content(tplPath)
        if not content then
            if onMissing then
                onMissing(kind, name, isStore, tplPath)
            end
            return
        end

        if not content_matches_whitelist(content, whitelistSearchStrings) then
            return
        end

        local parsed = getTemplateMeta(frame, tplPath)
        if type(parsed) ~= "table" then
            parsed = {}
        end

        local extra = ""
        local paramNames = get_template_params(tplPath, content)
        if dp then
            local dataPage = tplPath .. ".json"
            extra = get_selective_extra(id, dataPage, paramNames)
        end

        onEntity(parsed, {
            tplPath = tplPath,
            id = id,
            extra = extra,
            source = make_source(kind, name, pathName, tplPath),
            priority = resolve_priority(parsed)
        })
    end

    for compName in pairs(foundComponents) do
        if not anyEntityWhitelist or compHasWhitelist then
            processEntity("component", compName, false)
        end
    end
    for protoName in pairs(foundPrototypes) do
        if not anyEntityWhitelist or protoHasWhitelist then
            processEntity("prototype", protoName, false)
        end
    end

    local componentStoreDefs = load_module_data("component_store.json")
    if type(componentStoreDefs) == "table" and (not anyEntityWhitelist or compHasWhitelist) then
        local compStore = componentStoreDefs[id]
        if type(compStore) == "table" then
            for compName in pairs(compStore) do
                local allowed = true
                if compBlacklistSet[compName] then
                    allowed = false
                elseif compHasWhitelist and not compWhitelistSet[compName] then
                    allowed = false
                end
                if allowed then
                    processEntity("component", compName, true)
                end
            end
        end
    end

    if type(prototypeStoreDefs) == "table" and (not anyEntityWhitelist or protoHasWhitelist) then
        local protoStore = prototypeStoreDefs[id]
        if type(protoStore) == "table" then
            for protoName in pairs(protoStore) do
                local allowed = true
                if protoBlacklistSet[protoName] then
                    allowed = false
                elseif protoHasWhitelist and not protoWhitelistSet[protoName] then
                    allowed = false
                end
                if allowed then
                    processEntity("prototype", protoName, true)
                end
            end
        end
    end

    return true
end

local function collect_card_tag_text(frame, args, id)
    local filter = build_key_filter(args)
    local cardFilter = build_render_options(filter).cardFilter
    local entries = {}

    local ok = each_entity_data(frame, id, function(parsed, ctx)
        local keys = parsed.card or {}

        if type(keys) == "table" then
            for _, key in ipairs(keys) do
                local compositeKey = (key:find("_", 1, true)) and key or ("Сущность_" .. key)
                local isWhitelisted = matches_card_list(cardFilter.whitelist, key, compositeKey)
                local isBlacklisted = matches_card_list(cardFilter.blacklist, key, compositeKey)
                local allowCardEntry = not isWhitelisted and not isBlacklisted and
                    ((not cardFilter.hasWhitelist) or matches_card_list(cardFilter.cardWhitelist, key, compositeKey))

                if allowCardEntry then
                    entries[#entries + 1] = {
                        tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
                        priority = ctx.priority,
                        idx = #entries + 1
                    }
                end
            end
        end

        if parsed.cardTag and parsed.cardTag ~= "" then
            entries[#entries + 1] = {
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                priority = ctx.priority,
                idx = #entries + 1
            }
        end
    end)

    if not ok then
        return trim(args.tag or "")
    end

    sort_entries_by_priority(entries)

    local tags = {}
    local seen = {}
    for _, entry in ipairs(entries) do
        local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
        add_card_tag_value(tags, seen, tagText)
    end

    return merge_card_tag_text(table.concat(tags, ", "), args.tag)
end

p.mergeCardTagText = merge_card_tag_text
p.collectCardTagText = collect_card_tag_text

local function build_missing_template_error(kind, name, isStore, tplPath)
    local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "")
    local classType = baseType
    if isStore then
        classType = classType .. "Store"
    end
    local className = name .. baseType
    local tplLabel = "Template:" .. tplPath
    return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}"
end

local function renderBlocks(frame, state, renderOptions, entityId, showSource)
    local outLocal = {}
    local noHeaders = renderOptions and renderOptions.noHeaders
    local cardFilter = renderOptions and renderOptions.cardFilter
    for _, sw in ipairs(switchModeOrder) do
        local mode = switchModeRegistry[sw] or {}
        if mode.full then
            local outStr = ""
            if type(mode.render_full) == "function" then
                outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
                    entityId, noHeaders, showSource, cardFilter)
            end
            if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
        else
            for _, key in ipairs(state.keyOrder[sw] or {}) do
                local entries = state.keyToTemplates[sw][key] or {}
                if type(mode.render_key) == "function" then
                    local outStr = mode.render_key(frame, key, entries, noHeaders, showSource)
                    if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
                end
            end
        end
    end
    return outLocal
end

function p.get(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local id = args[1] or ""
    if id == "" then return "" end

    local showSource = trim(args.showSource or "") == ""

    local filter = build_key_filter(args)
    local renderOptions = build_render_options(filter)

    local state = new_switch_state()
    local errors = {}
    local ok = each_entity_data(frame, id, function(parsed, ctx)
        add_entries_from_meta(state, parsed, ctx, filter, false)
    end, function(kind, name, isStore, tplPath)
        if not filter.hasWhitelist then
            errors[#errors + 1] = build_missing_template_error(kind, name, isStore, tplPath)
        end
    end, filter)
    if not ok then return "" end

    local out = {}

    if #errors > 0 then
        out[#out + 1] = '{{сущность/infobox|' .. table.concat(errors, "\n") .. '}}'
    end

    renderOptions.noHeaders = filter.hasWhitelist

    local blocks = renderBlocks(frame, state, renderOptions, id, showSource)
    for _, b in ipairs(blocks) do
        out[#out + 1] = b
    end

    return frame:preprocess(table.concat(out, "\n"))
end

function p.preview(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local tplPath = args[1] or ""
    if tplPath == "" then return "" end

    local showSource = trim(args.nosource or "") == ""
    local previewFilter = build_key_filter(args)
    local renderOptions = build_render_options(previewFilter)

    local content = load_template_content(tplPath)
    if not content then
        return ""
    end

    local parsed = getTemplateMeta(frame, tplPath) or {}
    if type(parsed) ~= "table" then
        parsed = {}
    end

    local state = new_switch_state()
    add_entries_from_meta(state, parsed, {
        tplPath = tplPath,
        id = "",
        extra = "",
        source = make_source("", tplPath, tplPath, tplPath),
        priority = 1
    }, nil, true)

    local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist

    local out = {}
    local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
    for _, b in ipairs(blocks) do
        out[#out + 1] = b
    end

    return frame:preprocess(table.concat(out, "\n"))
end

return p