Модуль:GetField: различия между версиями

Материал из Space Station 14 Вики
Нет описания правки
мНет описания правки
 
(не показано 96 промежуточных версий этого же участника)
Строка 1: Строка 1:
local p = {}
local p = {}


local cache = {}
local JsonPaths = require('Module:JsonPaths')
local entryCache = {}
local getArgs = require('Module:Arguments').getArgs
local BASE_USER = "IanComradeBot/"


local function get_module_name(pagePath)
function p.loadCachedData(pagePath)
    return "Module:" .. BASE_USER .. pagePath .. "/data"
local titleName = JsonPaths.get(pagePath)
if not titleName or titleName == "" then
return nil
end
 
local ok, data = pcall(mw.loadData, titleName)
if not ok or type(data) ~= "table" then
return nil
end
 
return data
end
end


local function load_cached_data(moduleName)
local function parse_path(path)
    local data = cache[moduleName]
if not path or path == "" then
    if data then
return nil
        return data
end
    end


    local ok, loaded = pcall(mw.loadData, moduleName)
local parsed = {}
    if not ok or not loaded then
for part in string.gmatch(path, "([^%.]+)") do
        return nil
local key, idxStr = string.match(part, "^(.-)%[(%d+)%]$")
    end
if key then
parsed[#parsed + 1] = { key, tonumber(idxStr) }
else
local num = tonumber(part)
if num then
parsed[#parsed + 1] = { nil, num }
else
parsed[#parsed + 1] = { part, nil }
end
end
end


    cache[moduleName] = loaded
return parsed
    return loaded
end
end


local function parse_indexed_part(part)
local function navigate_path(tbl, path)
    local key, idx = string.match(part, "^(.-)%[(%d+)%]$")
if not tbl then return nil end
    if key then
if not path or path == "" then return tbl end
        return key, tonumber(idx)
 
    end
local cur = tbl
    local num = tonumber(part)
local start = 1
    if num then
local len = #path
        return nil, num
 
    end
while start <= len do
    return part, nil
local dotPos = string.find(path, ".", start, true)
end
local part
if dotPos then
part = string.sub(path, start, dotPos - 1)
start = dotPos + 1
else
part = string.sub(path, start)
start = len + 1
end
 
if type(cur) ~= "table" then return nil end
 
local bracketPos = string.find(part, "[", 1, true)
if bracketPos then
local key = string.sub(part, 1, bracketPos - 1)
local idx = tonumber(string.sub(part, bracketPos + 1, #part - 1))


local function get_by_path(tbl, path)
if key ~= "" then
    if not tbl or path == "" then return nil end
local val = cur[key]
    local cur = tbl
if val == nil then val = cur["!type:" .. key] end
    for part in string.gmatch(path, "([^%.]+)") do
cur = val
        local key, idx = parse_indexed_part(part)
end
        if key and key ~= "" then
            if type(cur) ~= "table" then return nil end
            local nextCur = cur[key]
            if nextCur == nil then
                nextCur = cur["!type:" .. key]
            end
            cur = nextCur
        end
        if idx then
            if type(cur) ~= "table" then return nil end
            cur = cur[idx]
        end
        if cur == nil then return nil end
    end
    return cur
end


local function format_value(v)
if idx then
    local okJson, json = pcall(mw.text.jsonEncode, v)
if type(cur) ~= "table" then return nil end
    if okJson and json == "null" then
cur = cur[idx]
        return "null"
end
    end
else
local num = tonumber(part)
if num then
cur = cur[num]
else
local val = cur[part]
if val == nil then val = cur["!type:" .. part] end
cur = val
end
end


    if v == nil then return "" end
if cur == nil then return nil end
end


    local t = type(v)
return cur
    if t == "string" or t == "number" or t == "boolean" then
        return tostring(v)
    elseif t == "table" then
        local ok, json2 = pcall(mw.text.jsonEncode, v)
        if ok and json2 then
            return json2
        end
        return ""
    else
        return tostring(v)
    end
end
end


local function to_nowiki(v)
local function format_value(v)
    return "<nowiki>" .. v .. "</nowiki>"
if v == nil then
return ""
end
 
local t = type(v)
if t == "string" or t == "number" or t == "boolean" then
return tostring(v)
elseif t == "table" then
local ok, json = pcall(mw.text.jsonEncode, v)
if ok and json then
return json
end
return ""
else
return tostring(v)
end
end
end


local function is_array(tbl)
local function is_array(tbl)
    local max = 0
local max = 0
    local count = 0
local count = 0
    for k in pairs(tbl) do
for k in pairs(tbl) do
        if type(k) ~= "number" then
if type(k) ~= "number" then
            return false
return false
        end
end
        if k > max then max = k end
if k > max then
        count = count + 1
max = k
    end
end
    return count > 0 and max == count
count = count + 1
end
return count > 0 and max == count
end
end


local function deep_copy(src)
local function deep_copy(src)
    local dst = {}
local dst = {}
    for k, v in pairs(src) do
for k, v in pairs(src) do
        if type(v) == "table" then
if type(v) == "table" then
            dst[k] = deep_copy(v)
dst[k] = deep_copy(v)
        else
else
            dst[k] = v
dst[k] = v
        end
end
    end
end
    return dst
return dst
end
end


local function deep_merge(dst, src)
local function deep_merge(dst, src)
    for k, v in pairs(src) do
for k, v in pairs(src) do
        if type(v) == "table" and type(dst[k]) == "table" then
if type(v) == "table" and type(dst[k]) == "table" then
            deep_merge(dst[k], v)
deep_merge(dst[k], v)
        elseif type(v) == "table" then
elseif type(v) == "table" then
            dst[k] = deep_copy(v)
dst[k] = deep_copy(v)
        else
else
            dst[k] = v
dst[k] = v
        end
end
    end
end
end
end


local function resolve_entry(data, id)
function p.ucfirst(s)
    if type(data) ~= "table" then
if not s or s == "" then return s end
        return nil
return string.upper(s:sub(1, 1)) .. (s:sub(2) or "")
    end
end


    if id and id ~= "" then
function p.normalizeComponentName(name)
        local direct = data[id]
if type(name) ~= "string" or name == "" then
        if direct ~= nil then
return nil
            return direct
end
        end
name = mw.text.trim(name)
if name:sub(1, 5) == "type:" then
name = name:sub(6)
elseif name:sub(1, 6) == "!type:" then
name = name:sub(7)
end
if name == "" then
return nil
end
return name
end


        local idsTable = data.id
local function split_data_page_path(pagePath)
        if type(idsTable) == "table" then
local kind, name = string.match(pagePath or "", "^(component)/([^/]+)%.json$")
            local specific = idsTable[id]
if not kind then
            if type(specific) == "table" then
return nil, nil
                local base = data["default"]
end
                if type(base) == "table" then
return kind, p.ucfirst(name)
                    local merged = deep_copy(base)
end
                    deep_merge(merged, specific)
                    return merged
                end
                return deep_copy(specific)
            end
        end
    end


    local base = data["default"]
local function get_component_from_entity(entity, componentName)
    if type(base) == "table" then
if type(entity) ~= "table" or type(entity.components) ~= "table" then
        return deep_copy(base)
return nil
    end
end
    return nil
 
return entity.components["type:" .. componentName]
or entity.components[componentName]
or entity.components["!type:" .. componentName]
end
end


local function contains_target(v, target)
local function load_specific_entry(pagePath, id, allowDefault)
    if type(v) == "table" then
local kind, name = split_data_page_path(pagePath)
        if is_array(v) then
if not kind or not id or id == "" then
            for _, item in ipairs(v) do
return nil
                if tostring(item) == target then
end
                    return true
 
                end
if kind ~= "component" then
            end
return nil
            return false
end
        end
 
local base
local defaultData = p.loadCachedData(kind .. "/" .. name .. "/defaultFields.json")
if type(defaultData) == "table" then
base = deep_copy(defaultData)
end
 
local entityPage = "prototype/Entity/" .. id .. ".json"
local entity = p.loadCachedData(entityPage)
local specific = get_component_from_entity(entity, name)
 
if type(specific) ~= "table" then
if allowDefault == false then
return nil
end
return base
end
 
if base then
deep_merge(base, specific)
return base
end
 
return specific
end
 
local function resolve_entry_from_data(data, id, allowDefault)
if type(data) ~= "table" then
return nil
end
 
if id and id ~= "" then
local direct = data[id]
if direct ~= nil then
return direct
end
 
local idsTable = data.id
if type(idsTable) == "table" then
local specific = idsTable[id]
if type(specific) == "table" then
local base = data["default"]
if type(base) == "table" then
local merged = deep_copy(base)
deep_merge(merged, specific)
return merged
end
return specific
end
end
end
 
if allowDefault == false then
return nil
end
 
local base = data["default"]
if type(base) == "table" then
return base
end
 
return nil
end
 
local function load_entry(id, pagePath, data, allowDefault)
local specificPageEntry = load_specific_entry(pagePath, id, allowDefault)
if specificPageEntry ~= nil then
return specificPageEntry
end
 
data = data or p.loadCachedData(pagePath)
if allowDefault == nil then
allowDefault = true
end
return resolve_entry_from_data(data, id, allowDefault)
end
 
local function collect_id_keys(data)
if type(data) ~= "table" then
return {}
end
 
local idsTable = data.id
local ids = {}
 
if type(idsTable) == "table" then
for k in pairs(idsTable) do
ids[#ids + 1] = k
end
return ids
end


        for _, item in pairs(v) do
for k in pairs(data) do
            if tostring(item) == target then
if k ~= "default" and k ~= "id" then
                return true
ids[#ids + 1] = k
            end
end
        end
end
        return false
    end


    return tostring(v) == target
return ids
end
end


local function find_matching_ids(idsTable, keyPath, searchValue)
local function contains_target(v, target)
    local target = tostring(searchValue)
if type(v) == "string" then
    local matches = {}
return v == target
end
if type(v) == "table" then
if is_array(v) then
for _, item in ipairs(v) do
if tostring(item) == target then
return true
end
end
return false
end
 
for _, item in pairs(v) do
if tostring(item) == target then
return true
end
end
return false
end


    for idKey, entry in pairs(idsTable) do
return tostring(v) == target
        if type(entry) == "table" then
end
            local v = get_by_path(entry, keyPath)
            if v ~= nil and contains_target(v, target) then
                matches[#matches + 1] = idKey
            end
        end
    end


    return matches
local function is_nonempty_value(v)
if v == nil then
return false
end
if type(v) == "table" then
return next(v) ~= nil
end
return true
end
end


local function preprocess_or_return(frame, text)
local function preprocess_or_return(frame, text)
    if type(frame.preprocess) == "function" then
if type(frame) == "table" and type(frame.preprocess) == "function" then
        return frame:preprocess(text)
return frame:preprocess(text)
    end
end
    return text
return text
end
end


local function get_field_loose(entry, fieldId)
local function get_field_loose(entry, fieldId)
    local value = entry[fieldId]
local value = entry[fieldId]
    if value ~= nil then return value end
if value ~= nil then
    if fieldId == "" then return nil end
return value
end
if fieldId == "" then
return nil
end
 
local first = string.sub(fieldId, 1, 1)
local tail = string.sub(fieldId, 2)
 
value = entry[string.lower(first) .. tail]
if value ~= nil then
return value
end
 
return entry[string.upper(first) .. tail]
end
 
local function apply_pattern(s, pattern, repl)
if not pattern or pattern == "" or not s then
return s
end
 
local text = tostring(s)
local replacement
if repl and repl ~= "" then
replacement = tostring(repl)
replacement = replacement:gsub("\\(%d)", "%%%1")
else
replacement = "%1"
end
 
local patt = pattern
if not patt:find("%^") and not patt:find("%$") then
patt = "^" .. patt .. "$"
end
 
return (text:gsub(patt, replacement))
end
 
local function flatten_parts(entry)
if type(entry) ~= "table" then
return {}
end
 
local parts = {}
 
local function walk(tbl, prefix)
local keys = {}
for k in pairs(tbl) do
keys[#keys + 1] = k
end
table.sort(keys, function(a, b)
return tostring(a) < tostring(b)
end)
 
for _, k in ipairs(keys) do
local v = tbl[k]
local kStr = tostring(k)
local key = (prefix == "" and kStr or prefix .. "." .. kStr)
 
if type(v) == "table" then
if next(v) ~= nil then
local ok, json = pcall(mw.text.jsonEncode, v)
if ok and json then
parts[#parts + 1] = key .. "=<nowiki>" .. json .. "</nowiki>"
end
if is_array(v) then
local first = v[1]
if type(first) == "table" then
walk(first, key)
end
else
walk(v, key)
end
end
else
parts[#parts + 1] = key .. "=" .. tostring(v)
end
end
end
 
walk(entry, "")
return parts
end
 
local function flatten_entry(entry)
local parts = flatten_parts(entry)
if #parts == 0 then
return ""
end
return table.concat(parts, "|")
end
 
local function append_flattened_part(parts, key, value)
if value == nil then
return
end
 
if type(value) == "table" then
if next(value) == nil then
return
end
 
local ok, json = pcall(mw.text.jsonEncode, value)
if ok and json then
parts[#parts + 1] = key .. "=<nowiki>" .. json .. "</nowiki>"
end
return
end
 
parts[#parts + 1] = key .. "=" .. tostring(value)
end
 
local function flatten_selected_parts(entry, keys)
if type(entry) ~= "table" or type(keys) ~= "table" then
return {}
end
 
local parts = {}
local seen = {}


    local first = string.sub(fieldId, 1, 1)
for i = 1, #keys do
    local tail = string.sub(fieldId, 2)
local key = keys[i]
    value = entry[string.lower(first) .. tail]
if type(key) == "string" and key ~= "" and not seen[key] then
    if value ~= nil then return value end
seen[key] = true
append_flattened_part(parts, key, navigate_path(entry, key))
end
end


    return entry[string.upper(first) .. tail]
return parts
end
 
local function resolve_template_path(tplPath)
local templatePath = tplPath
local project = JsonPaths.project()
if project ~= nil and project ~= "" then
templatePath = tplPath .. "/" .. project
templatePath = "{{#ifexist:Шаблон:" .. templatePath .. "|" .. templatePath .. "|" .. tplPath .. "}}"
end
 
return templatePath
end
 
local function split_template_spec(tplPath, tplArgs)
tplPath = mw.text.unstripNoWiki(tplPath or "")
tplArgs = mw.text.unstripNoWiki(tplArgs or "")
if tplArgs ~= "" and string.sub(tplArgs, 1, 1) == "|" then
tplArgs = string.sub(tplArgs, 2)
end
 
if tplArgs == "" then
local pipePos = string.find(tplPath, "|", 1, true)
if pipePos then
tplArgs = mw.text.unstripNoWiki(string.sub(tplPath, pipePos + 1))
if tplArgs ~= "" and string.sub(tplArgs, 1, 1) == "|" then
tplArgs = string.sub(tplArgs, 2)
end
tplPath = string.sub(tplPath, 1, pipePos - 1)
end
end
 
return tplPath, tplArgs
end
 
local function build_tpl(id, pagePath, tplPath, data, tplArgs, preEntry, strictExact)
if id == "" or pagePath == "" or tplPath == "" then
return ""
end
 
local entry
if strictExact then
entry = load_entry(id, pagePath, data, false)
else
entry = preEntry or load_entry(id, pagePath, data)
end
 
if entry == nil then
return ""
end
 
local extra = flatten_entry(entry)
local extraTplArgs = tplArgs or ""
 
local templatePath = resolve_template_path(tplPath)
 
local tplStr = "{{Шаблон:" .. templatePath
if extraTplArgs ~= "" then
tplStr = tplStr .. "|" .. extraTplArgs
end
tplStr = tplStr .. "|id=" .. tostring(id)
if extra ~= "" then
tplStr = tplStr .. "|" .. extra
end
tplStr = tplStr .. "}}"
 
return tplStr
end
end


function p.findInGenerator(frame)
function p.findInGenerator(frame)
    local args = frame.args or {}
local args = frame.args or {}
    local searchId = args[1] or ""
local searchId = args[1] or ""
    local kind = (args[2] or ""):lower()
local kind = (args[2] or ""):lower()
    local fieldId = args[3] or ""
local fieldId = p.ucfirst(args[3] or "")


    if searchId == "" or fieldId == "" then
if searchId == "" or fieldId == "" then
        return ""
return ""
    end
end
    if kind ~= "prototype" and kind ~= "component" then
if kind ~= "prototype" and kind ~= "component" then
        return ""
return ""
    end
end


    local storeName = (kind == "prototype") and "prototype_store.json" or "component_store.json"
local storeName = (kind == "prototype") and "prototype_store.json" or "component_store.json"
    local moduleName = get_module_name(storeName)
local data = p.loadCachedData(storeName)
    local data = load_cached_data(moduleName)
if not data then
    if not data then
return ""
        return ""
end
    end


    local entry = data[searchId]
local entry = data[searchId]
    if type(entry) ~= "table" then
if type(entry) ~= "table" then
        return ""
return ""
    end
end


    local value = get_field_loose(entry, fieldId)
local value = get_field_loose(entry, fieldId)
    if value == nil then
if value == nil then
        return ""
return ""
    end
end


    local out = {}
local out = {}
    local t = type(value)
local t = type(value)
    if t == "table" then
if t == "table" then
        for _, v in ipairs(value) do
for _, v in ipairs(value) do
            out[#out + 1] = v
out[#out + 1] = v
        end
end
    else
else
        out[1] = value
out[1] = value
    end
end


    return mw.text.jsonEncode(out)
return mw.text.jsonEncode(out)
end
end


function p.flattenField(frame)
function p.flattenField(frame)
    local args = frame.args or {}
local args = frame.args or {}
    local id = args[1] or ""
local id = args[1] or ""
    local pagePath = args[2] or ""
local pagePath = args[2] or ""
    if id == "" or pagePath == "" then return "" end
if id == "" or pagePath == "" then
return ""
end
 
local entry = load_entry(id, pagePath) or {}
return flatten_entry(entry)
end


    local moduleName = get_module_name(pagePath)
function p.flattenFieldSelective(frame)
    local data = load_cached_data(moduleName)
local args = frame.args or {}
    if not data then return "" end
local id = args[1] or ""
local pagePath = args[2] or ""
local keysJson = mw.text.unstripNoWiki(args[3] or args.keys or "")
if id == "" or pagePath == "" or keysJson == "" then
return ""
end


    local entry = resolve_entry(data, id) or {}
local okKeys, keys = pcall(mw.text.jsonDecode, keysJson)
if not okKeys or type(keys) ~= "table" or #keys == 0 then
return ""
end


    if type(entry) ~= "table" then return "" end
local entry = load_entry(id, pagePath) or {}
local parts = flatten_selected_parts(entry, keys)
if #parts == 0 then
return ""
end


    local parts = {}
return table.concat(parts, "|")
    local function append_table_json(key, value)
end
        local ok, json = pcall(mw.text.jsonEncode, value)
        if ok and json then
            parts[#parts + 1] = key .. "=" .. to_nowiki(json)
        end
    end


    local function walk(tbl, prefix)
function p.get(frame)
        local keys = {}
local args = getArgs(frame, { removeBlanks = false })
        for k in pairs(tbl) do keys[#keys + 1] = k end
local id = args[1] or ""
        table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
local pagePath = args[2] or ""
        for _, k in ipairs(keys) do
local keyPath = args[3] or ""
            local v = tbl[k]
            local kStr = tostring(k)
            local key = (prefix == "" and kStr or prefix .. "." .. kStr)
            if type(v) == "table" then
                if next(v) == nil then
                else
                    append_table_json(key, v)
                    if not is_array(v) then
                        walk(v, key)
                    end
                end
            else
                parts[#parts + 1] = key .. "=" .. tostring(v)
            end
        end
    end


    walk(entry, "")
if pagePath == "" then
return ""
end


    return table.concat(parts, "|")
local entry = load_entry(id, pagePath)
if entry == nil then
return ""
end
 
if keyPath == "" then
return format_value(entry)
end
 
return format_value(navigate_path(entry, keyPath))
end
end


function p.get(frame)
local function collect_by_parsed_path(tbl, parsedPath, pos, out)
    local args = frame.args or {}
if pos > #parsedPath then
    local id = args[1] or ""
out[#out + 1] = tbl
    local pagePath = args[2] or ""
return
    local keyPath = args[3] or ""
end
 
if type(tbl) ~= "table" then
return
end
 
local token = parsedPath[pos]
local key = token[1]
local idx = token[2]
 
if key == "*" then
for _, child in pairs(tbl) do
local nextCur = child
 
if idx then
if type(nextCur) ~= "table" then
nextCur = nil
else
nextCur = nextCur[idx]
end
end
 
collect_by_parsed_path(nextCur, parsedPath, pos + 1, out)
end
return
end
 
local nextCur
 
if key and key ~= "" then
nextCur = tbl[key]
if nextCur == nil then nextCur = tbl["!type:" .. key] end
else
nextCur = tbl
end
 
if idx then
if type(nextCur) ~= "table" then
return
end
nextCur = nextCur[idx]
end
 
collect_by_parsed_path(nextCur, parsedPath, pos + 1, out)
end
 
local function get_by_parsed_path_multi(tbl, parsedPath)
local out = {}
collect_by_parsed_path(tbl, parsedPath, 1, out)
return out
end
 
local function entry_matches_path(entry, parsedPath, searchValue, searchType)
local values = get_by_parsed_path_multi(entry, parsedPath)
local target = tostring(searchValue)
 
for _, v in ipairs(values) do
if searchType == "key" then
if type(v) == "table" and v[target] ~= nil then
return true
end
else
if contains_target(v, target) then
return true
end
end
end
 
return false
end
 
local function entry_has_any_nonempty_path(entry, parsedPath)
local values = get_by_parsed_path_multi(entry, parsedPath)
 
for _, v in ipairs(values) do
if is_nonempty_value(v) then
return true
end
end
 
return false
end
 
local function split_csv_list(text)
text = mw.text.unstripNoWiki(text or "")
local out = {}
 
for part in string.gmatch(text, "([^,]+)") do
part = mw.text.trim(part)
if part ~= "" then
out[#out + 1] = part
end
end
 
return out
end
 
function p.searchId(frame)
local args = getArgs(frame, { removeBlanks = false })
local searchValue = args[1] or ""
local pagePath = args[2] or ""
local keyPath = args[3] or ""
local searchType = (args.searchType or ""):lower()
 
if searchValue == "" or pagePath == "" or keyPath == "" then
return ""
end
if searchType == "" then
searchType = "value"
end
 
local data = p.loadCachedData(pagePath)
if not data then
return "[]"
end
 
local parsedPath = parse_path(keyPath)
if not parsedPath then
return ""
end
 
local ids = collect_id_keys(data)
if #ids == 0 then
return ""
end
 
local matches = {}
local target = tostring(searchValue)
 
for _, idKey in ipairs(ids) do
local entry = resolve_entry_from_data(data, idKey)
if type(entry) == "table" and entry_matches_path(entry, parsedPath, target, searchType) then
matches[#matches + 1] = idKey
end
end
 
if #matches == 0 then
return ""
end
 
local ok, json = pcall(mw.text.jsonEncode, matches)
if ok and json then
return json
end


    if pagePath == "" then return "" end
return ""
end


    local moduleName = get_module_name(pagePath)
local function collect_ids_from_entry(entry)
    local data = load_cached_data(moduleName)
local out = {}
    if not data then return "" end
local seen = {}


    local entryKey = moduleName .. "|" .. (id ~= "" and id or "default")
local function add(v)
    local entry = entryCache[entryKey]
if v == nil then
    if not entry then
return
        entry = resolve_entry(data, id)
end
        entryCache[entryKey] = entry
local s = tostring(v)
    end
if s ~= "" and not seen[s] then
    if entry == nil then return "" end
seen[s] = true
out[#out + 1] = s
end
end


    if keyPath == "" then
if type(entry) == "table" then
        return format_value(entry)
if is_array(entry) then
    end
for _, v in ipairs(entry) do
add(v)
end
else
for _, v in pairs(entry) do
if type(v) == "table" and is_array(v) then
for _, item in ipairs(v) do
add(item)
end
elseif type(v) ~= "table" then
add(v)
end
end
end
else
add(entry)
end


    local value = get_by_path(entry, keyPath)
return out
    return format_value(value)
end
end


function p.getId(frame)
function p.search(frame)
    local args = frame.args or {}
local args = getArgs(frame, { removeBlanks = false })
    local searchValue = args[1] or ""
local mode = (args[1] or ""):lower()
    local pagePath = args[2] or ""
local rawList = args[2] or ""
    local keyPath = args[3] or ""
 
if mode ~= "component" and mode ~= "tag" then
return ""
end


    if searchValue == "" or pagePath == "" or keyPath == "" then
local targets = split_csv_list(rawList)
        return ""
if #targets == 0 then
    end
return ""
end


    local moduleName = get_module_name(pagePath)
local pagePath = (mode == "component") and "component.json" or "tag.json"
    local data = load_cached_data(moduleName)
local data = p.loadCachedData(pagePath)
    if not data then return "[]" end
if not data then
return ""
end


    local idsTable = data.id
local matches = {}
    if type(idsTable) ~= "table" then
local seen = {}
        return ""
    end


    local matches = find_matching_ids(idsTable, keyPath, searchValue)
local function add_id(id)
local s = tostring(id)
if s ~= "" and not seen[s] then
seen[s] = true
matches[#matches + 1] = s
end
end


    if #matches == 0 then
for _, key in ipairs(targets) do
        return ""
local entry = resolve_entry_from_data(data, key)
    end
if entry ~= nil then
local ids = collect_ids_from_entry(entry)
for _, id in ipairs(ids) do
add_id(id)
end
end
end


    local ok, json = pcall(mw.text.jsonEncode, matches)
table.sort(matches, function(a, b)
    if ok and json then
return tostring(a) < tostring(b)
        return json
end)
    end


    return ""
local ok, json = pcall(mw.text.jsonEncode, matches)
if ok and json then
return json
end
 
return ""
end
end


function p.getTplId(frame)
function p.searchIdTpl(frame)
    local args = frame.args or {}
local args = getArgs(frame, { removeBlanks = false })
    local searchValue = args[1] or ""
local searchValue = args[1] or ""
    local pagePath = args[2] or ""
local pagePath = args[2] or ""
    local keyPath = args[3] or ""
local keyPath = args[3] or ""
    local tplPath = mw.text.unstripNoWiki(args[4] or "")
local tplPath = mw.text.unstripNoWiki(args[4] or "")
local tplArgs = args.tplArgs or args.templateArgs or ""
local searchType = (args.searchType or ""):lower()


    if searchValue == "" or pagePath == "" or keyPath == "" or tplPath == "" then
if searchType == "" then
        return ""
searchType = "value"
    end
end


    local moduleName = get_module_name(pagePath)
if searchType == "path" then
    local data = load_cached_data(moduleName)
searchValue = ""
    if not data then return "" end
pagePath = args[1] or ""
keyPath = args[2] or ""
tplPath = mw.text.unstripNoWiki(args[3] or "")
tplArgs = args[4] or args.tplArgs or args.templateArgs or ""
end


    local idsTable = data.id
tplPath, tplArgs = split_template_spec(tplPath, tplArgs)
    if type(idsTable) ~= "table" then
        return ""
    end


    local matches = find_matching_ids(idsTable, keyPath, searchValue)
if pagePath == "" or keyPath == "" or tplPath == "" then
return ""
end
if searchType ~= "path" and searchValue == "" then
return ""
end


    if #matches == 0 then
local data = p.loadCachedData(pagePath)
        return ""
if not data then
    end
return ""
end


    local out = {}
local parsedPath = parse_path(keyPath)
    for _, idKey in ipairs(matches) do
if not parsedPath then
        local tpl = p.getTpl({ args = { idKey, pagePath, tplPath } })
return ""
        if tpl ~= "" then
end
            out[#out + 1] = tpl
        end
    end


    if #out == 0 then
local ids = collect_id_keys(data)
        return ""
if #ids == 0 then
    end
return ""
end


    local result = table.concat(out, " ")
local matches = {}
    return preprocess_or_return(frame, result)
local entryCache = {}
 
if searchType == "path" then
for _, idKey in ipairs(ids) do
local entry = resolve_entry_from_data(data, idKey)
entryCache[idKey] = entry
if type(entry) == "table" and entry_has_any_nonempty_path(entry, parsedPath) then
matches[#matches + 1] = idKey
end
end
else
local target = tostring(searchValue)
for _, idKey in ipairs(ids) do
local entry = resolve_entry_from_data(data, idKey)
entryCache[idKey] = entry
if type(entry) == "table" and entry_matches_path(entry, parsedPath, target, searchType) then
matches[#matches + 1] = idKey
end
end
end
 
if #matches == 0 then
return ""
end
 
local out = {}
for _, idKey in ipairs(matches) do
local tpl = build_tpl(idKey, pagePath, tplPath, data, tplArgs, entryCache[idKey], true)
if tpl ~= "" then
out[#out + 1] = tpl
end
end
 
if #out == 0 then
return ""
end
 
local result = table.concat(out, " ")
return preprocess_or_return(frame, result)
end
end


function p.getTpl(frame)
function p.getTpl(frame)
    local args = frame.args or {}
local args = getArgs(frame, { removeBlanks = false })
    local id = args[1] or ""
local id = args[1] or ""
    local pagePath = args[2] or ""
local pagePath = args[2] or ""
    local tplPath = args[3] or ""
local tplPath = mw.text.unstripNoWiki(args[3] or "")
local tplArgs = args[4] or args.tplArgs or args.templateArgs or ""
tplPath, tplArgs = split_template_spec(tplPath, tplArgs)


    if id == "" or pagePath == "" or tplPath == "" then
if id == "" or pagePath == "" or tplPath == "" then
        return ""
return ""
    end
end


    local extra = p.flattenField({ args = { id, pagePath } }) or ""
local data = frame.data
    local tplStr = "{{" .. tostring(tplPath) .. "|id=" .. tostring(id)
local tplStr = build_tpl(id, pagePath, tplPath, data, tplArgs, nil, true)
    if extra ~= "" then
return preprocess_or_return(frame, tplStr)
        tplStr = tplStr .. "|" .. extra
end
    end
    tplStr = tplStr .. "}}"


    return preprocess_or_return(frame, tplStr)
function p.searchStoreTpl(frame)
local args = getArgs(frame, { removeBlanks = false })
local searchId = args[1] or ""
local kind = (args[2] or ""):lower()
local generatorId = p.ucfirst(args[3] or "")
local tplPath = mw.text.unstripNoWiki(args[4] or "")
local tplArgs = args[5] or args.tplArgs or args.templateArgs or ""
tplPath, tplArgs = split_template_spec(tplPath, tplArgs)
 
if searchId == "" or generatorId == "" or tplPath == "" then
return ""
end
if kind ~= "prototype" and kind ~= "component" then
return ""
end
 
local dir = (kind == "prototype") and "prototype/" or "component/"
local pagePath = dir .. generatorId .. ".json"
 
local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
if not ok or type(ids) ~= "table" or #ids == 0 then
return ""
end
 
local data = p.loadCachedData(pagePath)
if not data then
return ""
end
 
local out = {}
for _, id in ipairs(ids) do
local entry = resolve_entry_from_data(data, id)
if entry ~= nil then
local tpl = build_tpl(id, pagePath, tplPath, data, tplArgs, entry, true)
if tpl ~= "" then
out[#out + 1] = tpl
end
end
end
 
local result = table.concat(out, " ")
return preprocess_or_return(frame, result)
end
 
function p.flattenParams(entry)
return flatten_parts(entry)
end
end


function p.getTplGenerator(frame)
function p.searchStore(frame)
    local args = frame.args or {}
local args = getArgs(frame, { removeBlanks = false })
    local searchId = args[1] or ""
local searchId = args[1] or ""
    local kind = (args[2] or ""):lower()
local kind = (args[2] or ""):lower()
    local generatorId = args[3] or ""
local generatorId = p.ucfirst(args[3] or "")
    local tplPath = args[4] or ""
 
if searchId == "" or generatorId == "" then
return ""
end
if kind ~= "prototype" and kind ~= "component" then
return ""
end


    if searchId == "" or generatorId == "" or tplPath == "" then
local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
        return ""
local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
    end
if not ok or type(ids) ~= "table" or #ids == 0 then
    if kind ~= "prototype" and kind ~= "component" then
return ""
        return ""
end
    end


    local dir = (kind == "prototype") and "prototype/" or "component/"
local okOut, outJson = pcall(mw.text.jsonEncode, ids)
    local pagePath = dir .. generatorId .. ".json"
if okOut and outJson then
return outJson
end


    local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
return ""
    local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
end
    if not ok or type(ids) ~= "table" or #ids == 0 then
        return ""
    end


    local out = {}
function p.loadEntityData(entityId)
    for _, id in ipairs(ids) do
if entityId == "" then
        local tpl = p.getTpl({ args = { id, pagePath, tplPath } })
return nil
        if tpl ~= "" then
end
            out[#out + 1] = tpl
        end
    end


    local result = table.concat(out, " ")
local pagePath = "prototype/Entity/" .. entityId .. ".json"
    return preprocess_or_return(frame, result)
return p.loadCachedData(pagePath)
end
end


function p.getGenerator(frame)
function p.entityHasComponent(entity, compName)
    local args = frame.args or {}
if type(entity) ~= "table" or type(entity.components) ~= "table" then
    local searchId = args[1] or ""
return false
    local kind = (args[2] or ""):lower()
end
    local generatorId = args[3] or ""
 
return entity.components[compName] ~= nil
or entity.components["type:" .. compName] ~= nil
or entity.components["!type:" .. compName] ~= nil
end


    if searchId == "" or generatorId == "" then
function p.collectEntityComponents(entity)
        return ""
if type(entity) ~= "table" or type(entity.components) ~= "table" then
    end
return {}
    if kind ~= "prototype" and kind ~= "component" then
end
        return ""
    end


    local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
local out = {}
    local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
local seen = {}
    if not ok or type(ids) ~= "table" or #ids == 0 then
local comps = entity.components
        return ""
    end


    local okOut, outJson = pcall(mw.text.jsonEncode, ids)
if #comps > 0 then
    if okOut and outJson then
for _, v in ipairs(comps) do
        return outJson
local name = p.normalizeComponentName(v)
    end
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 = p.normalizeComponentName(k)
if name and not seen[name] then
seen[name] = true
out[#out + 1] = name
end
end
end


    return ""
table.sort(out)
return out
end
end


function p.hasComp(frame)
function p.hasComp(frame)
    local args = frame.args or {}
local args = getArgs(frame, { removeBlanks = false })
    local entityId = args[1] or ""
local entityId = args[1] or ""
    local compName = args[2] or ""
local compName = args[2] or ""


    if entityId == "" or compName == "" then
if entityId == "" or compName == "" then
        return "false"
return "false"
    end
end


    local moduleName = get_module_name("component.json")
local entity = p.loadEntityData(entityId)
    local data = load_cached_data(moduleName)
if not entity then
    if not data then
return "false"
        return "false"
end
    end


    if type(data) ~= "table" then
return p.entityHasComponent(entity, compName) and "true" or "false"
        return "false"
end
    end


    local entry = data[entityId]
function p.getComp(frame)
    if type(entry) ~= "table" then
local args = getArgs(frame, { removeBlanks = false })
        return "false"
local entityId = args[1] or ""
    end


    local target = tostring(compName)
if entityId == "" then
    for _, v in ipairs(entry) do
return ""
        if tostring(v) == target then
end
            return "true"
        end
    end


    return "false"
local entity = p.loadEntityData(entityId)
if not entity then
return ""
end
 
local out = p.collectEntityComponents(entity)
local ok, json = pcall(mw.text.jsonEncode, out)
if ok and json then
return json
end
 
return ""
end
end


function p.GeneratorId(frame)
function p.getAll(frame)
    local args = frame.args or {}
local args = getArgs(frame, { removeBlanks = false })
    local jsonStr = args[1] or ""
local pagePath = args[1] or ""
    if jsonStr == "" then return "" end
local replace = mw.text.unstripNoWiki(args.replace or "")
local pattern = mw.text.unstripNoWiki(args.pattern or "(.*)")


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


    local ids = {}
local data = p.loadCachedData(pagePath)
    if type(data.id) == "table" then
if not data then
        for k in pairs(data.id) do
return ""
            ids[#ids + 1] = k
end
        end
    end


    table.sort(ids)
local idsTable = data.id or data
if type(idsTable) ~= "table" then
return ""
end


    local okOut, outJson = pcall(mw.text.jsonEncode, ids)
local ids = {}
    if okOut and outJson then
for k in pairs(idsTable) do
        return outJson
ids[#ids + 1] = k
    end
end


    return ""
table.sort(ids)
 
if replace ~= "" then
local out = {}
for _, id in ipairs(ids) do
local text = apply_pattern(id, pattern, replace)
if text ~= "" then
out[#out + 1] = text
end
end
if #out == 0 then
return ""
end
return preprocess_or_return(frame, table.concat(out, "\n"))
end
 
local ok, json = pcall(mw.text.jsonEncode, ids)
if ok and json then
return json
end
 
return ""
end
 
function p.getAllTpl(frame)
local args = getArgs(frame, { removeBlanks = false })
local pagePath = args[1] or ""
local tplPath = args[2] or ""
local tplArgs = args[3] or args.tplArgs or args.templateArgs or ""
tplPath, tplArgs = split_template_spec(tplPath, tplArgs)
 
if pagePath == "" or tplPath == "" then
return ""
end
 
local data = p.loadCachedData(pagePath)
if not data then
return ""
end
 
local idsTable = data.id or data
if type(idsTable) ~= "table" then
return ""
end
 
local out = {}
 
for idKey in pairs(idsTable) do
local entry = resolve_entry_from_data(data, idKey)
if entry ~= nil then
local tpl = build_tpl(idKey, pagePath, tplPath, data, tplArgs, entry, true)
if tpl ~= "" then
out[#out + 1] = tpl
end
end
end
 
table.sort(out)
 
local result = table.concat(out, " ")
return preprocess_or_return(frame, result)
end
 
local function encode_nowiki_json(value)
local ok, json = pcall(mw.text.jsonEncode, value)
if ok and json then
return "<nowiki>" .. json .. "</nowiki>"
end
return nil
end
 
local function collect_sorted_keys(tbl, stringOnly)
local keys = {}
for k in pairs(tbl) do
if not stringOnly or type(k) == "string" then
keys[#keys + 1] = k
end
end
 
table.sort(keys, function(a, b)
return tostring(a) < tostring(b)
end)
 
return keys
end
 
local function choose_id_key(obj)
local keys = {}
for k in pairs(obj) do
if type(k) == "string" then
keys[#keys + 1] = k
end
end
 
if #keys == 0 then
return nil
end
 
table.sort(keys, function(a, b)
local av = obj[a]
local bv = obj[b]
 
local aPrimitive = type(av) ~= "table"
local bPrimitive = type(bv) ~= "table"
 
if aPrimitive ~= bPrimitive then
return not aPrimitive
end
 
return tostring(a) < tostring(b)
end)
 
return keys[1]
end
end


function p.GeneratorTplId(frame)
local function is_wrapper_block_key(key)
    local args = frame.args or {}
return type(key) == "string" and not key:match("^[%a_][%w_]*$")
    local jsonStr = args[1] or ""
end
    local tplPath = args[2] or ""


    if jsonStr == "" or tplPath == "" or pagePath == "" then
local function is_array_of_primitives(tbl)
        return ""
if type(tbl) ~= "table" or not is_array(tbl) then
    end
return false
end


    local ok, data = pcall(mw.text.jsonDecode, jsonStr)
for _, v in ipairs(tbl) do
    if not ok or type(data) ~= "table" then
if type(v) == "table" then
        return ""
return false
    end
end
end


    local idsTable = data.id
return true
    if type(idsTable) ~= "table" then
end
        return ""
    end


    local out = {}
local function append_table_fields(parts, value, options, prefix)
if type(value) ~= "table" or next(value) == nil then
return
end
 
if options.skipPrimitiveRoot and is_array_of_primitives(value) then
return
end
 
if prefix and options.includeJsonAtPrefix then
local json = encode_nowiki_json(value)
if json then
parts[#parts + 1] = prefix .. "=" .. json
end
end
 
local keys = collect_sorted_keys(value, false)
 
for _, k in ipairs(keys) do
if not (options.nestedKeyMode == "raw" and type(k) == "number") then
local v = value[k]
local key
if prefix then
key = prefix .. "." .. tostring(k)
else
key = tostring(k)
end
 
if type(v) == "table" then
if is_array_of_primitives(v) then
local json = encode_nowiki_json(v)
if json then
parts[#parts + 1] = key .. "=" .. json
end
elseif options.nestedKeyMode == "raw" then
local json = encode_nowiki_json(v)
if json then
parts[#parts + 1] = key .. "=" .. json
end
end
 
if next(v) ~= nil and not is_array_of_primitives(v) then
local childPrefix
if options.nestedKeyMode == "prefixed" then
childPrefix = key
elseif type(k) == "string" then
childPrefix = key
else
childPrefix = nil
end
append_table_fields(parts, v, options, childPrefix)
end
else
parts[#parts + 1] = key .. "=" .. tostring(v)
end
end
end
end
 
function p.json(frame)
local args = getArgs(frame, { removeBlanks = false })
local jsonStr = mw.text.unstripNoWiki(args[1] or args.json or "")
local tplPath = mw.text.unstripNoWiki(args[2] or args.template or "")
local tplArgs = args[3] or args.tplArgs or args.templateArgs or ""
tplPath, tplArgs = split_template_spec(tplPath, tplArgs)
 
if jsonStr == "" or tplPath == "" then
return ""
end
 
local ok, data = pcall(mw.text.jsonDecode, jsonStr)
if not ok or type(data) ~= "table" then
return ""
end
 
local calls = {}
local nestedOptions = {
includeJsonAtPrefix = true,
nestedKeyMode = "prefixed",
skipPrimitiveRoot = false,
}
local rawTypeOptions = {
includeJsonAtPrefix = false,
nestedKeyMode = "raw",
skipPrimitiveRoot = true,
}
 
local function is_object_map(tbl)
local count = 0
for k, v in pairs(tbl) do
if type(k) ~= "string" or type(v) ~= "table" then
return false
end
count = count + 1
end
return count > 1
end
 
local firstParamOverride = mw.text.unstripNoWiki(args.first or args.firstParam or args.idKey or "")
 
local function makeCall(obj)
if type(obj) ~= "table" then
return
end
 
local idKey = firstParamOverride
if idKey == "" then
idKey = choose_id_key(obj)
end
if not idKey then
return
end
 
local parts = { "{{Шаблон:" .. resolve_template_path(tplPath) }
 
if tplArgs ~= "" then
parts[#parts + 1] = tplArgs
end
 
parts[#parts + 1] = tostring(idKey)
local keys = collect_sorted_keys(obj, true)
 
for _, k in ipairs(keys) do
local v = obj[k]
 
if k == idKey then
if is_wrapper_block_key(k) then
if type(v) == "table" then
local json = encode_nowiki_json(v)
if json then
parts[#parts + 1] = "value=" .. json
end
append_table_fields(parts, v, rawTypeOptions, nil)
elseif v ~= nil then
parts[#parts + 1] = "value=" .. tostring(v)
end
elseif type(v) == "table" then
if is_array_of_primitives(v) then
local json = encode_nowiki_json(v)
if json then
parts[#parts + 1] = "value=" .. json
end
end
 
if next(v) ~= nil then
append_table_fields(parts, v, nestedOptions, k)
append_table_fields(parts, v, rawTypeOptions, nil)
end
elseif v ~= nil then
parts[#parts + 1] = "value=" .. tostring(v)
parts[#parts + 1] = k .. "=" .. tostring(v)
end
else
if type(v) == "table" then
if next(v) ~= nil then
append_table_fields(parts, v, nestedOptions, k)
end
elseif v ~= nil then
parts[#parts + 1] = k .. "=" .. tostring(v)
end
end
end
 
parts[#parts + 1] = "}}"
calls[#calls + 1] = table.concat(parts, "|")
end
 
if is_array(data) then
for _, item in ipairs(data) do
if type(item) == "table" then
makeCall(item)
elseif item ~= nil and item ~= "" then
makeCall({ [item] = {} })
end
end
elseif is_object_map(data) then
local keys = collect_sorted_keys(data, true)
for _, k in ipairs(keys) do
makeCall({ [k] = data[k] })
end
else
makeCall(data)
end
 
if #calls == 0 then
return ""
end
 
return frame:preprocess(table.concat(calls, " "))
end
 
function p.jsonList(frame)
local args = getArgs(frame, { removeBlanks = false })
local jsonStr = mw.text.unstripNoWiki(args[1] or args.json or "")
if jsonStr == "" then
return ""
end
 
local ok, data = pcall(mw.text.jsonDecode, jsonStr)
if not ok or type(data) ~= "table" then
return ""
end
 
local outputType = (args.type or "list")
 
local bullet = mw.text.unstripNoWiki(args.prefix or "* ")
local sep = mw.text.unstripNoWiki(args.sep or ": ")
 
if outputType == "none" then
bullet = ""
sep = ""
elseif outputType == "revertList" then
sep = mw.text.unstripNoWiki(args.sep or " ")
end
 
local keyPattern = mw.text.unstripNoWiki(args.key_pattern or "(.*)")
local keyReplace = mw.text.unstripNoWiki(args.key_replace or "\\1")
local valuePattern = mw.text.unstripNoWiki(args.value_pattern or "(.*)")
local valueReplace = mw.text.unstripNoWiki(args.value_replace or "\\1")
 
local pairPattern = mw.text.unstripNoWiki(args.pattern or "(.*)")
local pairReplace = mw.text.unstripNoWiki(args.replace or "\\1")
 
local maxItems = tonumber(args.max or args.limit or args.max_count or args.maxCount or "")
if maxItems ~= nil then
maxItems = math.floor(maxItems)
if maxItems < 0 then
maxItems = nil
end
end
 
local out = {}
 
if is_array(data) then
local processed = 0
 
for _, v in ipairs(data) do
processed = processed + 1
if maxItems ~= nil and processed > maxItems then
break
end
 
local text = ""
 
if type(v) == "table" then
if is_array(v) then
text = table.concat(v, ", ")
else
local okJson, jsonVal = pcall(mw.text.jsonEncode, v)
if okJson and jsonVal then
text = jsonVal
end
end
else
text = tostring(v)
end
 
if text ~= "" then
local patt = valuePattern ~= "" and valuePattern or keyPattern
local repl = valueReplace ~= "" and valueReplace or keyReplace
text = apply_pattern(text, patt, repl)
 
local line
if outputType == "enum" then
line = text
else
line = bullet .. text
end
 
if pairPattern ~= "" then
line = apply_pattern(line, pairPattern, pairReplace)
end
 
table.insert(out, line)
end
end
else
local keys = {}
for k in pairs(data) do
keys[#keys + 1] = k
end
table.sort(keys, function(a, b)
return tostring(a) < tostring(b)
end)
 
local processed = 0
 
for _, k in ipairs(keys) do
processed = processed + 1
if maxItems ~= nil and processed > maxItems then
break
end
 
local v = data[k]
local vStr
 
if type(v) == "table" then
local okJson, jsonVal = pcall(mw.text.jsonEncode, v)
if okJson and jsonVal then
vStr = jsonVal
else
vStr = ""
end
else
vStr = tostring(v)
end
 
local baseKey = apply_pattern(tostring(k), keyPattern, "\\1")
 
local MARK_KEY = "\31KEY\31"
local vRepl = (valueReplace or "\\1"):gsub("\\2", MARK_KEY)
local vStr0 = apply_pattern(vStr, valuePattern, vRepl)
vStr0 = tostring(vStr0):gsub(MARK_KEY, baseKey)
 
local MARK_VAL = "\31VAL\31"
local kRepl = (keyReplace or "\\1"):gsub("\\2", MARK_VAL)
local keyStr0 = apply_pattern(tostring(k), keyPattern, kRepl)
local keyStr = tostring(keyStr0):gsub(MARK_VAL, vStr0)
 
vStr = vStr0
 
if vStr ~= "" then
local line
if outputType == "enum" then
line = vStr .. " " .. keyStr
elseif outputType == "revertList" then
line = bullet .. vStr .. sep .. keyStr
else
line = bullet .. keyStr .. sep .. vStr
end
 
if pairPattern ~= "" then
line = apply_pattern(line, pairPattern, pairReplace)
end
 
table.insert(out, line)
end
end
end
 
if outputType == "enum" then
return frame:preprocess(table.concat(out, ", "))
elseif outputType == "list" or outputType == "revertList" then
return frame:preprocess(table.concat(out, "\n"))
else
return frame:preprocess(table.concat(out, " "))
end
end


    for idKey in pairs(idsTable) do
function p.flattenFieldSelectiveDirect(id, dataPage, paramNames)
        local tpl = p.getTpl({ args = { idKey, jsonStr, tplPath } })
if id == "" or dataPage == "" or type(paramNames) ~= "table" or #paramNames == 0 then
        if tpl ~= "" then
return ""
            out[#out + 1] = tpl
end
        end
    end


    table.sort(out)
local entry = load_entry(id, dataPage) or {}
local parts = flatten_selected_parts(entry, paramNames)
if #parts == 0 then
return ""
end


    local result = table.concat(out, " ")
return table.concat(parts, "|")
    return preprocess_or_return(frame, result)
end
end


return p
return p

Текущая версия от 18:03, 24 июня 2026

Документация

Модуль предназначен для получения данных из кэшированных JSON-страниц и их использования в шаблонах. С его помощью можно получить поле по пути, найти id по значению или сразу собрать вызов шаблона по найденным данным.

Поля берутся из json страниц

Для подпроектов

Основные функции

get

Возвращает запись целиком или отдельное поле по её id.

Использование:

  • {{#invoke:GetField|get|id|pagePath|keyPath}}

Простые значения возвращаются как текст, а таблицы в JSON-виде.

Например, json сущности MopItem из Участник:IanComradeBot/component/meleeWeapon.json выглядит так:

"MopItem": {
    "damage": {
        "types": {
            "Blunt": 10
        }
    },
...
},

то мы можем получить значение как как:

  • {{#invoke:GetField|get|MopItem|component/meleeWeapon.json|damage}} -> "types": {"Blunt": 10}}
  • {{#invoke:GetField|get|MopItem|component/meleeWeapon.json|damage.types}} -> {"Blunt": 10}
  • {{#invoke:GetField|get|MopItem|component/meleeWeapon.json|damage.types.Blunt}} -> 10
ПараметрОписаниеОбязателен?
|1 =Id записи.Да
|2 =Путь до JSON-страницы, например component/meleeWeapon.json.Да
|3 =Путь до поля внутри записи. Поддерживает вложенность через точку и индексы вида field.1Да

getTpl

Строит вызов шаблона для одного id передавая в него развёрнутые поля записи.

Использование:

  • {{#invoke:GetField|getTpl|id|pagePath|template}}

Шаблон вызывается в виде {{Имя шаблона|id=...|...поля записи...}}. Вложенные таблицы передаются как плоские параметры, а также в JSON-виде там, где это нужно для сохранения структуры.

Пример:

  • {{#invoke:GetField|getTpl|MopItem|сomponent/spillable.json|сomponent/spillable}}
ПараметрОписаниеОбязателен?
|1 =Id записи.Да
|2 =Путь до JSON-страницы.Да
|3 =Имя шаблона, который будет вызван как {{Имя шаблона|id=...|...}}.Да

searchId / searchIdTpl

Обе функции ищут id по значению в указанном поле. Разница в результате:

  • searchId возвращает найденные id в виде JSON-массива.
  • searchIdTpl по найденным id сразу вызывает шаблон.

Использование:

  • {{#invoke:GetField|searchId|searchValue|pagePath|keyPath}}
  • {{#invoke:GetField|searchIdTpl|searchValue|pagePath|keyPath|template}}
  • {{#invoke:GetField|searchIdTpl|pagePath|keyPath|template|searchType=path}}

Режимы поиска:

  • searchType=value — ищет id, у которых значение поля равно указанному значению. Это режим по умолчанию.
  • searchType=key — ищет id, у которых в поле-таблице существует ключ с указанным именем.
  • searchType=path — только для searchIdTpl; выводит все id, у которых поле по указанному пути существует и не пустое. В этом режиме первым параметром передаётся pagePath, а не значение для поиска.

searchIdTpl вызывает шаблон в виде {{Имя шаблона|id=...|...поля записи...}}. Если найдено несколько id, вызовы собираются подряд через пробел.

Примеры:

  • {{#invoke:GetField|searchId|Elements|prototype/reaction.json|group}} -> ["LemonLime","IrishCoffee","DemonsBlood","SapBoiling","DevilsKiss","MoscowMule","OrangeLimeSoda","RedMead","Vodka","Tortuga","Daiquiri","Andalusia","DoctorsDelight","LongIslandIcedTea","BloodyMary","Lemonade","SpaceGlue","AmmoniaFromBlood"]
ПараметрОписаниеОбязателен?
|1 =Значение для поиска.Да
|2 =Путь до JSON-страницы.Да
|3 =Путь до поля внутри записи. Поддерживает вложенность через точку и индексы вида field.1Да
|4 =Имя шаблона, который будет вызван для каждого найденного id.Только для searchIdTpl
|searchType =Режим поиска: value, key или path.Нет; value

search

Ищет сущности, у которых есть указанные компоненты или теги. Поддерживается поиск по нескольким значениям через запятую: сущность попадёт в результат только если содержит все перечисленные элементы.

Использование:

  • {{#invoke:GetField|search|component|componentName}}
  • {{#invoke:GetField|search|tag|tagName}}

Режимы:

  • component — ищет сущности по списку компонентов.
  • tag — ищет сущности по списку тегов.

Результат:

  • возвращается JSON-массив id найденных сущностей;
  • в режиме component результат также JSON-массив, без дополнительного форматирования.

Примеры:

  • {{#invoke:GetField|search|component|Emag}} -> ["AccessBreaker","AccessBreakerUnlimited","Emag","EmagUnlimited","MobRevenant"]
  • {{#invoke:GetField|search|tag|Crayon}} -> ["CrayonBlack","CrayonBlue","CrayonBorg","CrayonGreen","CrayonInfinite","CrayonMime","CrayonOrange","CrayonPurple","CrayonRainbow","CrayonRed","CrayonWhite","CrayonYellow"]
ПараметрОписаниеОбязателен?
|1 =Режим поиска: component или tag.Да
|2 =Список компонентов или тегов через запятую. Все указанные значения должны присутствовать у сущности.Да

searchStore / searchStoreTpl

Эти функции находят прототипы или компоненты содержащие указанный id, используя Участник:IanComradeBot/prototype_store.json или Участник:IanComradeBot/component_store.json.

Использование:

  • {{#invoke:GetField|searchStore|searchId|prototype|Название}}
  • {{#invoke:GetField|searchStore|searchId|component|Название}}
  • {{#invoke:GetField|searchStoreTpl|searchId|prototype|Название|Шаблон}}

searchStore возвращает JSON-массив id, найденных в компонентах/прототипах. searchStoreTpl по тем же id сразу вызывает шаблон, используя страницу вида prototype/Название.json или component/Название.json.

Примеры:

  • {{#invoke:GetField|searchStore|MopItem|component|itemBorgModule}} -> ["BorgModuleCleaning"]
  • {{#invoke:GetField|searchStore|MopItem|prototype|latheRecipe}} -> ["MopItem"]
ПараметрОписаниеОбязателен?
|1 =Id, который ищется в хранилище.Да
|2 =Тип: prototype или component.Да
|3 =Имя хранилищя без .json.Да
|4 =Имя шаблона, который будет вызван для каждого найденного id.Только для searchStoreTpl

getAll / getAllTpl

Эти функции получают все id прототипов или компонентов.

Использование:

  • {{#invoke:GetField|getAll|pagePath}}
  • {{#invoke:GetField|getAllTpl|pagePath|template}}

getAll по умолчанию возвращает JSON-массив id. getAllTpl вызывает шаблон для каждого id в виде {{Имя шаблона|id=...|...поля записи...}}.

Примеры:

  • {{#invoke:GetField|getAll|component/staticPrice.json}} -> выводит все id сущностей с этим компонентом в формате JSON
  • {{#invoke:GetField|getAllTpl|component/staticPrice.json|component/staticPrice/wrapper}} -> выводит все id сущностей с этим компонентом обёрнутым в шаблон {{component/staticPrice/wrapper}}
ПараметрОписаниеОбязателен?
|1 =Путь до JSON-страницы.Да
|2 =Имя шаблона для getAllTpl.Только для getAllTpl
|replace =Строка замены для getAll. Если задана, результат выводится построчно вместо JSON-массива.Нет

hasComp

Проверяет, есть ли у сущности указанный компонент.

Использование:

  • {{#invoke:GetField|hasComp|entityId|componentName}}

Возвращает строку true или false.

Пример:

  • {{#invoke:GetField|hasComp|MopItem|Item}}
ПараметрОписаниеОбязателен?
|1 =Id сущности.Да
|2 =Имя компонента для проверки.Да

getComp

Возвращает список всех компонентов сущности в виде JSON-массива.

Использование:

Пример:

  • {{#invoke:GetField|getComp|MopItem}} -> ["Item","Sprite","MeleeWeapon","SolutionContainerManager"...]
ПараметрОписаниеОбязателен?
|1 =Id сущности.Да

jsonList

Преобразует JSON в список, перечисление или простой текст. Подходит для быстрого вывода данных без отдельного шаблона.

Использование:

  • {{#invoke:GetField|jsonList|["a","b","c"]}}
  • {{#invoke:GetField|jsonList|{"MopItem":"Швабра"}}}
  • {{#invoke:GetField|jsonList|{"MopItem":"Швабра"}|type=enum}}
  • {{#invoke:GetField|jsonList|["a","b","c"]|max=1}}

Основные параметры:

  • type — формат вывода: list, enum или none.
  • prefix — префикс строки для режима списка. По умолчанию * .
  • sep — разделитель между ключом и значением. По умолчанию : .
  • replace — дополнительная обработка регулярным выражением уже собранной строки.
  • key_replace, value_replace — обработка ключей и значений регулярным выражением по отдельности.
  • max — максимальное количество элементов, которые нужно обработать. Если не задан, обрабатывается весь список.

Примеры:

  • {{#invoke:GetField|jsonList|["a","b","c"]}}
  • {{#invoke:GetField|jsonList|{"MopItem":"Швабра"}|type=list}}
  • {{#invoke:GetField|jsonList|{"MopItem":"Швабра"}|type=none|value_replace=<nowiki>[[\1]]</nowiki>}}
  • {{#invoke:GetField|jsonList|["a","b","c"]|max=1}}
ПараметрОписаниеОбязателен?
|1 =JSON-строка. Можно также передать именованным параметром json.Да
|type =Формат вывода: list, enum или none.Нет; list
|prefix =Префикс строки для режима list.Нет; *
|sep =Разделитель между ключом и значением.Нет; :
|key_replace =Строка замены для ключей.Нет; \1
|value_replace =Строка замены для значений.Нет; \1
|replace =Строка замены для всего вывода.Нет; \1
|max =Максимальное количество элементов для обработки.Нет; весь список

json

Преобразует JSON-объект или JSON-массив объектов в набор вызовов шаблона и сразу обрабатывает их.

Использование:

  • {{#invoke:GetField|json|{"MopItem":{"name":"Швабра"}}|Предмет}}

Если значение по id является объектом, его поля разворачиваются в параметры шаблона. Если значение простое, оно передаётся как value=....

Пример вызова, который будет собран функцией:

  • {{Предмет|id=MopItem|name=Швабра}}
ПараметрОписаниеОбязателен?
|1 =JSON-строка.Да
|2 =Имя шаблона.Да

См. также

Примечания

  • Если запись, поле или JSON-страница не найдены, функции обычно возвращают пустую строку.
  • Функция get возвращает таблицы в JSON-виде.
  • В searchId и searchIdTpl значения сравниваются как строки.
  • Параметр keyPath поддерживает доступ к вложенным полям и индексам.
  • getTpl и searchIdTpl удобны, когда нужно не получить сырые данные, а сразу отрендерить карточку или другой шаблон.
  • Функции с searchStore работают только с генераторными страницами и хранилищами, где структура данных уже подготовлена под поиск по id.
  • json и jsonList ожидают корректный JSON; если строка не разбирается, результат будет пустым.
local p = {}

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

function p.loadCachedData(pagePath)
	local titleName = JsonPaths.get(pagePath)
	if not titleName or titleName == "" then
		return nil
	end

	local ok, data = pcall(mw.loadData, titleName)
	if not ok or type(data) ~= "table" then
		return nil
	end

	return data
end

local function parse_path(path)
	if not path or path == "" then
		return nil
	end

	local parsed = {}
	for part in string.gmatch(path, "([^%.]+)") do
		local key, idxStr = string.match(part, "^(.-)%[(%d+)%]$")
		if key then
			parsed[#parsed + 1] = { key, tonumber(idxStr) }
		else
			local num = tonumber(part)
			if num then
				parsed[#parsed + 1] = { nil, num }
			else
				parsed[#parsed + 1] = { part, nil }
			end
		end
	end

	return parsed
end

local function navigate_path(tbl, path)
	if not tbl then return nil end
	if not path or path == "" then return tbl end

	local cur = tbl
	local start = 1
	local len = #path

	while start <= len do
		local dotPos = string.find(path, ".", start, true)
		local part
		if dotPos then
			part = string.sub(path, start, dotPos - 1)
			start = dotPos + 1
		else
			part = string.sub(path, start)
			start = len + 1
		end

		if type(cur) ~= "table" then return nil end

		local bracketPos = string.find(part, "[", 1, true)
		if bracketPos then
			local key = string.sub(part, 1, bracketPos - 1)
			local idx = tonumber(string.sub(part, bracketPos + 1, #part - 1))

			if key ~= "" then
				local val = cur[key]
				if val == nil then val = cur["!type:" .. key] end
				cur = val
			end

			if idx then
				if type(cur) ~= "table" then return nil end
				cur = cur[idx]
			end
		else
			local num = tonumber(part)
			if num then
				cur = cur[num]
			else
				local val = cur[part]
				if val == nil then val = cur["!type:" .. part] end
				cur = val
			end
		end

		if cur == nil then return nil end
	end

	return cur
end

local function format_value(v)
	if v == nil then
		return ""
	end

	local t = type(v)
	if t == "string" or t == "number" or t == "boolean" then
		return tostring(v)
	elseif t == "table" then
		local ok, json = pcall(mw.text.jsonEncode, v)
		if ok and json then
			return json
		end
		return ""
	else
		return tostring(v)
	end
end

local function is_array(tbl)
	local max = 0
	local count = 0
	for k in pairs(tbl) do
		if type(k) ~= "number" then
			return false
		end
		if k > max then
			max = k
		end
		count = count + 1
	end
	return count > 0 and max == count
end

local function deep_copy(src)
	local dst = {}
	for k, v in pairs(src) do
		if type(v) == "table" then
			dst[k] = deep_copy(v)
		else
			dst[k] = v
		end
	end
	return dst
end

local function deep_merge(dst, src)
	for k, v in pairs(src) do
		if type(v) == "table" and type(dst[k]) == "table" then
			deep_merge(dst[k], v)
		elseif type(v) == "table" then
			dst[k] = deep_copy(v)
		else
			dst[k] = v
		end
	end
end

function p.ucfirst(s)
	if not s or s == "" then return s end
	return string.upper(s:sub(1, 1)) .. (s:sub(2) or "")
end

function p.normalizeComponentName(name)
	if type(name) ~= "string" or name == "" then
		return nil
	end
	name = mw.text.trim(name)
	if name:sub(1, 5) == "type:" then
		name = name:sub(6)
	elseif name:sub(1, 6) == "!type:" then
		name = name:sub(7)
	end
	if name == "" then
		return nil
	end
	return name
end

local function split_data_page_path(pagePath)
	local kind, name = string.match(pagePath or "", "^(component)/([^/]+)%.json$")
	if not kind then
		return nil, nil
	end
	return kind, p.ucfirst(name)
end

local function get_component_from_entity(entity, componentName)
	if type(entity) ~= "table" or type(entity.components) ~= "table" then
		return nil
	end

	return entity.components["type:" .. componentName]
		or entity.components[componentName]
		or entity.components["!type:" .. componentName]
end

local function load_specific_entry(pagePath, id, allowDefault)
	local kind, name = split_data_page_path(pagePath)
	if not kind or not id or id == "" then
		return nil
	end

	if kind ~= "component" then
		return nil
	end

	local base
	local defaultData = p.loadCachedData(kind .. "/" .. name .. "/defaultFields.json")
	if type(defaultData) == "table" then
		base = deep_copy(defaultData)
	end

	local entityPage = "prototype/Entity/" .. id .. ".json"
	local entity = p.loadCachedData(entityPage)
	local specific = get_component_from_entity(entity, name)

	if type(specific) ~= "table" then
		if allowDefault == false then
			return nil
		end
		return base
	end

	if base then
		deep_merge(base, specific)
		return base
	end

	return specific
end

local function resolve_entry_from_data(data, id, allowDefault)
	if type(data) ~= "table" then
		return nil
	end

	if id and id ~= "" then
		local direct = data[id]
		if direct ~= nil then
			return direct
		end

		local idsTable = data.id
		if type(idsTable) == "table" then
			local specific = idsTable[id]
			if type(specific) == "table" then
				local base = data["default"]
				if type(base) == "table" then
					local merged = deep_copy(base)
					deep_merge(merged, specific)
					return merged
				end
				return specific
			end
		end
	end

	if allowDefault == false then
		return nil
	end

	local base = data["default"]
	if type(base) == "table" then
		return base
	end

	return nil
end

local function load_entry(id, pagePath, data, allowDefault)
	local specificPageEntry = load_specific_entry(pagePath, id, allowDefault)
	if specificPageEntry ~= nil then
		return specificPageEntry
	end

	data = data or p.loadCachedData(pagePath)
	if allowDefault == nil then
		allowDefault = true
	end
	return resolve_entry_from_data(data, id, allowDefault)
end

local function collect_id_keys(data)
	if type(data) ~= "table" then
		return {}
	end

	local idsTable = data.id
	local ids = {}

	if type(idsTable) == "table" then
		for k in pairs(idsTable) do
			ids[#ids + 1] = k
		end
		return ids
	end

	for k in pairs(data) do
		if k ~= "default" and k ~= "id" then
			ids[#ids + 1] = k
		end
	end

	return ids
end

local function contains_target(v, target)
	if type(v) == "string" then
		return v == target
	end
	if type(v) == "table" then
		if is_array(v) then
			for _, item in ipairs(v) do
				if tostring(item) == target then
					return true
				end
			end
			return false
		end

		for _, item in pairs(v) do
			if tostring(item) == target then
				return true
			end
		end
		return false
	end

	return tostring(v) == target
end

local function is_nonempty_value(v)
	if v == nil then
		return false
	end
	if type(v) == "table" then
		return next(v) ~= nil
	end
	return true
end

local function preprocess_or_return(frame, text)
	if type(frame) == "table" and type(frame.preprocess) == "function" then
		return frame:preprocess(text)
	end
	return text
end

local function get_field_loose(entry, fieldId)
	local value = entry[fieldId]
	if value ~= nil then
		return value
	end
	if fieldId == "" then
		return nil
	end

	local first = string.sub(fieldId, 1, 1)
	local tail = string.sub(fieldId, 2)

	value = entry[string.lower(first) .. tail]
	if value ~= nil then
		return value
	end

	return entry[string.upper(first) .. tail]
end

local function apply_pattern(s, pattern, repl)
	if not pattern or pattern == "" or not s then
		return s
	end

	local text = tostring(s)
	local replacement
	if repl and repl ~= "" then
		replacement = tostring(repl)
		replacement = replacement:gsub("\\(%d)", "%%%1")
	else
		replacement = "%1"
	end

	local patt = pattern
	if not patt:find("%^") and not patt:find("%$") then
		patt = "^" .. patt .. "$"
	end

	return (text:gsub(patt, replacement))
end

local function flatten_parts(entry)
	if type(entry) ~= "table" then
		return {}
	end

	local parts = {}

	local function walk(tbl, prefix)
		local keys = {}
		for k in pairs(tbl) do
			keys[#keys + 1] = k
		end
		table.sort(keys, function(a, b)
			return tostring(a) < tostring(b)
		end)

		for _, k in ipairs(keys) do
			local v = tbl[k]
			local kStr = tostring(k)
			local key = (prefix == "" and kStr or prefix .. "." .. kStr)

			if type(v) == "table" then
				if next(v) ~= nil then
					local ok, json = pcall(mw.text.jsonEncode, v)
					if ok and json then
						parts[#parts + 1] = key .. "=<nowiki>" .. json .. "</nowiki>"
					end
					if is_array(v) then
						local first = v[1]
						if type(first) == "table" then
							walk(first, key)
						end
					else
						walk(v, key)
					end
				end
			else
				parts[#parts + 1] = key .. "=" .. tostring(v)
			end
		end
	end

	walk(entry, "")
	return parts
end

local function flatten_entry(entry)
	local parts = flatten_parts(entry)
	if #parts == 0 then
		return ""
	end
	return table.concat(parts, "|")
end

local function append_flattened_part(parts, key, value)
	if value == nil then
		return
	end

	if type(value) == "table" then
		if next(value) == nil then
			return
		end

		local ok, json = pcall(mw.text.jsonEncode, value)
		if ok and json then
			parts[#parts + 1] = key .. "=<nowiki>" .. json .. "</nowiki>"
		end
		return
	end

	parts[#parts + 1] = key .. "=" .. tostring(value)
end

local function flatten_selected_parts(entry, keys)
	if type(entry) ~= "table" or type(keys) ~= "table" then
		return {}
	end

	local parts = {}
	local seen = {}

	for i = 1, #keys do
		local key = keys[i]
		if type(key) == "string" and key ~= "" and not seen[key] then
			seen[key] = true
			append_flattened_part(parts, key, navigate_path(entry, key))
		end
	end

	return parts
end

local function resolve_template_path(tplPath)
	local templatePath = tplPath
	local project = JsonPaths.project()
	if project ~= nil and project ~= "" then
		templatePath = tplPath .. "/" .. project
		templatePath = "{{#ifexist:Шаблон:" .. templatePath .. "|" .. templatePath .. "|" .. tplPath .. "}}"
	end

	return templatePath
end

local function split_template_spec(tplPath, tplArgs)
	tplPath = mw.text.unstripNoWiki(tplPath or "")
	tplArgs = mw.text.unstripNoWiki(tplArgs or "")
	if tplArgs ~= "" and string.sub(tplArgs, 1, 1) == "|" then
		tplArgs = string.sub(tplArgs, 2)
	end

	if tplArgs == "" then
		local pipePos = string.find(tplPath, "|", 1, true)
		if pipePos then
			tplArgs = mw.text.unstripNoWiki(string.sub(tplPath, pipePos + 1))
			if tplArgs ~= "" and string.sub(tplArgs, 1, 1) == "|" then
				tplArgs = string.sub(tplArgs, 2)
			end
			tplPath = string.sub(tplPath, 1, pipePos - 1)
		end
	end

	return tplPath, tplArgs
end

local function build_tpl(id, pagePath, tplPath, data, tplArgs, preEntry, strictExact)
	if id == "" or pagePath == "" or tplPath == "" then
		return ""
	end

	local entry
	if strictExact then
		entry = load_entry(id, pagePath, data, false)
	else
		entry = preEntry or load_entry(id, pagePath, data)
	end

	if entry == nil then
		return ""
	end

	local extra = flatten_entry(entry)
	local extraTplArgs = tplArgs or ""

	local templatePath = resolve_template_path(tplPath)

	local tplStr = "{{Шаблон:" .. templatePath
	if extraTplArgs ~= "" then
		tplStr = tplStr .. "|" .. extraTplArgs
	end
	tplStr = tplStr .. "|id=" .. tostring(id)
	if extra ~= "" then
		tplStr = tplStr .. "|" .. extra
	end
	tplStr = tplStr .. "}}"

	return tplStr
end

function p.findInGenerator(frame)
	local args = frame.args or {}
	local searchId = args[1] or ""
	local kind = (args[2] or ""):lower()
	local fieldId = p.ucfirst(args[3] or "")

	if searchId == "" or fieldId == "" then
		return ""
	end
	if kind ~= "prototype" and kind ~= "component" then
		return ""
	end

	local storeName = (kind == "prototype") and "prototype_store.json" or "component_store.json"
	local data = p.loadCachedData(storeName)
	if not data then
		return ""
	end

	local entry = data[searchId]
	if type(entry) ~= "table" then
		return ""
	end

	local value = get_field_loose(entry, fieldId)
	if value == nil then
		return ""
	end

	local out = {}
	local t = type(value)
	if t == "table" then
		for _, v in ipairs(value) do
			out[#out + 1] = v
		end
	else
		out[1] = value
	end

	return mw.text.jsonEncode(out)
end

function p.flattenField(frame)
	local args = frame.args or {}
	local id = args[1] or ""
	local pagePath = args[2] or ""
	if id == "" or pagePath == "" then
		return ""
	end

	local entry = load_entry(id, pagePath) or {}
	return flatten_entry(entry)
end

function p.flattenFieldSelective(frame)
	local args = frame.args or {}
	local id = args[1] or ""
	local pagePath = args[2] or ""
	local keysJson = mw.text.unstripNoWiki(args[3] or args.keys or "")
	if id == "" or pagePath == "" or keysJson == "" then
		return ""
	end

	local okKeys, keys = pcall(mw.text.jsonDecode, keysJson)
	if not okKeys or type(keys) ~= "table" or #keys == 0 then
		return ""
	end

	local entry = load_entry(id, pagePath) or {}
	local parts = flatten_selected_parts(entry, keys)
	if #parts == 0 then
		return ""
	end

	return table.concat(parts, "|")
end

function p.get(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local id = args[1] or ""
	local pagePath = args[2] or ""
	local keyPath = args[3] or ""

	if pagePath == "" then
		return ""
	end

	local entry = load_entry(id, pagePath)
	if entry == nil then
		return ""
	end

	if keyPath == "" then
		return format_value(entry)
	end

	return format_value(navigate_path(entry, keyPath))
end

local function collect_by_parsed_path(tbl, parsedPath, pos, out)
	if pos > #parsedPath then
		out[#out + 1] = tbl
		return
	end

	if type(tbl) ~= "table" then
		return
	end

	local token = parsedPath[pos]
	local key = token[1]
	local idx = token[2]

	if key == "*" then
		for _, child in pairs(tbl) do
			local nextCur = child

			if idx then
				if type(nextCur) ~= "table" then
					nextCur = nil
				else
					nextCur = nextCur[idx]
				end
			end

			collect_by_parsed_path(nextCur, parsedPath, pos + 1, out)
		end
		return
	end

	local nextCur

	if key and key ~= "" then
		nextCur = tbl[key]
		if nextCur == nil then nextCur = tbl["!type:" .. key] end
	else
		nextCur = tbl
	end

	if idx then
		if type(nextCur) ~= "table" then
			return
		end
		nextCur = nextCur[idx]
	end

	collect_by_parsed_path(nextCur, parsedPath, pos + 1, out)
end

local function get_by_parsed_path_multi(tbl, parsedPath)
	local out = {}
	collect_by_parsed_path(tbl, parsedPath, 1, out)
	return out
end

local function entry_matches_path(entry, parsedPath, searchValue, searchType)
	local values = get_by_parsed_path_multi(entry, parsedPath)
	local target = tostring(searchValue)

	for _, v in ipairs(values) do
		if searchType == "key" then
			if type(v) == "table" and v[target] ~= nil then
				return true
			end
		else
			if contains_target(v, target) then
				return true
			end
		end
	end

	return false
end

local function entry_has_any_nonempty_path(entry, parsedPath)
	local values = get_by_parsed_path_multi(entry, parsedPath)

	for _, v in ipairs(values) do
		if is_nonempty_value(v) then
			return true
		end
	end

	return false
end

local function split_csv_list(text)
	text = mw.text.unstripNoWiki(text or "")
	local out = {}

	for part in string.gmatch(text, "([^,]+)") do
		part = mw.text.trim(part)
		if part ~= "" then
			out[#out + 1] = part
		end
	end

	return out
end

function p.searchId(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchValue = args[1] or ""
	local pagePath = args[2] or ""
	local keyPath = args[3] or ""
	local searchType = (args.searchType or ""):lower()

	if searchValue == "" or pagePath == "" or keyPath == "" then
		return ""
	end
	if searchType == "" then
		searchType = "value"
	end

	local data = p.loadCachedData(pagePath)
	if not data then
		return "[]"
	end

	local parsedPath = parse_path(keyPath)
	if not parsedPath then
		return ""
	end

	local ids = collect_id_keys(data)
	if #ids == 0 then
		return ""
	end

	local matches = {}
	local target = tostring(searchValue)

	for _, idKey in ipairs(ids) do
		local entry = resolve_entry_from_data(data, idKey)
		if type(entry) == "table" and entry_matches_path(entry, parsedPath, target, searchType) then
			matches[#matches + 1] = idKey
		end
	end

	if #matches == 0 then
		return ""
	end

	local ok, json = pcall(mw.text.jsonEncode, matches)
	if ok and json then
		return json
	end

	return ""
end

local function collect_ids_from_entry(entry)
	local out = {}
	local seen = {}

	local function add(v)
		if v == nil then
			return
		end
		local s = tostring(v)
		if s ~= "" and not seen[s] then
			seen[s] = true
			out[#out + 1] = s
		end
	end

	if type(entry) == "table" then
		if is_array(entry) then
			for _, v in ipairs(entry) do
				add(v)
			end
		else
			for _, v in pairs(entry) do
				if type(v) == "table" and is_array(v) then
					for _, item in ipairs(v) do
						add(item)
					end
				elseif type(v) ~= "table" then
					add(v)
				end
			end
		end
	else
		add(entry)
	end

	return out
end

function p.search(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local mode = (args[1] or ""):lower()
	local rawList = args[2] or ""

	if mode ~= "component" and mode ~= "tag" then
		return ""
	end

	local targets = split_csv_list(rawList)
	if #targets == 0 then
		return ""
	end

	local pagePath = (mode == "component") and "component.json" or "tag.json"
	local data = p.loadCachedData(pagePath)
	if not data then
		return ""
	end

	local matches = {}
	local seen = {}

	local function add_id(id)
		local s = tostring(id)
		if s ~= "" and not seen[s] then
			seen[s] = true
			matches[#matches + 1] = s
		end
	end

	for _, key in ipairs(targets) do
		local entry = resolve_entry_from_data(data, key)
		if entry ~= nil then
			local ids = collect_ids_from_entry(entry)
			for _, id in ipairs(ids) do
				add_id(id)
			end
		end
	end

	table.sort(matches, function(a, b)
		return tostring(a) < tostring(b)
	end)

	local ok, json = pcall(mw.text.jsonEncode, matches)
	if ok and json then
		return json
	end

	return ""
end

function p.searchIdTpl(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchValue = args[1] or ""
	local pagePath = args[2] or ""
	local keyPath = args[3] or ""
	local tplPath = mw.text.unstripNoWiki(args[4] or "")
	local tplArgs = args.tplArgs or args.templateArgs or ""
	local searchType = (args.searchType or ""):lower()

	if searchType == "" then
		searchType = "value"
	end

	if searchType == "path" then
		searchValue = ""
		pagePath = args[1] or ""
		keyPath = args[2] or ""
		tplPath = mw.text.unstripNoWiki(args[3] or "")
		tplArgs = args[4] or args.tplArgs or args.templateArgs or ""
	end

	tplPath, tplArgs = split_template_spec(tplPath, tplArgs)

	if pagePath == "" or keyPath == "" or tplPath == "" then
		return ""
	end
	if searchType ~= "path" and searchValue == "" then
		return ""
	end

	local data = p.loadCachedData(pagePath)
	if not data then
		return ""
	end

	local parsedPath = parse_path(keyPath)
	if not parsedPath then
		return ""
	end

	local ids = collect_id_keys(data)
	if #ids == 0 then
		return ""
	end

	local matches = {}
	local entryCache = {}

	if searchType == "path" then
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry_from_data(data, idKey)
			entryCache[idKey] = entry
			if type(entry) == "table" and entry_has_any_nonempty_path(entry, parsedPath) then
				matches[#matches + 1] = idKey
			end
		end
	else
		local target = tostring(searchValue)
		for _, idKey in ipairs(ids) do
			local entry = resolve_entry_from_data(data, idKey)
			entryCache[idKey] = entry
			if type(entry) == "table" and entry_matches_path(entry, parsedPath, target, searchType) then
				matches[#matches + 1] = idKey
			end
		end
	end

	if #matches == 0 then
		return ""
	end

	local out = {}
	for _, idKey in ipairs(matches) do
		local tpl = build_tpl(idKey, pagePath, tplPath, data, tplArgs, entryCache[idKey], true)
		if tpl ~= "" then
			out[#out + 1] = tpl
		end
	end

	if #out == 0 then
		return ""
	end

	local result = table.concat(out, " ")
	return preprocess_or_return(frame, result)
end

function p.getTpl(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local id = args[1] or ""
	local pagePath = args[2] or ""
	local tplPath = mw.text.unstripNoWiki(args[3] or "")
	local tplArgs = args[4] or args.tplArgs or args.templateArgs or ""
	tplPath, tplArgs = split_template_spec(tplPath, tplArgs)

	if id == "" or pagePath == "" or tplPath == "" then
		return ""
	end

	local data = frame.data
	local tplStr = build_tpl(id, pagePath, tplPath, data, tplArgs, nil, true)
	return preprocess_or_return(frame, tplStr)
end

function p.searchStoreTpl(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchId = args[1] or ""
	local kind = (args[2] or ""):lower()
	local generatorId = p.ucfirst(args[3] or "")
	local tplPath = mw.text.unstripNoWiki(args[4] or "")
	local tplArgs = args[5] or args.tplArgs or args.templateArgs or ""
	tplPath, tplArgs = split_template_spec(tplPath, tplArgs)

	if searchId == "" or generatorId == "" or tplPath == "" then
		return ""
	end
	if kind ~= "prototype" and kind ~= "component" then
		return ""
	end

	local dir = (kind == "prototype") and "prototype/" or "component/"
	local pagePath = dir .. generatorId .. ".json"

	local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
	local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
	if not ok or type(ids) ~= "table" or #ids == 0 then
		return ""
	end

	local data = p.loadCachedData(pagePath)
	if not data then
		return ""
	end

	local out = {}
	for _, id in ipairs(ids) do
		local entry = resolve_entry_from_data(data, id)
		if entry ~= nil then
			local tpl = build_tpl(id, pagePath, tplPath, data, tplArgs, entry, true)
			if tpl ~= "" then
				out[#out + 1] = tpl
			end
		end
	end

	local result = table.concat(out, " ")
	return preprocess_or_return(frame, result)
end

function p.flattenParams(entry)
	return flatten_parts(entry)
end

function p.searchStore(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local searchId = args[1] or ""
	local kind = (args[2] or ""):lower()
	local generatorId = p.ucfirst(args[3] or "")

	if searchId == "" or generatorId == "" then
		return ""
	end
	if kind ~= "prototype" and kind ~= "component" then
		return ""
	end

	local idsJson = p.findInGenerator({ args = { searchId, kind, generatorId } })
	local ok, ids = pcall(mw.text.jsonDecode, idsJson or "")
	if not ok or type(ids) ~= "table" or #ids == 0 then
		return ""
	end

	local okOut, outJson = pcall(mw.text.jsonEncode, ids)
	if okOut and outJson then
		return outJson
	end

	return ""
end

function p.loadEntityData(entityId)
	if entityId == "" then
		return nil
	end

	local pagePath = "prototype/Entity/" .. entityId .. ".json"
	return p.loadCachedData(pagePath)
end

function p.entityHasComponent(entity, compName)
	if type(entity) ~= "table" or type(entity.components) ~= "table" then
		return false
	end

	return entity.components[compName] ~= nil
		or entity.components["type:" .. compName] ~= nil
		or entity.components["!type:" .. compName] ~= nil
end

function p.collectEntityComponents(entity)
	if type(entity) ~= "table" or type(entity.components) ~= "table" then
		return {}
	end

	local out = {}
	local seen = {}
	local comps = entity.components

	if #comps > 0 then
		for _, v in ipairs(comps) do
			local name = p.normalizeComponentName(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 = p.normalizeComponentName(k)
			if name and not seen[name] then
				seen[name] = true
				out[#out + 1] = name
			end
		end
	end

	table.sort(out)
	return out
end

function p.hasComp(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local entityId = args[1] or ""
	local compName = args[2] or ""

	if entityId == "" or compName == "" then
		return "false"
	end

	local entity = p.loadEntityData(entityId)
	if not entity then
		return "false"
	end

	return p.entityHasComponent(entity, compName) and "true" or "false"
end

function p.getComp(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local entityId = args[1] or ""

	if entityId == "" then
		return ""
	end

	local entity = p.loadEntityData(entityId)
	if not entity then
		return ""
	end

	local out = p.collectEntityComponents(entity)
	local ok, json = pcall(mw.text.jsonEncode, out)
	if ok and json then
		return json
	end

	return ""
end

function p.getAll(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local pagePath = args[1] or ""
	local replace = mw.text.unstripNoWiki(args.replace or "")
	local pattern = mw.text.unstripNoWiki(args.pattern or "(.*)")

	if pagePath == "" then
		return ""
	end

	local data = p.loadCachedData(pagePath)
	if not data then
		return ""
	end

	local idsTable = data.id or data
	if type(idsTable) ~= "table" then
		return ""
	end

	local ids = {}
	for k in pairs(idsTable) do
		ids[#ids + 1] = k
	end

	table.sort(ids)

	if replace ~= "" then
		local out = {}
		for _, id in ipairs(ids) do
			local text = apply_pattern(id, pattern, replace)
			if text ~= "" then
				out[#out + 1] = text
			end
		end
		if #out == 0 then
			return ""
		end
		return preprocess_or_return(frame, table.concat(out, "\n"))
	end

	local ok, json = pcall(mw.text.jsonEncode, ids)
	if ok and json then
		return json
	end

	return ""
end

function p.getAllTpl(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local pagePath = args[1] or ""
	local tplPath = args[2] or ""
	local tplArgs = args[3] or args.tplArgs or args.templateArgs or ""
	tplPath, tplArgs = split_template_spec(tplPath, tplArgs)

	if pagePath == "" or tplPath == "" then
		return ""
	end

	local data = p.loadCachedData(pagePath)
	if not data then
		return ""
	end

	local idsTable = data.id or data
	if type(idsTable) ~= "table" then
		return ""
	end

	local out = {}

	for idKey in pairs(idsTable) do
		local entry = resolve_entry_from_data(data, idKey)
		if entry ~= nil then
			local tpl = build_tpl(idKey, pagePath, tplPath, data, tplArgs, entry, true)
			if tpl ~= "" then
				out[#out + 1] = tpl
			end
		end
	end

	table.sort(out)

	local result = table.concat(out, " ")
	return preprocess_or_return(frame, result)
end

local function encode_nowiki_json(value)
	local ok, json = pcall(mw.text.jsonEncode, value)
	if ok and json then
		return "<nowiki>" .. json .. "</nowiki>"
	end
	return nil
end

local function collect_sorted_keys(tbl, stringOnly)
	local keys = {}
	for k in pairs(tbl) do
		if not stringOnly or type(k) == "string" then
			keys[#keys + 1] = k
		end
	end

	table.sort(keys, function(a, b)
		return tostring(a) < tostring(b)
	end)

	return keys
end

local function choose_id_key(obj)
	local keys = {}
	for k in pairs(obj) do
		if type(k) == "string" then
			keys[#keys + 1] = k
		end
	end

	if #keys == 0 then
		return nil
	end

	table.sort(keys, function(a, b)
		local av = obj[a]
		local bv = obj[b]

		local aPrimitive = type(av) ~= "table"
		local bPrimitive = type(bv) ~= "table"

		if aPrimitive ~= bPrimitive then
			return not aPrimitive
		end

		return tostring(a) < tostring(b)
	end)

	return keys[1]
end

local function is_wrapper_block_key(key)
	return type(key) == "string" and not key:match("^[%a_][%w_]*$")
end

local function is_array_of_primitives(tbl)
	if type(tbl) ~= "table" or not is_array(tbl) then
		return false
	end

	for _, v in ipairs(tbl) do
		if type(v) == "table" then
			return false
		end
	end

	return true
end

local function append_table_fields(parts, value, options, prefix)
	if type(value) ~= "table" or next(value) == nil then
		return
	end

	if options.skipPrimitiveRoot and is_array_of_primitives(value) then
		return
	end

	if prefix and options.includeJsonAtPrefix then
		local json = encode_nowiki_json(value)
		if json then
			parts[#parts + 1] = prefix .. "=" .. json
		end
	end

	local keys = collect_sorted_keys(value, false)

	for _, k in ipairs(keys) do
		if not (options.nestedKeyMode == "raw" and type(k) == "number") then
			local v = value[k]
			local key
			if prefix then
				key = prefix .. "." .. tostring(k)
			else
				key = tostring(k)
			end

			if type(v) == "table" then
				if is_array_of_primitives(v) then
					local json = encode_nowiki_json(v)
					if json then
						parts[#parts + 1] = key .. "=" .. json
					end
				elseif options.nestedKeyMode == "raw" then
					local json = encode_nowiki_json(v)
					if json then
						parts[#parts + 1] = key .. "=" .. json
					end
				end

				if next(v) ~= nil and not is_array_of_primitives(v) then
					local childPrefix
					if options.nestedKeyMode == "prefixed" then
						childPrefix = key
					elseif type(k) == "string" then
						childPrefix = key
					else
						childPrefix = nil
					end
					append_table_fields(parts, v, options, childPrefix)
				end
			else
				parts[#parts + 1] = key .. "=" .. tostring(v)
			end
		end
	end
end

function p.json(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local jsonStr = mw.text.unstripNoWiki(args[1] or args.json or "")
	local tplPath = mw.text.unstripNoWiki(args[2] or args.template or "")
	local tplArgs = args[3] or args.tplArgs or args.templateArgs or ""
	tplPath, tplArgs = split_template_spec(tplPath, tplArgs)

	if jsonStr == "" or tplPath == "" then
		return ""
	end

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

	local calls = {}
	local nestedOptions = {
		includeJsonAtPrefix = true,
		nestedKeyMode = "prefixed",
		skipPrimitiveRoot = false,
	}
	local rawTypeOptions = {
		includeJsonAtPrefix = false,
		nestedKeyMode = "raw",
		skipPrimitiveRoot = true,
	}

	local function is_object_map(tbl)
		local count = 0
		for k, v in pairs(tbl) do
			if type(k) ~= "string" or type(v) ~= "table" then
				return false
			end
			count = count + 1
		end
		return count > 1
	end

	local firstParamOverride = mw.text.unstripNoWiki(args.first or args.firstParam or args.idKey or "")

	local function makeCall(obj)
		if type(obj) ~= "table" then
			return
		end

		local idKey = firstParamOverride
		if idKey == "" then
			idKey = choose_id_key(obj)
		end
		if not idKey then
			return
		end

		local parts = { "{{Шаблон:" .. resolve_template_path(tplPath) }

		if tplArgs ~= "" then
			parts[#parts + 1] = tplArgs
		end

		parts[#parts + 1] = tostring(idKey)
		local keys = collect_sorted_keys(obj, true)

		for _, k in ipairs(keys) do
			local v = obj[k]

			if k == idKey then
				if is_wrapper_block_key(k) then
					if type(v) == "table" then
						local json = encode_nowiki_json(v)
						if json then
							parts[#parts + 1] = "value=" .. json
						end
						append_table_fields(parts, v, rawTypeOptions, nil)
					elseif v ~= nil then
						parts[#parts + 1] = "value=" .. tostring(v)
					end
				elseif type(v) == "table" then
					if is_array_of_primitives(v) then
						local json = encode_nowiki_json(v)
						if json then
							parts[#parts + 1] = "value=" .. json
						end
					end

					if next(v) ~= nil then
						append_table_fields(parts, v, nestedOptions, k)
						append_table_fields(parts, v, rawTypeOptions, nil)
					end
				elseif v ~= nil then
					parts[#parts + 1] = "value=" .. tostring(v)
					parts[#parts + 1] = k .. "=" .. tostring(v)
				end
			else
				if type(v) == "table" then
					if next(v) ~= nil then
						append_table_fields(parts, v, nestedOptions, k)
					end
				elseif v ~= nil then
					parts[#parts + 1] = k .. "=" .. tostring(v)
				end
			end
		end

		parts[#parts + 1] = "}}"
		calls[#calls + 1] = table.concat(parts, "|")
	end

	if is_array(data) then
		for _, item in ipairs(data) do
			if type(item) == "table" then
				makeCall(item)
			elseif item ~= nil and item ~= "" then
				makeCall({ [item] = {} })
			end
		end
	elseif is_object_map(data) then
		local keys = collect_sorted_keys(data, true)
		for _, k in ipairs(keys) do
			makeCall({ [k] = data[k] })
		end
	else
		makeCall(data)
	end

	if #calls == 0 then
		return ""
	end

	return frame:preprocess(table.concat(calls, " "))
end

function p.jsonList(frame)
	local args = getArgs(frame, { removeBlanks = false })
	local jsonStr = mw.text.unstripNoWiki(args[1] or args.json or "")
	if jsonStr == "" then
		return ""
	end

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

	local outputType = (args.type or "list")

	local bullet = mw.text.unstripNoWiki(args.prefix or "* ")
	local sep = mw.text.unstripNoWiki(args.sep or ": ")

	if outputType == "none" then
		bullet = ""
		sep = ""
	elseif outputType == "revertList" then
		sep = mw.text.unstripNoWiki(args.sep or " ")
	end

	local keyPattern = mw.text.unstripNoWiki(args.key_pattern or "(.*)")
	local keyReplace = mw.text.unstripNoWiki(args.key_replace or "\\1")
	local valuePattern = mw.text.unstripNoWiki(args.value_pattern or "(.*)")
	local valueReplace = mw.text.unstripNoWiki(args.value_replace or "\\1")

	local pairPattern = mw.text.unstripNoWiki(args.pattern or "(.*)")
	local pairReplace = mw.text.unstripNoWiki(args.replace or "\\1")

	local maxItems = tonumber(args.max or args.limit or args.max_count or args.maxCount or "")
	if maxItems ~= nil then
		maxItems = math.floor(maxItems)
		if maxItems < 0 then
			maxItems = nil
		end
	end

	local out = {}

	if is_array(data) then
		local processed = 0

		for _, v in ipairs(data) do
			processed = processed + 1
			if maxItems ~= nil and processed > maxItems then
				break
			end

			local text = ""

			if type(v) == "table" then
				if is_array(v) then
					text = table.concat(v, ", ")
				else
					local okJson, jsonVal = pcall(mw.text.jsonEncode, v)
					if okJson and jsonVal then
						text = jsonVal
					end
				end
			else
				text = tostring(v)
			end

			if text ~= "" then
				local patt = valuePattern ~= "" and valuePattern or keyPattern
				local repl = valueReplace ~= "" and valueReplace or keyReplace
				text = apply_pattern(text, patt, repl)

				local line
				if outputType == "enum" then
					line = text
				else
					line = bullet .. text
				end

				if pairPattern ~= "" then
					line = apply_pattern(line, pairPattern, pairReplace)
				end

				table.insert(out, line)
			end
		end
	else
		local keys = {}
		for k in pairs(data) do
			keys[#keys + 1] = k
		end
		table.sort(keys, function(a, b)
			return tostring(a) < tostring(b)
		end)

		local processed = 0

		for _, k in ipairs(keys) do
			processed = processed + 1
			if maxItems ~= nil and processed > maxItems then
				break
			end

			local v = data[k]
			local vStr

			if type(v) == "table" then
				local okJson, jsonVal = pcall(mw.text.jsonEncode, v)
				if okJson and jsonVal then
					vStr = jsonVal
				else
					vStr = ""
				end
			else
				vStr = tostring(v)
			end

			local baseKey = apply_pattern(tostring(k), keyPattern, "\\1")

			local MARK_KEY = "\31KEY\31"
			local vRepl = (valueReplace or "\\1"):gsub("\\2", MARK_KEY)
			local vStr0 = apply_pattern(vStr, valuePattern, vRepl)
			vStr0 = tostring(vStr0):gsub(MARK_KEY, baseKey)

			local MARK_VAL = "\31VAL\31"
			local kRepl = (keyReplace or "\\1"):gsub("\\2", MARK_VAL)
			local keyStr0 = apply_pattern(tostring(k), keyPattern, kRepl)
			local keyStr = tostring(keyStr0):gsub(MARK_VAL, vStr0)

			vStr = vStr0

			if vStr ~= "" then
				local line
				if outputType == "enum" then
					line = vStr .. " " .. keyStr
				elseif outputType == "revertList" then
					line = bullet .. vStr .. sep .. keyStr
				else
					line = bullet .. keyStr .. sep .. vStr
				end

				if pairPattern ~= "" then
					line = apply_pattern(line, pairPattern, pairReplace)
				end

				table.insert(out, line)
			end
		end
	end

	if outputType == "enum" then
		return frame:preprocess(table.concat(out, ", "))
	elseif outputType == "list" or outputType == "revertList" then
		return frame:preprocess(table.concat(out, "\n"))
	else
		return frame:preprocess(table.concat(out, " "))
	end
end

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

	local entry = load_entry(id, dataPage) or {}
	local parts = flatten_selected_parts(entry, paramNames)
	if #parts == 0 then
		return ""
	end

	return table.concat(parts, "|")
end

return p