Documentation for this module may be created at Module:Collapsible/doc

-- Module:Collapsible (safe args version)
-- Renders a simple collapsible box using MediaWiki's mw-collapsible JS/CSS.
-- Exposed functions:
--   * p.collapsible(frame)
--   * p.main(frame)  -- alias
--
-- FIX: never write to frame.args or parent.args. Always copy to a plain table.

local p = {}

-- ---------- helpers ----------
local function shallow_copy(t)
	if type(t) ~= 'table' then return {} end
	local out = {}
	for k, v in pairs(t) do out[k] = v end
	return out
end

local function trim(s)
	if type(s) ~= 'string' then return s end
	if mw and mw.text and mw.text.trim then
		return mw.text.trim(s)
	end
	return (s:gsub('^%s+', ''):gsub('%s+$', ''))
end

-- Get args as a **plain, writable** table
local function getArgsPlain(frame)
	-- Try Module:Arguments first
	local ok, Arguments = pcall(require, 'Module:Arguments')
	if ok and type(Arguments) == 'table' and type(Arguments.getArgs) == 'function' then
		-- Some wikis return a proxy; copy again to be 100% writable
		local a = Arguments.getArgs(frame, { parentOnly = true })
		return shallow_copy(a)
	end
	-- Manual fallback: copy parent/child args
	local src = (frame.getParent and frame:getParent() and frame:getParent().args) or frame.args or {}
	-- src may be read-only; copy to a new table:
	return shallow_copy(src)
end

-- Normalize without mutating the original
local function normalizeArgs(inArgs)
	local a = {}
	for k, v in pairs(inArgs) do
		if type(v) == 'string' then
			local t = trim(v)
			if t ~= '' then a[k] = t end
		else
			a[k] = v
		end
	end
	return a
end

-- ---------- renderer ----------
local function renderBox(a)
	-- Params (read only)
	local title   = a.title or a.heading or 'Details'
	local state   = a.state or a.collapse or ''        -- 'collapsed' to start closed
	local content = a.content or a[1] or ''            -- |content= or |1=
	local width   = a.width or ''
	local class   = a.class or ''
	local style   = a.style or ''

	-- sanitize width
	if width ~= '' then
		if width:match('^%d+$') then
			width = width .. 'px'
		elseif not width:match('^%d+%s*[%a%%]+$') and not width:match('^[%d%.]+%s*ch$') then
			width = '' -- ignore suspicious values
		end
	end

	-- classes for collapsible behavior
	local collClass = 'mw-collapsible'
	if state == 'collapsed' or state == 'close' or state == 'closed' then
		collClass = collClass .. ' mw-collapsed'
	end
	if class ~= '' then
		collClass = collClass .. ' ' .. class
	end

	-- wrapper styles (Wikipedia-ish)
	local baseStyle = 'border:1px solid #a2a9b1;background:#f8f9fa;border-radius:2px;overflow:hidden;margin:0.4em 0;'
	if width ~= '' then baseStyle = baseStyle .. 'width:' .. width .. ';' end
	if style ~= '' then baseStyle = baseStyle .. style end

	local out = {}
	out[#out+1] = string.format('<div class="%s" style="%s">', collClass, baseStyle)

	-- Header with toggle
	out[#out+1] = '<div style="background:#eaecf0;border-bottom:1px solid #a2a9b1;padding:6px 8px;font-weight:600;display:flex;align-items:center;gap:8px;">'
	out[#out+1] = '<span class="mw-collapsible-toggle"></span>'
	out[#out+1] = title
	out[#out+1] = '</div>'

	-- Content
	out[#out+1] = '<div class="mw-collapsible-content" style="padding:8px 10px;">'
	out[#out+1] = content or ''
	out[#out+1] = '</div>'

	out[#out+1] = '</div>'
	return table.concat(out)
end

-- ---------- entry points ----------
function p.collapsible(frame)
	-- ALWAYS work on a writable copy
	local raw = getArgsPlain(frame)
	local args = normalizeArgs(raw)
	return renderBox(args)
end

p.main = p.collapsible

return p