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

Материал из Space Station 14 Вики
мНет описания правки
Нет описания правки
 
(не показано 247 промежуточных версий этого же участника)
Строка 1: Строка 1:
-- Вспомогательная функция для итерации по «массивоподобным» таблицам
local p = {}
local function iterateArray(t)
local getArgs = require('Module:Arguments').getArgs
     if type(t) ~= "table" then
local JsonPaths = require('Module:JsonPaths')
         return function() return nil end
 
local dpOk, dpModule = pcall(require, "Module:GetField")
local dp = dpOk and dpModule or nil
 
local switchModeRegistry = {}
local switchModeOrder = {}
 
local function trim(s)
     if not s then return s end
    return (s:gsub("^%s*(.-)%s*$", "%1"))
end
 
local function each_csv_value(str, fn)
    if not str or str == "" then
         return
     end
     end
    local isArray = false
     for item in string.gmatch(str, "[^,]+") do
    local count = 0
         local value = trim(item)
     for k, _ in pairs(t) do
        if value ~= "" then
         if type(k) == "number" then
             fn(value)
             count = count + 1
         end
         end
     end
     end
     if count > 0 then
end
         isArray = true
 
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
     end
     if isArray then
 
         local keys = {}
    return name
         for k, _ in pairs(t) do
end
             if type(k) == "number" then
 
                 table.insert(keys, k)
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
         end
         end
         table.sort(keys)
    else
        local i = 0
         for k in pairs(comps) do
        return function()
            local name = normalize_component_name(k)
             i = i + 1
             if name and not seen[name] then
            local key = keys[i]
                seen[name] = true
            if key then
                 out[#out + 1] = name
                 return key, t[key]
             end
             end
         end
         end
    else
        return pairs(t)
     end
     end
    table.sort(out)
    return out
end
end


-- Загрузка данных через mw.loadData
local function load_entity_components_from_dp(entityId)
local itemData          = mw.loadData("Модуль:IanComradeBot/prototypes/fills/Item.json/data")
     if not dp then
local itemSlotsData     = mw.loadData("Модуль:IanComradeBot/prototypes/ItemSlots.json/data")
        return nil
local itemStackData     = mw.loadData("Модуль:IanComradeBot/prototypes/fills/stack.json/data")
     end
local chemData          = mw.loadData("Модуль:IanComradeBot/prototypes/fills/chem.json/data")
local chemTranslateData = mw.loadData("Модуль:IanComradeBot/chem prototypes.json/data")


local p = {}
    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 } })
-- Функция для поиска данных по ID (поддерживает оба формата таблиц)
     if not ok or result == nil or result == "" then
local findDataById
         return nil
findDataById = function(data, id)
     if not data then  
         return nil  
     end
     end


    -- Если данные индексированы по ID, сразу возвращаем нужный элемент
     if type(result) == "table" then
     if data[id] then
         return result
         return data[id]
     end
     end


     -- Перебираем таблицу с использованием iterateArray (обеспечивает порядок для массива)
     if type(result) == "string" then
    for _, item in iterateArray(data) do
        local okJson, decoded = pcall(mw.text.jsonDecode, result)
         if type(item) == "table" and item.id == id then
         if okJson and type(decoded) == "table" then
             return item
             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)
local function formatContent(content)
     if type(viaDp) == "table" and next(viaDp) ~= nil then
     if type(content) == "table" and not content.id then
        local out = {}
         return "Ошибка: отсутствует id у элемента."
        local seen = {}
        if #viaDp > 0 then
            for _, v in ipairs(viaDp) do
                local name = normalize_component_name(v)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        else
            for k in pairs(viaDp) do
                local name = normalize_component_name(k)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        end
        table.sort(out)
        return out
    end
 
    local entity = load_entity_data(entityId)
    return collect_entity_components(entity)
end
 
p.loadEntityData = load_entity_data
p.collectEntityComponents = collect_entity_components
p.loadEntityComponents = load_entity_components
p.entityHasComponent = function(entityOrId, compName)
    if not compName or compName == "" then
         return false
    end
 
    if type(entityOrId) == "string" then
        local entity = load_entity_data(entityOrId)
        if not entity then
            return false
        end
        local comps = collect_entity_components(entity)
        for _, v in ipairs(comps) do
            if v == compName then
                return true
            end
        end
        return false
     end
     end


     local id = content.id or content
     local comps = collect_entity_components(entityOrId)
      
     for _, v in ipairs(comps) do
    local amountNumber = 1
         if v == compName then
    if content.amount then
             return true
         if type(content.amount) == "table" then
             amountNumber = content.amount.value or 1
        else
            amountNumber = content.amount
         end
         end
     end
     end
     local amount = (amountNumber and amountNumber ~= 1) and string.format(" [%d]", amountNumber) or ""
    return false
      
end
     local prob = ""
 
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


     if content.weight then
local function collect_template_params(content)
         content.prob = content.weight / 100
    local params = {}
    local seen = {}
 
     if not content or content == "" then
         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
    for param in content:gmatch("{{{%s*([^|}]+)%s*}}") do
        add_template_param(params, seen, param)
     end
     end


     return string.format(
     return params
        '{{#invoke:Prototypes/Предмет/Содержание|main|%s|%s%s}}',
        id, amount, prob
    )
end
end


---------------------------------------------------------------------
local function get_template_params(tplPath, content)
-- Получение содержимого через таблицу
    return collect_template_params(content)
local function getContentsOutput(contents)
end
     local result = ""
 
     for _, content in iterateArray(contents) do
local function sort_entries_by_priority(entries)
         result = result .. formatContent(content)
    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
     end
     return result
     return state
end
end


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


     local result = ""
local function add_switch_entry(state, sw, key, entry)
    for _, child in iterateArray(children) do
     local bucket = ensure_switch_key(state, sw, key)
        if child.id then
    entry.idx = #bucket + 1
if not visited[child.id] then
    bucket[#bucket + 1] = entry
    result = result .. formatContent(child)
end
end
 
        elseif child["!type"] == "NestedSelector" then
local function collect_tpl_calls(entries)
            result = result .. handleNestedSelector(child, true, visited)
    local tplCalls = {}
        elseif child["!type"] == "GroupSelector" then
    local sources = {}
             result = result .. handleGroupSelector(child, visited)
    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
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 "")


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


---------------------------------------------------------------------
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
-- Обработка таблиц (для table.json)
    local parts = {}
local function getTableOutput(tableId, visited)
    if tplCalls and #tplCalls > 0 then
    visited = visited or {}
        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 = mw.loadData("Модуль:IanComradeBot/prototypes/table.json/data")
     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
            group.blocks[#group.blocks + 1] = {
         return 'Таблица не найдена.'
                subTitle = subTitle,
                entries = keyToTemplates[key] or {}
            }
         end
     end
     end


     if tableDataIndex['!type:GroupSelector'] then
     local out = {}
         return handleGroupSelector(tableDataIndex['!type:GroupSelector'], visited)
    for _, mainTitle in ipairs(groupOrder) do
    elseif tableDataIndex['!type:AllSelector'] then
        local group = groups[mainTitle]
        return processNestedSelectors(tableDataIndex['!type:AllSelector'].children, visited)
        local parts = {}
 
        if not noHeaders then
            parts[#parts + 1] = "<h2>" .. mw.text.encode(mainTitle) .. "</h2>"
        end
 
         for _, block in ipairs(group.blocks or {}) do
            local tplCalls, sources = collect_tpl_calls(block.entries or {})
            local blockText = renderTitleBlock(block.subTitle or mainTitle, tplCalls, sources, false, frame, showSource)
            if blockText ~= "" then
                if block.subTitle and not noHeaders then
                    parts[#parts + 1] = "<h3>" .. mw.text.encode(block.subTitle) .. "</h3>"
                end
                parts[#parts + 1] = blockText
            end
        end
 
        if #parts > 0 then
            out[#out + 1] = table.concat(parts, "\n")
        end
     end
     end


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


---------------------------------------------------------------------
local function normalizeFilterKey(s)
-- Формирование списка содержащихся предметов или таблиц
     s = trim(s or "")
local function getContainedOutput(itemData, id, visited)
    s = s:gsub("%s*_%s*", "_")
     visited = visited or {}
    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  
         parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
         return ''
     end
     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


     -- Обработка EntityTableContainerFill
     for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
     elseif item.EntityTableContainerFill and item.EntityTableContainerFill.containers then
        if displayLabel and displayLabel ~= "" then
         local containers = item.EntityTableContainerFill.containers
            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


        -- Обработка entity_storage
                local allowCardEntry = not isWhitelisted and not isBlacklisted and
        if containers.entity_storage then
                    ((not cardFilter) or (not cardFilter.hasWhitelist) or
            if containers.entity_storage.children then
                        matches_card_list(cardFilter.cardWhitelist, callKey, compositeKey))
                 for _, child in iterateArray(containers.entity_storage.children) do
 
                     if child.id then
                 if allowCardEntry and (displayLabel ~= "" or content ~= "") then
                         result[#result + 1] = formatContent(child)
                     if not merged.sectionsMap[section] then
                     elseif child["!type"] == "GroupSelector" then
                         merged.sectionsMap[section] = true
                         result[#result + 1] = handleGroupSelector(child, visited)
                        merged.sections[#merged.sections + 1] = section
                    elseif child["!type"] == "AllSelector" then
                    end
                         result[#result + 1] = processNestedSelectors(child.children, visited)
                    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
             end
        end
    end


             if containers.entity_storage.tableId then
    each_csv_value(frame.args.cardTag or "", function(extraTag)
                result[#result + 1] = getTableOutput(containers.entity_storage.tableId, visited)
        if not merged.tagSet[extraTag] then
             merged.tagSet[extraTag] = true
            merged.tags[#merged.tags + 1] = extraTag
        end
    end)
 
    local out = {}
    if #rawContentParts > 0 then
        out[#out + 1] = table.concat(rawContentParts, "\n")
    end
 
    local cardCall = buildCardCall(merged, entityId)
 
    if noHeaders then
        local hasLabel = false
        for _, v in pairs(merged.labelOverrides or {}) do
            if v and v ~= "" then
                hasLabel = true
                break
            end
        end
        if not hasLabel then
            for _, lst in pairs(merged.labelLists or {}) do
                if #lst > 0 then
                    hasLabel = true
                    break
                end
            end
        end
 
        local hasContent = false
        for _, v in pairs(merged.contentByKey or {}) do
            if v and v ~= "" then
                hasContent = true
                break
             end
             end
         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 iterateArray(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
    return data
end


            if containers.storagebase.tableId then
local function parseListArg(str)
                 result[#result + 1] = getTableOutput(containers.storagebase.tableId, visited)
    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
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


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


---------------------------------------------------------------------
local function should_include_key(filter, sw, key)
-- Обработка AllSelector
     if filter.hasWhitelist then
local function handleAllSelector(allSelector)
        if filter.whitelist[sw] and filter.whitelist[sw][key] then
     if not allSelector.children then return '' end
            return true
     return processNestedSelectors(allSelector.children)
        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


---------------------------------------------------------------------
local function parse_csv_set(str)
-- Обработка GroupSelector
    local res = {}
handleGroupSelector = function(groupSelector, visited)
    each_csv_value(str, function(name)
     if not groupSelector.children then return '' end
        res[name] = true
     local result = ""
     end)
    local wrapperStart, wrapperEnd = "", ""
     return res
end


    if groupSelector.weight and groupSelector.weight ~= "default" then
local function apply_entity_set_filters(foundSet, whitelistSet, blacklistSet)
        wrapperStart = string.format('{{LinkСard/Сollapsible|name=Группа предметов %s%%|content=', groupSelector.weight)
     local hasWhitelist = next(whitelistSet or {}) ~= nil
        wrapperEnd = "}}"
     elseif groupSelector["!type"] == "GroupSelector" and not groupSelector.weight then
        wrapperStart = '{{LinkСard/Сollapsible|name=Может выпасть лишь один из:|content='
        wrapperEnd = "}}"
    end


     for _, child in iterateArray(groupSelector.children) do
     if hasWhitelist then
        if child["!type"] == "GroupSelector" then
        for name in pairs(foundSet) do
            result = result .. handleGroupSelector(child, visited)
            if not whitelistSet[name] then
        elseif child["!type"] == "AllSelector" then
                foundSet[name] = nil
            result = result .. string.format('{{LinkСard/Сollapsible|name=Выпадают только вместе:|content=%s}}', handleAllSelector(child))
             end
        elseif child.id then
            result = result .. formatContent(child)
        else
             result = result .. "<div>Ошибка: отсутствует id у элемента.</div>"
         end
         end
     end
     end


     return wrapperStart .. result .. wrapperEnd
     for name in pairs(blacklistSet or {}) do
        foundSet[name] = nil
    end
end
end


---------------------------------------------------------------------
local function collect_entity_sets(id, prototypeStoreDefs, componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
-- Обработка NestedSelector
    local foundComponents, foundPrototypes = {}, {}
handleNestedSelector = function(nestedSelector, wrapped, visited)
visited = visited or {}
    if not nestedSelector.tableId then return '' end


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


     if wrapped then
    local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
         if nestedSelector.rolls and nestedSelector.rolls.range then
     if type(protoStore) == "table" then
            local rollsResult = processRolls(nestedSelector.rolls)
         for protoName in pairs(protoStore) do
             if rollsResult and #rollsResult > 0 then
             if type(protoName) == "string" and protoName ~= "" then
                 classesRolls = ', максимум может выпасть: ' .. rollsResult
                 foundPrototypes[protoName] = true
             end
             end
         end
         end
         if nestedSelector.prob then
    end
             classesProb = string.format(" <div>%s%%</div>", nestedSelector.prob * 100 >= 1 and math.floor(nestedSelector.prob * 100) or nestedSelector.prob * 100)
 
    apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
    apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
 
    return foundComponents, foundPrototypes
end
 
local function resolve_priority(parsed)
    local basePriority = 1
    if type(parsed) == "table" and parsed.priority ~= nil then
         if type(parsed.priority) == "number" then
             basePriority = parsed.priority
        else
            local pnum = tonumber(parsed.priority)
            if pnum then
                basePriority = pnum
            end
         end
         end
    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
     end


     if wrapped and (classesRolls or classesProb) then
     return dp.flattenFieldSelectiveDirect(id, dataPage, paramNames) or ""
        result = result .. string.format('{{LinkСard/Сollapsible|name=Группа предметов%s%s|content=', classesRolls or "", classesProb or "")
end
 
local function add_card_tag_value(tags, seen, value)
    value = trim(value or "")
    if value == "" or seen[value] then
        return
     end
     end
    seen[value] = true
    tags[#tags + 1] = value
end


    result = result .. getTableOutput(nestedSelector.tableId, visited)
local function merge_card_tag_text(...)
    local tags = {}
    local seen = {}


     if wrapped and (classesRolls or classesProb) then
     for i = 1, select("#", ...) do
         result = result .. "}}"
         each_csv_value(select(i, ...), function(value)
            add_card_tag_value(tags, seen, value)
        end)
     end
     end


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


---------------------------------------------------------------------
local function add_entries_from_meta(state, parsed, ctx, filter, isPreview)
-- Функция для преобразования диапазона (rolls)
    for _, sw in ipairs(switchModeOrder) do
local function processRolls(rolls)
        local mode = switchModeRegistry[sw] or {}
    local result = ""
         local keys
    if rolls and rolls.range then
         if type(mode.get_keys) == "function" then
         local min, max = rolls.range:match("(%d+),%s*(%d+)")
             keys = mode.get_keys(parsed)
         min, max = tonumber(min), tonumber(max)
        if min and max then
             result = result .. string.format('[%d-%d]', min + 1, max + 1)
         else
         else
             result = result .. 'Некорректный формат для range.'
             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
    elseif rolls and rolls.value then
        result = result .. string.format('[%d]', rolls.value)
    else
        result = result .. 'Не указан параметр rolls.'
     end
     end
    return result
end
end


---------------------------------------------------------------------
local function extract_whitelist_search_strings(keyFilter)
-- Формирование списка химии
     if not keyFilter or not keyFilter.hasWhitelist then
local function getChemOutput(itemData, id)
         return nil
    local item = findDataById(itemData, id)
     if not item
      or not item.SolutionContainerManager
      or not item.SolutionContainerManager.solutions then  
         return ''
     end
     end


     local allReagents = {}
     local strings = {}
     for _, solution in pairs(item.SolutionContainerManager.solutions) do
     for sw, keys in pairs(keyFilter.whitelist) do
         if solution and solution.reagents then
         if type(keys) == "table" then
             for _, reagent in iterateArray(solution.reagents) do
             for key in pairs(keys) do
                 table.insert(allReagents, reagent)
                 strings[#strings + 1] = key
             end
             end
         end
         end
     end
     end


     local hasNonFiber = false
     if #strings == 0 then
     for _, reagent in iterateArray(allReagents) do
        return nil
         if reagent.ReagentId ~= "Fiber" then
    end
             hasNonFiber = true
 
            break
    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
     end
     end


     if not hasNonFiber then
    return false
         return ''
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
     end


     local function processSolution(solution)
     local foundComponents, foundPrototypes = collect_entity_sets(id, prototypeStoreDefs,
        local output = ""
        componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
        if solution and solution.reagents then
 
            for _, reagent in iterateArray(solution.reagents) do
    local compWhitelistSet = parse_csv_set(componentWhitelist)
                local chemInfo = chemTranslateData[reagent.ReagentId]
    local compBlacklistSet = parse_csv_set(componentBlacklist)
                local displayName = chemInfo and chemInfo.name or reagent.ReagentId
    local protoWhitelistSet = parse_csv_set(prototypeWhitelist)
                output = output .. string.format(
    local protoBlacklistSet = parse_csv_set(prototypeBlacklist)
                    '<li>[[Химия#chem_%s|%s]] (%d ед.)</li>',
 
                    reagent.ReagentId, displayName, reagent.Quantity
    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
             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
         end
         return output
 
         onEntity(parsed, {
            tplPath = tplPath,
            id = id,
            extra = extra,
            source = make_source(kind, name, pathName, tplPath),
            priority = resolve_priority(parsed)
        })
     end
     end


     local result = ""
     for compName in pairs(foundComponents) do
    result = result .. processSolution(item.SolutionContainerManager.solutions["drink"])
        if not anyEntityWhitelist or compHasWhitelist then
     result = result .. processSolution(item.SolutionContainerManager.solutions["food"])
            processEntity("component", compName, false)
     result = result .. processSolution(item.SolutionContainerManager.solutions["beaker"])
        end
    result = result .. processSolution(item.SolutionContainerManager.solutions["injector"])
     end
     result = result .. processSolution(item.SolutionContainerManager.solutions["pen"])
     for protoName in pairs(foundPrototypes) do
        if not anyEntityWhitelist or protoHasWhitelist then
            processEntity("prototype", protoName, false)
        end
     end


     return result ~= "" and string.format('<ul class="1">%s</ul>', result) or ''
     local componentStoreDefs = load_module_data("component_store.json")
end
    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
-- Функция поиска count в itemStackData
        local protoStore = prototypeStoreDefs[id]
local function getStackCount(id)
        if type(protoStore) == "table" then
    local item = findDataById(itemStackData, id)
            for protoName in pairs(protoStore) do
    if item and item.Stack and item.Stack.count then
                local allowed = true
         return item.Stack.count
                if protoBlacklistSet[protoName] then
                    allowed = false
                elseif protoHasWhitelist and not protoWhitelistSet[protoName] then
                    allowed = false
                end
                if allowed then
                    processEntity("prototype", protoName, true)
                end
            end
         end
     end
     end
     return nil
 
     return true
end
end


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


    if not id then return 'Не указан ID.' end
        if type(keys) == "table" then
   
            for _, key in ipairs(keys) do
    local itemDataIndex = itemData
                local compositeKey = (key:find("_", 1, true)) and key or ("Сущность_" .. key)
   
                local isWhitelisted = matches_card_list(cardFilter.whitelist, key, compositeKey)
    if not itemData then return 'Не удалось загрузить данные.' end
                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 mode == 'framing' then
                if allowCardEntry then
        local subMode = frame.args[2]
                    entries[#entries + 1] = {
        local id = frame.args[3]
                        tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
                        priority = ctx.priority,
                        idx = #entries + 1
                    }
                end
            end
        end


         if not id then
         if parsed.cardTag and parsed.cardTag ~= "" then
             return 'Не указан ID для режима framing.'
             entries[#entries + 1] = {
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                priority = ctx.priority,
                idx = #entries + 1
            }
         end
         end
    end)


        if subMode == 'chem' then
    if not ok then
            return frame:preprocess('{{СollapsibleMenu|color=#3e7c82|' .. getChemOutput(chemData, id) .. '}}')
        return trim(args.tag or "")
         elseif subMode == 'contained' then
    end
            return frame:preprocess('{{СollapsibleMenu|' .. getContainedOutput(itemDataIndex, id) .. '}}')
 
        elseif subMode == "slot" then
    sort_entries_by_priority(entries)
            local itemDataEntry = findDataById(itemSlotsData, id)
 
            if not itemDataEntry then return "" end
    local tags = {}
            local startingItem = nil
    local seen = {}
            if itemDataEntry.ItemSlots and itemDataEntry.ItemSlots.slots then
    for _, entry in ipairs(entries) do
                for _, slot in iterateArray(itemDataEntry.ItemSlots.slots) do
         local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
                    if slot.startingItem and slot.startingItem ~= "" then
        add_card_tag_value(tags, seen, tagText)
                        startingItem = slot.startingItem
    end
                        break
 
                     end
    return merge_card_tag_text(table.concat(tags, ", "), args.tag)
                end
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
             end
             if not startingItem then return "" end
             if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
            return frame:preprocess('{{СollapsibleMenu|color=#71702a|' .. formatContent(startingItem) .. '}}')
        elseif subMode == "stack" then
            local count = getStackCount(id)
            return count and '(' .. count .. ')' or ""
         else
         else
             return 'Неизвестный подрежим для framing: ' .. subMode
             for _, key in ipairs(state.keyOrder[sw] or {}) do
        end
                local entries = state.keyToTemplates[sw][key] or {}
    elseif mode == 'stack' then
                if type(mode.render_key) == "function" then
        local count = getStackCount(id)
                    local outStr = mode.render_key(frame, key, entries, noHeaders, showSource)
        return count or ""
                    if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
    elseif mode == 'chem' then
        return getChemOutput(chemData, id)
    elseif mode == 'contained' then
        return frame:preprocess(getContainedOutput(itemDataIndex, id))
    elseif mode == "slot" then
        local itemDataEntry = findDataById(itemSlotsData, id)
        if not itemDataEntry then return "" end
        local startingItem = nil
        if itemDataEntry.ItemSlots and itemDataEntry.ItemSlots.slots then
            for _, slot in iterateArray(itemDataEntry.ItemSlots.slots) do
                if slot.startingItem and slot.startingItem ~= "" then
                    startingItem = slot.startingItem
                    break
                 end
                 end
             end
             end
         end
         end
        if not startingItem then return "" end
    end
        return frame:preprocess(formatContent(startingItem))
    return outLocal
     elseif mode == 'rolls' then
end
        local entity = findDataById(itemDataIndex, id)
 
         if not entity then return 'ID не найден в данных.' end
function p.get(frame)
         if entity.EntityTableContainerFill then
    local args = getArgs(frame, { removeBlanks = false })
             local containers = entity.EntityTableContainerFill.containers
    local id = args[1] or ""
            if containers.entity_storage and containers.entity_storage.rolls then
    if id == "" then return "" end
                return processRolls(containers.entity_storage.rolls)
 
            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
         return ''
    end, filter)
     else
    if not ok then return "" end
         return 'Неизвестный режим: ' .. mode
 
    local out = {}
 
    if #errors > 0 then
         out[#out + 1] = '{{сущность/infobox|' .. table.concat(errors, "\n") .. '}}'
     end
 
    renderOptions.noHeaders = filter.hasWhitelist
 
    local blocks = renderBlocks(frame, state, renderOptions, id, showSource)
    for _, b in ipairs(blocks) do
         out[#out + 1] = b
    end
 
    return frame:preprocess(table.concat(out, "\n"))
end
 
function p.preview(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local tplPath = args[1] or ""
    if tplPath == "" then return "" end
 
    local showSource = trim(args.nosource or "") == ""
    local previewFilter = build_key_filter(args)
    local renderOptions = build_render_options(previewFilter)
 
    local content = load_template_content(tplPath)
    if not content then
        return ""
    end
 
    local parsed = getTemplateMeta(frame, tplPath) or {}
    if type(parsed) ~= "table" then
        parsed = {}
    end
 
    local state = new_switch_state()
    add_entries_from_meta(state, parsed, {
        tplPath = tplPath,
        id = "",
        extra = "",
        source = make_source("", tplPath, tplPath, tplPath),
        priority = 1
    }, nil, true)
 
    local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist
 
    local out = {}
    local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
    for _, b in ipairs(blocks) do
        out[#out + 1] = b
     end
     end
    return frame:preprocess(table.concat(out, "\n"))
end
end


return p
return p

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

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

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

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

local switchModeRegistry = {}
local switchModeOrder = {}

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

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

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

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

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

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

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

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

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

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

    return name
end

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

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

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

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

    table.sort(out)
    return out
end

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

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

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

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

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

    return nil
end

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

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

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

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

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

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

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

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

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

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

    return params
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if #parts == 0 then
        return ""
    end

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

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

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

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

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

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

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

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

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

    local cardCall = buildCardCall(merged, entityId)

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

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

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

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

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

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

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

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

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

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

    return data
end

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

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

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

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

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

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

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

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

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

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

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

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

    return foundComponents, foundPrototypes
end

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

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

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

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

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

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

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

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

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

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

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

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

    if #strings == 0 then
        return nil
    end

    return strings
end

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

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

    return false
end

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

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

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

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

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

    local whitelistSearchStrings = extract_whitelist_search_strings(keyFilter)

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

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

        if not content_matches_whitelist(content, whitelistSearchStrings) then
            return
        end

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

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

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

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

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

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

    return true
end

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

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

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

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

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

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

    sort_entries_by_priority(entries)

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

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

p.mergeCardTagText = merge_card_tag_text
p.collectCardTagText = collect_card_tag_text

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

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

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

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

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

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

    local out = {}

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

    renderOptions.noHeaders = filter.hasWhitelist

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

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

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

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

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

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

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

    local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist

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

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

return p