Protected page

Module:Userbox

From Uncyclopedia, the content-free encyclopedia
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