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

Нет описания правки
Нет описания правки
 
(не показано 12 промежуточных версий этого же участника)
Строка 1: Строка 1:
local p = {}
local p = {}
local getArgs = require('Module:Arguments').getArgs
local JsonPaths = require('Module:JsonPaths')
local JsonPaths = require('Module:JsonPaths')


local function deepEqual(t1, t2)
local dpOk, dpModule = pcall(require, "Module:GetField")
    if t1 == t2 then return true end
local dp = dpOk and dpModule or nil
    if type(t1) ~= "table" or type(t2) ~= "table" then return false end


     local function isArray(t)
local switchModeRegistry = {}
         local count = 0
local switchModeOrder = {}
         for k in pairs(t) do
 
             if type(k) ~= "number" then return false end
local function trim(s)
             count = count + 1
    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 count == #t
     end
     end


     if isArray(t1) and isArray(t2) then
    return nil
         if #t1 ~= #t2 then return false end
end
         for i = 1, #t1 do
 
            if not deepEqual(t1[i], t2[i]) then
local function load_entity_components(entityId)
                 return false
    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
         end
         end
         return true
        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
     end


     for k, v in pairs(t1) do
     if type(entityOrId) == "string" then
         if t2[k] == nil or not deepEqual(v, t2[k]) then
         local entity = load_entity_data(entityOrId)
        if not entity then
             return false
             return false
         end
         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
     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


     for k, v in pairs(t2) do
local function collect_tpl_calls(entries)
        if t1[k] == nil or not deepEqual(v, t1[k]) then
    local tplCalls = {}
            return false
     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
     end
    return tplCalls, sources
end


     return true
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
end


local function findFieldInsensitive(tbl, fieldName)
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
     for key, value in pairs(tbl) do
     local parts = {}
        if type(key) == "string" and mw.ustring.lower(key) == mw.ustring.lower(fieldName) then
    if tplCalls and #tplCalls > 0 then
             return value
        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
         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
     end
     return nil
     return key, nil
end
end


local function getSpritePath(entry)
local function renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
     local iconField = findFieldInsensitive(entry, "Icon")
     local groups = {}
     local spriteField = findFieldInsensitive(entry, "Sprite")
    local groupOrder = {}
   
 
    if iconField and iconField.sprite then
    for _, key in ipairs(keyOrder or {}) do
        return iconField.sprite
        local mainTitle, subTitle = split_title_key(key)
    elseif spriteField and spriteField.sprite then
        if mainTitle and mainTitle ~= "" then
         return spriteField.sprite
            local group = groups[mainTitle]
    elseif spriteField and spriteField.layers then
            if not group then
         for _, layer in pairs(spriteField.layers) do
                group = { blocks = {} }
             if layer.sprite then
                groups[mainTitle] = group
                 return layer.sprite
                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
        end
        if #parts > 0 then
            out[#out + 1] = table.concat(parts, "\n")
         end
         end
     end
     end
     return nil
 
     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
end


local function getSpriteStates(entry)
local function buildCardCall(merged, entityId)
     local result = {}
     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


     local function addState(state, sprite)
     for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
         table.insert(result, { state = state, sprite = sprite })
         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
     end


     local spritePath = getSpritePath(entry)
     if #parts == 0 then
    local iconBlock = findFieldInsensitive(entry, "Icon")
        return ""
     local spriteBlock = findFieldInsensitive(entry, "Sprite")
     end


     if iconBlock and iconBlock.state then
     return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
         addState(iconBlock.state, iconBlock.sprite or spritePath)
end
    else
 
         if spriteBlock and spriteBlock.layers then
local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
             for _, layer in ipairs(spriteBlock.layers) do
    local merged = {
                 if layer.visible ~= false then
        sections = {},
                     local stateName = layer.state or (iconBlock and iconBlock.state) or "icon"
        sectionsMap = {},
                     local s = layer.sprite or (iconBlock and iconBlock.sprite) or spritePath
        labelLists = {},
                     addState(stateName, s)
        labelSets = {},
                     break
        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
        elseif spriteBlock and spriteBlock.state then
            addState(spriteBlock.state, spriteBlock.sprite or spritePath)
         end
         end
     end
     end


     if spriteBlock and spriteBlock.layers then
    each_csv_value(frame.args.cardTag or "", function(extraTag)
         for _, layer in ipairs(spriteBlock.layers) do
        if not merged.tagSet[extraTag] then
             local alreadyAdded = false
            merged.tagSet[extraTag] = true
             for _, r in ipairs(result) do
            merged.tags[#merged.tags + 1] = extraTag
                local layerState = layer.state or "icon"
        end
                 if r.state == layerState then
    end)
                     alreadyAdded = true
 
    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
                     break
                 end
                 end
             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" }
    }


            if not alreadyAdded and layer.visible ~= false then
    local ok, data = pcall(mw.text.jsonDecode, expanded)
                local stateName = layer.state or "icon"
    if not ok or type(data) ~= "table" then
                local s = layer.sprite or (iconBlock and iconBlock.sprite) or spritePath
        return ""
    end


                if s then
    if data.card == nil then
                    addState(stateName, s)
        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
             end
         end
         end
        data.card = cardKeys
     end
     end


     if #result == 0 and (iconBlock and (iconBlock.sprite or iconBlock.state) or spritePath) then
     return data
         local s = (iconBlock and iconBlock.sprite) or spritePath
end
         addState("icon", s)
 
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
     end
    return not (filter.blacklist[sw] and filter.blacklist[sw][key])
end


     return (#result > 0) and result or nil
local function parse_csv_set(str)
     local res = {}
    each_csv_value(str, function(name)
        res[name] = true
    end)
    return res
end
end


local function getTextureBaseUrl(project)
local function apply_entity_set_filters(foundSet, whitelistSet, blacklistSet)
     if project == "Goob" then
     local hasWhitelist = next(whitelistSet or {}) ~= nil
         return "https://github.com/space-syndicate/Goob-Station/blob/master/Resources/Textures/"
 
    if hasWhitelist then
         for name in pairs(foundSet) do
            if not whitelistSet[name] then
                foundSet[name] = nil
            end
        end
     end
     end


     return "https://github.com/space-syndicate/space-station-14/blob/master/Resources/Textures/"
     for name in pairs(blacklistSet or {}) do
        foundSet[name] = nil
    end
end
end


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


     for _, entry in pairs(data) do
     local compList = load_entity_components(id)
        local found = false
    if type(compList) == "table" then
         for _, group in pairs(spriteGroups) do
         for _, v in ipairs(compList) do
             if deepEqual(findFieldInsensitive(entry, "Sprite"), findFieldInsensitive(group[1], "Sprite")) and
             if type(v) == "string" and v ~= "" then
              deepEqual(entry.EntityStorageVisuals, group[1].EntityStorageVisuals) and
                 foundComponents[v] = true
              deepEqual(findFieldInsensitive(entry, "Icon"), findFieldInsensitive(group[1], "Icon")) then
                 table.insert(group, entry)
                found = true
                break
             end
             end
         end
         end
    end


        if not found then
    local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
             table.insert(spriteGroups, { entry })
    if type(protoStore) == "table" then
        for protoName in pairs(protoStore) do
             if type(protoName) == "string" and protoName ~= "" then
                foundPrototypes[protoName] = true
            end
         end
         end
     end
     end


     local result = {}
     apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
     for _, group in pairs(spriteGroups) do
     apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
        if #group > 1 then
 
            local idLinks = {}
    return foundComponents, foundPrototypes
            for _, entry in pairs(group) do
end
                local prefix = (project ~= "" and JsonPaths.has(entry.id)) and (project .. ":") or ""
 
                table.insert(idLinks, "[[:Файл:" .. prefix .. entry.id .. ".png]]")
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
            table.insert(result, mw.getCurrentFrame():preprocess(
                "{{Entity Sprite/Repeat|" .. table.concat(idLinks, " ") .. "|" .. group[1].id .. "}}"
            ))
         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
     end


     return table.concat(result, "\n")
     return table.concat(tags, ", ")
end
end


local function generateTemplate(entry, baseUrl, project)
local function add_entries_from_meta(state, parsed, ctx, filter, isPreview)
     local spritePath = getSpritePath(entry)
     for _, sw in ipairs(switchModeOrder) do
     if not entry.id or not spritePath then
        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
         return nil
     end
     end


     local prefix = (project ~= "" and JsonPaths.has(entry.id)) and (project .. ":") or ""
     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 states = getSpriteStates(entry)
     local componentStoreDefs = load_module_data("component_store.json")
     local stateStr = ""
     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 states then
     if type(prototypeStoreDefs) == "table" and (not anyEntityWhitelist or protoHasWhitelist) then
         local links = {}
         local protoStore = prototypeStoreDefs[id]
         for _, item in ipairs(states) do
         if type(protoStore) == "table" then
            local url = baseUrl .. item.sprite .. "/" .. item.state .. ".png"
            for protoName in pairs(protoStore) do
            table.insert(links, "[" .. url .. " " .. item.state .. "]")
                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
        stateStr = table.concat(links, ", ")
     end
     end


     return mw.getCurrentFrame():preprocess(
     return true
        "{{Entity Sprite/Image|" .. prefix .. entry.id ..
        "|" .. baseUrl .. spritePath ..
        "|" .. stateStr .. "}}"
    )
end
end


function p.main(frame)
local function collect_card_tag_text(frame, args, id)
     local action = frame.args[1]
    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 {}


    local project = JsonPaths.project()
        if type(keys) == "table" then
    local baseUrl = getTextureBaseUrl(project)
            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


    local dataPage = JsonPaths.get("prototype/sprite.json")
        if parsed.cardTag and parsed.cardTag ~= "" then
    local spriteData = mw.loadData(dataPage)
            entries[#entries + 1] = {
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                priority = ctx.priority,
                idx = #entries + 1
            }
        end
    end)


     if not spriteData or type(spriteData) ~= "table" then
     if not ok then
         return "Ошибка загрузки JSON: " .. dataPage
         return trim(args.tag or "")
     end
     end


     if action == "repeat" then
     sort_entries_by_priority(entries)
         return generateRepeatTemplate(spriteData, project)
 
     elseif action == "image" then
    local tags = {}
         local result = {}
    local seen = {}
        for _, entry in pairs(spriteData) do
    for _, entry in ipairs(entries) do
             local t = generateTemplate(entry, baseUrl, project)
        local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
             if t then
         add_card_tag_value(tags, seen, tagText)
                 table.insert(result, t)
    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
         end
         return table.concat(result, "\n")
    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
     end


     return nil
    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
end


return p
return p