Jump to content

Module:Footballer positions

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by S.A. Julio (talk | contribs) at 04:13, 4 December 2025 (add aliases). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
local p = {}

local mHList = require('Module:List')

-- Position definitions: {names/abbrevs}, link, display
-- Use %D for dash placeholder, %C for centre/center placeholder
local positionDefs = {
	-- Goalkeepers
	{{"goalkeeper", "goalie", "keeper", "gk"}, "Goalkeeper (association football)", "goalkeeper"},
	-- Defenders
	{{"defender", "defence", "defense", "df"}, "Defender (association football)", "defender"},
	{{"centreback", "centralback", "cb"}, "Defender (association football)#Centre-back", "%Ccentre%Dback"},
	{{"centraldefender", "cd"}, "Defender (association football)#Centre-back", "central defender"},
	{{"centrehalf"}, "Defender (association football)#Centre-back", "%Ccentre%Dhalf"},
	{{"fullback", "outsideback", "fb"}, "Defender (association football)#Full-back", "full%Dback"},
	{{"wingback", "wb"}, "Defender (association football)#Wing-back", "wing%Dback"},
	{{"sweeper", "sw"}, "Defender (association football)#Sweeper", "sweeper"},
	{{"libero"}, "Defender (association football)#Sweeper", "libero"},
	{{"rightback", "rb"}, "Defender (association football)#Full-back", "right%Dback"},
	{{"rightfullback"}, "Defender (association football)#Full-back", "right full%Dback"},
	{{"leftback", "lb"}, "Defender (association football)#Full-back", "left%Dback"},
	{{"leftfullback"}, "Defender (association football)#Full-back", "left full%Dback"},
	{{"rightwingback", "rwb"}, "Defender (association football)#Wing-back", "right wing%Dback"},
	{{"leftwingback", "lwb"}, "Defender (association football)#Wing-back", "left wing%Dback"},
	-- Midfielders
	{{"midfielder", "midfield", "mf"}, "Midfielder", "midfielder"},
	{{"halfback", "hb"}, "Midfielder", "half%Dback"},
	{{"centralmidfielder", "centralmidfield", "cm"}, "Midfielder#Central midfielder", "central midfielder"},
	{{"centremidfielder", "centremidfield"}, "Midfielder#Central midfielder", "%Ccentre midfielder"},
	{{"defensivemidfielder", "defensivemidfield", "dm"}, "Midfielder#Defensive midfielder", "defensive midfielder"},
	{{"attackingmidfielder", "attackingmidfield", "offensivemidfielder", "offensivemidfield", "am"}, "Midfielder#Attacking midfielder", "attacking midfielder"},
	{{"widemidfielder", "wm"}, "Midfielder#Wide midfielder", "wide midfielder"},
	{{"winger", "wi"}, "Midfielder#Winger", "winger"},
	{{"wing"}, "Midfielder#Winger", "wing"},
	{{"winghalf", "wh"}, "Midfielder#Wing-half", "wing%Dhalf"},
	{{"centraldefensivemidfielder", "cdm"}, "Midfielder#Defensive midfielder", "central defensive midfielder"},
	{{"centredefensivemidfielder"}, "Midfielder#Defensive midfielder", "%Ccentre defensive midfielder"},
	{{"leftdefensivemidfielder", "ldm"}, "Midfielder#Defensive midfielder", "left defensive midfielder"},
	{{"rightdefensivemidfielder", "rdm"}, "Midfielder#Defensive midfielder", "right defensive midfielder"},
	{{"centralattackingmidfielder", "cam"}, "Midfielder#Attacking midfielder", "central attacking midfielder"},
	{{"centreattackingmidfielder"}, "Midfielder#Attacking midfielder", "%Ccentre attacking midfielder"},
	{{"leftattackingmidfielder", "lam"}, "Midfielder#Attacking midfielder", "left attacking midfielder"},
	{{"rightattackingmidfielder", "ram"}, "Midfielder#Attacking midfielder", "right attacking midfielder"},
	{{"holdingmidfielder", "holdingmidfield", "hm"}, "Midfielder#Defensive midfielder", "holding midfielder"},
	{{"rightmidfielder", "rightmidfield", "rm"}, "Midfielder#Wide midfielder", "right midfielder"},
	{{"leftmidfielder", "leftmidfield", "lm"}, "Midfielder#Wide midfielder", "left midfielder"},
	{{"rightwinger", "rw"}, "Midfielder#Winger", "right winger"},
	{{"rightwing"}, "Midfielder#Winger", "right wing"},
	{{"leftwinger", "lw"}, "Midfielder#Winger", "left winger"},
	{{"leftwing"}, "Midfielder#Winger", "left wing"},
	{{"oldcentrehalf", "ch"}, "Midfielder#Centre-half", "%Ccentre%Dhalf"},
	{{"righthalf", "righthalfback", "rh"}, "Midfielder#Wing-half", "right half"},
	{{"lefthalf", "lefthalfback", "lh"}, "Midfielder#Wing-half", "left half"},
	-- Forwards
	{{"forward", "attacker", "fw"}, "Forward (association football)", "forward"},
	{{"centreforward", "centralforward", "cf"}, "Forward (association football)#Centre-forward", "%Ccentre%Dforward"},
	{{"striker", "st"}, "Forward (association football)#Striker", "striker"},
	{{"secondstriker", "secondarystriker", "ss"}, "Forward (association football)#Second striker", "second striker"},
	{{"insideforward", "if"}, "Forward (association football)#Inside forward", "inside forward"},
	{{"outsideforward", "of"}, "Forward (association football)#Outside forward", "outside forward"},
	{{"rightforward", "rf"}, "Forward (association football)", "right forward"},
	{{"leftforward", "lf"}, "Forward (association football)", "left forward"},
	{{"insideright", "ir"}, "Forward (association football)#Inside forward", "inside right"},
	{{"insideleft", "il"}, "Forward (association football)#Inside forward", "inside left"},
	{{"outsideright", "outisderight", "or"}, "Forward (association football)#Outside forward", "outside right"},
	{{"outsideleft", "ol"}, "Forward (association football)#Outside forward", "outside left"},
	-- Utility player
	{{"utilityplayer", "ut"}, "Utility player#Association football", "utility player"},
}

-- Build lookup table from definitions
local positionData = {}
for _, def in ipairs(positionDefs) do
	local names, link, display = def[1], def[2], def[3]
	local data = {link, display}
	for _, name in ipairs(names) do
		positionData[name] = data
	end
end
-- Normalize HTML entities
local function decodeEntities(str)
	str = str:gsub(" ", " ")
	str = mw.text.decode(str)
	return str
end

-- Check for hlist div
local function hasHlistDiv(str)
	-- Case-insensitive search for hlist div
	return str:lower():find('<div class="hlist">', 1, true) ~= nil
end

-- Delink wikilinks
local function delink(str)
	return str:gsub("%[%[(.-)%]%]", function(match)
		local pipePos = match:find("|")
		if pipePos then
			return match:sub(pipePos + 1)
		else
			return match
		end
	end)
end

-- Extract reference placeholders from end of string
local function extractRefs(str)
	local refs = ""
	-- Pattern for MediaWiki strip markers
	local refPattern = "\127'\"`UNIQ.-QINU`\"'\127"
	while true do
		local s, e = str:find(refPattern .. "%s*$")
		if s then
			refs = str:sub(s, e):gsub("%s*$", "") .. refs
			str = str:sub(1, s - 1)
		else
			break
		end
	end
	return mw.text.trim(str), refs
end

-- Normalize for lookup: lowercase, remove dashes/spaces, convert center->centre
local function normalizeForLookup(str)
	local s = str:lower()
	s = s:gsub("[-–—]", "")
	s = s:gsub(" ", "")
	s = s:gsub("center", "centre")
	return s
end

-- Detect original formatting for centre/center and dash/space
local function detectFormatting(original)
	local usesCenter = original:lower():find("center") ~= nil
	local dashChar = nil
	local spaceInsteadOfDash = false
	local noSeparator = false
	
	-- Check for dash variants
	local dashMatch = original:match("[-–—]")
	if dashMatch then
		dashChar = dashMatch
	else
		-- Check if there's a space where a dash might go (e.g., "centre back" vs "centreback")
		local lowerOrig = original:lower()
		local compoundPatterns = {
			{"centre%s+back", "centreback"}, {"center%s+back", "centerback"},
			{"full%s+back", "fullback"}, {"wing%s+back", "wingback"},
			{"half%s+back", "halfback"}, {"centre%s+half", "centrehalf"},
			{"center%s+half", "centerhalf"}, {"wing%s+half", "winghalf"},
			{"centre%s+forward", "centreforward"}, {"center%s+forward", "centerforward"},
		}
		for _, pat in ipairs(compoundPatterns) do
			if lowerOrig:match(pat[1]) then
				spaceInsteadOfDash = true
				break
			elseif lowerOrig:match("^" .. pat[2] .. "$") then
				noSeparator = true
				break
			end
		end
	end
	
	return usesCenter, dashChar, spaceInsteadOfDash, noSeparator
end

-- Apply formatting to display text
local function applyFormatting(display, usesCenter, dashChar, spaceInsteadOfDash, noSeparator)
	-- Handle %C placeholder (centre/center)
	if usesCenter then
		display = display:gsub("%%Ccentre", "center")
	else
		display = display:gsub("%%C", "")
	end
	
	-- Handle %D placeholder (dash)
	if dashChar then
		display = display:gsub("%%D", dashChar)
	elseif spaceInsteadOfDash then
		display = display:gsub("%%D", " ")
	elseif noSeparator then
		display = display:gsub("%%D", "")
	else
		display = display:gsub("%%D", "-")
	end
	
	return display
end

-- Process a single position
local function processPosition(pos, isFirst, trackingNeeded)
	local backup = pos
	
	-- Extract references
	local refs
	pos, refs = extractRefs(pos)
	pos = mw.text.trim(pos)
	
	-- Handle special centre-half wikilink
	local lowerPos = pos:lower()
	if lowerPos == "[[midfielder#centre-half|centre-half]]" or 
	   lowerPos == "[[midfielder#centre-half|center-half]]" then
		pos = "old centre-half"
	end
	
	-- Delink
	pos = delink(pos)
	pos = mw.text.trim(pos)
	
	-- Lookup
	local usesCenter, dashChar, spaceInsteadOfDash, noSeparator = detectFormatting(pos)
	local normalized = normalizeForLookup(pos)
	local data = positionData[normalized]
	
	local result
	if data then
		local link, display = data[1], data[2]
		display = applyFormatting(display, usesCenter, dashChar, spaceInsteadOfDash, noSeparator)
		
		-- Capitalize first position, lowercase others
		if isFirst then
			display = display:sub(1, 1):upper() .. display:sub(2):lower()
		else
			display = display:lower()
		end
		
		result = "[[" .. link .. "|" .. display .. "]]"
	else
		-- No match found, use backup
		result = backup
		trackingNeeded[1] = true
	end
	
	-- Re-add references
	if refs ~= "" then
		result = result .. refs
	end
	
	return result
end

function p.main(frame)
	local args = frame.args
	local input = args[1]
	-- If called from a template, get parent args
	if input == nil or input == "" then
		args = frame:getParent().args
		input = args[1]
	end
	input = input or ""
	
	-- No input = no output
	if input == "" or mw.text.trim(input) == "" then
		return ""
	end
	
	-- Normalize HTML entities
	input = decodeEntities(input)
	
	-- Trim and replace line breaks with /
	input = mw.text.trim(input)
	input = input:gsub("<[bB][rR]%s*/?>", "/")
	
	-- Check for tags
	if input:match("<[^>]+>") then
		if not hasHlistDiv(input) then
			-- Add tracking category
			local category = frame:expandTemplate{
				title = "main other",
				args = {"[[Category:Pages using infobox football biography with tags in position parameter]]"}
			}
			return input .. category
		end
		return input
	end
	
	-- Split by separators (/ or ,)
	local positions = {}
	local refPattern = "\127'\"`UNIQ.-QINU`\"'\127"
	
	-- First, split by separators
	local rawParts = {}
	local lastEnd = 1
	for sepStart, sepEnd in input:gmatch("()%s*[/,]%s*()") do
		table.insert(rawParts, input:sub(lastEnd, sepStart - 1))
		lastEnd = sepEnd
	end
	table.insert(rawParts, input:sub(lastEnd))
	
	-- Now process parts: if a part starts with ref(s), move them to previous position
	for i, part in ipairs(rawParts) do
		local leadingRefs = ""
		local remainder = part
		
		-- Extract leading refs
		while true do
			local refStart, refEnd = remainder:find("^%s*" .. refPattern)
			if refStart then
				leadingRefs = leadingRefs .. remainder:sub(refStart, refEnd)
				remainder = remainder:sub(refEnd + 1)
			else
				break
			end
		end
		
		-- Attach leading refs to previous position
		if leadingRefs ~= "" and #positions > 0 then
			positions[#positions] = positions[#positions] .. leadingRefs
		end
		
		-- Add remainder as new position (if not empty)
		remainder = mw.text.trim(remainder)
		if remainder ~= "" then
			table.insert(positions, remainder)
		end
	end
	
	-- If no positions found, treat whole input as one position
	if #positions == 0 then
		positions = {mw.text.trim(input)}
	end
	
	-- Filter out management/executive positions
	local excludeWords = {'manager', 'coach', 'trainer', 'director', 'president', 'executive', 'chairman', 'ceo'}
	local filteredPositions = {}
	for _, pos in ipairs(positions) do
		local lowerPos = pos:lower()
		local exclude = false
		for _, word in ipairs(excludeWords) do
			if lowerPos:find(word, 1, true) then
				exclude = true
				break
			end
		end
		if not exclude then
			table.insert(filteredPositions, pos)
		end
	end
	positions = filteredPositions
	
	-- If all positions were filtered out, return nothing
	if #positions == 0 then
		return ""
	end
	
	-- Process each position
	local trackingNeeded = {false}
	local results = {}
	for i, pos in ipairs(positions) do
		table.insert(results, processPosition(pos, i == 1, trackingNeeded))
	end
	
	-- Build output
	local output
	if #results == 1 then
		output = results[1]
	else
		-- Use Module:List hlist
		output = mHList.makeList('horizontal', results)
	end
	
	-- Add tracking category if needed
	if trackingNeeded[1] then
		local category = frame:expandTemplate{
			title = "main other",
			args = {"[[Category:Pages using infobox football biography with irregular position]]"}
		}
		output = output .. category
	end
	
	return output
end

return p