Jump to content

Module:Footballer positions

Permanently protected module
From Wikipedia, the free encyclopedia
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"},
	{{"centredefender"}, "Defender (association football)#Centre-back", "%Ccentre-back"},
	{{"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", "rightdefender", "rb"}, "Defender (association football)#Full-back", "right%Dback"},
	{{"rightfullback"}, "Defender (association football)#Full-back", "right full%Dback"},
	{{"leftback", "leftdefender", "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", "defendingmidfielder", "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", "attack", "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"},
	{{"supportingstriker"}, "Forward (association football)#Second striker", "supporting striker"},
	{{"deeplyingstriker"}, "Forward (association football)#Second striker", "deep-lying 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", "utility", "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"},
			{"right%s+back", "rightback"}, {"left%s+back", "leftback"},
			{"half%s+back", "halfback"}, {"centre%s+half", "centrehalf"},
			{"center%s+half", "centerhalf"}, {"wing%s+half", "winghalf"},
			{"centre%s+forward", "centreforward"}, {"center%s+forward", "centerforward"},
			{"centre%s+defender", "centerdefender"}, {"center%s+defender", "centerdefender"},
			{"deep%s+lying", "deeplying"}, {"outside%s+back", "outsideback"},
		}
		for _, pat in ipairs(compoundPatterns) do
			if lowerOrig:match(pat[1]) then
				spaceInsteadOfDash = true
				break
			elseif lowerOrig:find(pat[2], 1, true) 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, linkTargetReplacements)
	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
		
		-- Simplify wikilink if link target equals display text (case insensitive)
		if link:lower() == display:lower() then
			result = "[[" .. display .. "]]"
		else
			result = "[[" .. link .. "|" .. display .. "]]"
		end
	else
		-- No match found - check if this came from a link target replacement
		if linkTargetReplacements and linkTargetReplacements[pos] then
			-- Use the original link with separator
			result = linkTargetReplacements[pos]
		else
			-- Use backup
			result = backup
		end
		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
	input = mw.text.trim(input)
	
	-- Break tag pattern
	local brPattern = "<[bB][rR]%s*/?%s*>"
	
	-- Handle and/or adjacent to break tags (convert to just /)
	input = input:gsub("%s+and%s*" .. brPattern, "/")
	input = input:gsub("%s+or%s*" .. brPattern, "/")
	input = input:gsub(brPattern .. "%s*and%s+", "/")
	input = input:gsub(brPattern .. "%s*or%s+", "/")
	
	-- Replace remaining br tags with /
	input = input:gsub(brPattern, "/")
	
	-- 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
	
	-- Replace remaining line breaks with /
	input = input:gsub("\n", "/")
	
	-- Check for separators in wikilink display text and delink if found
	local hadSeparatorInDisplayText = false
	local originalInputBeforeDelink = input
	local separatorPattern = "[/,;+&]"
	local hasWordSeparator = function(text)
		return text:match("%s+and%s+") or text:match("%s+or%s+")
	end
	local linkTargetReplacements = {} -- Track original links that had separators in target
	
	-- Check each wikilink for separators in display text
	for wikilink in input:gmatch("%[%[.-%]%]") do
		local pipePos = wikilink:find("|")
		local textToCheck
		if pipePos then
			-- Has display text - check the part after the pipe
			textToCheck = wikilink:sub(pipePos + 1, -3) -- Extract display text (excluding ]])
			
			-- Also check if separator is in the link target (before pipe)
			local linkTarget = wikilink:sub(3, pipePos - 1)
			if linkTarget:match(separatorPattern) or hasWordSeparator(linkTarget) then
				-- Store the original link and its display text for later restoration if needed
				local displayText = textToCheck
				linkTargetReplacements[displayText] = wikilink
				-- Remove separators from the link target in the input
				local cleanedTarget = linkTarget:gsub(separatorPattern, ""):gsub("%s+and%s+", ""):gsub("%s+or%s+", "")
				input = input:gsub("%[%[" .. linkTarget:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "|" .. displayText:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "%]%]",
				                   "[[" .. cleanedTarget .. "|" .. displayText .. "]]")
			end
		else
			-- No pipe - check the link target itself (excluding [[]])
			textToCheck = wikilink:sub(3, -3)
		end
		if textToCheck:match(separatorPattern) or hasWordSeparator(textToCheck) then
			hadSeparatorInDisplayText = true
		end
	end
	
	-- If separator found in display text, delink the input
	if hadSeparatorInDisplayText then
		input = delink(input)
	end
	
	-- Convert word separators to / before splitting, but only outside of wikilinks
	-- First, temporarily replace wikilinks with placeholders
	local wikilinks = {}
	local placeholderIndex = 0
	input = input:gsub("%[%[.-%]%]", function(wikilink)
		placeholderIndex = placeholderIndex + 1
		local placeholder = "\127WIKILINK" .. placeholderIndex .. "\127"
		wikilinks[placeholder] = wikilink
		return placeholder
	end)
	
	-- Now convert word separators
	input = input:gsub("%s+and%s+", "/")
	input = input:gsub("%s+or%s+", "/")
	
	-- Restore wikilinks
	for placeholder, wikilink in pairs(wikilinks) do
		input = input:gsub(placeholder, wikilink)
	end
	
	-- Split by separators
	local positions = {}
	local refPattern = "\127'\"`UNIQ.-QINU`\"'\127"
	
	-- First, split by separators
	local rawParts = {}
	local lastEnd = 1
	for sepStart, sepEnd in input:gmatch("()%s*" .. separatorPattern .. "%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 = {'retired', 'unknown', '?', 'manager', 'coach', 'trainer', 'director', 'president', 'executive', 'chairman', 'ceo', 'referee'}
	local filteredPositions = {}
	local hasInvalidPosition = false
	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
				hasInvalidPosition = true
				break
			end
		end
		if not exclude then
			table.insert(filteredPositions, pos)
		end
	end
	positions = filteredPositions
	
	-- Add tracking category for invalid positions
	local invalidCategory = ""
	if hasInvalidPosition then
		invalidCategory = frame:expandTemplate{
			title = "main other",
			args = {"[[Category:Pages using infobox football biography with invalid position]]"}
		}
	end
	
	-- If all positions were filtered out, return only categories
	if #positions == 0 then
		return invalidCategory
	end
	
	-- Process each position
	local trackingNeeded = {false}
	local results = {}
	for i, pos in ipairs(positions) do
		table.insert(results, processPosition(pos, i == 1, trackingNeeded, linkTargetReplacements))
	end
	
	-- Special handling: if we delinked due to separator in display text,
	-- and ANY position didn't match, return original input
	if hadSeparatorInDisplayText and trackingNeeded[1] then
		local irregularCategory = frame:expandTemplate{
			title = "main other",
			args = {"[[Category:Pages using infobox football biography with irregular position]]"}
		}
		return originalInputBeforeDelink .. invalidCategory .. irregularCategory
	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
	local irregularCategory = ""
	if trackingNeeded[1] then
		irregularCategory = frame:expandTemplate{
			title = "main other",
			args = {"[[Category:Pages using infobox football biography with irregular position]]"}
		}
	end
	
	return output .. invalidCategory .. irregularCategory
end

return p