Модуль:Песочница/Pok: различия между версиями
Материал из Space Station 14 Вики
Pok (обсуждение | вклад) Полностью удалено содержимое страницы Метки: очистка ручная отмена |
Pok (обсуждение | вклад) Нет описания правки |
||
| Строка 1: | Строка 1: | ||
local p = {} | |||
local getArgs = require('Module:Arguments').getArgs | |||
local BASE_USER = "IanComradeBot/" | |||
local moduleDataCache = {} | |||
local switchModeRegistry = {} | |||
local switchModeOrder = {} | |||
local function trim(s) | |||
if not s then return s end | |||
return (s:gsub("^%s*(.-)%s*$", "%1")) | |||
end | |||
local function load_module_data(page) | |||
local moduleName = "Module:" .. BASE_USER .. page .. "/data" | |||
if moduleDataCache[moduleName] ~= nil then | |||
return moduleDataCache[moduleName] | |||
end | |||
local ok, data = pcall(mw.loadData, moduleName) | |||
if not ok then | |||
moduleDataCache[moduleName] = nil | |||
return nil | |||
end | |||
moduleDataCache[moduleName] = data | |||
return data | |||
end | |||
local function load_template_content(path) | |||
local title = mw.title.new("Template:" .. path) | |||
if not title then return nil end | |||
local ok, content = pcall(function() return title:getContent() end) | |||
if not ok then return nil end | |||
return content | |||
end | |||
local function lcfirst(s) | |||
if not s or s == "" then return s end | |||
return string.lower(s:sub(1, 1)) .. (s:sub(2) or "") | |||
end | |||
local function makeTplCall(tplPath, sw, key, id, extra) | |||
local tplStr = "{{" .. tplPath .. "|" .. sw .. "|" .. key | |||
tplStr = tplStr .. "|id=" .. tostring(id) | |||
if extra and extra ~= "" then tplStr = tplStr .. "|" .. extra end | |||
tplStr = tplStr .. "}}" | |||
return tplStr | |||
end | |||
local function sort_entries_by_priority(entries) | |||
table.sort(entries, function(a, b) | |||
if a.priority == b.priority then return a.idx < b.idx end | |||
return a.priority > b.priority | |||
end) | |||
end | |||
local function make_source(kind, name, pathName, tplPath) | |||
return { kind = kind, name = name, pathName = pathName, tplPath = tplPath } | |||
end | |||
local function register_switch_mode(name, cfg) | |||
switchModeRegistry[name] = cfg or {} | |||
switchModeOrder[#switchModeOrder + 1] = name | |||
end | |||
local function new_switch_state() | |||
local state = { keyOrder = {}, keyToTemplates = {}, keySources = {} } | |||
for _, sw in ipairs(switchModeOrder) do | |||
state.keyOrder[sw] = {} | |||
state.keyToTemplates[sw] = {} | |||
state.keySources[sw] = {} | |||
end | |||
return state | |||
end | |||
local function ensure_switch_key(state, sw, key) | |||
local byKey = state.keyToTemplates[sw] | |||
if not byKey[key] then | |||
byKey[key] = {} | |||
table.insert(state.keyOrder[sw], key) | |||
end | |||
return byKey[key] | |||
end | |||
local function add_switch_entry(state, sw, key, entry) | |||
local bucket = ensure_switch_key(state, sw, key) | |||
entry.idx = #bucket + 1 | |||
bucket[#bucket + 1] = entry | |||
end | |||
local function collect_tpl_calls(entries) | |||
local tplCalls = {} | |||
local sources = {} | |||
if #entries > 0 then | |||
sort_entries_by_priority(entries) | |||
for _, e in ipairs(entries) do | |||
tplCalls[#tplCalls + 1] = e.tpl | |||
sources[#sources + 1] = e.source | |||
end | |||
end | |||
return tplCalls, sources | |||
end | |||
local function makeSourceLink(s) | |||
local className = | |||
(s.name:sub(1,1):upper() .. s.name:sub(2)) .. | |||
(s.kind and (s.kind:sub(1,1):upper() .. s.kind:sub(2)) or "") | |||
local tplLabel = "Template:" .. s.tplPath | |||
return "[[" .. tplLabel .. "|" .. className .. "]]" | |||
end | |||
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame) | |||
local parts = {} | |||
if tplCalls and #tplCalls > 0 then | |||
for i, tpl in ipairs(tplCalls) do | |||
local add = true | |||
if frame then | |||
local expanded = frame:preprocess(tpl) | |||
add = expanded and trim(expanded) ~= "" | |||
end | |||
if add then | |||
local src = sources and sources[i] | |||
local line = '<div>' .. tpl .. '</div><div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>' | |||
table.insert(parts, '<div class="ts-Сущность">' .. line .. '</div>') | |||
end | |||
end | |||
if #parts == 0 then | |||
return "" | |||
end | |||
if includeHeader then | |||
table.insert(parts, 1, "<h2>" .. mw.text.encode(key) .. "</h2>") | |||
end | |||
end | |||
return table.concat(parts, "\n") | |||
end | |||
local function buildCardCall(merged, entityId) | |||
local parts = {} | |||
-- id сущности | |||
if entityId and entityId ~= "" then | |||
parts[#parts + 1] = "id=" .. mw.text.encode(entityId) | |||
end | |||
-- типы | |||
if merged.tags and #merged.tags > 0 then | |||
table.sort(merged.tags) | |||
parts[#parts + 1] = "тип=" .. table.concat(merged.tags, ", ") | |||
end | |||
-- секции карточки | |||
if merged.sections and #merged.sections > 0 then | |||
table.sort(merged.sections, function(a, b) | |||
if a == "Сущность" then return true end | |||
if b == "Сущность" then return false end | |||
return a < b | |||
end) | |||
parts[#parts + 1] = "sections=" .. table.concat(merged.sections, ", ") | |||
for _, section in ipairs(merged.sections) do | |||
local labels = merged.labelLists[section] | |||
if labels and #labels > 0 then | |||
local enc = {} | |||
for i = 1, #labels do | |||
enc[i] = mw.text.encode(labels[i]) | |||
end | |||
parts[#parts + 1] = mw.text.encode(section) .. "=" .. table.concat(enc, ", ") | |||
end | |||
end | |||
end | |||
-- содержимое полей | |||
for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do | |||
if displayLabel and displayLabel ~= "" then | |||
parts[#parts + 1] = mw.text.encode(compositeKey .. "_label") .. "=" .. displayLabel | |||
end | |||
end | |||
for compositeKey, content in pairs(merged.contentByKey or {}) do | |||
if content and content ~= "" then | |||
parts[#parts + 1] = mw.text.encode(compositeKey) .. "=" .. content | |||
end | |||
end | |||
if #parts == 0 then | |||
return "" | |||
end | |||
return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}" | |||
end | |||
local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders) | |||
local merged = { | |||
sections = {}, | |||
sectionsMap = {}, | |||
labelLists = {}, | |||
labelSets = {}, | |||
labelOverrides = {}, | |||
contentByKey = {}, | |||
tags = {}, | |||
tagSet = {} | |||
} | |||
for _, callKey in ipairs(keyOrder or {}) do | |||
local entries = keyToTemplates[callKey] or {} | |||
if #entries > 0 then | |||
sort_entries_by_priority(entries) | |||
for _, e in ipairs(entries) do | |||
local displayLabel = trim(frame:preprocess(e.tplLabel or "") or "") | |||
local content = trim(frame:preprocess(e.tplContent or "") or "") | |||
local tagText = "" | |||
if e.tplTag then | |||
tagText = trim(frame:preprocess(e.tplTag or "") or "") | |||
end | |||
local compositeKey = (callKey:find("_", 1, true)) and callKey or ("Сущность_" .. callKey) | |||
local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность" | |||
if displayLabel ~= "" or content ~= "" then | |||
if not merged.sectionsMap[section] then | |||
merged.sectionsMap[section] = true | |||
table.insert(merged.sections, section) | |||
end | |||
if displayLabel ~= "" and (not merged.labelOverrides[compositeKey] or merged.labelOverrides[compositeKey] == "") then | |||
merged.labelOverrides[compositeKey] = displayLabel | |||
end | |||
if content ~= "" then | |||
local prev = merged.contentByKey[compositeKey] | |||
if prev and prev ~= "" then | |||
merged.contentByKey[compositeKey] = prev .. "\n" .. content | |||
else | |||
merged.contentByKey[compositeKey] = content | |||
end | |||
end | |||
merged.labelSets[section] = merged.labelSets[section] or {} | |||
if not merged.labelSets[section][compositeKey] then | |||
merged.labelSets[section][compositeKey] = true | |||
local cur = merged.labelLists[section] or {} | |||
table.insert(cur, compositeKey) | |||
merged.labelLists[section] = cur | |||
end | |||
end | |||
if tagText ~= "" then | |||
if not merged.tagSet[tagText] then | |||
merged.tagSet[tagText] = true | |||
table.insert(merged.tags, tagText) | |||
end | |||
end | |||
end | |||
end | |||
end | |||
if noHeaders then | |||
local hasLabel = false | |||
for _, v in pairs(merged.labelOverrides or {}) do | |||
if v and v ~= "" then hasLabel = true break end | |||
end | |||
if not hasLabel then | |||
for _, lst in pairs(merged.labelLists or {}) do | |||
if #lst > 0 then hasLabel = true break end | |||
end | |||
end | |||
local hasContent = false | |||
for _, v in pairs(merged.contentByKey or {}) do | |||
if v and v ~= "" then hasContent = true break end | |||
end | |||
if not hasLabel and not hasContent then | |||
return "" | |||
end | |||
end | |||
return buildCardCall(merged, entityId) | |||
end | |||
register_switch_mode("card", { | |||
full = true, | |||
build_entry = function(ctx, key) | |||
return { | |||
tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ctx.id, ctx.extra), | |||
tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ctx.id, ctx.extra), | |||
tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra), | |||
source = ctx.source, | |||
priority = ctx.priority | |||
} | |||
end, | |||
build_preview_entry = function(ctx, key) | |||
return { | |||
tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ""), | |||
tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ""), | |||
source = ctx.source, | |||
priority = ctx.priority | |||
} | |||
end, | |||
add_entity_extras = function(state, parsed, ctx) | |||
if type(parsed) == "table" and parsed.cardTag and parsed.cardTag ~= "" then | |||
add_switch_entry(state, "card", "cardTag", { | |||
tplLabel = "", | |||
tplContent = "", | |||
tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra), | |||
source = ctx.source, | |||
priority = ctx.priority | |||
}) | |||
end | |||
end, | |||
render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders) | |||
return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders) | |||
end | |||
}) | |||
register_switch_mode("title", { | |||
build_entry = function(ctx, key) | |||
return { | |||
tpl = makeTplCall(ctx.tplPath, "title", key, ctx.id, ctx.extra), | |||
source = ctx.source, | |||
priority = ctx.priority | |||
} | |||
end, | |||
build_preview_entry = function(ctx, key) | |||
return { | |||
tpl = makeTplCall(ctx.tplPath, "title", key, ""), | |||
source = ctx.source, | |||
priority = ctx.priority | |||
} | |||
end, | |||
render_key = function(frame, key, entries, noHeaders) | |||
local tplCalls, sources = collect_tpl_calls(entries) | |||
return renderTitleBlock(key, tplCalls, sources, not noHeaders, frame) | |||
end | |||
}) | |||
local function getTemplateMeta(frame, tplPath) | |||
local expanded = frame:expandTemplate { | |||
title = tplPath, | |||
args = { "json" } | |||
} | |||
local ok, data = pcall(mw.text.jsonDecode, expanded) | |||
if not ok or type(data) ~= "table" then | |||
return "" | |||
end | |||
if data.card == nil then | |||
local cardKeys = {} | |||
local seen = {} | |||
for base, labels in pairs(data) do | |||
if type(base) == "string" and base ~= "card" and base:sub(1, 4) == "card" and type(labels) == "table" then | |||
for _, lab in ipairs(labels) do | |||
if not seen[lab] then | |||
seen[lab] = true | |||
table.insert(cardKeys, lab) | |||
end | |||
end | |||
end | |||
end | |||
data.card = cardKeys | |||
end | |||
return data | |||
end | |||
local function parseListArg(str) | |||
local res = {} | |||
if not str or str == "" then return res end | |||
for item in string.gmatch(str, "[^,]+") do | |||
local s = trim(item) | |||
if s ~= "" then | |||
local a, b = s:match("^([^_]+)_(.+)$") | |||
if a and b then | |||
res[a] = res[a] or {} | |||
res[a][b] = true | |||
end | |||
end | |||
end | |||
return res | |||
end | |||
local function build_key_filter(args) | |||
local filter = {} | |||
filter.blacklist = parseListArg(args.blacklist or "") | |||
filter.whitelist = parseListArg(args.whitelist or "") | |||
filter.hasWhitelist = next(filter.whitelist) ~= nil | |||
return filter | |||
end | |||
local function should_include_key(filter, sw, key) | |||
if filter.hasWhitelist then | |||
return filter.whitelist[sw] and filter.whitelist[sw][key] | |||
end | |||
return not (filter.blacklist[sw] and filter.blacklist[sw][key]) | |||
end | |||
local function each_csv_value(str, fn) | |||
if not str or str == "" then | |||
return | |||
end | |||
for item in string.gmatch(str, "[^,]+") do | |||
local value = trim(item) | |||
if value ~= "" then | |||
fn(value) | |||
end | |||
end | |||
end | |||
local function collect_entity_sets(id, componentDefs, prototypeStoreDefs, ignoreComponents, ignorePrototypes) | |||
local foundComponents, foundPrototypes = {}, {} | |||
local compList = componentDefs[id] | |||
if type(compList) == "table" then | |||
for _, v in ipairs(compList) do | |||
if type(v) == "string" then | |||
foundComponents[v] = true | |||
end | |||
end | |||
end | |||
each_csv_value(id, function(name) | |||
if name ~= id then | |||
if componentDefs[name] ~= nil then | |||
foundComponents[name] = true | |||
elseif prototypeStoreDefs[name] ~= nil then | |||
foundPrototypes[name] = true | |||
end | |||
end | |||
end) | |||
each_csv_value(ignoreComponents, function(name) | |||
foundComponents[name] = nil | |||
end) | |||
each_csv_value(ignorePrototypes, function(name) | |||
foundPrototypes[name] = nil | |||
end) | |||
return foundComponents, foundPrototypes | |||
end | |||
local function resolve_priority(parsed) | |||
local basePriority = 1 | |||
if type(parsed) == "table" and parsed.priority ~= nil then | |||
if type(parsed.priority) == "number" then | |||
basePriority = parsed.priority | |||
else | |||
local pnum = tonumber(parsed.priority) | |||
if pnum then | |||
basePriority = pnum | |||
end | |||
end | |||
end | |||
return basePriority | |||
end | |||
local function add_entries_from_meta(state, parsed, ctx, filter, isPreview) | |||
for _, sw in ipairs(switchModeOrder) do | |||
local mode = switchModeRegistry[sw] or {} | |||
local keys | |||
if type(mode.get_keys) == "function" then | |||
keys = mode.get_keys(parsed) | |||
else | |||
keys = (type(parsed) == "table" and parsed[sw]) or {} | |||
end | |||
if type(keys) == "table" then | |||
for _, key in ipairs(keys) do | |||
if (not filter) or should_include_key(filter, sw, key) then | |||
local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry | |||
if type(buildFn) == "function" then | |||
local entry = buildFn(ctx, key) | |||
if entry then | |||
add_switch_entry(state, sw, key, entry) | |||
end | |||
end | |||
end | |||
end | |||
end | |||
if (not isPreview) and type(mode.add_entity_extras) == "function" then | |||
mode.add_entity_extras(state, parsed, ctx) | |||
end | |||
end | |||
end | |||
local function build_missing_template_error(kind, name, isStore, tplPath) | |||
local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "") | |||
local classType = baseType | |||
if isStore then | |||
classType = classType .. "Store" | |||
end | |||
local className = name .. baseType | |||
local tplLabel = "Template:" .. tplPath | |||
return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}" | |||
end | |||
local function renderBlocks(frame, state, noHeaders, entityId) | |||
local outLocal = {} | |||
for _, sw in ipairs(switchModeOrder) do | |||
local mode = switchModeRegistry[sw] or {} | |||
if mode.full then | |||
local outStr = "" | |||
if type(mode.render_full) == "function" then | |||
outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw], | |||
entityId, noHeaders) | |||
end | |||
if outStr and outStr ~= "" then table.insert(outLocal, outStr) end | |||
else | |||
for _, key in ipairs(state.keyOrder[sw] or {}) do | |||
local entries = state.keyToTemplates[sw][key] or {} | |||
if type(mode.render_key) == "function" then | |||
local outStr = mode.render_key(frame, key, entries, noHeaders) | |||
if outStr and outStr ~= "" then table.insert(outLocal, outStr) end | |||
end | |||
end | |||
end | |||
end | |||
return outLocal | |||
end | |||
function p.get(frame) | |||
local args = getArgs(frame, { removeBlanks = false }) | |||
local id = args[1] or "" | |||
if id == "" then return "" end | |||
local filter = build_key_filter(args) | |||
local ignoreComponents = args.ignoreComponents or args.ignoreComponent or "" | |||
local ignorePrototypes = args.ignorePrototypes or args.ignorePrototype or "" | |||
local componentDefs = load_module_data("component.json") | |||
local prototypeStoreDefs = load_module_data("prototype_store.json") | |||
local componentStoreDefs = load_module_data("component_store.json") | |||
if not componentDefs or not prototypeStoreDefs or not componentStoreDefs then return "" end | |||
local foundComponents, foundPrototypes = collect_entity_sets(id, componentDefs, prototypeStoreDefs, ignoreComponents, | |||
ignorePrototypes) | |||
local state = new_switch_state() | |||
local ok, dp = pcall(require, "Module:GetField") | |||
local errors = {} | |||
local function processEntity(kind, name, isStore) | |||
local pathName = lcfirst(name) | |||
local tplPath = kind .. "/" .. pathName | |||
if isStore then | |||
tplPath = tplPath .. "/store" | |||
end | |||
local content = load_template_content(tplPath) | |||
if not content then | |||
if filter.hasWhitelist then | |||
return | |||
end | |||
errors[#errors + 1] = build_missing_template_error(kind, name, isStore, tplPath) | |||
return | |||
end | |||
local parsed = getTemplateMeta(frame, tplPath) | |||
if type(parsed) ~= "table" then | |||
parsed = {} | |||
end | |||
local extra = "" | |||
if ok and dp and dp.flattenField then | |||
local dataPage = tplPath .. ".json" | |||
extra = dp.flattenField({ args = { id, dataPage } }) | |||
end | |||
add_entries_from_meta(state, parsed, { | |||
tplPath = tplPath, | |||
id = id, | |||
extra = extra, | |||
source = make_source(kind, name, pathName, tplPath), | |||
priority = resolve_priority(parsed) | |||
}, filter, false) | |||
end | |||
for compName in pairs(foundComponents) do | |||
processEntity("component", compName, false) | |||
end | |||
for protoName in pairs(foundPrototypes) do | |||
processEntity("prototype", protoName, false) | |||
end | |||
if type(componentStoreDefs) == "table" then | |||
local compStore = componentStoreDefs[id] | |||
if type(compStore) == "table" then | |||
for compName in pairs(compStore) do | |||
processEntity("component", compName, true) | |||
end | |||
end | |||
end | |||
if type(prototypeStoreDefs) == "table" then | |||
local protoStore = prototypeStoreDefs[id] | |||
if type(protoStore) == "table" then | |||
for protoName in pairs(protoStore) do | |||
processEntity("prototype", protoName, true) | |||
end | |||
end | |||
end | |||
local out = {} | |||
if #errors > 0 then | |||
table.insert(out, '{{сущность/infobox|' .. table.concat(errors, "\n") .. '}}') | |||
end | |||
local blocks = renderBlocks(frame, state, filter.hasWhitelist, id) | |||
for _, b in ipairs(blocks) do | |||
table.insert(out, b) | |||
end | |||
return frame:preprocess(table.concat(out, "\n") .. "[[Категория:Сущности]]") | |||
end | |||
function p.preview(frame) | |||
local args = getArgs(frame, { removeBlanks = false }) | |||
local tplPath = args[1] or "" | |||
if tplPath == "" then return "" end | |||
local content = load_template_content(tplPath) | |||
if not content then | |||
return "" | |||
end | |||
local parsed = getTemplateMeta(frame, tplPath) or {} | |||
if type(parsed) ~= "table" then | |||
parsed = {} | |||
end | |||
local state = new_switch_state() | |||
add_entries_from_meta(state, parsed, { | |||
tplPath = tplPath, | |||
id = "", | |||
extra = "", | |||
source = make_source("", tplPath, tplPath, tplPath), | |||
priority = 1 | |||
}, nil, true) | |||
local whitelist = parseListArg(args.whitelist or "") | |||
local hasWhitelist = next(whitelist) ~= nil | |||
local out = {} | |||
local blocks = renderBlocks(frame, state, hasWhitelist, "") | |||
for _, b in ipairs(blocks) do | |||
table.insert(out, b) | |||
end | |||
return frame:preprocess(table.concat(out, "\n")) | |||
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 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 | |||
function p.json(frame) | |||
local args = getArgs(frame, { removeBlanks = false }) | |||
local jsonStr = mw.text.unstripNoWiki(trim(args[1] or args.json or "")) | |||
local tplPath = mw.text.unstripNoWiki(trim(args[2] or args.template or "")) | |||
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 okDp, dp = pcall(require, "Module:GetField") | |||
local calls = {} | |||
local function makeCall(id, obj) | |||
if type(id) ~= "string" then return end | |||
local parts = { "{{" .. tplPath, "id=" .. id } | |||
if type(obj) == "table" then | |||
if okDp and dp and type(dp.flattenParams) == "function" then | |||
local extra = dp.flattenParams(obj) | |||
for i = 1, #extra do | |||
parts[#parts + 1] = extra[i] | |||
end | |||
else | |||
for k, v in pairs(obj) do | |||
if v ~= nil then | |||
parts[#parts + 1] = tostring(k) .. "=" .. tostring(v) | |||
end | |||
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 | |||
for k, v in pairs(item) do | |||
makeCall(k, v) | |||
end | |||
end | |||
end | |||
else | |||
for k, v in pairs(data) do | |||
makeCall(k, v) | |||
end | |||
end | |||
if #calls == 0 then | |||
return "" | |||
end | |||
local rendered = table.concat(calls, " ") | |||
return frame:preprocess(rendered) | |||
end | |||
function p.jsonList(frame) | |||
local args = getArgs(frame, { removeBlanks = false }) | |||
local jsonStr = mw.text.unstripNoWiki(trim(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"):lower() | |||
local bullet = mw.text.unstripNoWiki(args.prefix or "* ") | |||
local sep = mw.text.unstripNoWiki(args.sep or ": ") | |||
if outputType == "none" then | |||
bullet = "" | |||
sep = "" | |||
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 out = {} | |||
if is_array(data) then | |||
for _, v in ipairs(data) do | |||
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) | |||
for _, k in ipairs(keys) do | |||
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 | |||
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, ", ")) | |||
else | |||
return frame:preprocess(table.concat(out, "\n")) | |||
end | |||
end | |||
return p | |||
Версия от 18:40, 15 марта 2026
Для документации этого модуля может быть создана страница Модуль:Песочница/Pok/doc
local p = {}
local getArgs = require('Module:Arguments').getArgs
local BASE_USER = "IanComradeBot/"
local moduleDataCache = {}
local switchModeRegistry = {}
local switchModeOrder = {}
local function trim(s)
if not s then return s end
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
local function load_module_data(page)
local moduleName = "Module:" .. BASE_USER .. page .. "/data"
if moduleDataCache[moduleName] ~= nil then
return moduleDataCache[moduleName]
end
local ok, data = pcall(mw.loadData, moduleName)
if not ok then
moduleDataCache[moduleName] = nil
return nil
end
moduleDataCache[moduleName] = data
return data
end
local function load_template_content(path)
local title = mw.title.new("Template:" .. path)
if not title then return nil end
local ok, content = pcall(function() return title:getContent() end)
if not ok then return nil end
return content
end
local function lcfirst(s)
if not s or s == "" then return s end
return string.lower(s:sub(1, 1)) .. (s:sub(2) or "")
end
local function makeTplCall(tplPath, sw, key, id, extra)
local tplStr = "{{" .. tplPath .. "|" .. sw .. "|" .. key
tplStr = tplStr .. "|id=" .. tostring(id)
if extra and extra ~= "" then tplStr = tplStr .. "|" .. extra end
tplStr = tplStr .. "}}"
return tplStr
end
local function sort_entries_by_priority(entries)
table.sort(entries, function(a, b)
if a.priority == b.priority then return a.idx < b.idx end
return a.priority > b.priority
end)
end
local function make_source(kind, name, pathName, tplPath)
return { kind = kind, name = name, pathName = pathName, tplPath = tplPath }
end
local function register_switch_mode(name, cfg)
switchModeRegistry[name] = cfg or {}
switchModeOrder[#switchModeOrder + 1] = name
end
local function new_switch_state()
local state = { keyOrder = {}, keyToTemplates = {}, keySources = {} }
for _, sw in ipairs(switchModeOrder) do
state.keyOrder[sw] = {}
state.keyToTemplates[sw] = {}
state.keySources[sw] = {}
end
return state
end
local function ensure_switch_key(state, sw, key)
local byKey = state.keyToTemplates[sw]
if not byKey[key] then
byKey[key] = {}
table.insert(state.keyOrder[sw], key)
end
return byKey[key]
end
local function add_switch_entry(state, sw, key, entry)
local bucket = ensure_switch_key(state, sw, key)
entry.idx = #bucket + 1
bucket[#bucket + 1] = entry
end
local function collect_tpl_calls(entries)
local tplCalls = {}
local sources = {}
if #entries > 0 then
sort_entries_by_priority(entries)
for _, e in ipairs(entries) do
tplCalls[#tplCalls + 1] = e.tpl
sources[#sources + 1] = e.source
end
end
return tplCalls, sources
end
local function makeSourceLink(s)
local className =
(s.name:sub(1,1):upper() .. s.name:sub(2)) ..
(s.kind and (s.kind:sub(1,1):upper() .. s.kind:sub(2)) or "")
local tplLabel = "Template:" .. s.tplPath
return "[[" .. tplLabel .. "|" .. className .. "]]"
end
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame)
local parts = {}
if tplCalls and #tplCalls > 0 then
for i, tpl in ipairs(tplCalls) do
local add = true
if frame then
local expanded = frame:preprocess(tpl)
add = expanded and trim(expanded) ~= ""
end
if add then
local src = sources and sources[i]
local line = '<div>' .. tpl .. '</div><div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
table.insert(parts, '<div class="ts-Сущность">' .. line .. '</div>')
end
end
if #parts == 0 then
return ""
end
if includeHeader then
table.insert(parts, 1, "<h2>" .. mw.text.encode(key) .. "</h2>")
end
end
return table.concat(parts, "\n")
end
local function buildCardCall(merged, entityId)
local parts = {}
-- id сущности
if entityId and entityId ~= "" then
parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
end
-- типы
if merged.tags and #merged.tags > 0 then
table.sort(merged.tags)
parts[#parts + 1] = "тип=" .. table.concat(merged.tags, ", ")
end
-- секции карточки
if merged.sections and #merged.sections > 0 then
table.sort(merged.sections, function(a, b)
if a == "Сущность" then return true end
if b == "Сущность" then return false end
return a < b
end)
parts[#parts + 1] = "sections=" .. table.concat(merged.sections, ", ")
for _, section in ipairs(merged.sections) do
local labels = merged.labelLists[section]
if labels and #labels > 0 then
local enc = {}
for i = 1, #labels do
enc[i] = mw.text.encode(labels[i])
end
parts[#parts + 1] = mw.text.encode(section) .. "=" .. table.concat(enc, ", ")
end
end
end
-- содержимое полей
for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
if displayLabel and displayLabel ~= "" then
parts[#parts + 1] = mw.text.encode(compositeKey .. "_label") .. "=" .. displayLabel
end
end
for compositeKey, content in pairs(merged.contentByKey or {}) do
if content and content ~= "" then
parts[#parts + 1] = mw.text.encode(compositeKey) .. "=" .. content
end
end
if #parts == 0 then
return ""
end
return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
end
local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders)
local merged = {
sections = {},
sectionsMap = {},
labelLists = {},
labelSets = {},
labelOverrides = {},
contentByKey = {},
tags = {},
tagSet = {}
}
for _, callKey in ipairs(keyOrder or {}) do
local entries = keyToTemplates[callKey] or {}
if #entries > 0 then
sort_entries_by_priority(entries)
for _, e in ipairs(entries) do
local displayLabel = trim(frame:preprocess(e.tplLabel or "") or "")
local content = trim(frame:preprocess(e.tplContent or "") or "")
local tagText = ""
if e.tplTag then
tagText = trim(frame:preprocess(e.tplTag or "") or "")
end
local compositeKey = (callKey:find("_", 1, true)) and callKey or ("Сущность_" .. callKey)
local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность"
if displayLabel ~= "" or content ~= "" then
if not merged.sectionsMap[section] then
merged.sectionsMap[section] = true
table.insert(merged.sections, section)
end
if displayLabel ~= "" and (not merged.labelOverrides[compositeKey] or merged.labelOverrides[compositeKey] == "") then
merged.labelOverrides[compositeKey] = displayLabel
end
if content ~= "" then
local prev = merged.contentByKey[compositeKey]
if prev and prev ~= "" then
merged.contentByKey[compositeKey] = prev .. "\n" .. content
else
merged.contentByKey[compositeKey] = content
end
end
merged.labelSets[section] = merged.labelSets[section] or {}
if not merged.labelSets[section][compositeKey] then
merged.labelSets[section][compositeKey] = true
local cur = merged.labelLists[section] or {}
table.insert(cur, compositeKey)
merged.labelLists[section] = cur
end
end
if tagText ~= "" then
if not merged.tagSet[tagText] then
merged.tagSet[tagText] = true
table.insert(merged.tags, tagText)
end
end
end
end
end
if noHeaders then
local hasLabel = false
for _, v in pairs(merged.labelOverrides or {}) do
if v and v ~= "" then hasLabel = true break end
end
if not hasLabel then
for _, lst in pairs(merged.labelLists or {}) do
if #lst > 0 then hasLabel = true break end
end
end
local hasContent = false
for _, v in pairs(merged.contentByKey or {}) do
if v and v ~= "" then hasContent = true break end
end
if not hasLabel and not hasContent then
return ""
end
end
return buildCardCall(merged, entityId)
end
register_switch_mode("card", {
full = true,
build_entry = function(ctx, key)
return {
tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ctx.id, ctx.extra),
tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ctx.id, ctx.extra),
tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
source = ctx.source,
priority = ctx.priority
}
end,
build_preview_entry = function(ctx, key)
return {
tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ""),
tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ""),
source = ctx.source,
priority = ctx.priority
}
end,
add_entity_extras = function(state, parsed, ctx)
if type(parsed) == "table" and parsed.cardTag and parsed.cardTag ~= "" then
add_switch_entry(state, "card", "cardTag", {
tplLabel = "",
tplContent = "",
tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
source = ctx.source,
priority = ctx.priority
})
end
end,
render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders)
return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders)
end
})
register_switch_mode("title", {
build_entry = function(ctx, key)
return {
tpl = makeTplCall(ctx.tplPath, "title", key, ctx.id, ctx.extra),
source = ctx.source,
priority = ctx.priority
}
end,
build_preview_entry = function(ctx, key)
return {
tpl = makeTplCall(ctx.tplPath, "title", key, ""),
source = ctx.source,
priority = ctx.priority
}
end,
render_key = function(frame, key, entries, noHeaders)
local tplCalls, sources = collect_tpl_calls(entries)
return renderTitleBlock(key, tplCalls, sources, not noHeaders, frame)
end
})
local function getTemplateMeta(frame, tplPath)
local expanded = frame:expandTemplate {
title = tplPath,
args = { "json" }
}
local ok, data = pcall(mw.text.jsonDecode, expanded)
if not ok or type(data) ~= "table" then
return ""
end
if data.card == nil then
local cardKeys = {}
local seen = {}
for base, labels in pairs(data) do
if type(base) == "string" and base ~= "card" and base:sub(1, 4) == "card" and type(labels) == "table" then
for _, lab in ipairs(labels) do
if not seen[lab] then
seen[lab] = true
table.insert(cardKeys, lab)
end
end
end
end
data.card = cardKeys
end
return data
end
local function parseListArg(str)
local res = {}
if not str or str == "" then return res end
for item in string.gmatch(str, "[^,]+") do
local s = trim(item)
if s ~= "" then
local a, b = s:match("^([^_]+)_(.+)$")
if a and b then
res[a] = res[a] or {}
res[a][b] = true
end
end
end
return res
end
local function build_key_filter(args)
local filter = {}
filter.blacklist = parseListArg(args.blacklist or "")
filter.whitelist = parseListArg(args.whitelist or "")
filter.hasWhitelist = next(filter.whitelist) ~= nil
return filter
end
local function should_include_key(filter, sw, key)
if filter.hasWhitelist then
return filter.whitelist[sw] and filter.whitelist[sw][key]
end
return not (filter.blacklist[sw] and filter.blacklist[sw][key])
end
local function each_csv_value(str, fn)
if not str or str == "" then
return
end
for item in string.gmatch(str, "[^,]+") do
local value = trim(item)
if value ~= "" then
fn(value)
end
end
end
local function collect_entity_sets(id, componentDefs, prototypeStoreDefs, ignoreComponents, ignorePrototypes)
local foundComponents, foundPrototypes = {}, {}
local compList = componentDefs[id]
if type(compList) == "table" then
for _, v in ipairs(compList) do
if type(v) == "string" then
foundComponents[v] = true
end
end
end
each_csv_value(id, function(name)
if name ~= id then
if componentDefs[name] ~= nil then
foundComponents[name] = true
elseif prototypeStoreDefs[name] ~= nil then
foundPrototypes[name] = true
end
end
end)
each_csv_value(ignoreComponents, function(name)
foundComponents[name] = nil
end)
each_csv_value(ignorePrototypes, function(name)
foundPrototypes[name] = nil
end)
return foundComponents, foundPrototypes
end
local function resolve_priority(parsed)
local basePriority = 1
if type(parsed) == "table" and parsed.priority ~= nil then
if type(parsed.priority) == "number" then
basePriority = parsed.priority
else
local pnum = tonumber(parsed.priority)
if pnum then
basePriority = pnum
end
end
end
return basePriority
end
local function add_entries_from_meta(state, parsed, ctx, filter, isPreview)
for _, sw in ipairs(switchModeOrder) do
local mode = switchModeRegistry[sw] or {}
local keys
if type(mode.get_keys) == "function" then
keys = mode.get_keys(parsed)
else
keys = (type(parsed) == "table" and parsed[sw]) or {}
end
if type(keys) == "table" then
for _, key in ipairs(keys) do
if (not filter) or should_include_key(filter, sw, key) then
local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry
if type(buildFn) == "function" then
local entry = buildFn(ctx, key)
if entry then
add_switch_entry(state, sw, key, entry)
end
end
end
end
end
if (not isPreview) and type(mode.add_entity_extras) == "function" then
mode.add_entity_extras(state, parsed, ctx)
end
end
end
local function build_missing_template_error(kind, name, isStore, tplPath)
local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "")
local classType = baseType
if isStore then
classType = classType .. "Store"
end
local className = name .. baseType
local tplLabel = "Template:" .. tplPath
return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}"
end
local function renderBlocks(frame, state, noHeaders, entityId)
local outLocal = {}
for _, sw in ipairs(switchModeOrder) do
local mode = switchModeRegistry[sw] or {}
if mode.full then
local outStr = ""
if type(mode.render_full) == "function" then
outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
entityId, noHeaders)
end
if outStr and outStr ~= "" then table.insert(outLocal, outStr) end
else
for _, key in ipairs(state.keyOrder[sw] or {}) do
local entries = state.keyToTemplates[sw][key] or {}
if type(mode.render_key) == "function" then
local outStr = mode.render_key(frame, key, entries, noHeaders)
if outStr and outStr ~= "" then table.insert(outLocal, outStr) end
end
end
end
end
return outLocal
end
function p.get(frame)
local args = getArgs(frame, { removeBlanks = false })
local id = args[1] or ""
if id == "" then return "" end
local filter = build_key_filter(args)
local ignoreComponents = args.ignoreComponents or args.ignoreComponent or ""
local ignorePrototypes = args.ignorePrototypes or args.ignorePrototype or ""
local componentDefs = load_module_data("component.json")
local prototypeStoreDefs = load_module_data("prototype_store.json")
local componentStoreDefs = load_module_data("component_store.json")
if not componentDefs or not prototypeStoreDefs or not componentStoreDefs then return "" end
local foundComponents, foundPrototypes = collect_entity_sets(id, componentDefs, prototypeStoreDefs, ignoreComponents,
ignorePrototypes)
local state = new_switch_state()
local ok, dp = pcall(require, "Module:GetField")
local errors = {}
local function processEntity(kind, name, isStore)
local pathName = lcfirst(name)
local tplPath = kind .. "/" .. pathName
if isStore then
tplPath = tplPath .. "/store"
end
local content = load_template_content(tplPath)
if not content then
if filter.hasWhitelist then
return
end
errors[#errors + 1] = build_missing_template_error(kind, name, isStore, tplPath)
return
end
local parsed = getTemplateMeta(frame, tplPath)
if type(parsed) ~= "table" then
parsed = {}
end
local extra = ""
if ok and dp and dp.flattenField then
local dataPage = tplPath .. ".json"
extra = dp.flattenField({ args = { id, dataPage } })
end
add_entries_from_meta(state, parsed, {
tplPath = tplPath,
id = id,
extra = extra,
source = make_source(kind, name, pathName, tplPath),
priority = resolve_priority(parsed)
}, filter, false)
end
for compName in pairs(foundComponents) do
processEntity("component", compName, false)
end
for protoName in pairs(foundPrototypes) do
processEntity("prototype", protoName, false)
end
if type(componentStoreDefs) == "table" then
local compStore = componentStoreDefs[id]
if type(compStore) == "table" then
for compName in pairs(compStore) do
processEntity("component", compName, true)
end
end
end
if type(prototypeStoreDefs) == "table" then
local protoStore = prototypeStoreDefs[id]
if type(protoStore) == "table" then
for protoName in pairs(protoStore) do
processEntity("prototype", protoName, true)
end
end
end
local out = {}
if #errors > 0 then
table.insert(out, '{{сущность/infobox|' .. table.concat(errors, "\n") .. '}}')
end
local blocks = renderBlocks(frame, state, filter.hasWhitelist, id)
for _, b in ipairs(blocks) do
table.insert(out, b)
end
return frame:preprocess(table.concat(out, "\n") .. "[[Категория:Сущности]]")
end
function p.preview(frame)
local args = getArgs(frame, { removeBlanks = false })
local tplPath = args[1] or ""
if tplPath == "" then return "" end
local content = load_template_content(tplPath)
if not content then
return ""
end
local parsed = getTemplateMeta(frame, tplPath) or {}
if type(parsed) ~= "table" then
parsed = {}
end
local state = new_switch_state()
add_entries_from_meta(state, parsed, {
tplPath = tplPath,
id = "",
extra = "",
source = make_source("", tplPath, tplPath, tplPath),
priority = 1
}, nil, true)
local whitelist = parseListArg(args.whitelist or "")
local hasWhitelist = next(whitelist) ~= nil
local out = {}
local blocks = renderBlocks(frame, state, hasWhitelist, "")
for _, b in ipairs(blocks) do
table.insert(out, b)
end
return frame:preprocess(table.concat(out, "\n"))
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 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
function p.json(frame)
local args = getArgs(frame, { removeBlanks = false })
local jsonStr = mw.text.unstripNoWiki(trim(args[1] or args.json or ""))
local tplPath = mw.text.unstripNoWiki(trim(args[2] or args.template or ""))
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 okDp, dp = pcall(require, "Module:GetField")
local calls = {}
local function makeCall(id, obj)
if type(id) ~= "string" then return end
local parts = { "{{" .. tplPath, "id=" .. id }
if type(obj) == "table" then
if okDp and dp and type(dp.flattenParams) == "function" then
local extra = dp.flattenParams(obj)
for i = 1, #extra do
parts[#parts + 1] = extra[i]
end
else
for k, v in pairs(obj) do
if v ~= nil then
parts[#parts + 1] = tostring(k) .. "=" .. tostring(v)
end
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
for k, v in pairs(item) do
makeCall(k, v)
end
end
end
else
for k, v in pairs(data) do
makeCall(k, v)
end
end
if #calls == 0 then
return ""
end
local rendered = table.concat(calls, " ")
return frame:preprocess(rendered)
end
function p.jsonList(frame)
local args = getArgs(frame, { removeBlanks = false })
local jsonStr = mw.text.unstripNoWiki(trim(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"):lower()
local bullet = mw.text.unstripNoWiki(args.prefix or "* ")
local sep = mw.text.unstripNoWiki(args.sep or ": ")
if outputType == "none" then
bullet = ""
sep = ""
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 out = {}
if is_array(data) then
for _, v in ipairs(data) do
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)
for _, k in ipairs(keys) do
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
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, ", "))
else
return frame:preprocess(table.concat(out, "\n"))
end
end
return p