Module:Userbox
Jump to navigation
Jump to search
-- This module implements {{userbox}}.
-- This version supports backwards compatibility with old userbox parameters.
-- You can find default styling in [[Module:Userbox/styles.css]].
-- If you feel a demonic aura coming from this module, contact the Hellbusters!
--
-- COMPATIBILITY NOTE: This module is designed to match the behavior of the
-- original wikitext template as closely as possible, including parameter
-- precedence chains, conditional defaults, and pass-through of CSS values.
-- So if you're scratching your head thinking, "Why the hell does it do that?"
-- It's so the 1000 or so userbox templates written the old way don't explode!
local categoryHandler = require('Module:Category handler').main
local p = {}
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
local function isEmpty(val)
-- Check if a value is nil or empty string
return val == nil or val == ''
end
local function firstNonEmpty(...)
-- Returns the first non-nil, non-empty value from arguments
local args = {...}
for i = 1, select('#', ...) do
local v = args[i]
if not isEmpty(v) then
return v
end
end
return nil
end
local function normalizeUnit(val, suffix)
-- Normalizes a value that may or may not have a unit.
-- - If nil/empty, returns nil
-- - If already has a unit (letters or %), returns as-is
-- - If bare number, appends the suffix
-- - If non-numeric string (like 'inherit'), returns as-is
if not val or val == '' then
return nil
end
val = tostring(val)
-- Check if it already ends with a unit (pt, px, em, %, etc.)
if val:match('[a-zA-Z%%]+%s*$') then
return val
end
-- Try to parse as number and append suffix
local num = tonumber(val)
if num then
return tostring(num) .. suffix
end
-- Fallback: return as-is (handles CSS keywords like 'inherit', 'auto')
return val
end
local function makeCat(cat, sort)
-- Makes a category link.
if sort then
return mw.ustring.format('[[Category:%s|%s]]', cat, sort)
else
return mw.ustring.format('[[Category:%s]]', cat)
end
end
--------------------------------------------------------------------------------
-- Refactored helper functions for common patterns
--------------------------------------------------------------------------------
local function calcMargin(args)
-- Calculate margin based on float value
-- Wikitext: margin:{{{m|{{#if:{{{float|}}}|{{#ifeq:{{{float|}}}|none||2px 5px; margin-{{{float}}}:0}}|2px auto}}}}}
if not isEmpty(args.m) then
return args.m
elseif not isEmpty(args.float) and args.float ~= 'none' then
return '2px 5px; margin-' .. args.float .. ':0'
end
return nil -- Use default '2px auto'
end
local function getWithUnit(val, suffix)
-- Get a value with unit normalization, returns nil if empty
if isEmpty(val) then
return nil
end
return normalizeUnit(val, suffix)
end
local function getFromChainWithUnit(suffix, ...)
-- Get first non-empty value from chain, then normalize with unit
local val = firstNonEmpty(...)
return getWithUnit(val, suffix)
end
local function getVerticalAlign(specific, general, default)
-- Get vertical alignment with fallback chain
local val = firstNonEmpty(specific, general)
if isEmpty(val) then
return default or 'middle'
end
return val
end
local function isBorderShorthand(val)
-- Detect if a value looks like a full border shorthand
-- (contains border-style keywords or multiple space-separated values)
if isEmpty(val) then return false end
val = tostring(val):lower()
-- Check for border-style keywords
if val:match('solid') or val:match('dashed') or val:match('dotted') or
val:match('double') or val:match('groove') or val:match('ridge') or
val:match('inset') or val:match('outset') or val:match('none') or
val:match('hidden') then
return true
end
-- Check for multiple space-separated values
if val:match('%S+%s+%S+') then
return true
end
return false
end
local function processBorderParams(args, paramList)
-- Process border parameters, detecting full shorthands
-- paramList is array of param names to check in order
-- Returns borderFull (if shorthand) or borderWidth
local borderFull, borderWidth
for _, param in ipairs(paramList) do
local val = args[param]
if not isEmpty(val) then
if isBorderShorthand(val) then
borderFull = val
break
elseif not borderWidth then
borderWidth = normalizeUnit(val, 'px')
end
end
end
return borderFull, borderWidth
end
local function getContainerData(args)
-- Extract common container parameters used by all userbox variants
return {
containerId = args.ID,
containerClass = args.class,
float = args.float,
clear = args.clear,
margin = calcMargin(args),
bodyClass = args.bodyclass,
outerStyles = args.os,
borderStyles = args.bs,
tableStyles = args.ts,
verticalAlign = args.va,
imageStyles = args.imagestyles,
fontWeight = args.fw,
contentStyles = args.contentstyles
}
end
--------------------------------------------------------------------------------
-- Render helper functions
--------------------------------------------------------------------------------
local function applyStyle(element, property, value)
-- Apply a single CSS property if value is not nil/empty
if not isEmpty(value) then
element:css(property, value)
end
end
local function renderIdCell(tablerow, data, isRight)
-- Render an ID cell (left or right)
-- isRight: boolean, true for right cell (uses id2* properties with id* fallbacks)
local cell = tablerow:tag('td')
cell:addClass(data.idClass)
local vAlign, width, height, lineHeight, textAlign, fontSize, color, padding, background, otherParams, content
local display
if isRight then
vAlign = data.id2VerticalAlign or data.idVerticalAlign or 'middle'
width = data.id2Width or data.idWidth or '45px'
height = data.id2Height or data.idHeight or '45px'
lineHeight = data.id2LineHeight or data.idLineHeight or '1.25em'
textAlign = data.id2TextAlign or data.idTextAlign or 'center'
fontSize = data.id2FontSize or data.idFontSize or '14pt'
color = data.id2Color or data.idColor or 'inherit'
padding = data.id2Padding or data.idPadding or '0'
background = data.id2BackgroundColor or data.idBackgroundColor or '#DDD'
otherParams = data.id2OtherParams or data.idOtherParams
content = data.id2 or data.id or ''
-- Right cell shows if explicitly id2 or if imagePosition is 'right'
local showIdRight = data.showId and (data.imagePosition == 'right')
display = (showIdRight or data.showId2) and 'table-cell' or 'none'
else
vAlign = data.idVerticalAlign or 'middle'
width = data.idWidth or '45px'
height = data.idHeight or '45px'
lineHeight = data.idLineHeight or '1.25em'
textAlign = data.idTextAlign or 'center'
fontSize = data.idFontSize or '14pt'
color = data.idColor or 'inherit'
padding = data.idPadding or '0'
background = data.idBackgroundColor or '#DDD'
otherParams = data.idOtherParams
content = data.id or ''
-- Left cell shows if showId and position is not 'right'
local showIdLeft = data.showId and (data.imagePosition ~= 'right')
display = showIdLeft and 'table-cell' or 'none'
end
cell:attr('valign', vAlign)
cell:css('display', display)
cell:css('vertical-align', vAlign)
cell:css('width', width)
cell:css('height', height)
cell:css('line-height', lineHeight)
cell:css('text-align', textAlign)
applyStyle(cell, 'font-weight', data.fontWeight or 'inherit')
cell:css('font-size', fontSize)
cell:css('color', color)
cell:css('padding', padding)
cell:css('background', background)
if not isEmpty(otherParams) then
cell:cssText(otherParams)
end
-- Content wrapper div
local contentDiv = cell:tag('div')
if not isEmpty(data.imageStyles) then
contentDiv:cssText(data.imageStyles)
end
contentDiv:wikitext(content)
return cell
end
local function renderInfoCell(tablerow, data)
-- Render the info/text cell
local cell = tablerow:tag('td')
cell:addClass(data.infoClass)
local vAlign = data.infoVerticalAlign or 'middle'
cell:attr('valign', vAlign)
cell:css('vertical-align', vAlign)
cell:css('padding', data.infoPadding or '0 4px 1px 4px')
cell:css('line-height', data.infoLineHeight or '1.25em')
cell:css('font-size', data.infoFontSize or '10pt')
cell:css('color', data.infoColor or 'inherit')
if not isEmpty(data.infoOtherParams) then
cell:cssText(data.infoOtherParams)
end
-- Content wrapper div
local contentDiv = cell:tag('div')
if not isEmpty(data.contentStyles) then
contentDiv:cssText(data.contentStyles)
end
contentDiv:wikitext(data.info)
return cell
end
--------------------------------------------------------------------------------
-- Argument processing
--------------------------------------------------------------------------------
local function makeInvokeFunc(funcName)
return function (frame)
local origArgs = require('Module:Arguments').getArgs(frame)
local args = {}
for k, v in pairs(origArgs) do
args[k] = v
end
return p.main(funcName, args, frame)
end
end
p.userbox = makeInvokeFunc('_userbox')
p['userbox-2'] = makeInvokeFunc('_userbox-2')
p['userbox-r'] = makeInvokeFunc('_userbox-r')
--------------------------------------------------------------------------------
-- Main functions
--------------------------------------------------------------------------------
function p.main(funcName, args, frame)
local userboxData = p[funcName](args)
local userbox = p.render(userboxData)
local cats = p.categories(args)
-- Add TemplateStyles
local styles = ''
if frame then
styles = frame:extensionTag{
name = 'templatestyles',
args = { src = 'Module:Userbox/styles.css' }
}
end
return styles .. userbox .. (cats or '')
end
function p._userbox(args)
-- Does argument processing for {{userbox}} with extended parameter support.
-- Parameter precedence chains match the original wikitext template exactly.
-- Start with common container data
local data = getContainerData(args)
-- IMPORTANT: Wikitext conditionals check ONLY {{{info}}}, not {{{4}}} or {{{text}}}
local hasInfoParam = not isEmpty(args.info)
data.hasInfo = hasInfoParam
-- Border handling
local borderFull, borderWidth = processBorderParams(args,
{'border', 'border-width', 'border-s', 'b'})
data.borderFull = borderFull
data.borderWidth = borderWidth
-- Border color: 1 → border-color → border-c → id-c
data.borderColor = firstNonEmpty(
args[1], args['border-color'], args['border-c'], args['id-c']
)
if isEmpty(data.borderColor) and hasInfoParam then
data.borderColor = '#999'
end
-- Border div background (longer fallback chain)
data.borderDivBackground = firstNonEmpty(
args[1], args['border-color'], args['border-c'], args['id-c'],
args.bg, args[2], args['info-background'], args['info-c']
) or '#999'
-- Background color: bg → 2 → info-background → info-c
data.backgroundColor = firstNonEmpty(
args.bg, args[2], args['info-background'], args['info-c']
)
-- Info content: text → 4 → info
data.info = firstNonEmpty(args.text, args[4], args.info) or "<code>{{{info}}}</code>"
-- Text alignment with conditional default
data.infoTextAlign = firstNonEmpty(args.ta, args.ca, args.a, args['info-a'])
if isEmpty(data.infoTextAlign) then
data.infoTextAlign = hasInfoParam and 'left' or 'center'
end
-- Info font size: fs → 5 → info-size, then info-s separately
data.infoFontSize = getFromChainWithUnit('pt', args.fs, args[5], args['info-size'])
or getWithUnit(args['info-s'], 'pt')
-- Info padding with conditional default and pl support
local infoPadding = firstNonEmpty(args.p, args['info-padding'], args['info-p'])
if not isEmpty(infoPadding) then
data.infoPadding = infoPadding
else
local pl = args.pl
if hasInfoParam then
data.infoPadding = '0 4px 1px ' .. (isEmpty(pl) and '4px' or pl)
else
data.infoPadding = '4pt 4pt 5pt ' .. (isEmpty(pl) and '4pt' or pl)
end
end
-- Info line height: lh → info-line-height → info-lh
data.infoLineHeight = firstNonEmpty(args.lh, args['info-line-height'], args['info-lh'])
-- Info color: color → c → 6 → info-color → info-fc
data.infoColor = firstNonEmpty(
args.color, args.c, args[6], args['info-color'], args['info-fc']
)
-- Info other params
data.infoOtherParams = firstNonEmpty(args.styles, args['info-other-param'], args['info-op'])
data.infoClass = args['info-class']
data.infoVerticalAlign = getVerticalAlign(args.cva, data.verticalAlign, 'middle')
-- ID/Logo values
local id = firstNonEmpty(args.img, args[3], args.id, args.logo)
data.id = id
data.showId = not isEmpty(id)
data.imagePosition = args['if'] or 'left'
-- ID dimensions
data.idWidth = getFromChainWithUnit('px', args.sw, args['logo-width'], args['id-w'])
data.idHeight = getFromChainWithUnit('px', args.h, args['logo-height'], args['id-h'])
-- ID background: s → 1 → logo-background → id-c
data.idBackgroundColor = firstNonEmpty(args.s, args[1], args['logo-background'], args['id-c'])
-- ID text alignment
data.idTextAlign = firstNonEmpty(args['id-a'], args.sa)
-- ID font size: ss → logo-size, then id-s separately
data.idFontSize = getFromChainWithUnit('pt', args.ss, args['logo-size'])
or getWithUnit(args['id-s'], 'pt')
-- ID color: color → sc → logo-color → id-fc
data.idColor = firstNonEmpty(args.color, args.sc, args['logo-color'], args['id-fc'])
-- ID padding with conditional default
local idPadding = firstNonEmpty(args.sp, args['logo-padding'], args['id-p'])
data.idPadding = not isEmpty(idPadding) and idPadding or (hasInfoParam and '0 1px 0 0' or '0')
-- ID line height: slh → logo-line-height → id-lh → ss
local idLineHeight = firstNonEmpty(args.slh, args['logo-line-height'], args['id-lh'], args.ss)
data.idLineHeight = getWithUnit(idLineHeight, 'em')
-- ID other params
data.idOtherParams = firstNonEmpty(args.is, args['logo-other-param'], args['id-op'])
data.idClass = args['id-class']
data.idVerticalAlign = getVerticalAlign(args.iva, data.verticalAlign, 'middle')
return data
end
p['_userbox-2'] = function (args)
-- Does argument processing for {{userbox-2}}.
local data = getContainerData(args)
local hasInfo = not isEmpty(args.info or args[4])
data.hasInfo = hasInfo
-- Border
data.borderWidth = getFromChainWithUnit('px', args['border-s'], args[9])
data.borderColor = firstNonEmpty(args['border-c'], args[6], args['id1-c'], args[1])
data.borderDivBackground = data.borderColor
-- Background
data.backgroundColor = firstNonEmpty(args['info-c'], args[2])
-- Info values
data.info = firstNonEmpty(args.info, args[4]) or "<code>{{{info}}}</code>"
data.infoTextAlign = args['info-a']
data.infoFontSize = getWithUnit(args['info-s'], 'pt')
data.infoColor = firstNonEmpty(args['info-fc'], args[8])
data.infoPadding = args['info-p']
data.infoLineHeight = args['info-lh']
data.infoOtherParams = args['info-op']
data.infoVerticalAlign = getVerticalAlign(args.cva, data.verticalAlign, 'middle')
-- ID1 values (left)
data.showId = true
data.id = firstNonEmpty(args.logo, args[3], args.id1) or 'id1'
data.idWidth = getWithUnit(args['id1-w'], 'px')
data.idHeight = getWithUnit(args['id-h'], 'px')
data.idBackgroundColor = firstNonEmpty(args['id1-c'], args[1])
data.idTextAlign = args['id1-a']
data.idFontSize = getWithUnit(args['id1-s'], 'pt')
data.idLineHeight = args['id1-lh']
data.idColor = firstNonEmpty(args['id1-fc'], data.infoColor)
data.idPadding = args['id1-p']
data.idOtherParams = args['id1-op']
data.idVerticalAlign = getVerticalAlign(args.iva, data.verticalAlign, 'middle')
-- ID2 values (right)
data.showId2 = true
data.id2 = firstNonEmpty(args.logo, args[5], args.id2) or 'id2'
data.id2Width = getWithUnit(args['id2-w'], 'px')
data.id2Height = data.idHeight
data.id2BackgroundColor = firstNonEmpty(args['id2-c'], args[7], args[1])
data.id2TextAlign = args['id2-a']
data.id2FontSize = getWithUnit(args['id2-s'], 'pt')
data.id2LineHeight = args['id2-lh']
data.id2Color = firstNonEmpty(args['id2-fc'], data.infoColor)
data.id2Padding = args['id2-p']
data.id2OtherParams = args['id2-op']
data.id2VerticalAlign = getVerticalAlign(args.iva, data.verticalAlign, 'middle')
return data
end
p['_userbox-r'] = function (args)
-- Does argument processing for {{userbox-r}}.
local data = getContainerData(args)
local hasInfo = not isEmpty(args.info or args[4])
data.hasInfo = hasInfo
-- Border
data.borderWidth = getFromChainWithUnit('px', args['border-width'], args['border-s'])
data.borderColor = firstNonEmpty(args['border-color'], args['border-c'], args[1], args['id-c'])
data.borderDivBackground = data.borderColor
-- Background
data.backgroundColor = firstNonEmpty(args['info-background'], args[2], args['info-c'])
-- No left ID
data.showId = false
-- Info values
data.info = firstNonEmpty(args.info, args[4]) or "<code>{{{info}}}</code>"
data.infoTextAlign = firstNonEmpty(args['info-align'], args['info-a'])
data.infoFontSize = getFromChainWithUnit('pt', args['info-size'], args['info-s'])
data.infoPadding = firstNonEmpty(args['info-padding'], args['info-p'])
data.infoLineHeight = firstNonEmpty(args['info-line-height'], args['info-lh'])
data.infoColor = firstNonEmpty(args['info-color'], args['info-fc'])
data.infoOtherParams = firstNonEmpty(args['info-other-param'], args['info-op'])
data.infoVerticalAlign = getVerticalAlign(args.cva, data.verticalAlign, 'middle')
-- ID2 values (shown on right)
data.showId2 = true
data.id2 = firstNonEmpty(args.logo, args[3], args.id) or 'id'
data.id2Width = getFromChainWithUnit('px', args['logo-width'], args['id-w'])
data.id2Height = getFromChainWithUnit('px', args['logo-height'], args['id-h'])
data.id2BackgroundColor = firstNonEmpty(args['logo-background'], args[1], args['id-c'])
data.id2TextAlign = args['id-a']
data.id2FontSize = getFromChainWithUnit('pt', args['logo-size'], args[5], args['id-s'])
data.id2Color = firstNonEmpty(args['logo-color'], args['id-fc'], data.infoColor)
data.id2Padding = firstNonEmpty(args['logo-padding'], args['id-p'])
data.id2LineHeight = firstNonEmpty(args['logo-line-height'], args['id-lh'])
data.id2OtherParams = firstNonEmpty(args['logo-other-param'], args['id-op'])
data.id2VerticalAlign = getVerticalAlign(args.iva, data.verticalAlign, 'middle')
return data
end
function p.render(data)
-- Renders the userbox html using the content of the data table.
-- Outer container div
local root = mw.html.create('div')
if not isEmpty(data.containerId) then
root:attr('id', data.containerId)
end
root:addClass('userbox-container'):addClass(data.containerClass)
root:css('display', 'table')
applyStyle(root, 'text-align', data.infoTextAlign)
applyStyle(root, 'float', data.float or 'none')
applyStyle(root, 'clear', data.clear or 'none')
applyStyle(root, 'margin', data.margin or '2px auto')
root:css('width', '278px')
-- Body class wrapper div
local bodyDiv = root:tag('div')
bodyDiv:addClass('userbox'):addClass(data.bodyClass)
if not isEmpty(data.outerStyles) then
bodyDiv:cssText(data.outerStyles)
end
-- Border/background wrapper div
local borderDiv = bodyDiv:tag('div')
applyStyle(borderDiv, 'background', data.borderDivBackground or '#999')
if not isEmpty(data.borderStyles) then
borderDiv:cssText(data.borderStyles)
end
-- Main table
local tableroot = borderDiv:tag('table')
tableroot
:attr('cellspacing', '0')
:attr('cellpadding', '0')
:css('margin', '0 auto')
:css('width', '100%')
:css('min-height', data.idHeight or '45px')
-- Border on table
local borderWidth = data.borderWidth or '1px'
local borderColor = data.borderColor or (data.hasInfo and '#999' or '')
if not isEmpty(data.borderFull) then
tableroot:css('border', data.borderFull)
elseif not isEmpty(borderColor) then
tableroot:css('border', borderWidth .. ' solid ' .. borderColor)
else
tableroot:css('border-width', borderWidth)
tableroot:css('border-style', 'solid')
end
tableroot:css('background', data.backgroundColor or '#EEE')
if not isEmpty(data.tableStyles) then
tableroot:cssText(data.tableStyles)
end
-- Table row with cells
local tablerow = tableroot:tag('tr')
renderIdCell(tablerow, data, false) -- Left ID
renderInfoCell(tablerow, data) -- Info
renderIdCell(tablerow, data, true) -- Right ID
return tostring(root)
end
function p.categories(args, page)
-- Gets categories from [[Module:Category handler]].
local cats = {}
for i = 1, 5 do
local catKey = i == 1 and 'usercategory' or ('usercategory' .. i)
if not isEmpty(args[catKey]) then
cats[#cats + 1] = args[catKey]
end
end
if #cats == 0 then
return nil
end
local title = page and mw.title.new(page) or mw.title.getCurrentTitle()
local chargs = {
page = page,
nocat = args.nocat,
main = '[[Category:Pages with misplaced templates]]',
subpage = args.notcatsubpages and 'no' or nil
}
-- User namespace
local userCats = ''
for _, cat in ipairs(cats) do
userCats = userCats .. makeCat(cat)
end
chargs.user = userCats
-- Template namespace
local templateCats = ''
for _, cat in ipairs(cats) do
templateCats = templateCats .. makeCat(cat, ' ' .. title.baseText)
end
chargs.template = templateCats
return categoryHandler(chargs)
end
return p