This Lua module is used on 410,000+ pages, or roughly 1068% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them.
This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing.
This module depends on the following other modules:
-- Module:Sidebar — safe, collapsible version
-- Fixes "args table is read-only" by ALWAYS copying frame args to a plain table.
-- Features:
-- * p.main(frame): build a sidebar (title, optional image, up to 10 sections)
-- * Each section can be collapsible: |collapsibleN=yes| (N=1..10), optional |stateN=collapsed|
-- * Section content via |listN= (semicolon OR newline separated) OR |contentN=
-- * p.collapsible(frame): simple collapsible box (for legacy {{#invoke:Sidebar|collapsible|...}} calls)
local p = {}
------------------------------------------------------------
-- Safe argument handling
------------------------------------------------------------
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
-- Return a brand-new, writable args table (never the read-only proxy)
local function getArgsPlain(frame, opt)
opt = opt or { parentOnly = true }
local ok, Arguments = pcall(require, 'Module:Arguments')
if ok and type(Arguments) == 'table' and type(Arguments.getArgs) == 'function' then
-- Make an extra copy in case getArgs returns a proxy
return shallow_copy(Arguments.getArgs(frame, opt))
end
local srcFrame = frame
if opt.parentOnly ~= false and frame.getParent then
srcFrame = frame:getParent() or frame
end
return shallow_copy(srcFrame.args or {})
end
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
local function is_blank(s)
return s == nil or (type(s) == 'string' and trim(s) == '')
end
------------------------------------------------------------
-- Utilities
------------------------------------------------------------
local function split_items(s)
if type(s) ~= 'string' then return {} end
s = trim(s); if s == '' then return {} end
s = s:gsub('\r\n?', '\n')
local items = {}
if s:find(';', 1, true) then
for part in s:gmatch('([^;]+)') do
local t = trim(part); if t ~= '' then table.insert(items, t) end
end
else
for line in s:gmatch('([^\n]+)') do
local t = trim(line); if t ~= '' then table.insert(items, t) end
end
end
return items
end
local function normalize_width(w)
w = trim(w or '')
if w == '' then return '22em' end
if w:match('^%d+$') then return w .. 'px' end
if w:match('^%d+%s*[%a%%]+$') or w:match('^[%d%.]+%s*ch$') then return w end
return '22em'
end
------------------------------------------------------------
-- Pieces
------------------------------------------------------------
local function buildImage(args)
local file = args.image or args.file or args.img
if is_blank(file) then return '' end
local size = trim(args.image_size or args.size or '220px')
local alt = trim(args.image_alt or args.alt or '')
local cap = trim(args.image_caption or args.caption or '')
if not file:match('^[Ff]ile:') and not file:match('^[Ii]mage:') then
file = 'File:' .. file
end
local opts = size
if not is_blank(alt) then opts = opts .. '|alt=' .. alt end
local out = {}
out[#out+1] = "<div style='text-align:center; padding:6px 8px;'>"
out[#out+1] = string.format('[[%s|%s]]', file, opts)
if not is_blank(cap) then
out[#out+1] = string.format("<div style='color:#54595d; font-size:90%%; margin-top:4px;'>%s</div>", cap)
end
out[#out+1] = "</div>"
return table.concat(out)
end
-- Collapsible section wrapper
local function wrapCollapsible(innerHtml, headingText, initialState)
local classes = 'mw-collapsible'
if initialState == 'collapsed' or initialState == 'close' or initialState == 'closed' then
classes = classes .. ' mw-collapsed'
end
local out = {}
out[#out+1] = string.format('<div class="%s" style="border-top:1px solid #a2a9b1;">', classes)
-- 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] = headingText or ''
out[#out+1] = '</div>'
-- Collapsible content
out[#out+1] = '<div class="mw-collapsible-content" style="padding:6px 8px;">'
out[#out+1] = innerHtml or ''
out[#out+1] = '</div>'
out[#out+1] = '</div>'
return table.concat(out)
end
local function buildSection(args, i)
local heading = args['heading'..i] or args['h'..i]
local listRaw = args['list'..i]
local content = args['content'..i]
local collaps = args['collapsible'..i] or args['collapse'..i] -- yes/true to enable collapsible
local stateInit = args['state'..i] or args['collapsed'..i] -- collapsed/expanded
if is_blank(heading) and is_blank(listRaw) and is_blank(content) then
return ''
end
-- Build body (list or custom content)
local body = {}
body[#body+1] = "<div>"
if not is_blank(listRaw) then
local items = split_items(listRaw)
if #items > 0 then
body[#body+1] = "<ul style='margin:0; padding-left:18px;'>"
for _, li in ipairs(items) do
body[#body+1] = '* ' .. li
end
body[#body+1] = "</ul>"
end
elseif not is_blank(content) then
body[#body+1] = content
end
body[#body+1] = "</div>"
local bodyHtml = table.concat(body)
-- Collapsible?
if collaps and collaps:lower() ~= '' and collaps:lower() ~= 'no' and collaps:lower() ~= 'false' then
return wrapCollapsible(bodyHtml, heading or '', stateInit or '')
end
-- Non-collapsible section (Wikipedia-ish chrome)
local out = {}
if not is_blank(heading) then
out[#out+1] = string.format(
"<div style='font-weight:600; padding:6px 8px; background:#eaecf0; border-top:1px solid #a2a9b1;'>%s</div>",
heading
)
else
out[#out+1] = "<div style='border-top:1px solid #a2a9b1'></div>"
end
out[#out+1] = "<div style='padding:6px 8px'>" .. bodyHtml .. "</div>"
return table.concat(out)
end
------------------------------------------------------------
-- Renderer
------------------------------------------------------------
function p._render(args)
local title = args.title or args.name or 'Sidebar'
local title_link = args.title_link or args.titlelink
local align = (args.align == 'left') and 'left' or 'right'
local width = normalize_width(args.width)
local floatStyle = (align == 'left')
and "float:left; clear:left; margin:0 1em 1em 0;"
or "float:right; clear:right; margin:0 0 1em 1em;"
local out = {}
out[#out+1] = string.format(
"<div class='sidebar plainlinks noprint' style='%s width:%s; background:#f8f9fa; border:1px solid #a2a9b1; border-radius:2px; overflow:hidden;'>",
floatStyle, width
)
if not is_blank(title_link) then
title = string.format('[[%s|%s]]', title_link, title)
end
out[#out+1] = string.format("<div style='background:#eaecf0; border-bottom:1px solid #a2a9b1; padding:8px 10px; font-weight:700;'>%s</div>", title)
-- Optional image
out[#out+1] = buildImage(args)
-- Sections 1..10
for i = 1, 10 do
out[#out+1] = buildSection(args, i)
end
out[#out+1] = "</div>"
return table.concat(out)
end
------------------------------------------------------------
-- Entry points
------------------------------------------------------------
function p.main(frame)
local raw = getArgsPlain(frame, { parentOnly = true })
local args = normalizeArgs(raw) -- make a fresh, trimmed copy
return p._render(args)
end
-- Legacy / convenience: collapsible box builder
function p.collapsible(frame)
-- If a dedicated Module:Collapsible exists, delegate to it
local ok, Collapsible = pcall(require, 'Module:Collapsible')
if ok and type(Collapsible) == 'table' and type(Collapsible.collapsible) == 'function' then
return Collapsible.collapsible(frame)
end
-- Inline fallback
local raw = getArgsPlain(frame, { parentOnly = true })
local a = normalizeArgs(raw)
local title = a.title or a.heading or 'Details'
local state = a.state or a.collapse or ''
local content = a.content or a[1] or ''
local width = a.width or ''
local class = a.class or ''
local style = a.style or ''
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 = '' end
end
local classes = 'mw-collapsible' .. ((state == 'collapsed' or state == 'close' or state == 'closed') and ' mw-collapsed' or '')
if class ~= '' then classes = classes .. ' ' .. class end
local boxStyle = 'border:1px solid #a2a9b1;background:#f8f9fa;border-radius:2px;overflow:hidden;margin:0.4em 0;'
if width ~= '' then boxStyle = boxStyle .. 'width:' .. width .. ';' end
if style ~= '' then boxStyle = boxStyle .. style end
local out = {}
out[#out+1] = string.format('<div class="%s" style="%s">', classes, boxStyle)
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>'
out[#out+1] = '<div class="mw-collapsible-content" style="padding:8px 10px;">'
out[#out+1] = content
out[#out+1] = '</div></div>'
return table.concat(out)
end
-- Roovet compatibility: imported Template:Sidebar invokes
-- the historical "sidebar" entry point.
p.sidebar = p.main
return p