Module:GameInfo: Difference between revisions
From SpiritVale Wiki
More actions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
-- Module:GameInfo | -- Module:GameInfo | ||
-- | -- Phase 4.1 (Refactor): stable entrypoint + strict routing + shared scaffolding. | ||
-- | -- | ||
-- | -- Contract (new, strict): | ||
-- | -- - Pages are generated/updated by SVWT + pywikibot (compiled artifacts). | ||
-- | -- - No legacy wrappers: read args from frame.args ONLY. | ||
-- - Submodules are pure renderers: | |||
-- return require("Module:GameInfo/Skills").render(frame) | |||
-- Optional: | |||
-- return require("Module:GameInfo/Skills").STYLE_SRC = "Module:GameInfo/skills.css" | |||
-- | -- | ||
-- | -- Entry points: | ||
-- | -- {{#invoke:GameInfo|Skills|id=...|index=...|notes=...|data=...}} | ||
-- | -- {{#invoke:GameInfo|Category|name=Skills|...}} -- allowlist only | ||
- | |||
local p = {} | local p = {} | ||
-- | -- Default TemplateStyles source (may be overridden by submodules via STYLE_SRC). | ||
local DEFAULT_STYLE_SRC = "Module:GameInfo/styles.css" | local DEFAULT_STYLE_SRC = "Module:GameInfo/styles.css" | ||
-- Allowlist routes (no dynamic require by user input). | |||
local ROUTES = { | |||
Skills = "Module:GameInfo/Skills", | |||
-- Future: | |||
-- Monsters = "Module:GameInfo/Monsters", | |||
-- Equips = "Module:GameInfo/Equips", | |||
} | |||
-- ----------------------------------------------------------------------------- | |||
-- Small utils | |||
-- ----------------------------------------------------------------------------- | |||
local function _trim(v) | local function _trim(v) | ||
if v == nil then return | if v == nil then return "" end | ||
local s = tostring(v) | local s = tostring(v) | ||
if mw.text and mw.text.trim then | |||
return mw.text.trim(s) | |||
end | |||
return (s:gsub("^%s+", ""):gsub("%s+$", "")) | |||
end | end | ||
local function | local function _to_int(v, fallback) | ||
local n = tonumber(v) | |||
return | if not n then return fallback end | ||
return math.floor(n + 0.0) | |||
end | end | ||
local function | local function _is_html_node(x) | ||
return type(x) == "table" | |||
and type(x.tag) == "function" | |||
and type(x.wikitext) == "function" | |||
end | end | ||
local function _error_box(msg) | local function _error_box(msg) | ||
-- Keep | -- Keep obvious + wiki-friendly (TemplateStyles will still load). | ||
return tostring( | |||
mw.html.create("div") | |||
:addClass("sv-gi-error") | |||
:wikitext(tostring(msg)) | |||
) | |||
end | end | ||
-- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
-- | -- Args (strict: frame.args only) | ||
-- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
function p.arg(frame, key, fallback) | |||
local args = frame and frame.args or {} | local args = frame and frame.args or {} | ||
local v = _trim(args[key]) | local v = _trim(args[key]) | ||
if | if v ~= "" then return v end | ||
return fallback | |||
end | |||
function p.int(frame, key, fallback, minv, maxv) | |||
local | local n = _to_int(p.arg(frame, key, nil), fallback) | ||
if minv ~= nil and n < minv then n = minv end | |||
if | if maxv ~= nil and n > maxv then n = maxv end | ||
return n | |||
end | |||
return | function p.bool(frame, key, fallback) | ||
local v = p.arg(frame, key, nil) | |||
if v == nil then return fallback end | |||
v = _trim(v):lower() | |||
if v == "1" or v == "true" or v == "yes" or v == "y" then return true end | |||
if v == "0" or v == "false" or v == "no" or v == "n" then return false end | |||
return fallback | |||
end | end | ||
-- ----------------------------------------------------------------------------- | |||
-- TemplateStyles | |||
-- ----------------------------------------------------------------------------- | |||
function p.styles(frame, src) | function p.styles(frame, src) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
| Line 102: | Line 98: | ||
end | end | ||
-- | -- ----------------------------------------------------------------------------- | ||
-- | -- Shared container builder | ||
function p. | -- ----------------------------------------------------------------------------- | ||
-- Canonical builder: returns { root=<div>, top=<div>, bottom=<div> }. | |||
-- Submodules fill top/bottom and tostring(root). | |||
function p.box(opts) | |||
opts = opts or {} | opts = opts or {} | ||
| Line 118: | Line 118: | ||
:addClass("sv-gi-card") | :addClass("sv-gi-card") | ||
:attr("data-gi", "1") | :attr("data-gi", "1") | ||
:attr("data-gi-phase", "4.1") | |||
:attr("data-level", tostring(level)) | :attr("data-level", tostring(level)) | ||
:attr("data-max-level", tostring(max_level)) | :attr("data-max-level", tostring(max_level)) | ||
if root_id and root_id ~= "" then | if root_id and tostring(root_id) ~= "" then | ||
root:attr("id", root_id) | root:attr("id", tostring(root_id)) | ||
end | end | ||
if opts.variant and tostring(opts.variant) ~= "" then | |||
if opts.variant and opts.variant ~= "" then | |||
root:addClass("sv-gi--" .. tostring(opts.variant)) | root:addClass("sv-gi--" .. tostring(opts.variant)) | ||
end | end | ||
| Line 133: | Line 133: | ||
local bottom = root:tag("div"):addClass("sv-gi-bottom") | local bottom = root:tag("div"):addClass("sv-gi-bottom") | ||
return { | return { root = root, top = top, bottom = bottom } | ||
end | end | ||
-- | -- Backward-friendly alias for your own codebase (not “reverse wiki compat”). | ||
p.new_box = p.box | |||
function p.render_box(opts) | function p.render_box(opts) | ||
local box = p. | local box = p.box(opts) | ||
if opts and opts.top ~= nil then | |||
if _is_html_node(opts.top) then box.top:node(opts.top) else box.top:wikitext(tostring(opts.top)) end | |||
end | |||
if opts and opts.bottom ~= nil then | |||
if _is_html_node(opts.bottom) then box.bottom:node(opts.bottom) else box.bottom:wikitext(tostring(opts.bottom)) end | |||
end | |||
return tostring(box.root) | return tostring(box.root) | ||
end | end | ||
-- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
-- | -- Dispatch | ||
-- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
local | local function _require_submodule(module_title) | ||
local ok, mod = pcall(require, module_title) | |||
if not ok then | |||
return nil, "GameInfo: failed to require " .. tostring(module_title) | |||
end | |||
if type(mod) ~= "table" then | |||
return nil, "GameInfo: " .. tostring(module_title) .. " did not return a table" | |||
end | |||
if type(mod.render) ~= "function" then | |||
return nil, "GameInfo: " .. tostring(module_title) .. " must export render(frame)" | |||
if type(mod) ~= "table" then return nil | end | ||
return mod, nil | |||
if type(mod.render) | |||
return nil | |||
end | end | ||
local function | local function _invoke(frame, module_title) | ||
local | local mod, err = _require_submodule(module_title) | ||
if not | if not mod then | ||
return | return p.styles(frame) .. _error_box(err) | ||
end | end | ||
local | local style_src = mod.STYLE_SRC or DEFAULT_STYLE_SRC | ||
local | local ok, out = pcall(mod.render, frame) | ||
if not | if not ok then | ||
return _error_box("GameInfo: error inside " .. module_title) | return p.styles(frame, style_src) .. _error_box("GameInfo: error inside " .. tostring(module_title)) | ||
end | end | ||
return out | return p.styles(frame, style_src) .. tostring(out or "") | ||
end | end | ||
-- | -- ----------------------------------------------------------------------------- | ||
-- Public entrypoints | |||
-- ----------------------------------------------------------------------------- | |||
function p.Skills(frame) | function p.Skills(frame) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
return | return _invoke(frame, ROUTES.Skills) | ||
end | end | ||
function p.Category(frame) | function p.Category(frame) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
local name = p.arg(frame, "name", p.arg(frame, "category", nil)) | |||
local name = | |||
if not name or name == "" then | if not name or name == "" then | ||
return _error_box("GameInfo.Category: missing |name= | return p.styles(frame) .. _error_box("GameInfo.Category: missing |name=") | ||
end | end | ||
local module_title = ROUTES[name] | local module_title = ROUTES[name] | ||
if not module_title then | if not module_title then | ||
return p.styles(frame) .. _error_box("GameInfo.Category: unsupported name=" .. tostring(name)) | |||
end | end | ||
return | return _invoke(frame, module_title) | ||
end | end | ||
-- Optional: | -- Optional: sanity test for box + CSS wiring | ||
-- {{#invoke:GameInfo|skeleton|id=sv-gi-test-1|level=1|max=10}} | -- {{#invoke:GameInfo|skeleton|id=sv-gi-test-1|level=1|max=10|variant=skills}} | ||
function p.skeleton(frame) | function p.skeleton(frame) | ||
frame = frame or mw.getCurrentFrame() | frame = frame or mw.getCurrentFrame() | ||
local id = | local id = p.arg(frame, "id", "sv-gi-skeleton-1") | ||
local level = | local level = p.int(frame, "level", 1, 1, 999) | ||
local max_level = | local max_level = p.int(frame, "max", 10, 1, 999) | ||
local variant = | local variant = p.arg(frame, "variant", nil) | ||
local top = mw.html.create("div"):wikitext("GameInfo Top (locked container)") | local top = mw.html.create("div"):wikitext("GameInfo Top (locked container)") | ||
| Line 250: | Line 222: | ||
return p.styles(frame) .. p.render_box({ | return p.styles(frame) .. p.render_box({ | ||
id = id, | |||
level = level, | level = level, | ||
max_level = max_level, | max_level = max_level, | ||