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

Материал из Space Station 14 Вики
мНет описания правки
Нет описания правки
 
(не показано 106 промежуточных версий этого же участника)
Строка 1: Строка 1:
---                                        ---
local p = {}
---    LOCAL ENVIRONMENT                  ---
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 = {}


--[[ Abstract utilities ]]--
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)
-- Helper function for `string.gsub()` (for managing zero-padded numbers)
    if not str or str == "" then
local function zero_padded (str)
        return
return ('%03d%s'):format(#str, str)
    end
    for item in string.gmatch(str, "[^,]+") do
        local value = trim(item)
        if value ~= "" then
            fn(value)
        end
    end
end
end


 
local function load_module_data(page)
-- Helper function for `table.sort()` (for natural sorting)
    local moduleName = JsonPaths.get(page)
local function natural_sort (var1, var2)
    local ok, data = pcall(mw.loadData, moduleName)
return tostring(var1):gsub('%d+', zero_padded) <
    if not ok then
tostring(var2):gsub('%d+', zero_padded)
        return nil
    end
    return data
end
end


 
local function load_template_content(path)
-- Return a copy or a reference to a table
    local title = mw.title.new("Template:" .. path)
local function copy_or_ref_table (src, refonly)
    if not title then
if refonly then return src end
        return nil
newtab = {}
    end
for key, val in pairs(src) do newtab[key] = val end
    local ok, content = pcall(title.getContent, title)
return newtab
    if not ok then
        return nil
    end
    return content
end
end


 
local function lcfirst(s)
-- Remove numerical elements from a table, shifting everything to the left
    if not s or s == "" then return s end
local function remove_numerical_keys (tbl, idx, len)
    return string.lower(s:sub(1, 1)) .. (s:sub(2) or "")
local cache = {}
local tmp = idx + len - 1
for key, val in pairs(tbl) do
if type(key) == 'number' and key >= idx then
if key > tmp then cache[key - len] = val end
tbl[key] = nil
end
end
for key, val in pairs(cache) do tbl[key] = val end
end
end


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


-- Make a reduced copy of a table (shifting in both directions if necessary)
    local page = "prototype/Entity/" .. entityId .. ".json"
local function copy_table_reduced (tbl, idx, len)
    local moduleName = JsonPaths.get(page)
local ret = {}
    local ok, data = pcall(mw.loadData, moduleName)
local tmp = idx + len - 1
    if not ok or type(data) ~= "table" then
if idx > 0 then
        return nil
for key, val in pairs(tbl) do
    end
if type(key) ~= 'number' or key < idx then
    return data
ret[key] = val
elseif key > tmp then ret[key - len] = val end
end
elseif tmp > 0 then
local nshift = 1 - idx
for key, val in pairs(tbl) do
if type(key) ~= 'number' then ret[key] = val
elseif key > tmp then ret[key - tmp] = val
elseif key < idx then ret[key + nshift] = val end
end
else
for key, val in pairs(tbl) do
if type(key) ~= 'number' or key > tmp then
ret[key] = val
elseif key < idx then ret[key + len] = val end
end
end
return ret
end
end


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


-- Make an expanded copy of a table (shifting in both directions if necessary)
    name = trim(name)
--[[
    if name == "" then
local function copy_table_expanded (tbl, idx, len)
        return nil
local ret = {}
    end
local tmp = idx + len - 1
if idx > 0 then
for key, val in pairs(tbl) do
if type(key) ~= 'number' or key < idx then
ret[key] = val
else ret[key + len] = val end
end
elseif tmp > 0 then
local nshift = idx - 1
for key, val in pairs(tbl) do
if type(key) ~= 'number' then ret[key] = val
elseif key > 0 then ret[key + tmp] = val
elseif key < 1 then ret[key + nshift] = val end
end
else
for key, val in pairs(tbl) do
if type(key) ~= 'number' or key > tmp then
ret[key] = val
else ret[key - len] = val end
end
end
return ret
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


-- Move a key from a table to another, but only if under a different name and
    return name
-- always parsing numerical strings as numbers
local function steal_if_renamed (val, src, skey, dest, dkey)
local realkey = tonumber(dkey) or dkey:match'^%s*(.-)%s*$'
if skey ~= realkey then
dest[realkey] = val
src[skey] = nil
end
end
end


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


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


--[[ Public strings ]]--
    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


-- Special match keywords (functions and modifiers MUST avoid these names)
    table.sort(out)
local mkeywords = {
    return out
['or'] = 0,
end
pattern = 1,
plain = 2,
strict = 3
}


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


-- Sort functions (functions and modifiers MUST avoid these names)
    local getter = dp.getEntityComponents or dp.collectEntityComponents or dp.getComp
local sortfunctions = {
    if type(getter) ~= "function" then
--alphabetically = false, -- Simply uncommenting enables the option
        return nil
naturally = natural_sort
    end
}


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


-- Callback styles for the `mapping_*` and `renaming_*` class of modifiers
    if type(result) == "table" then
-- (functions and modifiers MUST avoid these names)
        return result
--[[
    end


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


  col[1] = Loop type (0-3)
    return nil
  col[2] = Number of module arguments that the style requires (1-3)
end
  col[3] = Minimum number of sequential parameters passed to the callback
  col[4] = Name of the callback parameter where to place each parameter name
  col[5] = Name of the callback parameter where to place each parameter value
  col[6] = Argument in the modifier's invocation that will override `col[4]`
  col[7] = Argument in the modifier's invocation that will override `col[5]`


A value of `-1` indicates that no meaningful value is stored (i.e. `nil`)
local function load_entity_components(entityId)
    local viaDp = load_entity_components_from_dp(entityId)
    if type(viaDp) == "table" and next(viaDp) ~= nil then
        local out = {}
        local seen = {}
        if #viaDp > 0 then
            for _, v in ipairs(viaDp) do
                local name = normalize_component_name(v)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        else
            for k in pairs(viaDp) do
                local name = normalize_component_name(k)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        end
        table.sort(out)
        return out
    end


]]--
    local entity = load_entity_data(entityId)
local mapping_styles = {
    return collect_entity_components(entity)
names_and_values = { 3, 2, 2, 1, 2, -1, -1 },
end
values_and_names = { 3, 2, 2, 2, 1, -1, -1 },
values_only = { 1, 2, 1, -1, 1, -1, -1 },
names_only = { 2, 2, 1, 1, -1, -1, -1 },
names_and_values_as = { 3, 4, 0, -1, -1, 2, 3 },
names_only_as = { 2, 3, 0, -1, -1, 2, -1 },
values_only_as = { 1, 3, 0, -1, -1, -1, 2 },
blindly = { 0, 2, 0, -1, -1, -1, -1 }
}


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


-- Memory slots (functions and modifiers MUST avoid these names)
    if type(entityOrId) == "string" then
local memoryslots = {
        local entity = load_entity_data(entityOrId)
i = 'itersep',
        if not entity then
l = 'lastsep',
            return false
p = 'pairsep',
        end
h = 'header',
        local comps = collect_entity_components(entity)
f = 'footer',
        for _, v in ipairs(comps) do
n = 'ifngiven'
            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


-- Functions and modifiers MUST avoid these names too: `let`
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 = {}


--[[ Module's private environment ]]--
    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


-- Functions listed here declare that they don't need the `frame.args`
    return params
-- metatable to be copied into a regular table; if they are modifiers they also
end
-- guarantee that they will make available their own (modified) copy
local refpipe = {
count = true,
value_of = true,
list = true,
list_values = true,
for_each = true,
call_for_each_group = true
}


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


-- Functions listed here declare that they don't need the
local function sort_entries_by_priority(entries)
-- `frame:getParent().args` metatable to be copied into a regular table; if
    table.sort(entries, function(a, b)
-- they are modifiers they also guarantee that they will make available their
        if a.priority == b.priority then return a.idx < b.idx end
-- own (modified) copy
        return a.priority > b.priority
local refparams = {
    end)
--inserting = true,
end
grouping_by_calling = true,
count = true,
concat_and_call = true,
concat_and_invoke = true,
concat_and_magic = true,
value_of = true,
call_for_each_group = true
}


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


-- Maximum number of numerical parameters that can be filled, if missing (we
local function register_switch_mode(name, cfg)
-- chose an arbitrary number for this constant; you can discuss about its
    switchModeRegistry[name] = cfg or {}
-- optimal value at Module talk:Params)
    switchModeOrder[#switchModeOrder + 1] = name
local maxfill = 1024
end


 
local function new_switch_state()
-- The private table of functions
    local state = { keyOrder = {}, keyToTemplates = {}, keySources = {} }
local library = {}
    for _, sw in ipairs(switchModeOrder) do
 
        state.keyOrder[sw] = {}
 
        state.keyToTemplates[sw] = {}
-- Functions that can only be invoked in first position
        state.keySources[sw] = {}
local static_iface = {}
    end
 
    return state
 
-- Create a new context
local function context_new ()
local ctx = {}
ctx.luaname = 'Module:Params' --[[ or `frame:getTitle()` ]]--
ctx.iterfunc = pairs
ctx.sorttype = 0
ctx.firstposonly = static_iface
ctx.n_available = maxfill
return ctx
end
end


 
local function ensure_switch_key(state, sw, key)
-- Move to the next action within the user-given list
    local byKey = state.keyToTemplates[sw]
local function context_iterate (ctx, n_forward)
    if not byKey[key] then
local nextfn
        byKey[key] = {}
if ctx.pipe[n_forward] ~= nil then
        state.keyOrder[sw][#state.keyOrder[sw] + 1] = key
nextfn = ctx.pipe[n_forward]:match'^%s*(.*%S)'
    end
end
    return byKey[key]
if nextfn == nil then error(ctx.luaname ..
': You must specify a function to call', 0) end
if library[nextfn] == nil then
if ctx.firstposonly[nextfn] == nil then error(ctx.luaname ..
': The function ‘' .. nextfn .. '’ does not exist', 0)
else error(ctx.luaname .. ': The ‘' .. nextfn ..
'’ directive can only appear in first position', 0)
end
end
remove_numerical_keys(ctx.pipe, 1, n_forward)
return library[nextfn]
end
end


 
local function add_switch_entry(state, sw, key, entry)
-- Main loop
    local bucket = ensure_switch_key(state, sw, key)
local function main_loop (ctx, start_with)
    entry.idx = #bucket + 1
local fn = start_with
    bucket[#bucket + 1] = entry
repeat fn = fn(ctx) until not fn
end
end


 
local function collect_tpl_calls(entries)
-- Parse user arguments of type `...|[let]|[...][number of additional
    local tplCalls = {}
-- parameters]|[parameter 1]|[parameter 2]|[...]`
    local sources = {}
local function parse_child_args (src, start_from, append_after)
    if #entries > 0 then
local names
        sort_entries_by_priority(entries)
local tmp
        for _, e in ipairs(entries) do
local dest = {}
            tplCalls[#tplCalls + 1] = e.tpl
local pin = start_from
            sources[#sources + 1] = e.source
if src[pin] ~= nil and src[pin]:match'^%s*let%s*$' then
        end
names = {}
    end
repeat
    return tplCalls, sources
tmp = src[pin + 1] or ''
names[tonumber(tmp) or tmp:match'^%s*(.-)%s*$' or ''] =
src[pin + 2]
pin = pin + 3
until src[pin] == nil or not src[pin]:match'^%s*let%s*$'
end
tmp = tonumber(src[pin])
if tmp ~= nil then
if tmp < 0 then tmp = -1 end
local shf = append_after - pin
for idx = pin + 1, pin + tmp do dest[idx + shf] = src[idx] end
pin = pin + tmp + 1
end
if names ~= nil then
for key, val in pairs(names) do dest[key] = val end
end
return dest, pin
end
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 "")


-- Parse the arguments of some of the `mapping_*` and `renaming_*` class of
    local tplLabel = "Template:" .. s.tplPath
-- modifiers
    return "[[" .. tplLabel .. "|" .. className .. "]]"
local function parse_callback_args (src, n_skip, default_style)
local style
local shf
local tmp = src[n_skip + 1]
if tmp ~= nil then style = mapping_styles[tmp:match'^%s*(.-)%s*$'] end
if style == nil then
style = default_style
shf = n_skip - 1
else shf = n_skip end
local n_exist = style[3]
local karg = style[4]
local varg = style[5]
tmp = style[6]
if tmp > -1 then
tmp = src[tmp + shf]
karg = tonumber(tmp)
if karg == nil then karg = tmp:match'^%s*(.-)%s*$'
else n_exist = math.max(n_exist, karg) end
end
tmp = style[7]
if tmp > -1 then
tmp = src[tmp + shf]
varg = tonumber(tmp)
if varg == nil then varg = tmp:match'^%s*(.-)%s*$'
else n_exist = math.max(n_exist, varg) end
end
local dest, nargs = parse_child_args(src, style[2] + shf, n_exist)
tmp = style[1]
if (tmp == 3 or tmp == 2) and dest[karg] ~= nil then
tmp = tmp - 2 end
if (tmp == 3 or tmp == 1) and dest[varg] ~= nil then
tmp = tmp - 1 end
return dest, nargs, tmp, karg, varg
end
end


 
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
-- Parse the arguments of some of the `mapping_*` and `renaming_*` class of
    local parts = {}
-- modifiers
    if tplCalls and #tplCalls > 0 then
local function parse_replace_args (opts, fname)
        for i, tpl in ipairs(tplCalls) do
if opts[1] == nil then error(ctx.luaname ..
            local add = true
', ‘' .. fname .. '’: No pattern string was given', 0) end
            if frame then
if opts[2] == nil then error(ctx.luaname ..
                local expanded = frame:preprocess(tpl)
', ‘' .. fname .. '’: No replacement string was given', 0) end
                add = expanded and trim(expanded) ~= ""
local ptn = opts[1]
            end
local repl = opts[2]
            if add then
local argc = 3
                local src = sources and sources[i]
local nmax = tonumber(opts[3])
                local line = '<div>' .. tpl .. '</div>'
if nmax ~= nil or (opts[3] or ''):match'^%s*$' ~= nil then argc = 4 end
                if showSource and src then
local flg = opts[argc]
                    line = line .. '<div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
if flg ~= nil then flg = mkeywords[flg:match'^%s*(.-)%s*$'] end
                end
if flg == 0 then flg = nil elseif flg ~= nil then argc = argc + 1 end
                parts[#parts + 1] = '<div class="ts-Сущность">' .. line .. '</div>'
return ptn, repl, nmax, flg == 3, argc, (nmax ~= nil and nmax < 1) or
            end
(flg == 3 and ptn == repl)
        end
        if #parts == 0 then
            return ""
        end
        if includeHeader then
            table.insert(parts, 1, "<h2>" .. mw.text.encode(key) .. "</h2>")
        end
    end
    return table.concat(parts, "\n")
end
end


 
local function split_title_key(key)
-- Parse the arguments of the `with_*_matching` class of modifiers
    local main, sub = (key or ""):match("^([^_]+)_(.+)$")
local function parse_pattern_args (ctx, fname)
    if main and sub then
local state = 0
        return main, sub
local cnt = 1
    end
local keyw
    return key, nil
local nptns = 0
local ptns = {}
for _, val in ipairs(ctx.pipe) do
if state == 0 then
nptns = nptns + 1
ptns[nptns] = { val, false, false }
state = -1
else
keyw = val:match'^%s*(.*%S)'
if keyw == nil or mkeywords[keyw] == nil or (
state > 0 and mkeywords[keyw] > 0
) then break
else
state = mkeywords[keyw]
if state > 1 then ptns[nptns][2] = true end
if state == 3 then ptns[nptns][3] = true end
end
end
cnt = cnt + 1
end
if state == 0 then error(ctx.luaname .. ', ‘' .. fname ..
'’: No pattern was given', 0) end
return ptns, cnt
end
end


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


-- Map parameters' values using a custom callback and a referenced table
    for _, key in ipairs(keyOrder or {}) do
local value_maps = {
        local mainTitle, subTitle = split_title_key(key)
[0] = function (tbl, margs, karg, varg, fn)
        if mainTitle and mainTitle ~= "" then
for key in pairs(tbl) do tbl[key] = fn() end
            local group = groups[mainTitle]
end,
            if not group then
[1] = function (tbl, margs, karg, varg, fn)
                group = { blocks = {} }
for key, val in pairs(tbl) do
                groups[mainTitle] = group
margs[varg] = val
                groupOrder[#groupOrder + 1] = mainTitle
tbl[key] = fn()
            end
end
end,
[2] = function (tbl, margs, karg, varg, fn)
for key in pairs(tbl) do
margs[karg] = key
tbl[key] = fn()
end
end,
[3] = function (tbl, margs, karg, varg, fn)
for key, val in pairs(tbl) do
margs[karg] = key
margs[varg] = val
tbl[key] = fn()
end
end
}


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


-- Private table for `map_names()`
    local out = {}
local name_thieves_maps = {
    for _, mainTitle in ipairs(groupOrder) do
[0] = function (cache, tbl, rargs, karg, varg, fn)
        local group = groups[mainTitle]
for key, val in pairs(tbl) do
        local parts = {}
steal_if_renamed(val, tbl, key, cache, fn())
end
end,
[1] = function (cache, tbl, rargs, karg, varg, fn)
for key, val in pairs(tbl) do
rargs[varg] = val
steal_if_renamed(val, tbl, key, cache, fn())
end
end,
[2] = function (cache, tbl, rargs, karg, varg, fn)
for key, val in pairs(tbl) do
rargs[karg] = key
steal_if_renamed(val, tbl, key, cache, fn())
end
end,
[3] = function (cache, tbl, rargs, karg, varg, fn)
for key, val in pairs(tbl) do
rargs[karg] = key
rargs[varg] = val
steal_if_renamed(val, tbl, key, cache, fn())
end
end
}


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


-- Map parameters' names using a custom callback and a referenced table
        for _, block in ipairs(group.blocks or {}) do
local function map_names (tbl, rargs, karg, varg, looptype, fn)
            local tplCalls, sources = collect_tpl_calls(block.entries or {})
local cache = {}
            local blockText = renderTitleBlock(block.subTitle or mainTitle, tplCalls, sources, false, frame, showSource)
name_thieves_maps[looptype](cache, tbl, rargs, karg, varg, fn)
            if blockText ~= "" then
for key, val in pairs(cache) do tbl[key] = val end
                if block.subTitle and not noHeaders then
end
                    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 a new table that contains `src` regrouped according to the numerical
    return table.concat(out, "\n")
-- suffixes in its keys
local function make_groups (src)
-- NOTE: `src` might be the original metatable!
local tmp
local prefix
local gid
local groups = {}
for key, val in pairs(src) do
-- `key` must only be a string or a number...
gid = tonumber(key)
if gid == nil then
prefix, gid = key:match'^%s*(.-)%s*(%-?%d*)%s*$'
gid = tonumber(gid) or ''
else prefix = '' end
if groups[gid] == nil then groups[gid] = {} end
tmp = tonumber(prefix)
if tmp ~= nil then
if tmp < 1 then prefix = tmp - 1 else prefix = tmp end
end
groups[gid][prefix] = val
end
return groups
end
end


 
local function normalizeFilterKey(s)
-- Concatenate the numerical keys from the table of parameters to the numerical
    s = trim(s or "")
-- keys from the table of options; non-numerical keys from the table of options
    s = s:gsub("%s*_%s*", "_")
-- will prevail over colliding non-numerical keys from the table of parameters
    return s
local function concat_params (ctx)
local tbl = ctx.params
local size = table.maxn(ctx.pipe)
local retval = {}
if ctx.subset == 1 then
-- We need only the sequence
for key, val in ipairs(tbl) do retval[key + size] = val end
else
if ctx.subset == -1 then
for key, val in ipairs(tbl) do tbl[key] = nil end
end
for key, val in pairs(tbl) do
if type(key) == 'number' then retval[key + size] = val
else retval[key] = val end
end
end
for key, val in pairs(ctx.pipe) do retval[key] = val end
return retval
end
end


 
local function matches_card_list(list, callKey, compositeKey)
-- Flush the parameters by calling a custom function for each value (after this
    if not list then
-- function has been invoked `ctx.params` will be no longer usable)
        return false
local function flush_params (ctx, fn)
    end
local tbl = ctx.params
    callKey = normalizeFilterKey(callKey)
if ctx.subset == 1 then
    compositeKey = normalizeFilterKey(compositeKey)
for key, val in ipairs(tbl) do fn(key, val) end
    return list[callKey] or list[compositeKey] or false
return
end
if ctx.subset == -1 then
for key, val in ipairs(tbl) do tbl[key] = nil end
end
if ctx.sorttype > 0 then
local nums = {}
local words = {}
local nn = 0
local nw = 0
for key, val in pairs(tbl) do
if type(key) == 'number' then
nn = nn + 1
nums[nn] = key
else
nw = nw + 1
words[nw] = key
end
end
table.sort(nums)
table.sort(words, natural_sort)
if ctx.sorttype == 2 then
for idx = 1, nw do fn(words[idx], tbl[words[idx]]) end
for idx = 1, nn do fn(nums[idx], tbl[nums[idx]]) end
return
end
for idx = 1, nn do fn(nums[idx], tbl[nums[idx]]) end
for idx = 1, nw do fn(words[idx], tbl[words[idx]]) end
return
end
if ctx.subset ~= -1 then
for key, val in ipairs(tbl) do
fn(key, val)
tbl[key] = nil
end
end
for key, val in pairs(tbl) do fn(key, val) end
end
end


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


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


--[[ Modifiers ]]--
    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


-- Syntax:  #invoke:params|sequential|pipe to
    for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
library.sequential = function (ctx)
        if displayLabel and displayLabel ~= "" then
if ctx.subset == -1 then error(ctx.luaname ..
            parts[#parts + 1] = mw.text.encode(compositeKey .. "_label") .. "=" .. displayLabel
': The two directives ‘non-sequential’ and ‘sequential’ are in contradiction with each other', 0) end
        end
if ctx.sorttype > 0 then error(ctx.luaname ..
    end
': The ‘all_sorted’ and ‘reassorted’ directives are redundant when followed by ‘sequential’', 0) end
    for compositeKey, content in pairs(merged.contentByKey or {}) do
ctx.iterfunc = ipairs
        if content and content ~= "" then
ctx.subset = 1
            parts[#parts + 1] = mw.text.encode(compositeKey) .. "=" .. content
return context_iterate(ctx, 1)
        end
end
    end


    if #parts == 0 then
        return ""
    end


-- Syntax:  #invoke:params|non-sequential|pipe to
    return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
library['non-sequential'] = function (ctx)
if ctx.subset == 1 then error(ctx.luaname ..
': The two directives ‘sequential’ and ‘non-sequential’ are in contradiction with each other', 0) end
ctx.iterfunc = pairs
ctx.subset = -1
return context_iterate(ctx, 1)
end
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 "Сущность"


-- Syntax:  #invoke:params|all_sorted|pipe to
                local isWhitelisted = cardFilter and matches_card_list(cardFilter.whitelist, callKey, compositeKey) or
library.all_sorted = function (ctx)
                    false
if ctx.subset == 1 then error(ctx.luaname ..
                local isBlacklisted = cardFilter and matches_card_list(cardFilter.blacklist, callKey, compositeKey) or
': The ‘all_sorted’ directive is redundant after ‘sequential’', 0) end
                    false
if ctx.sorttype == 2 then error(ctx.luaname ..
                if isWhitelisted and content ~= "" then
': The two directives ‘reassorted’ and ‘sequential’ are in contradiction with each other', 0) end
                    rawContentParts[#rawContentParts + 1] = content
ctx.sorttype = 1
                end
return context_iterate(ctx, 1)
end


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


-- Syntax:  #invoke:params|reassorted|pipe to
                if allowCardEntry and (displayLabel ~= "" or content ~= "") then
library.reassorted = function (ctx)
                    if not merged.sectionsMap[section] then
if ctx.subset == 1 then error(ctx.luaname ..
                        merged.sectionsMap[section] = true
': The ‘reassorted’ directive is redundant after ‘sequential’', 0) end
                        merged.sections[#merged.sections + 1] = section
if ctx.sorttype == 1 then error(ctx.luaname ..
                    end
': The two directives ‘sequential’ and ‘reassorted’ are in contradiction with each other', 0) end
                    if displayLabel ~= "" and (not merged.labelOverrides[compositeKey] or merged.labelOverrides[compositeKey] == "") then
ctx.sorttype = 2
                        merged.labelOverrides[compositeKey] = displayLabel
return context_iterate(ctx, 1)
                    end
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


-- Syntax:  #invoke:params|setting|directives|...|pipe to
                if allowCardEntry and tagText ~= "" then
library.setting = function (ctx)
                    if not merged.tagSet[tagText] then
local opts = ctx.pipe
                        merged.tagSet[tagText] = true
local cmd = opts[1]
                        merged.tags[#merged.tags + 1] = tagText
if cmd ~= nil then
                    end
cmd = cmd:gsub('%s+', ''):gsub('/+', '/'):match'^/*(.*[^/])'
                end
end
            end
if cmd == nil then error(ctx.luaname ..
        end
', ‘setting’: No directive was given', 0) end
    end
local sep = string.byte('/')
local argc = 2
local dest = {}
local vname
local chr
for idx = 1, #cmd do
chr = cmd:byte(idx)
if chr == sep then
for key, val in ipairs(dest) do
ctx[val] = opts[argc]
dest[key] = nil
end
argc = argc + 1
else
vname = memoryslots[string.char(chr)]
if vname == nil then error(ctx.luaname ..
', ‘setting’: Unknown slot ‘' ..
string.char(chr) .. '’', 0) end
table.insert(dest, vname)
end
end
for key, val in ipairs(dest) do ctx[val] = opts[argc] end
return context_iterate(ctx, argc + 1)
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)


-- Syntax:  #invoke:params|squeezing|pipe to
    local out = {}
library.squeezing = function (ctx)
    if #rawContentParts > 0 then
local tbl = ctx.params
        out[#out + 1] = table.concat(rawContentParts, "\n")
local store = {}
    end
local indices = {}
local newlen = 0
for key, val in pairs(tbl) do
if type(key) == 'number' then
newlen = newlen + 1
indices[newlen] = key
store[key] = val
tbl[key] = nil
end
end
table.sort(indices)
for idx = 1, newlen do tbl[idx] = store[indices[idx]] end
return context_iterate(ctx, 1)
end


    local cardCall = buildCardCall(merged, entityId)


-- Syntax:  #invoke:params|filling_the_gaps|pipe to
    if noHeaders then
library.filling_the_gaps = function (ctx)
        local hasLabel = false
local tbl = ctx.params
        for _, v in pairs(merged.labelOverrides or {}) do
local nmin = 1
            if v and v ~= "" then
local nmax = nil
                hasLabel = true
local nnums = -1
                break
local tmp = {}
            end
for key, val in pairs(tbl) do
        end
if type(key) == 'number' then
        if not hasLabel then
if nmax == nil then
            for _, lst in pairs(merged.labelLists or {}) do
if key < nmin then nmin = key end
                if #lst > 0 then
nmax = key
                    hasLabel = true
elseif key > nmax then nmax = key
                    break
elseif key < nmin then nmin = key end
                end
nnums = nnums + 1
            end
tmp[key] = val
        end
end
end
if nmax ~= nil and nmax - nmin > nnums then
ctx.n_available = ctx.n_available + nmin + nnums - nmax
if ctx.n_available < 0 then error(ctx.luaname ..
', ‘filling_the_gaps’: It is possible to fill at most ' ..
tostring(maxfill) .. ' parameters', 0) end
for idx = nmin, nmax, 1 do tbl[idx] = '' end
for key, val in pairs(tmp) do tbl[key] = val end
end
return context_iterate(ctx, 1)
end


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


-- Syntax:  #invoke:params|clearing|pipe to
        if not hasLabel and not hasContent then
library.clearing = function (ctx)
            return table.concat(out, "\n")
local tbl = ctx.params
        end
local numericals = {}
    end
for key, val in pairs(tbl) do
if type(key) == 'number' then
numericals[key] = val
tbl[key] = nil
end
end
for key, val in ipairs(numericals) do tbl[key] = val end
return context_iterate(ctx, 1)
end


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


-- Syntax:  #invoke:params|cutting|left cut|right cut|pipe to
    return table.concat(out, "\n")
library.cutting = function (ctx)
local lcut = tonumber(ctx.pipe[1])
if lcut == nil then error(ctx.luaname ..
', ‘cutting’: Left cut must be a number', 0) end
local rcut = tonumber(ctx.pipe[2])
if rcut == nil then error(ctx.luaname ..
', ‘cutting’: Right cut must be a number', 0) end
local tbl = ctx.params
local len = #tbl
if lcut < 0 then lcut = len + lcut end
if rcut < 0 then rcut = len + rcut end
local tot = lcut + rcut
if tot > 0 then
local cache = {}
if tot >= len then
for key in ipairs(tbl) do tbl[key] = nil end
tot = len
else
for idx = len - rcut + 1, len, 1 do tbl[idx] = nil end
for idx = 1, lcut, 1 do tbl[idx] = nil end
end
for key, val in pairs(tbl) do
if type(key) == 'number' and key > 0 then
if key > len then cache[key - tot] = val
else cache[key - lcut] = val end
tbl[key] = nil
end
end
for key, val in pairs(cache) do tbl[key] = val end
end
return context_iterate(ctx, 3)
end
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
})


-- Syntax:  #invoke:params|cropping|left crop|right crop|pipe to
register_switch_mode("title", {
library.cropping = function (ctx)
    full = true,
local lcut = tonumber(ctx.pipe[1])
    build_entry = function(ctx, key)
if lcut == nil then error(ctx.luaname ..
        return {
', ‘cropping’: Left crop must be a number', 0) end
            tpl = makeTplCall(ctx.tplPath, "title", key, ctx.id, ctx.extra),
local rcut = tonumber(ctx.pipe[2])
            source = ctx.source,
if rcut == nil then error(ctx.luaname ..
            priority = ctx.priority
', ‘cropping’: Right crop must be a number', 0) end
        }
local tbl = ctx.params
    end,
local nmin
    build_preview_entry = function(ctx, key)
local nmax
        return {
for key in pairs(tbl) do
            tpl = makeTplCall(ctx.tplPath, "title", key, ""),
if type(key) == 'number' then
            source = ctx.source,
if nmin == nil then
            priority = ctx.priority
nmin = key
        }
nmax = key
    end,
elseif key > nmax then nmax = key
    render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource)
elseif key < nmin then nmin = key end
        return renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
end
    end
end
})
if nmin ~= nil then
local len = nmax - nmin + 1
if lcut < 0 then lcut = len + lcut end
if rcut < 0 then rcut = len + rcut end
if lcut + rcut - len > -1 then
for key in pairs(tbl) do
if type(key) == 'number' then tbl[key] = nil end
end
elseif lcut + rcut > 0 then
for idx = nmax - rcut + 1, nmax do tbl[idx] = nil end
for idx = nmin, nmin + lcut - 1 do tbl[idx] = nil end
local lshift = nmin + lcut - 1
if lshift > 0 then
for idx = lshift + 1, nmax, 1 do
tbl[idx - lshift] = tbl[idx]
tbl[idx] = nil
end
end
end
end
return context_iterate(ctx, 3)
end


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


-- Syntax:  #invoke:params|purging|start offset|length|pipe to
    local ok, data = pcall(mw.text.jsonDecode, expanded)
library.purging = function (ctx)
    if not ok or type(data) ~= "table" then
local idx = tonumber(ctx.pipe[1])
        return ""
if idx == nil then error(ctx.luaname ..
    end
', ‘purging’: Start offset must be a number', 0) end
local len = tonumber(ctx.pipe[2])
if len == nil then error(ctx.luaname ..
', ‘purging’: Length must be a number', 0) end
local tbl = ctx.params
if len < 1 then
len = len + table.maxn(tbl)
if idx > len then return context_iterate(ctx, 3) end
len = len - idx + 1
end
ctx.params = copy_table_reduced(tbl, idx, len)
return context_iterate(ctx, 3)
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


-- Syntax:  #invoke:params|backpurging|start offset|length|pipe to
    return data
library.backpurging = function (ctx)
local last = tonumber(ctx.pipe[1])
if last == nil then error(ctx.luaname ..
', ‘backpurging’: Start offset must be a number', 0) end
local len = tonumber(ctx.pipe[2])
if len == nil then error(ctx.luaname ..
', ‘backpurging’: Length must be a number', 0) end
local idx
local tbl = ctx.params
if len > 0 then
idx = last - len + 1
else
for key in pairs(tbl) do
if type(key) == 'number' and (idx == nil or
key < idx) then idx = key end
end
if idx == nil then return context_iterate(ctx, 3) end
idx = idx - len
if last < idx then return context_iterate(ctx, 3) end
len = last - idx + 1
end
ctx.params = copy_table_reduced(ctx.params, idx, len)
return context_iterate(ctx, 3)
end
end


 
local function parseListArg(str)
-- Syntax:  #invoke:params|rotating|pipe to
    local res = {}
library.rotating = function (ctx)
    if not str or str == "" then return res end
local tbl = ctx.params
    for item in string.gmatch(str, "[^,]+") do
local numericals = {}
        local s = normalizeFilterKey(item)
local nmax = 0
        if s ~= "" then
for key, val in pairs(tbl) do
            local a, b = s:match("^([^_]+)_(.+)$")
if type(key) == 'number' then
            if a and b then
numericals[key] = val
                res[a] = res[a] or {}
tbl[key] = nil
                res[a][b] = true
if key > nmax then nmax = key end
            end
end
        end
end
    end
for key, val in pairs(numericals) do tbl[nmax - key + 1] = val end
    return res
return context_iterate(ctx, 1)
end
end


 
local function build_key_filter(args)
-- Syntax:  #invoke:params|pivoting|pipe to
    local filter = {}
--[[
    filter.blacklist = parseListArg(args.blacklist or "")
library.pivoting = function (ctx)
    filter.whitelist = parseListArg(args.whitelist or "")
local tbl = ctx.params
    filter.hasWhitelist = false
local shift = #tbl + 1
    for _, sw in ipairs(switchModeOrder) do
if shift < 2 then return library.rotating(ctx) end
        if filter.whitelist[sw] and next(filter.whitelist[sw]) ~= nil then
local numericals = {}
            filter.hasWhitelist = true
for key, val in pairs(tbl) do
            break
if type(key) == 'number' then
        end
numericals[key] = val
    end
tbl[key] = nil
    if not filter.hasWhitelist and filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil then
end
        filter.hasWhitelist = true
end
    end
for key, val in pairs(numericals) do tbl[shift - key] = val end
    return filter
return context_iterate(ctx, 1)
end
end
]]--


 
local function build_render_options(filter)
-- Syntax:  #invoke:params|mirroring|pipe to
    return {
--[[
        noHeaders = false,
library.mirroring = function (ctx)
        cardFilter = {
local tbl = ctx.params
            hasWhitelist = filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil or false,
local numericals = {}
            cardWhitelist = filter.whitelist.card or {},
local nmax
            blacklist = filter.blacklist.cardContent or {},
local nmin
            whitelist = filter.whitelist.cardContent or {}
for key, val in pairs(tbl) do
        }
if type(key) == 'number' then
    }
numericals[key] = val
tbl[key] = nil
if nmax == nil then
nmax = key
nmin = key
elseif key > nmax then nmax = key
elseif key < nmin then nmin = key end
end
end
for key, val in pairs(numericals) do tbl[nmax + nmin - key] = val end
return context_iterate(ctx, 1)
end
end
]]--


 
local function should_include_key(filter, sw, key)
-- Syntax:  #invoke:params|swapping|pipe to
    if filter.hasWhitelist then
--[[
        if filter.whitelist[sw] and filter.whitelist[sw][key] then
library.swapping = function (ctx)
            return true
local tbl = ctx.params
        end
local cache = {}
        if sw == "card" and filter.whitelist.cardContent and filter.whitelist.cardContent[key] then
local nsize = 0
            return true
local tmp
        end
for key in pairs(tbl) do
        return false
if type(key) == 'number' then
    end
nsize = nsize + 1
    return not (filter.blacklist[sw] and filter.blacklist[sw][key])
cache[nsize] = key
end
end
table.sort(cache)
for idx = math.floor(nsize / 2), 1, -1 do
tmp = tbl[cache[idx] ]
tbl[cache[idx] ] = tbl[cache[nsize - idx + 1] ]
tbl[cache[nsize - idx + 1] ] = tmp
end
return context_iterate(ctx, 1)
end
end
]]--


-- Syntax:  #invoke:params|sorting_sequential_values|[criterion]|pipe to
local function parse_csv_set(str)
library.sorting_sequential_values = function (ctx)
    local res = {}
local sortfn
    each_csv_value(str, function(name)
if ctx.pipe[1] ~= nil then sortfn = sortfunctions[ctx.pipe[1]] end
        res[name] = true
if sortfn then table.sort(ctx.params, sortfn)
    end)
else table.sort(ctx.params) end -- i.e. either `false` or `nil`
    return res
if sortfn == nil then return context_iterate(ctx, 1) end
return context_iterate(ctx, 2)
end
end


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


-- Syntax:  #invoke:params|inserting|position|how many|...|pipe to
    if hasWhitelist then
--[[
        for name in pairs(foundSet) do
library.inserting = function (ctx)
            if not whitelistSet[name] then
-- NOTE: `ctx.params` might be the original metatable! As a modifier,
                foundSet[name] = nil
-- this function MUST create a copy of it before returning
            end
local idx = tonumber(ctx.pipe[1])
        end
if idx == nil then error(ctx.luaname ..
    end
', ‘inserting’: Position must be a number', 0) end
local len = tonumber(ctx.pipe[2])
if len == nil or len < 1 then error(ctx.luaname ..
', ‘inserting’: The amount must be a number greater than zero', 0) end
local opts = ctx.pipe
local tbl = copy_table_expanded(ctx.params, idx, len)
for key = idx, idx + len - 1 do tbl[key] = opts[key - idx + 3] end
ctx.params = tbl
return context_iterate(ctx, len + 3)
end
]]--
 


-- Syntax:  #invoke:params|imposing|name|value|pipe to
    for name in pairs(blacklistSet or {}) do
library.imposing = function (ctx)
        foundSet[name] = nil
if ctx.pipe[1] == nil then error(ctx.luaname ..
    end
', ‘imposing’: Missing parameter name to impose', 0) end
local key = ctx.pipe[1]:match'^%s*(.-)%s*$'
ctx.params[tonumber(key) or key] = ctx.pipe[2]
return context_iterate(ctx, 3)
end
end


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


-- Syntax:  #invoke:params|providing|name|value|pipe to
    local compList = load_entity_components(id)
library.providing = function (ctx)
    if type(compList) == "table" then
if ctx.pipe[1] == nil then error(ctx.luaname ..
        for _, v in ipairs(compList) do
', ‘providing’: Missing parameter name to provide', 0) end
            if type(v) == "string" and v ~= "" then
local key = ctx.pipe[1]:match'^%s*(.-)%s*$'
                foundComponents[v] = true
key = tonumber(key) or key
            end
if ctx.params[key] == nil then ctx.params[key] = ctx.pipe[2] end
        end
return context_iterate(ctx, 3)
    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


-- Syntax:  #invoke:params|discarding|name|[how many]|pipe to
    apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
library.discarding = function (ctx)
    apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
if ctx.pipe[1] == nil then error(ctx.luaname ..
', ‘discarding’: Missing parameter name to discard', 0) end
local key = ctx.pipe[1]
local len = tonumber(ctx.pipe[2])
if len == nil then
ctx.params[tonumber(key) or key:match'^%s*(.-)%s*$'] = nil
return context_iterate(ctx, 2)
end
key = tonumber(key)
if key == nil then error(ctx.luaname ..
', ‘discarding’: A range was provided, but the initial parameter name is not numerical', 0) end
if len < 1 then error(ctx.luaname ..
', ‘discarding’: A range can only be a number greater than zero', 0) end
for idx = key, key + len - 1 do ctx.params[idx] = nil end
return context_iterate(ctx, 3)
end


 
    return foundComponents, foundPrototypes
-- Syntax:  #invoke:params|with_name_matching|target 1|[plain flag 1]|[or]
--            |[target 2]|[plain flag 2]|[or]|[...]|[target N]|[plain flag
--            N]|pipe to
library.with_name_matching = function (ctx)
local tbl = ctx.params
local targets, argc = parse_pattern_args(ctx, targets,
'with_name_matching')
local nomatch
for key in pairs(tbl) do
nomatch = true
for _, ptn in ipairs(targets) do
if not ptn[3] then
if string.find(key, ptn[1], 1, ptn[2]) then
nomatch = false
break
end
elseif key == ptn[1] then
nomatch = false
break
end
end
if nomatch then tbl[key] = nil end
end
return context_iterate(ctx, argc)
end
end


 
local function resolve_priority(parsed)
-- Syntax:  #invoke:params|with_name_not_matching|target 1|[plain flag 1]
    local basePriority = 1
--            |[and]|[target 2]|[plain flag 2]|[and]|[...]|[target N]|[plain
    if type(parsed) == "table" and parsed.priority ~= nil then
--            flag N]|pipe to
        if type(parsed.priority) == "number" then
library.with_name_not_matching = function (ctx)
            basePriority = parsed.priority
local tbl = ctx.params
        else
local targets, argc = parse_pattern_args(ctx, targets,
            local pnum = tonumber(parsed.priority)
'with_name_not_matching')
            if pnum then
local yesmatch
                basePriority = pnum
for key in pairs(tbl) do
            end
yesmatch = true
        end
for _, ptn in ipairs(targets) do
    end
if ptn[3] then
    return basePriority
if key ~= ptn[1] then
yesmatch = false
break
end
elseif not string.find(key, ptn[1], 1, ptn[2]) then
yesmatch = false
break
end
end
if yesmatch then tbl[key] = nil end
end
return context_iterate(ctx, argc)
end
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


-- Syntax:  #invoke:params|with_value_matching|target 1|[plain flag 1]|[or]
    return dp.flattenFieldSelectiveDirect(id, dataPage, paramNames) or ""
--            |[target 2]|[plain flag 2]|[or]|[...]|[target N]|[plain flag
--            N]|pipe to
library.with_value_matching = function (ctx)
local tbl = ctx.params
local targets, argc = parse_pattern_args(ctx, targets,
'with_value_matching')
local nomatch
for key, val in pairs(tbl) do
nomatch = true
for _, ptn in ipairs(targets) do
if ptn[3] then
if val == ptn[1] then
nomatch = false
break
end
elseif string.find(val, ptn[1], 1, ptn[2]) then
nomatch = false
break
end
end
if nomatch then tbl[key] = nil end
end
return context_iterate(ctx, argc)
end
end


 
local function add_card_tag_value(tags, seen, value)
-- Syntax:  #invoke:params|with_value_not_matching|target 1|[plain flag 1]
    value = trim(value or "")
--            |[and]|[target 2]|[plain flag 2]|[and]|[...]|[target N]|[plain
    if value == "" or seen[value] then
--            flag N]|pipe to
        return
library.with_value_not_matching = function (ctx)
    end
local tbl = ctx.params
    seen[value] = true
local targets, argc = parse_pattern_args(ctx, targets,
    tags[#tags + 1] = value
'with_value_not_matching')
local yesmatch
for key, val in pairs(tbl) do
yesmatch = true
for _, ptn in ipairs(targets) do
if ptn[3] then
if val ~= ptn[1] then
yesmatch = false
break
end
elseif not string.find(val, ptn[1], 1, ptn[2]) then
yesmatch = false
break
end
end
if yesmatch then tbl[key] = nil end
end
return context_iterate(ctx, argc)
end
end


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


-- Syntax:  #invoke:params|trimming_values|pipe to
    for i = 1, select("#", ...) do
library.trimming_values = function (ctx)
        each_csv_value(select(i, ...), function(value)
local tbl = ctx.params
            add_card_tag_value(tags, seen, value)
for key, val in pairs(tbl) do tbl[key] = val:match'^%s*(.-)%s*$' end
        end)
return context_iterate(ctx, 1)
    end
end


 
    return table.concat(tags, ", ")
-- Syntax:  #invoke:params|mapping_by_calling|template name|[call
--            style]|[let]|[...][number of additional parameters]|[parameter
--            1]|[parameter 2]|[...]|[parameter N]|pipe to
library.mapping_by_calling = function (ctx)
local opts = ctx.pipe
local tname
if opts[1] ~= nil then tname = opts[1]:match'^%s*(.*%S)' end
if tname == nil then error(ctx.luaname ..
', ‘mapping_by_calling’: No template name was provided', 0) end
local margs, argc, looptype, karg, varg = parse_callback_args(opts, 1,
mapping_styles.values_only)
local model = { title = tname, args = margs }
value_maps[looptype](ctx.params, margs, karg, varg, function ()
return ctx.frame:expandTemplate(model)
end)
return context_iterate(ctx, argc)
end
 
 
-- Syntax:  #invoke:params|mapping_by_invoking|module name|function
--            name|[call style]|[let]|[...]|[number of additional
--            arguments]|[argument 1]|[argument 2]|[...]|[argument N]|pipe to
library.mapping_by_invoking = function (ctx)
local opts = ctx.pipe
local mname
local fname
if opts[1] ~= nil then mname = opts[1]:match'^%s*(.*%S)' end
if mname == nil then error(ctx.luaname ..
', ‘mapping_by_invoking’: No module name was provided', 0) end
if opts[2] ~= nil then fname = opts[2]:match'^%s*(.*%S)' end
if fname == nil then error(ctx.luaname ..
', ‘mapping_by_invoking’: No function name was provided', 0) end
local margs, argc, looptype, karg, varg = parse_callback_args(opts, 2,
mapping_styles.values_only)
local model = { title = 'Module:' .. mname, args = margs }
local mfunc = require(model.title)[fname]
if mfunc == nil then error(ctx.luaname ..
', ‘mapping_by_invoking’: The function ‘' .. fname ..
'’ does not exist', 0) end
value_maps[looptype](ctx.params, margs, karg, varg, function ()
return mfunc(ctx.frame:newChild(model))
end)
return context_iterate(ctx, argc)
end
 
 
-- Syntax:  #invoke:params|mapping_by_magic|parser function|[call
--            style]|[let]|[...][number of additional arguments]|[argument
--            1]|[argument 2]|[...]|[argument N]|pipe to
library.mapping_by_magic = function (ctx)
local opts = ctx.pipe
local magic
if opts[1] ~= nil then magic = opts[1]:match'^%s*(.*%S)' end
if magic == nil then error(ctx.luaname ..
', ‘mapping_by_magic’: No parser function was provided', 0) end
local margs, argc, looptype, karg, varg = parse_callback_args(opts, 1,
mapping_styles.values_only)
value_maps[looptype](ctx.params, margs, karg, varg, function ()
return ctx.frame:callParserFunction(magic, margs)
end)
return context_iterate(ctx, argc)
end
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


-- Syntax:  #invoke:params|mapping_by_replacing|target|replace|[count]|[plain
        if type(keys) == "table" then
--            flag]|pipe to
            for _, key in ipairs(keys) do
library.mapping_by_replacing = function (ctx)
                if (not filter) or should_include_key(filter, sw, key) then
local ptn, repl, nmax, is_strict, argc, die =
                    local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry
parse_replace_args(ctx.pipe, 'mapping_by_replacing')
                    if type(buildFn) == "function" then
if die then return context_iterate(ctx, argc) end
                        local entry = buildFn(ctx, key)
local tbl = ctx.params
                        if entry then
if is_strict then
                            add_switch_entry(state, sw, key, entry)
for key, val in pairs(tbl) do
                        end
if val == ptn then tbl[key] = repl end
                    end
end
                end
else
            end
if flg == 2 then
        end
-- Copied from Module:String's `str._escapePattern()`
ptn = ptn:gsub('[%(%)%.%%%+%-%*%?%[%^%$%]]', '%%%0')
end
for key, val in pairs(tbl) do
tbl[key] = val:gsub(ptn, repl, nmax)
end
end
return context_iterate(ctx, argc)
end


 
        if (not isPreview) and type(mode.add_entity_extras) == "function" then
-- Syntax:  #invoke:params|renaming_by_calling|template name|[call
            mode.add_entity_extras(state, parsed, ctx)
--            style]|[let]|[...][number of additional parameters]|[parameter
        end
--            1]|[parameter 2]|[...]|[parameter N]|pipe to
    end
library.renaming_by_calling = function (ctx)
local opts = ctx.pipe
local tname
if opts[1] ~= nil then tname = opts[1]:match'^%s*(.*%S)' end
if tname == nil then error(ctx.luaname ..
', ‘renaming_by_calling’: No template name was provided', 0) end
local rargs, argc, looptype, karg, varg = parse_callback_args(opts, 1,
mapping_styles.names_only)
local model = { title = tname, args = rargs }
map_names(ctx.params, rargs, karg, varg, looptype, function ()
return ctx.frame:expandTemplate(model)
end)
return context_iterate(ctx, argc)
end
end


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


-- Syntax:  #invoke:params|renaming_by_invoking|module name|function
    local strings = {}
--            name|[call style]|[let]|[...]|[number of additional
    for sw, keys in pairs(keyFilter.whitelist) do
--            arguments]|[argument 1]|[argument 2]|[...]|[argument N]|pipe to
        if type(keys) == "table" then
library.renaming_by_invoking = function (ctx)
            for key in pairs(keys) do
local opts = ctx.pipe
                strings[#strings + 1] = key
local mname
            end
local fname
        end
if opts[1] ~= nil then mname = opts[1]:match'^%s*(.*%S)' end
    end
if mname == nil then error(ctx.luaname ..
', ‘renaming_by_invoking’: No module name was provided', 0) end
if opts[2] ~= nil then fname = opts[2]:match'^%s*(.*%S)' end
if fname == nil then error(ctx.luaname ..
', ‘renaming_by_invoking’: No function name was provided', 0) end
local rargs, argc, looptype, karg, varg = parse_callback_args(opts, 2,
mapping_styles.names_only)
local model = { title = 'Module:' .. mname, args = rargs }
local mfunc = require(model.title)[fname]
if mfunc == nil then error(ctx.luaname ..
', ‘renaming_by_invoking’: The function ‘' .. fname ..
'’ does not exist', 0) end
map_names(ctx.params, rargs, karg, varg, looptype, function ()
return mfunc(ctx.frame:newChild(model))
end)
return context_iterate(ctx, argc)
end


    if #strings == 0 then
        return nil
    end


-- Syntax:  #invoke:params|renaming_by_magic|parser function|[call
    return strings
--            style]|[let]|[...][number of additional arguments]|[argument
--            1]|[argument 2]|[...]|[argument N]|pipe to
library.renaming_by_magic = function (ctx)
local opts = ctx.pipe
local magic
if opts[1] ~= nil then magic = opts[1]:match'^%s*(.*%S)' end
if magic == nil then error(ctx.luaname ..
', ‘renaming_by_magic’: No parser function was provided', 0) end
local rargs, argc, looptype, karg, varg = parse_callback_args(opts, 1,
mapping_styles.names_only)
map_names(ctx.params, rargs, karg, varg, looptype, function ()
return ctx.frame:callParserFunction(magic, rargs)
end)
return context_iterate(ctx, argc)
end
end


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


-- Syntax:  #invoke:params|renaming_by_replacing|target|replace|[count]|[plain
    for _, s in ipairs(searchStrings) do
--            flag]|pipe to
        if string.find(content, s, 1, true) then
library.renaming_by_replacing = function (ctx)
            return true
local ptn, repl, nmax, is_strict, argc, die =
        end
parse_replace_args(ctx.pipe, 'renaming_by_replacing')
    end
if die then return context_iterate(ctx, argc) end
local tbl = ctx.params
if is_strict then
local key = tonumber(ptn) or ptn:match'^%s*(.-)%s*$'
local val = tbl[key]
tbl[key] = nil
tbl[tonumber(repl) or repl:match'^%s*(.-)%s*$'] = val
else
if flg == 2 then
-- Copied from Module:String's `str._escapePattern()`
ptn = ptn:gsub('[%(%)%.%%%+%-%*%?%[%^%$%]]', '%%%0')
end
local cache = {}
for key, val in pairs(tbl) do
steal_if_renamed(val, tbl, key, cache,
tostring(key):gsub(ptn, repl, nmax))
end
for key, val in pairs(cache) do tbl[key] = val end
end
return context_iterate(ctx, argc)
end
 


-- Syntax:  #invoke:params|grouping_by_calling|template
    return false
--            name|[let]|[...]|[number of additional arguments]|[argument
--            1]|[argument 2]|[...]|[argument N]|pipe to
library.grouping_by_calling = function (ctx)
-- NOTE: `ctx.params` might be the original metatable! As a modifier,
-- this function MUST create a copy of it before returning
local opts = ctx.pipe
local tmp
if opts[1] ~= nil then tmp = opts[1]:match'^%s*(.*%S)' end
if tmp == nil then error(ctx.luaname ..
', ‘grouping_by_calling’: No template name was provided', 0) end
local model = { title = tmp }
local tmp, argc = parse_child_args(opts, 2, 0)
local gargs = {}
for key, val in pairs(tmp) do
if type(key) == 'number' and key < 1 then gargs[key - 1] = val
else gargs[key] = val end
end
local groups = make_groups(ctx.params)
for gid, group in pairs(groups) do
for key, val in pairs(gargs) do group[key] = val end
group[0] = gid
model.args = group
groups[gid] = ctx.frame:expandTemplate(model)
end
ctx.params = groups
return context_iterate(ctx, argc)
end
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


--[[ Functions ]]--
    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)


-- Syntax:  #invoke:params|count
    local compHasWhitelist = next(compWhitelistSet) ~= nil
library.count = function (ctx)
    local protoHasWhitelist = next(protoWhitelistSet) ~= nil
-- NOTE: `ctx.pipe` and `ctx.params` might be the original metatables!
    local anyEntityWhitelist = compHasWhitelist or protoHasWhitelist
local retval = 0
for _ in ctx.iterfunc(ctx.params) do retval = retval + 1 end
if ctx.subset == -1 then retval = retval - #ctx.params end
ctx.text = retval
return false
end


    local whitelistSearchStrings = extract_whitelist_search_strings(keyFilter)


-- Syntax:  #invoke:args|concat_and_call|template name|[prepend 1]|[prepend 2]
    local function processEntity(kind, name, isStore)
--            |[...]|[item n]|[named item 1=value 1]|[...]|[named item n=value
        local pathName = lcfirst(name)
--            n]|[...]
        local tplPath = kind .. "/" .. pathName
library.concat_and_call = function (ctx)
        if isStore then
-- NOTE: `ctx.params` might be the original metatable!
            tplPath = tplPath .. "/store"
local opts = ctx.pipe
        end
local tname
if opts[1] ~= nil then tname = opts[1]:match'^%s*(.*%S)' end
if tname == nil then error(ctx.luaname ..
', ‘concat_and_call’: No template name was provided', 0) end
remove_numerical_keys(opts, 1, 1)
ctx.text = ctx.frame:expandTemplate{
title = tname,
args = concat_params(ctx)
}
return false
end


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


-- Syntax:  #invoke:args|concat_and_invoke|module name|function name|[prepend
        if not content_matches_whitelist(content, whitelistSearchStrings) then
--            1]|[prepend 2]|[...]|[item n]|[named item 1=value 1]|[...]|[named
            return
--            item n=value n]|[...]
        end
library.concat_and_invoke = function (ctx)
-- NOTE: `ctx.params` might be the original metatable!
local opts = ctx.pipe
local mname
local fname
if opts[1] ~= nil then mname = opts[1]:match'^%s*(.*%S)' end
if mname == nil then error(ctx.luaname ..
', ‘concat_and_invoke’: No module name was provided', 0) end
if opts[2] ~= nil then fname = opts[2]:match'^%s*(.*%S)' end
if fname == nil then error(ctx.luaname ..
', ‘concat_and_invoke’: No function name was provided', 0) end
remove_numerical_keys(opts, 1, 2)
local mfunc = require('Module:' .. mname)[fname]
if mfunc == nil then error(ctx.luaname ..
', ‘concat_and_invoke’: The function ‘' .. fname ..
'’ does not exist', 0) end
ctx.text = mfunc(ctx.frame:newChild{
title = 'Module:' .. fname,
args = concat_params(ctx)
})
return false
end


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


-- Syntax:  #invoke:args|concat_and_magic|parser function|[prepend 1]|[prepend
        local extra = ""
--            2]|[...]|[item n]|[named item 1=value 1]|[...]|[named item n=
        local paramNames = get_template_params(tplPath, content)
--            value n]|[...]
        if dp then
library.concat_and_magic = function (ctx)
            local dataPage = tplPath .. ".json"
-- NOTE: `ctx.params` might be the original metatable!
            extra = get_selective_extra(id, dataPage, paramNames)
local opts = ctx.pipe
        end
local magic
if opts[1] ~= nil then magic = opts[1]:match'^%s*(.*%S)' end
if magic == nil then error(ctx.luaname ..
', ‘concat_and_magic’: No parser function was provided', 0) end
remove_numerical_keys(opts, 1, 1)
ctx.text = ctx.frame:callParserFunction(magic, concat_params(ctx))
return false
end


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


-- Syntax:  #invoke:params|value_of|parameter name
    for compName in pairs(foundComponents) do
library.value_of = function (ctx)
        if not anyEntityWhitelist or compHasWhitelist then
-- NOTE: `ctx.pipe` and `ctx.params` might be the original metatables!
            processEntity("component", compName, false)
local opts = ctx.pipe
        end
local kstr
    end
if opts[1] ~= nil then kstr = opts[1]:match'^%s*(.*%S)' end
    for protoName in pairs(foundPrototypes) do
if kstr == nil then error(ctx.luaname ..
        if not anyEntityWhitelist or protoHasWhitelist then
', ‘value_of’: No parameter name was provided', 0) end
            processEntity("prototype", protoName, false)
local knum = tonumber(kstr)
        end
local len = #ctx.params
    end
local val = ctx.params[knum or kstr]
if val ~= nil and (
ctx.subset ~= -1 or knum == nil or knum > len or knum < 1
) and (
ctx.subset ~= 1 or (knum ~= nil and knum <= len and knum > 0)
) then
ctx.text = (ctx.header or '') .. val .. (ctx.footer or '')
return false
end
ctx.text = ctx.ifngiven or ''
return false
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


-- Syntax:  #invoke:params|list
    if type(prototypeStoreDefs) == "table" and (not anyEntityWhitelist or protoHasWhitelist) then
library.list = function (ctx)
        local protoStore = prototypeStoreDefs[id]
-- NOTE: `ctx.pipe` might be the original metatable!
        if type(protoStore) == "table" then
local kvs = ctx.pairsep or ''
            for protoName in pairs(protoStore) do
local pps = ctx.itersep or ''
                local allowed = true
local ret = {}
                if protoBlacklistSet[protoName] then
local nss = 0
                    allowed = false
flush_params(
                elseif protoHasWhitelist and not protoWhitelistSet[protoName] then
ctx,
                    allowed = false
function (key, val)
                end
ret[nss + 1] = pps
                if allowed then
ret[nss + 2] = key
                    processEntity("prototype", protoName, true)
ret[nss + 3] = kvs
                end
ret[nss + 4] = val
            end
nss = nss + 4
        end
end
    end
)
if nss > 0 then
if nss > 4 and ctx.lastsep ~= nil then
ret[nss - 3] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end


 
    return true
-- Syntax:  #invoke:params|list_values
library.list_values = function (ctx)
-- NOTE: `ctx.pipe` might be the original metatable!
local pps = ctx.itersep or ''
local ret = {}
local nss = 0
flush_params(
ctx,
function (key, val)
ret[nss + 1] = pps
ret[nss + 2] = val
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end
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 = {}


-- Syntax:  #invoke:params|for_each|wikitext
    local ok = each_entity_data(frame, id, function(parsed, ctx)
library.for_each = function (ctx)
        local keys = parsed.card or {}
-- NOTE: `ctx.pipe` might be the original metatable!
local txt = ctx.pipe[1] or ''
local pps = ctx.itersep or ''
local ret = {}
local nss = 0
flush_params(
ctx,
function (key, val)
ret[nss + 1] = pps
ret[nss + 2] = txt:gsub('%$#', key):gsub('%$@', val)
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end


        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))


-- Syntax:  #invoke:params|call_for_each|template name|[append 1]|[append 2]
                if allowCardEntry then
--            |[...]|[append n]|[named param 1=value 1]|[...]|[named param
                    entries[#entries + 1] = {
--            n=value n]|[...]
                        tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
library.call_for_each = function (ctx)
                        priority = ctx.priority,
local opts = ctx.pipe
                        idx = #entries + 1
local tname
                    }
if opts[1] ~= nil then tname = opts[1]:match'^%s*(.*%S)' end
                end
if tname == nil then error(ctx.luaname ..
            end
', ‘call_for_each’: No template name was provided', 0) end
        end
local model = { title = tname, args = opts }
local ccs = ctx.itersep or ''
local ret = {}
local nss = 0
table.insert(opts, 1, true)
flush_params(
ctx,
function (key, val)
opts[1] = key
opts[2] = val
ret[nss + 1] = ccs
ret[nss + 2] = ctx.frame:expandTemplate(model)
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
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)


-- Syntax:  #invoke:params|invoke_for_each|module name|module function|[append
    if not ok then
--            1]|[append 2]|[...]|[append n]|[named param 1=value 1]|[...]
        return trim(args.tag or "")
--            |[named param n=value n]|[...]
    end
library.invoke_for_each = function (ctx)
local opts = ctx.pipe
local mname
local fname
if opts[1] ~= nil then mname = opts[1]:match'^%s*(.*%S)' end
if mname == nil then error(ctx.luaname ..
', ‘invoke_for_each’: No module name was provided', 0) end
if opts[2] ~= nil then fname = opts[2]:match'^%s*(.*%S)' end
if fname == nil then error(ctx.luaname ..
', ‘invoke_for_each’: No function name was provided', 0) end
local model = { title = 'Module:' .. mname, args = opts }
local mfunc = require(model.title)[fname]
local ccs = ctx.itersep or ''
local ret = {}
local nss = 0
flush_params(
ctx,
function (key, val)
opts[1] = key
opts[2] = val
ret[nss + 1] = ccs
ret[nss + 2] = mfunc(ctx.frame:newChild(model))
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end


    sort_entries_by_priority(entries)


-- Syntax:  #invoke:params|magic_for_each|parser function|[append 1]|[append 2]
    local tags = {}
--            |[...]|[append n]|[named param 1=value 1]|[...]|[named param
    local seen = {}
--            n=value n]|[...]
    for _, entry in ipairs(entries) do
library.magic_for_each = function (ctx)
        local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
local opts = ctx.pipe
        add_card_tag_value(tags, seen, tagText)
local magic
    end
if opts[1] ~= nil then magic = opts[1]:match'^%s*(.*%S)' end
if magic == nil then error(ctx.luaname ..
', ‘magic_for_each’: No parser function was provided', 0) end
local ccs = ctx.itersep or ''
local ret = {}
local nss = 0
table.insert(opts, 1, true)
flush_params(
ctx,
function (key, val)
opts[1] = key
opts[2] = val
ret[nss + 1] = ccs
ret[nss + 2] = ctx.frame:callParserFunction(magic,
opts)
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end


 
    return merge_card_tag_text(table.concat(tags, ", "), args.tag)
-- Syntax:  #invoke:params|call_for_each_value|template name|[append 1]|[append
--            2]|[...]|[append n]|[named param 1=value 1]|[...]|[named param
--            n=value n]|[...]
library.call_for_each_value = function (ctx)
local opts = ctx.pipe
local tname
if opts[1] ~= nil then tname = opts[1]:match'^%s*(.*%S)' end
if tname == nil then error(ctx.luaname ..
', ‘call_for_each_value’: No template name was provided', 0) end
local model = { title = tname, args = opts }
local ccs = ctx.itersep or ''
local ret = {}
local nss = 0
flush_params(
ctx,
function (key, val)
opts[1] = val
ret[nss + 1] = ccs
ret[nss + 2] = ctx.frame:expandTemplate(model)
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end
end


p.mergeCardTagText = merge_card_tag_text
p.collectCardTagText = collect_card_tag_text


-- Syntax:  #invoke:params|invoke_for_each_value|module name|[append 1]|[append
local function build_missing_template_error(kind, name, isStore, tplPath)
--            2]|[...]|[append n]|[named param 1=value 1]|[...]|[named param
    local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "")
--            n=value n]|[...]
    local classType = baseType
library.invoke_for_each_value = function (ctx)
    if isStore then
local opts = ctx.pipe
        classType = classType .. "Store"
local mname
    end
local fname
    local className = name .. baseType
if opts[1] ~= nil then mname = opts[1]:match'^%s*(.*%S)' end
    local tplLabel = "Template:" .. tplPath
if mname == nil then error(ctx.luaname ..
    return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}"
', ‘invoke_for_each_value’: No module name was provided', 0) end
if opts[2] ~= nil then fname = opts[2]:match'^%s*(.*%S)' end
if fname == nil then error(ctx.luaname ..
', ‘invoke_for_each_value’: No function name was provided', 0) end
local model = { title = 'Module:' .. mname, args = opts }
local mfunc = require(model.title)[fname]
local ccs = ctx.itersep or ''
local ret = {}
local nss = 0
remove_numerical_keys(opts, 1, 1)
flush_params(
ctx,
function (key, val)
opts[1] = val
ret[nss + 1] = ccs
ret[nss + 2] = mfunc(ctx.frame:newChild(model))
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end
end


 
local function renderBlocks(frame, state, renderOptions, entityId, showSource)
-- Syntax:  #invoke:params|magic_for_each_value|parser function|[append 1]
    local outLocal = {}
--            |[append 2]|[...]|[append n]|[named param 1=value 1]|[...]|[named
    local noHeaders = renderOptions and renderOptions.noHeaders
--            param n=value n]|[...]
    local cardFilter = renderOptions and renderOptions.cardFilter
library.magic_for_each_value = function (ctx)
    for _, sw in ipairs(switchModeOrder) do
local opts = ctx.pipe
        local mode = switchModeRegistry[sw] or {}
local magic
        if mode.full then
if opts[1] ~= nil then magic = opts[1]:match'^%s*(.*%S)' end
            local outStr = ""
if magic == nil then error(ctx.luaname ..
            if type(mode.render_full) == "function" then
', ‘magic_for_each_value’: No parser function was provided', 0) end
                outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
local ccs = ctx.itersep or ''
                    entityId, noHeaders, showSource, cardFilter)
local ret = {}
            end
local nss = 0
            if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
flush_params(
        else
ctx,
            for _, key in ipairs(state.keyOrder[sw] or {}) do
function (key, val)
                local entries = state.keyToTemplates[sw][key] or {}
opts[1] = val
                if type(mode.render_key) == "function" then
ret[nss + 1] = ccs
                    local outStr = mode.render_key(frame, key, entries, noHeaders, showSource)
ret[nss + 2] = ctx.frame:callParserFunction(magic,
                    if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
opts)
                end
nss = nss + 2
            end
end
        end
)
    end
if nss > 0 then
    return outLocal
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end
end


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


-- Syntax:  #invoke:params|call_for_each_group|template name|[append 1]|[append
    local showSource = trim(args.showSource or "") == ""
--            2]|[...]|[append n]|[named param 1=value 1]|[...]|[named param
--            n=value n]|[...]
library.call_for_each_group = function (ctx)
-- NOTE: `ctx.pipe` and `ctx.params` might be the original metatables!
local opts = ctx.pipe
local tmp
if opts[1] ~= nil then tmp = opts[1]:match'^%s*(.*%S)' end
if tmp == nil then error(ctx.luaname ..
', ‘call_for_each_group’: No template name was provided', 0) end
local model = { title = tmp }
local ccs = ctx.itersep or ''
local nss = 0
local ret = {}
opts = {}
for key, val in pairs(ctx.pipe) do
if type(key) == 'number' then opts[key - 1] = val
else opts[key] = val end
end
ctx.pipe = opts
ctx.params = make_groups(ctx.params)
flush_params(
ctx,
function (gid, group)
for key, val in pairs(opts) do group[key] = val end
group[0] = gid
model.args = group
ret[nss + 1] = ccs
ret[nss + 2] = ctx.frame:expandTemplate(model)
nss = nss + 2
end
)
if nss > 0 then
if nss > 2 and ctx.lastsep ~= nil then
ret[nss - 1] = ctx.lastsep
end
ret[1] = ctx.header or ''
if ctx.footer ~= nil then ret[nss + 1] = ctx.footer end
ctx.text = table.concat(ret)
return false
end
ctx.text = ctx.ifngiven or ''
return false
end
 


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


---                                        ---
    local state = new_switch_state()
---     PUBLIC ENVIRONMENT                ---
    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


--[[ First-position-only modifiers ]]--
    renderOptions.noHeaders = filter.hasWhitelist
---------------------------------------


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


-- Syntax:  #invoke:params|new|pipe to
    return frame:preprocess(table.concat(out, "\n"))
--[[
static_iface.new = function (frame)
local ctx = context_new()
ctx.frame = frame:getParent()
ctx.pipe = copy_or_ref_table(frame.args, false)
ctx.params = {}
main_loop(ctx, context_iterate(ctx, 1))
return ctx.text
end
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)


--[[ First-position-only functions ]]--
    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


-- Syntax:  #invoke:params|self
    local state = new_switch_state()
static_iface.self = function (frame)
    add_entries_from_meta(state, parsed, {
return frame:getParent():getTitle()
        tplPath = tplPath,
end
        id = "",
        extra = "",
        source = make_source("", tplPath, tplPath, tplPath),
        priority = 1
    }, nil, true)


    local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist


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


--[[ Public metatable of functions ]]--
    return frame:preprocess(table.concat(out, "\n"))
---------------------------------------
end


 
return p
return setmetatable(static_iface, {
__index = function (iface, _fname_)
local ctx = context_new()
local fname = _fname_:match'^%s*(.*%S)'
if fname == nil then error(ctx.luaname ..
': You must specify a function to call', 0) end
if library[fname] == nil then error(ctx.luaname ..
': The function ‘' .. fname .. '’ does not exist', 0) end
local func = library[fname]
return function (frame)
ctx.frame = frame:getParent()
ctx.pipe = copy_or_ref_table(frame.args,
refpipe[fname])
ctx.params = copy_or_ref_table(ctx.frame.args,
refparams[fname])
main_loop(ctx, func)
return ctx.text
end
end
})

Текущая версия от 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