Модуль:Сущность/data: различия между версиями

Материал из Space Station 14 Вики
Нет описания правки
Отмена версии 213664, сделанной Pok (обсуждение)
Метка: отмена
 
(не показано 9 промежуточных версий этого же участника)
Строка 4: Строка 4:


local moduleDataCache = {}
local moduleDataCache = {}
local templateContentCache = {}
local templateMetaCache = {}
local templateArgCache = {}
local flattenExtraCache = {}
local switchModeRegistry = {}
local switchModeRegistry = {}
local switchModeOrder = {}
local switchModeOrder = {}
Строка 13: Строка 17:


local function load_module_data(page)
local function load_module_data(page)
     local moduleName = JsonPaths.getModuleName(page)
     local moduleName = JsonPaths.get(page)


     if moduleDataCache[moduleName] ~= nil then
     if moduleDataCache[moduleName] ~= nil then
Строка 30: Строка 34:


local function load_template_content(path)
local function load_template_content(path)
    if templateContentCache[path] ~= nil then
        return templateContentCache[path] or nil
    end
     local title = mw.title.new("Template:" .. path)
     local title = mw.title.new("Template:" .. path)
     if not title then return nil end
     if not title then
        templateContentCache[path] = false
        return nil
    end
     local ok, content = pcall(function() return title:getContent() end)
     local ok, content = pcall(function() return title:getContent() end)
     if not ok then return nil end
     if not ok then
        templateContentCache[path] = false
        return nil
    end
 
    templateContentCache[path] = content or false
     return content
     return content
end
end
Строка 48: Строка 64:
     tplStr = tplStr .. "}}"
     tplStr = tplStr .. "}}"
     return 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)
    local cached = templateArgCache[tplPath]
    if cached ~= nil then
        return cached
    end
    local params = collect_template_params(content)
    templateArgCache[tplPath] = params
    return params
end
end


Строка 106: Строка 162:
local function makeSourceLink(s)
local function makeSourceLink(s)
     local className =
     local className =
         (s.name:sub(1,1):upper() .. s.name:sub(2)) ..
         (s.name:sub(1, 1):upper() .. s.name:sub(2)) ..
         (s.kind and (s.kind:sub(1,1):upper() .. s.kind:sub(2)) or "")
         (s.kind and (s.kind:sub(1, 1):upper() .. s.kind:sub(2)) or "")


     local tplLabel = "Template:" .. s.tplPath
     local tplLabel = "Template:" .. s.tplPath
Строка 113: Строка 169:
end
end


local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame)
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
     local parts = {}
     local parts = {}
     if tplCalls and #tplCalls > 0 then
     if tplCalls and #tplCalls > 0 then
Строка 124: Строка 180:
             if add then
             if add then
                 local src = sources and sources[i]
                 local src = sources and sources[i]
                 local line = '<div>' .. tpl .. '</div><div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
                 local line = '<div>' .. tpl .. '</div>'
                if showSource and src then
                    line = line .. '<div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
                end
                 table.insert(parts, '<div class="ts-Сущность">' .. line .. '</div>')
                 table.insert(parts, '<div class="ts-Сущность">' .. line .. '</div>')
             end
             end
Строка 136: Строка 195:
     end
     end
     return table.concat(parts, "\n")
     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 matches_card_list(list, callKey, compositeKey)
    if not list then
        return false
    end
    return list[callKey] or list[compositeKey] or false
end
end


Строка 141: Строка 264:
     local parts = {}
     local parts = {}


    -- id сущности
     if entityId and entityId ~= "" then
     if entityId and entityId ~= "" then
         parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
         parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
     end
     end


    -- типы
     if merged.tags and #merged.tags > 0 then
     if merged.tags and #merged.tags > 0 then
         table.sort(merged.tags)
         table.sort(merged.tags)
Строка 152: Строка 273:
     end
     end


    -- секции карточки
     if merged.sections and #merged.sections > 0 then
     if merged.sections and #merged.sections > 0 then
         table.sort(merged.sections, function(a, b)
         table.sort(merged.sections, function(a, b)
Строка 172: Строка 292:
     end
     end


    -- содержимое полей
     for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
     for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
         if displayLabel and displayLabel ~= "" then
         if displayLabel and displayLabel ~= "" then
Строка 191: Строка 310:
end
end


local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders)
local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
     local merged = {
     local merged = {
         sections = {},
         sections = {},
Строка 202: Строка 321:
         tagSet = {}
         tagSet = {}
     }
     }
    local rawContentParts = {}
     for _, callKey in ipairs(keyOrder or {}) do
     for _, callKey in ipairs(keyOrder or {}) do
         local entries = keyToTemplates[callKey] or {}
         local entries = keyToTemplates[callKey] or {}
Строка 216: Строка 336:
                 local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность"
                 local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность"


                 if displayLabel ~= "" or content ~= "" then
                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
                     if not merged.sectionsMap[section] then
                         merged.sectionsMap[section] = true
                         merged.sectionsMap[section] = true
Строка 242: Строка 374:
                 end
                 end


                 if tagText ~= "" then
                 if allowCardEntry and tagText ~= "" then
                     if not merged.tagSet[tagText] then
                     if not merged.tagSet[tagText] then
                         merged.tagSet[tagText] = true
                         merged.tagSet[tagText] = true
Строка 251: Строка 383:
         end
         end
     end
     end
    local out = {}
    if #rawContentParts > 0 then
        out[#out + 1] = table.concat(rawContentParts, "\n")
    end
    local cardCall = buildCardCall(merged, entityId)


     if noHeaders then
     if noHeaders then
         local hasLabel = false
         local hasLabel = false
         for _, v in pairs(merged.labelOverrides or {}) do
         for _, v in pairs(merged.labelOverrides or {}) do
             if v and v ~= "" then hasLabel = true break end
             if v and v ~= "" then
                hasLabel = true
                break
            end
         end
         end
         if not hasLabel then
         if not hasLabel then
             for _, lst in pairs(merged.labelLists or {}) do
             for _, lst in pairs(merged.labelLists or {}) do
                 if #lst > 0 then hasLabel = true break end
                 if #lst > 0 then
                    hasLabel = true
                    break
                end
             end
             end
         end
         end
Строка 265: Строка 410:
         local hasContent = false
         local hasContent = false
         for _, v in pairs(merged.contentByKey or {}) do
         for _, v in pairs(merged.contentByKey or {}) do
             if v and v ~= "" then hasContent = true break end
             if v and v ~= "" then
                hasContent = true
                break
            end
         end
         end


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


     return buildCardCall(merged, entityId)
    if cardCall ~= "" then
        out[#out + 1] = cardCall
    end
 
     return table.concat(out, "\n")
end
end


Строка 306: Строка 458:
         end
         end
     end,
     end,
     render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders)
     render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource, cardFilter)
         return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders)
         return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
     end
     end
})
})


register_switch_mode("title", {
register_switch_mode("title", {
    full = true,
     build_entry = function(ctx, key)
     build_entry = function(ctx, key)
         return {
         return {
Строка 326: Строка 479:
         }
         }
     end,
     end,
     render_key = function(frame, key, entries, noHeaders)
     render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource)
        local tplCalls, sources = collect_tpl_calls(entries)
         return renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
         return renderTitleBlock(key, tplCalls, sources, not noHeaders, frame)
     end
     end
})
})


local function getTemplateMeta(frame, tplPath)
local function getTemplateMeta(frame, tplPath)
    if templateMetaCache[tplPath] ~= nil then
        return templateMetaCache[tplPath] or ""
    end
     local expanded = frame:expandTemplate {
     local expanded = frame:expandTemplate {
         title = tplPath,
         title = tplPath,
Строка 340: Строка 496:
     local ok, data = pcall(mw.text.jsonDecode, expanded)
     local ok, data = pcall(mw.text.jsonDecode, expanded)
     if not ok or type(data) ~= "table" then
     if not ok or type(data) ~= "table" then
        templateMetaCache[tplPath] = false
         return ""
         return ""
     end
     end
Строка 359: Строка 516:
     end
     end


    templateMetaCache[tplPath] = data
     return data
     return data
end
end
Строка 382: Строка 540:
     filter.blacklist = parseListArg(args.blacklist or "")
     filter.blacklist = parseListArg(args.blacklist or "")
     filter.whitelist = parseListArg(args.whitelist or "")
     filter.whitelist = parseListArg(args.whitelist or "")
     filter.hasWhitelist = next(filter.whitelist) ~= nil
     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
     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
end


local function should_include_key(filter, sw, key)
local function should_include_key(filter, sw, key)
     if filter.hasWhitelist then
     if filter.hasWhitelist then
         return filter.whitelist[sw] and filter.whitelist[sw][key]
         if filter.whitelist[sw] and filter.whitelist[sw][key] then
            return true
        end
        if sw == "card" and filter.whitelist.cardContent and filter.whitelist.cardContent[key] then
            return true
        end
        return false
     end
     end
     return not (filter.blacklist[sw] and filter.blacklist[sw][key])
     return not (filter.blacklist[sw] and filter.blacklist[sw][key])
Строка 405: Строка 590:
end
end


local function collect_entity_sets(id, componentDefs, prototypeStoreDefs, ignoreComponents, ignorePrototypes)
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, componentDefs, prototypeStoreDefs,
                                  componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
     local foundComponents, foundPrototypes = {}, {}
     local foundComponents, foundPrototypes = {}, {}


Строка 427: Строка 637:
     end)
     end)


     each_csv_value(ignoreComponents, function(name)
     apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
        foundComponents[name] = nil
     apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
    end)
 
     each_csv_value(ignorePrototypes, function(name)
        foundPrototypes[name] = nil
    end)


     return foundComponents, foundPrototypes
     return foundComponents, foundPrototypes
Строка 451: Строка 656:
     end
     end
     return basePriority
     return basePriority
end
local function get_selective_extra(dp, id, dataPage, paramNames)
    if not dp or type(dp.flattenFieldSelective) ~= "function" then
        return ""
    end
    if type(paramNames) ~= "table" or #paramNames == 0 then
        return ""
    end
    local okJson, keysJson = pcall(mw.text.jsonEncode, paramNames)
    if not okJson or not keysJson or keysJson == "" then
        return ""
    end
    local cacheKey = dataPage .. "\31" .. id .. "\31" .. keysJson
    if flattenExtraCache[cacheKey] ~= nil then
        return flattenExtraCache[cacheKey]
    end
    local extra = dp.flattenFieldSelective({ args = { id, dataPage, keysJson } }) or ""
    flattenExtraCache[cacheKey] = extra
    return extra
end
end


Строка 494: Строка 722:
end
end


local function renderBlocks(frame, state, noHeaders, entityId)
local function renderBlocks(frame, state, renderOptions, entityId, showSource)
     local outLocal = {}
     local outLocal = {}
    local noHeaders = renderOptions and renderOptions.noHeaders
    local cardFilter = renderOptions and renderOptions.cardFilter
     for _, sw in ipairs(switchModeOrder) do
     for _, sw in ipairs(switchModeOrder) do
         local mode = switchModeRegistry[sw] or {}
         local mode = switchModeRegistry[sw] or {}
Строка 502: Строка 732:
             if type(mode.render_full) == "function" then
             if type(mode.render_full) == "function" then
                 outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
                 outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
                     entityId, noHeaders)
                     entityId, noHeaders, showSource, cardFilter)
             end
             end
             if outStr and outStr ~= "" then table.insert(outLocal, outStr) end
             if outStr and outStr ~= "" then table.insert(outLocal, outStr) end
Строка 509: Строка 739:
                 local entries = state.keyToTemplates[sw][key] or {}
                 local entries = state.keyToTemplates[sw][key] or {}
                 if type(mode.render_key) == "function" then
                 if type(mode.render_key) == "function" then
                     local outStr = mode.render_key(frame, key, entries, noHeaders)
                     local outStr = mode.render_key(frame, key, entries, noHeaders, showSource)
                     if outStr and outStr ~= "" then table.insert(outLocal, outStr) end
                     if outStr and outStr ~= "" then table.insert(outLocal, outStr) end
                 end
                 end
Строка 522: Строка 752:
     local id = args[1] or ""
     local id = args[1] or ""
     if id == "" then return "" end
     if id == "" then return "" end
    local showSource = trim(args.showSource or "") == ""


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


     local ignoreComponents = args.ignoreComponents or args.ignoreComponent or ""
     local componentWhitelist = args.componentWhitelist or args.componentwhitelist or ""
     local ignorePrototypes = args.ignorePrototypes or args.ignorePrototype or ""
     local componentBlacklist = args.componentBlacklist or args.componentblacklist or ""
    local prototypeWhitelist = args.prototypeWhitelist or args.prototypewhitelist or ""
    local prototypeBlacklist = args.prototypeBlacklist or args.prototypeblacklist or ""


     local componentDefs = load_module_data("component.json")
     local componentDefs = load_module_data("component.json")
     local prototypeStoreDefs = load_module_data("prototype_store.json")
     local prototypeStoreDefs = load_module_data("prototype_store.json")
    local componentStoreDefs = load_module_data("component_store.json")
     if not componentDefs or not prototypeStoreDefs then return "" end
     if not componentDefs or not prototypeStoreDefs or not componentStoreDefs then return "" end


     local foundComponents, foundPrototypes = collect_entity_sets(id, componentDefs, prototypeStoreDefs, ignoreComponents,
     local foundComponents, foundPrototypes = collect_entity_sets(id, componentDefs, prototypeStoreDefs,
         ignorePrototypes)
         componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
     local state = new_switch_state()
     local state = new_switch_state()


Строка 562: Строка 796:


         local extra = ""
         local extra = ""
         if ok and dp and dp.flattenField then
        local paramNames = get_template_params(tplPath, content)
         if ok and dp then
             local dataPage = tplPath .. ".json"
             local dataPage = tplPath .. ".json"
             extra = dp.flattenField({ args = { id, dataPage } })
             extra = get_selective_extra(dp, id, dataPage, paramNames)
         end
         end


Строка 583: Строка 818:
     end
     end


    local componentStoreDefs = load_module_data("component_store.json")
     if type(componentStoreDefs) == "table" then
     if type(componentStoreDefs) == "table" then
         local compStore = componentStoreDefs[id]
         local compStore = componentStoreDefs[id]
Строка 607: Строка 843:
     end
     end


     local blocks = renderBlocks(frame, state, filter.hasWhitelist, id)
    renderOptions.noHeaders = filter.hasWhitelist
 
     local blocks = renderBlocks(frame, state, renderOptions, id, showSource)
     for _, b in ipairs(blocks) do
     for _, b in ipairs(blocks) do
         table.insert(out, b)
         table.insert(out, b)
Строка 619: Строка 857:
     local tplPath = args[1] or ""
     local tplPath = args[1] or ""
     if tplPath == "" then return "" end
     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)
     local content = load_template_content(tplPath)
Строка 639: Строка 881:
     }, nil, true)
     }, nil, true)


     local whitelist = parseListArg(args.whitelist or "")
     local whitelist = previewFilter.whitelist
     local hasWhitelist = next(whitelist) ~= nil
     local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist


     local out = {}
     local out = {}
     local blocks = renderBlocks(frame, state, hasWhitelist, "")
     local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
     for _, b in ipairs(blocks) do
     for _, b in ipairs(blocks) do
         table.insert(out, b)
         table.insert(out, b)

Текущая версия от 21:40, 3 мая 2026

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

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

local moduleDataCache = {}
local templateContentCache = {}
local templateMetaCache = {}
local templateArgCache = {}
local flattenExtraCache = {}
local switchModeRegistry = {}
local switchModeOrder = {}

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

local function load_module_data(page)
    local moduleName = JsonPaths.get(page)

    if moduleDataCache[moduleName] ~= nil then
        return moduleDataCache[moduleName]
    end

    local ok, data = pcall(mw.loadData, moduleName)
    if not ok then
        moduleDataCache[moduleName] = nil
        return nil
    end

    moduleDataCache[moduleName] = data
    return data
end

local function load_template_content(path)
    if templateContentCache[path] ~= nil then
        return templateContentCache[path] or nil
    end

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

    templateContentCache[path] = content or false
    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 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)
    local cached = templateArgCache[tplPath]
    if cached ~= nil then
        return cached
    end

    local params = collect_template_params(content)
    templateArgCache[tplPath] = params
    return params
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] = {}
        table.insert(state.keyOrder[sw], 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
                table.insert(parts, '<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 matches_card_list(list, callKey, compositeKey)
    if not list then
        return false
    end
    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
                        table.insert(merged.sections, 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 {}
                        table.insert(cur, compositeKey)
                        merged.labelLists[section] = cur
                    end
                end

                if allowCardEntry and tagText ~= "" then
                    if not merged.tagSet[tagText] then
                        merged.tagSet[tagText] = true
                        table.insert(merged.tags, tagText)
                    end
                end
            end
        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)
    if templateMetaCache[tplPath] ~= nil then
        return templateMetaCache[tplPath] or ""
    end

    local expanded = frame:expandTemplate {
        title = tplPath,
        args = { "json" }
    }

    local ok, data = pcall(mw.text.jsonDecode, expanded)
    if not ok or type(data) ~= "table" then
        templateMetaCache[tplPath] = false
        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
                        table.insert(cardKeys, lab)
                    end
                end
            end
        end
        data.card = cardKeys
    end

    templateMetaCache[tplPath] = data
    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 = trim(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 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 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, componentDefs, prototypeStoreDefs,
                                   componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
    local foundComponents, foundPrototypes = {}, {}

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

    each_csv_value(id, function(name)
        if name ~= id then
            if componentDefs[name] ~= nil then
                foundComponents[name] = true
            elseif prototypeStoreDefs[name] ~= nil then
                foundPrototypes[name] = 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(dp, id, dataPage, paramNames)
    if not dp or type(dp.flattenFieldSelective) ~= "function" then
        return ""
    end
    if type(paramNames) ~= "table" or #paramNames == 0 then
        return ""
    end

    local okJson, keysJson = pcall(mw.text.jsonEncode, paramNames)
    if not okJson or not keysJson or keysJson == "" then
        return ""
    end

    local cacheKey = dataPage .. "\31" .. id .. "\31" .. keysJson
    if flattenExtraCache[cacheKey] ~= nil then
        return flattenExtraCache[cacheKey]
    end

    local extra = dp.flattenFieldSelective({ args = { id, dataPage, keysJson } }) or ""
    flattenExtraCache[cacheKey] = extra
    return extra
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 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 table.insert(outLocal, 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 table.insert(outLocal, 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 componentWhitelist = args.componentWhitelist or args.componentwhitelist or ""
    local componentBlacklist = args.componentBlacklist or args.componentblacklist or ""
    local prototypeWhitelist = args.prototypeWhitelist or args.prototypewhitelist or ""
    local prototypeBlacklist = args.prototypeBlacklist or args.prototypeblacklist or ""

    local componentDefs = load_module_data("component.json")
    local prototypeStoreDefs = load_module_data("prototype_store.json")
    if not componentDefs or not prototypeStoreDefs then return "" end

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

    local ok, dp = pcall(require, "Module:GetField")
    local errors = {}

    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 filter.hasWhitelist then
                return
            end
            errors[#errors + 1] = build_missing_template_error(kind, name, isStore, tplPath)
            return
        end

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

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

        add_entries_from_meta(state, parsed, {
            tplPath = tplPath,
            id = id,
            extra = extra,
            source = make_source(kind, name, pathName, tplPath),
            priority = resolve_priority(parsed)
        }, filter, false)
    end

    for compName in pairs(foundComponents) do
        processEntity("component", compName, false)
    end
    for protoName in pairs(foundPrototypes) do
        processEntity("prototype", protoName, false)
    end

    local componentStoreDefs = load_module_data("component_store.json")
    if type(componentStoreDefs) == "table" then
        local compStore = componentStoreDefs[id]
        if type(compStore) == "table" then
            for compName in pairs(compStore) do
                processEntity("component", compName, true)
            end
        end
    end

    if type(prototypeStoreDefs) == "table" then
        local protoStore = prototypeStoreDefs[id]
        if type(protoStore) == "table" then
            for protoName in pairs(protoStore) do
                processEntity("prototype", protoName, true)
            end
        end
    end

    local out = {}

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

    renderOptions.noHeaders = filter.hasWhitelist

    local blocks = renderBlocks(frame, state, renderOptions, id, showSource)
    for _, b in ipairs(blocks) do
        table.insert(out, 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 whitelist = previewFilter.whitelist
    local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist

    local out = {}
    local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
    for _, b in ipairs(blocks) do
        table.insert(out, b)
    end

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

return p