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

мНет описания правки
Нет описания правки
 
(не показаны 282 промежуточные версии этого же участника)
Строка 1: Строка 1:
-- Загрузка данных
local p = {}
local itemData = mw.loadData("Модуль:IanComradeBot/prototypes/fills/Item.json/data")
local getArgs = require('Module:Arguments').getArgs
local itemSlotsData = mw.loadData("Модуль:IanComradeBot/prototypes/ItemSlots.json/data")
local JsonPaths = require('Module:JsonPaths')
local chemData = mw.loadData("Модуль:IanComradeBot/prototypes/fills/chem.json/data")
 
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 p = {}
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
local loadData = function(filePath)
    local page = mw.title.new(filePath)
    local content = page:getContent()
     return content and mw.text.jsonDecode(content) or nil
end
end


-- Функция processRolls для преобразования диапазона
local function collect_entity_components(entity)
local processRolls = function(rolls)
     local out = {}
     local result = ""
    local seen = {}
     if rolls and rolls.range then
 
         -- Если указан range
     if type(entity) ~= "table" then
        local min, max = rolls.range:match("(%d+),%s*(%d+)")
         return out
         min, max = tonumber(min), tonumber(max)
    end
        if min and max then
 
            result = result .. string.format('[%d-%d]', min + 1, max + 1)
    local comps = entity.components
        else
    if type(comps) ~= "table" then
             result = result .. 'Некорректный формат для range.'
        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
         end
    elseif rolls and rolls.value then
        -- Если указано value
        result = result .. string.format('[%d]', rolls.value)
     else
     else
         result = result .. 'Не указан параметр rolls.'
         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
     end
     return result
 
    table.sort(out)
     return out
end
end


-- Функция для поиска первого startingItem в slots
local function load_entity_components_from_dp(entityId)
local function getFirstStartingItem(data)
     if not dp then
     if not data or not data.ItemSlots or not data.ItemSlots.slots then  
        return nil
         return nil  
    end
 
    local getter = dp.getEntityComponents or dp.collectEntityComponents or dp.getComp
    if type(getter) ~= "function" then
         return nil
     end
     end
      
 
     for _, slot in pairs(data.ItemSlots.slots) do
     local ok, result = pcall(getter, { args = { entityId } })
         if slot.startingItem and slot.startingItem ~= "" then
     if not ok or result == nil or result == "" then
             return slot.startingItem
        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
     end
     end
   
 
     return nil
     return nil
end
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)
local findDataById, formatContent, getContentsOutput, processNestedSelectors, getTableOutput, getContainedOutput, getChemOutput, handleGroupSelector, handleAllSelector, handleNestedSelector
    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


-- Поиск данных по ID через индекс
local function makeTplCall(tplPath, sw, key, id, extra)
findDataById = function(itemDataIndex, id)
     local tplStr = "{{" .. tplPath .. "|" .. sw .. "|" .. key
     if not itemDataIndex then return nil end
    tplStr = tplStr .. "|id=" .. tostring(id)
for _, item in ipairs(itemDataIndex) do
    if extra and extra ~= "" then tplStr = tplStr .. "|" .. extra end
if item.id == id then
    tplStr = tplStr .. "}}"
return item
    return tplStr
end
end
end
end


-- Форматирование содержимого
local function add_template_param(params, seen, raw)
formatContent = function(content)
    local param = trim(raw or "")
     if type(content) == "table" and not content.id then
     if param == "" or param == "id" or param:match("^%d+$") then
         return "Ошибка: отсутствует id у элемента."
         return
     end
     end
    if not seen[param] then
        seen[param] = true
        params[#params + 1] = param
    end
end


    -- Если передан объект с id
local function collect_template_params(content)
    local id = content.id or content
     local params = {}
     local name = string.format('{{#invoke:Entity Lookup|getname|%s}}', id)
     local seen = {}
     local image = string.format('%s.png', id)
    local amount = (content.amount and content.amount ~= 1) and string.format(" [%d]", content.amount) or ""
    local prob = ""


     if content.weight then
     if not content or content == "" then
         content.prob = content.weight / 100
         return params
     end
     end


     if content.prob then
     for param in content:gmatch("{{{%s*([^|}]+)%s*|") do
        prob = string.format(" <div>%s%%</div>", content.prob * 100 >= 1 and math.floor(content.prob * 100) or content.prob * 100)
        add_template_param(params, seen, param)
     end
     end
    for param in content:gmatch("{{{%s*([^|}]+)%s*}}") do
        add_template_param(params, seen, param)
    end
    return params
end


    return string.format(
local function get_template_params(tplPath, content)
        '{{LinkСard|SideStyle=1|background-color=#cbcbff0a|image=%s|name=%s%s%s {{#invoke:Песочница/Pok|main|framing|contained|%s}} {{#invoke:Песочница/Pok|main|framing|slot|%s}} {{#invoke:Песочница/Pok|main|framing|chem|%s}} }}',
     return collect_template_params(content)
        image, name, amount, prob, id, id, id
     )
end
end


-- Получение содержимого через таблицу
local function sort_entries_by_priority(entries)
getContentsOutput = function(contents)
    table.sort(entries, function(a, b)
     local result = ""
        if a.priority == b.priority then return a.idx < b.idx end
     for _, content in ipairs(contents) do
        return a.priority > b.priority
         result = result .. formatContent(content)
    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
     end
     return result
     return state
end
end


-- Обработка вложенных таблиц
local function ensure_switch_key(state, sw, key)
processNestedSelectors = function(children, visited)
    local byKey = state.keyToTemplates[sw]
visited = visited or {}
     if not byKey[key] then
     if not children or #children == 0 then return "" end
        byKey[key] = {}
        state.keyOrder[sw][#state.keyOrder[sw] + 1] = key
    end
    return byKey[key]
end


     local result = ""
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 _, child in ipairs(children) do
local function collect_tpl_calls(entries)
        if child.id then
    local tplCalls = {}
if not visited[child.id] then
    local sources = {}
    result = result .. formatContent(child)
    if #entries > 0 then
end
        sort_entries_by_priority(entries)
         elseif child["!type"] == "NestedSelector" then
         for _, e in ipairs(entries) do
            result = result .. handleNestedSelector(child, true, visited)
            tplCalls[#tplCalls + 1] = e.tpl
        elseif child["!type"] == "GroupSelector" then
             sources[#sources + 1] = e.source
             result = result .. handleGroupSelector(child, visited)
         end
         end
     end
     end
    return tplCalls, sources
end


     return result
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 renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
getTableOutput = function(tableId, visited)
    local parts = {}
     visited = visited or {}
    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


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


     visited[tableId] = true
local function renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
     local groups = {}
    local groupOrder = {}


     local tableData = loadData('User:IanComradeBot/prototypes/table.json')
     for _, key in ipairs(keyOrder or {}) do
    local tableDataIndex = findDataById(tableData, tableId)
        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


     if not tableDataIndex then return 'Таблица не найдена.' 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 tableDataIndex['!type:GroupSelector'] then
        if #parts > 0 then
        return handleGroupSelector(tableDataIndex['!type:GroupSelector'], visited)
            out[#out + 1] = table.concat(parts, "\n")
    elseif tableDataIndex['!type:AllSelector'] then
         end
         return processNestedSelectors(tableDataIndex['!type:AllSelector'].children, visited)
     end
     end


     return 'Таблица не содержит элементов.'
     return table.concat(out, "\n")
end
end


-- Формирование списка содержащихся предметов или таблиц
local function normalizeFilterKey(s)
getContainedOutput = function(itemData, id, visited)
    s = trim(s or "")
     visited = visited or {}
    s = s:gsub("%s*_%s*", "_")
     return s
end


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


     visited[id] = true
local function buildCardCall(merged, entityId)
     local parts = {}


     local item = findDataById(itemData, id)
     if entityId and entityId ~= "" then
     if not item then return '' end
        parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
     end


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


     -- Обработка StorageFill
     if merged.sections and #merged.sections > 0 then
     if item.StorageFill and item.StorageFill.contents then
        table.sort(merged.sections, function(a, b)
         result[#result + 1] = getContentsOutput(item.StorageFill.contents)
            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


    -- Обработка EntityTableContainerFill
                    merged.labelSets[section] = merged.labelSets[section] or {}
    elseif item.EntityTableContainerFill and item.EntityTableContainerFill.containers then
                    if not merged.labelSets[section][compositeKey] then
        local containers = item.EntityTableContainerFill.containers
                        merged.labelSets[section][compositeKey] = true
                        local cur = merged.labelLists[section] or {}
                        cur[#cur + 1] = compositeKey
                        merged.labelLists[section] = cur
                    end
                end


        -- Обработка entity_storage
                if allowCardEntry and tagText ~= "" then
        if containers.entity_storage then
                     if not merged.tagSet[tagText] then
            -- Если есть просто элементы с id, то форматируем их как обычные предметы
                         merged.tagSet[tagText] = true
            if containers.entity_storage.children then
                         merged.tags[#merged.tags + 1] = tagText
                for _, child in ipairs(containers.entity_storage.children) do
                     if child.id then
                        result[#result + 1] = formatContent(child) -- Теперь одиночные id не игнорируются
                    elseif child["!type"] == "GroupSelector" then
                         result[#result + 1] = handleGroupSelector(child, visited)
                    elseif child["!type"] == "AllSelector" then
                         result[#result + 1] = processNestedSelectors(child.children, visited)
                     end
                     end
                 end
                 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
             if containers.entity_storage.tableId then
        for _, v in pairs(merged.contentByKey or {}) do
                 result[#result + 1] = getTableOutput(containers.entity_storage.tableId, visited)
             if v and v ~= "" then
                 hasContent = true
                break
             end
             end
         end
         end


         -- Обработка storagebase (аналогично entity_storage)
         if not hasLabel and not hasContent then
         if containers.storagebase then
            return table.concat(out, "\n")
             if containers.storagebase.children then
         end
                 for _, child in ipairs(containers.storagebase.children) do
    end
                    if child.id then
 
                        result[#result + 1] = formatContent(child)
    if cardCall ~= "" then
                    elseif child["!type"] == "GroupSelector" then
        out[#out + 1] = cardCall
                        result[#result + 1] = handleGroupSelector(child, visited)
    end
                    elseif child["!type"] == "AllSelector" then
 
                         result[#result + 1] = processNestedSelectors(child.children, visited)
    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
             end
             end
        end
        data.card = cardKeys
    end


            if containers.storagebase.tableId then
    return data
                 result[#result + 1] = getTableOutput(containers.storagebase.tableId, visited)
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
         end
     end
     end
    return res
end


     return table.concat(result)
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
end


-- Обработка AllSelector
local function parse_csv_set(str)
handleAllSelector = function(allSelector)
    local res = {}
     if not allSelector.children then return '' end
    each_csv_value(str, function(name)
     return processNestedSelectors(allSelector.children)
        res[name] = true
     end)
     return res
end
end


-- Обработка GroupSelector
local function apply_entity_set_filters(foundSet, whitelistSet, blacklistSet)
handleGroupSelector = function(groupSelector)
    local hasWhitelist = next(whitelistSet or {}) ~= nil
     if not groupSelector.children then return '' end
 
     local result = ""
     if hasWhitelist then
    local wrapperStart, wrapperEnd = "", ""
        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


     -- Проверка для контейнера EntityTableContainerFill
     local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
     if groupSelector.weight and groupSelector.weight ~= "default" then
     if type(protoStore) == "table" then
         wrapperStart = string.format('{{LinkСard/Сollapsible|name=Группа предметов %s%%|content=', groupSelector.weight)
         for protoName in pairs(protoStore) do
        wrapperEnd = "}}"
            if type(protoName) == "string" and protoName ~= "" then
    elseif groupSelector["!type"] == "GroupSelector" and not groupSelector.weight then
                foundPrototypes[protoName] = true
        wrapperStart = '{{LinkСard/Сollapsible|name=Может выпасть лишь один из:|content='
            end
         wrapperEnd = "}}"
         end
     end
     end


     for _, child in ipairs(groupSelector.children) do
     apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
        if child["!type"] == "GroupSelector" then
    apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
            result = result .. handleGroupSelector(child)
 
        elseif child["!type"] == "AllSelector" then
    return foundComponents, foundPrototypes
             result = result .. string.format('{{LinkСard/Сollapsible|name=Выпадают только вместе:|content=%s}}', handleAllSelector(child))
end
        elseif child.id then
 
            result = result .. formatContent(child)
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
         else
             result = result .. "<div>Ошибка: отсутствует id у элемента.</div>"
             local pnum = tonumber(parsed.priority)
            if pnum then
                basePriority = pnum
            end
         end
         end
     end
     end
    return basePriority
end


     return wrapperStart .. result .. wrapperEnd
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
end


-- Обработка NestedSelector
local function add_card_tag_value(tags, seen, value)
handleNestedSelector = function(nestedSelector, wrapped, visited)
    value = trim(value or "")
visited = visited or {}
     if value == "" or seen[value] then
     if not nestedSelector.tableId then return '' end
        return
    end
    seen[value] = true
    tags[#tags + 1] = value
end


     local result = ""
local function merge_card_tag_text(...)
     local classesRolls, classesProb
     local tags = {}
     local seen = {}


     if wrapped then
     for i = 1, select("#", ...) do
         if nestedSelector.rolls and nestedSelector.rolls.range then
         each_csv_value(select(i, ...), function(value)
             local rollsResult = processRolls(nestedSelector.rolls)
             add_card_tag_value(tags, seen, value)
             if rollsResult and #rollsResult > 0 then
        end)
                 classesRolls = ', максимум может выпасть: ' .. rollsResult
    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
         end
         end
         if nestedSelector.prob then
 
             classesProb = string.format(" <div>%s%%</div>", nestedSelector.prob * 100 >= 1 and math.floor(nestedSelector.prob * 100) or nestedSelector.prob * 100)
         if (not isPreview) and type(mode.add_entity_extras) == "function" then
             mode.add_entity_extras(state, parsed, ctx)
         end
         end
     end
     end
end


     if wrapped and (classesRolls or classesProb) then
local function extract_whitelist_search_strings(keyFilter)
         result = result .. string.format('{{LinkСard/Сollapsible|name=Группа предметов%s%s|content=', classesRolls or "", classesProb or "")
     if not keyFilter or not keyFilter.hasWhitelist then
         return nil
     end
     end


     result = result .. getTableOutput(nestedSelector.tableId, visited)
     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 wrapped and (classesRolls or classesProb) then
     if #strings == 0 then
         result = result .. "}}"
         return nil
     end
     end


     return result
     return strings
end
end


-- Формирование списка химии
local function content_matches_whitelist(content, searchStrings)
getChemOutput = function(itemData, id)
     if not searchStrings then
    local item = findDataById(itemData, id)
        return true
     if not item
    end
      or not item.SolutionContainerManager
    if not content then
      or not item.SolutionContainerManager.solutions then  
         return false
         return ''
     end
     end


     local foodSolution = item.SolutionContainerManager.solutions["food"]
     for _, s in ipairs(searchStrings) do
    if not foodSolution or not foodSolution.reagents then  
        if string.find(content, s, 1, true) then
         return ''
            return true
         end
     end
     end


     local result = ""
     return false
     for _, reagent in ipairs(foodSolution.reagents) do
end
        result = result .. string.format(
 
            '<li>[[Химия#chem_%s|%s]] (%d ед.)</li>',
local function each_entity_data(frame, id, onEntity, onMissing, keyFilter)
            reagent.ReagentId, reagent.ReagentId, reagent.Quantity
    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
     end


     return string.format('<ul class="1">%s</ul>', result)
     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
end


-- Основная функция модуля
local function collect_card_tag_text(frame, args, id)
function p.main(frame)
     local filter = build_key_filter(args)
     local mode = frame.args[1]
     local cardFilter = build_render_options(filter).cardFilter
     local id = frame.args[2]
    local entries = {}


     if not id then return 'Не указан ID.' end
     local ok = each_entity_data(frame, id, function(parsed, ctx)
   
        local keys = parsed.card or {}
    local itemDataIndex = itemData
   
    if not itemData then return 'Не удалось загрузить данные.' end


    if mode == 'framing' then
        if type(keys) == "table" then
        local subMode = frame.args[2]
            for _, key in ipairs(keys) do
        local id = frame.args[3]
                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 not id then
                if allowCardEntry then
             return 'Не указан ID для режима framing.'
                    entries[#entries + 1] = {
                        tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
                        priority = ctx.priority,
                        idx = #entries + 1
                    }
                end
             end
         end
         end


         if subMode == 'chem' then
         if parsed.cardTag and parsed.cardTag ~= "" then
             return frame:preprocess('{{СollapsibleMenu|' .. getChemOutput(chemData, id) .. '}}')
             entries[#entries + 1] = {
         elseif subMode == 'contained' then
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
            return frame:preprocess('{{СollapsibleMenu|' .. getContainedOutput(itemDataIndex, id) .. '}}')
                priority = ctx.priority,
elseif subMode == "slot" then
                idx = #entries + 1
    local itemDataEntry = findDataById(itemSlotsData, id)
            }
    if not itemDataEntry then return "" end
        end
    end)
    local startingItem = getFirstStartingItem(itemDataEntry)
 
    if not startingItem then return "" end
    if not ok then
        return trim(args.tag or "")
    return frame:preprocess('{{СollapsibleMenu|' .. formatContent(startingItem) .. '}}')
    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
         else
             return 'Неизвестный подрежим для framing: ' .. subMode
             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
     elseif mode == 'chem' then
     end
        return frame:preprocess(getChemOutput(chemData, id))
     return outLocal
     elseif mode == 'contained' then
end
        return frame:preprocess(getContainedOutput(itemDataIndex, id))
elseif mode == "slot" then
    local itemDataEntry = findDataById(itemSlotsData, id)
    if not itemDataEntry then return "" end
    local startingItem = getFirstStartingItem(itemDataEntry)
    if not startingItem then return "" end
    return frame:preprocess(formatContent(startingItem))
    elseif mode == 'rolls' then
        local entity = findDataById(itemDataIndex, id)
        if not entity then return 'ID не найден в данных.' end


        if entity.EntityTableContainerFill then
function p.get(frame)
            local containers = entity.EntityTableContainerFill.containers
    local args = getArgs(frame, { removeBlanks = false })
            if containers.entity_storage and containers.entity_storage.rolls then
    local id = args[1] or ""
                return processRolls(containers.entity_storage.rolls)
    if id == "" then return "" end
            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
    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)


         return ''
    local content = load_template_content(tplPath)
     else
    if not content then
         return 'Неизвестный режим: ' .. mode
         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
     end
    return frame:preprocess(table.concat(out, "\n"))
end
end


return p
return p