Module:Footballer positions
Appearance
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