-- Module to build tables for aggregated match results in sports-- See documentation for details
local p =
-- Function to parse and expand a template with given parameterslocal function expandTemplate(frame, templateName, params) return frame:expandTemplateend
-- Function to check the existence of flagTemplatelocal function templateExists(templateName) local title = mw.title.new('Template:' .. templateName) return title and title.existsend
-- Function to process country codes and variants, dividing parameters by the "+" signlocal function processIcon(iconString) if not iconString or iconString:match("^%s*$") then return nil, nil -- Return nil for both iconCode and variant if the input is empty or only whitespace elseif iconString:find('+') then local parts = mw.text.split(iconString, '+', true) local iconCode = parts[1] local variant = parts[2] return iconCode, variant else return iconString, nil -- Return the input string as iconCode if no "+" is present endend
-- Function to determine the correct ordinal suffix for a given number for the headinglocal function ordinal(n) local last_digit = n % 10 local last_two_digits = n % 100 if last_digit
2 and last_two_digits ~= 12 then return n .. 'nd' elseif last_digit
-- Function to replace wiki links with their display text or link textlocal function replaceLink(match) local pipePos = match:find("|") if pipePos then return match:sub(pipePos + 1, -3) -- Return text after the '|' else return match:sub(3, -3) -- Return text without the brackets endend
-- Function to clean and process the aggregate score for comparisonlocal function cleanScore(score) -- Return an empty string if score is nil or empty to avoid errors if not score or score:match("^%s*$") then return end
-- Replace wiki links score = score:gsub("%[%[.-%]%]", replaceLink)
-- Remove MediaWiki's unique placeholder sequences for references score = score:gsub('\127%\'"`UNIQ.-QINU`"%\'\127', )
-- Remove superscript tags and their contents score = score:gsub('
-- Convert dashes to a standard format score = score:gsub('[–—―‒−]+', '-')
-- Strip all characters except numbers, dashes and parentheses return score:gsub('[^0-9%-]+', )end
-- Function to determine the winner based on scores within parentheses (first) or regular format (second)local function determineWinner(cleanAggregate, team1, team2, boldWinner, colorWinner, aggregate, isFBRStyle, legs, leg1Score, leg2Score, disableAwayGoals, skipAutoWinner, aggFormat) local team1Winner, team2Winner = false, false local score1, score2 local manualBold = false local manualColor = false local isDraw = false
-- Handling for manual bolding if team1 and type(team1)
'string' then manualBold2 = team2:find("") and not (team2:gsub("", ""):match("^%s*$")) team2 = team2:gsub("", "") end
if manualBold1 then team1Winner = true manualBold = true end if manualBold2 then team2Winner = true manualBold = true end
-- Handling for manual coloring of team or aggregate cells if team1 and type(team1)
'string' then manualColor2 = team2:find("") and not (team2:gsub("", ""):match("^%s*$")) team2 = team2:gsub("", "") end if aggregate then if aggFormat
'both' then aggregate = "" .. aggregate .. "" end manualColorDraw = aggFormat
'both' end
if manualColor1 then if not team1Winner then team1Winner = true end manualColor = true end if manualColor2 then if not team2Winner then team2Winner = true end manualColor = true end if manualColorDraw then isDraw = true manualColor = true end
-- Regular winner determination logic if manual bolding or coloring is not conclusive if not team1Winner and not team2Winner and not isDraw and not skipAutoWinner and (boldWinner or colorWinner or isFBRStyle) then local parenthetical = cleanAggregate:match('%((%d+%-+%d+)%)') local outsideParenthetical = cleanAggregate:match('^(%d+%-+%d+)') if parenthetical then -- Prioritize checking score inside parenthetical score1, score2 = parenthetical:match('(%d+)%-+(%d+)') elseif outsideParenthetical then score1, score2 = outsideParenthetical:match('(%d+)%-+(%d+)') end
if score1 and score2 then score1 = tonumber(score1) score2 = tonumber(score2)
if score1 > score2 then team1Winner = true elseif score1 < score2 then team2Winner = true elseif score1
2 and not disableAwayGoals then -- Apply away goals rule local cleanLeg1 = cleanScore(leg1Score):gsub('[]', ) local cleanLeg2 = cleanScore(leg2Score):gsub('[]', ) local _, team2AwayGoals = cleanLeg1:match('(%d+)%-+(%d+)') local team1AwayGoals = cleanLeg2:match('(%d+)%-+(%d+)')
if team1AwayGoals and team2AwayGoals then team1AwayGoals, team2AwayGoals = tonumber(team1AwayGoals), tonumber(team2AwayGoals)
if team1AwayGoals > team2AwayGoals then team1Winner = true elseif team2AwayGoals > team1AwayGoals then team2Winner = true end end end
if (colorWinner or isFBRStyle) and legs
return team1, team2, team1Winner, team2Winner, manualBold, manualColor, isDraw, aggregateend
-- Function to process score bold/italic formattingfunction processScore(s) if not s or s
local scoreFormat = false
-- Check for 5+ apostrophes (both bold and italic) if s:match("+") then scoreFormat = "both" s = s:gsub("+", "") return s, scoreFormat end
-- Check for 3+ apostrophes (bold) if s:match("+") then scoreFormat = "bold" s = s:gsub("+", "") return s, scoreFormat end
-- Check for 2 apostrophes (italic) if s:match("") then scoreFormat = "italic" s = s:gsub("+", "") return s, scoreFormat end
-- If no matches found, return original string and false return s, scoreFormatend
-- Function to check if any parameter in a given row is non-nil and non-emptylocal function anyParameterPresent(startIndex, step, args) -- Check regular parameters for index = startIndex, startIndex + step - 1 do if args[index] and args[index]:match("^%s*(.-)%s*$") ~= "" then return true end end
-- Check aggregate note local rowIndex = math.floor((startIndex - 1) / step) + 1 local aggNote = args['note_agg_' .. rowIndex] if aggNote and aggNote:match("^%s*(.-)%s*$") ~= "" then return true end
-- Check leg notes local numLegs = step - (noFlagIcons and 3 or 5) -- Calculate number of legs for leg = 1, numLegs do local legNote = args['note_leg' .. leg .. '_' .. rowIndex] if legNote and legNote:match("^%s*(.-)%s*$") ~= "" then return true end end
return falseend
-- Function to check whether to reduce font size for upcoming matcheslocal function checkSmallText(str) -- Check for font size or small/big HTML tags if str:match("font%s?%-?size") or str:match("") or str:match("") then return false end
-- Remove MediaWiki's unique placeholder sequences for references str = str:gsub('\127%\'"`UNIQ.-QINU`"%\'\127', )
-- Remove superscript tags and their contents str = str:gsub('
-- Check for walkover-related strings (never shown in small text) if str:lower:match("walkover") or str:lower:match("w%.o%.") or str:lower:match("w/o") then return false end
-- Replace wiki links with their display text or link text str = str:gsub("%[%[.-%]%]", replaceLink)
-- Remove all text inside parentheses str = str:gsub("%b", "")
-- Exit if string contains only en/em dash if str
"–" then return false end
-- Convert dashes to a standard format str = str:gsub('[–—―‒−]+', '-')
-- Remove opening and closing HTML tags str = str:gsub("?%w+[^>]*>", "")
-- Remove apostrophes str = str:gsub("+", "")
-- Remove all whitespace str = str:gsub("%s+", "")
-- Check if the string matches only a scoreline if str:match("^%d+-%d+$") then return false else return true endend
-- Function to format the dashes and winning notes for aggregate/leg score parameters, and divide the score from references/notes/superscriptslocal function format_and_extract_score(s, addSpan) if not s then return , end -- Return empty strings if input is nil
local function format_dash(pattern) s = mw.ustring.gsub(s, '^' .. pattern, '%1–%2') s = mw.ustring.gsub(s, '%(' .. pattern, '(%1–%2') end
-- Format dashes format_dash('%s*([%d%.]+)%s*[–—―‒−%-]%s*([%d%.]+)') format_dash('%s*([%d%.]+)%s*&[MmNn][Dd][Aa][Ss][Hh];%s*([%d%.]+)') format_dash('%s*(%[%[[^%[%]]*%|[%d%.]+)%s*[–—―‒−%-]%s*([%d%.]+)') format_dash('%s*(%^%[%]%s*%s+[%d%.]+)%s*[–—―‒−%-]%s*([%d%.]+)') format_dash('%s*(%[%[[^%[%]]*%|[%d%.]+)%s*&[MmNn][Dd][Aa][Ss][Hh];%s*([%d%.]+)') format_dash('%s*(%^%[%]%s*%s+[%d%.]+)%s*&[MmNn][Dd][Aa][Ss][Hh];%s*([%d%.]+)')
-- Extract end text local supStart = s:find('
-- Function to find the first parenthesis outside of wikilinks local function find_paren_outside_wikilinks(s) local pos = 1 while true do pos = s:find('%(', pos) if not pos then break end local beforeParen = s:sub(1, pos - 1) local openLinks = select(2, beforeParen:gsub('%[%[', '')) - select(2, beforeParen:gsub('%]%]', )) if openLinks local parenStart = find_paren_outside_wikilinks(s) local startPositions = if supStart then table.insert(startPositions, supStart) end if placeholderStart then table.insert(startPositions, placeholderStart) end if parenStart then table.insert(startPositions, parenStart) end local scoreMatch, endText if #startPositions > 0 then local startPos = math.min(unpack(startPositions)) -- Find the last non-whitespace character before startPos local scoreEnd = s:sub(1, startPos - 1):match(".*%S") or "" scoreEnd = #scoreEnd -- Extract the score and endText scoreMatch = s:sub(1, scoreEnd) endText = s:sub(scoreEnd + 1) else -- If no match found, return the entire score scoreMatch = s endText = "" end -- Format winning notes in brackets (only if endText is not empty) if endText ~= "" then if addSpan then endText = mw.ustring.gsub(endText, '(%(%d+%s*–%s*%d+)%s+[Pp]%.?[EeSs]?%.?[NnOo]?%.?%)', ' return scoreMatch, endTextend -- Function to clean team names and generate linkslocal function cleanAndGenerateLinks(team1, team2, score, isSecondLeg) local function cleanTeam(str, defaultName) if str and str ~= "" then str = str:gsub(' team1 = cleanTeam(team1, "Team 1") team2 = cleanTeam(team2, "Team 2") if score and score:match("%S") then local linkScore = score if score:find('%[') then
linkScore = score:match('^([%d%.]+–[%d%.]+)') if not linkScore then return score end end if linkScore then local link if isSecondLeg then link = "" .. linkScore .. "" else link = "" .. linkScore .. "" end return link .. score:sub(#linkScore + 1) end end return scoreend -- Function to process notes for aggregate and leg scoreslocal function processNote(frame, notes, noteKey, noteText, endText, rowIndex, rand_val, noteGroup) if not noteText then return endText, notes end if noteText:match("^%s*
local function createInlineNote(name) return frame:extensionTag end -- Check if noteText is a reference to another note local referenced_note = noteText:match("^(agg_%d+)$") or noteText:match("^(leg%d+_%d+)$") if referenced_note then local referenced_note_id = '"table_note_' .. referenced_note .. '_' .. rand_val .. '"' return endText .. createInlineNote(referenced_note_id), notes end local note_id = '"table_note_' .. noteKey .. '_' .. rowIndex .. '_' .. rand_val .. '"' if not notes[note_id] then notes[note_id] = noteText end return endText .. createInlineNote(note_id), notesend -- Function to generate the footer if necessarylocal function createFooter(frame, notes, noteGroup, isFBRStyle, displayNotes, externalNotes, legs) local needFooter = (isFBRStyle and legs if not needFooter then return -- Return an empty string if no footer is needed end local divContent = mw.html.create('div') :addClass('sports-series-notes') if isFBRStyle and legs if (next(notes) ~= nil and not externalNotes) or displayNotes then divContent:wikitext((isFBRStyle and legs local footer = tostring(divContent) if next(notes) ~= nil or displayNotes then local noteDefinitions = for noteId, noteText in pairs(notes) do if type(noteId) if externalNotes then local hiddenRefs = mw.html.create('span') :addClass('sports-series-hidden') :wikitext(table.concat(noteDefinitions)) if isFBRStyle and legs return footerend -- Main function that processes input and returns the wikitablefunction p.main(frame) local args = require('Module:Arguments').getArgs(frame,) local yesno = require('Module:Yesno') -- Check for section transclusion local tsection = frame:getParent.args['transcludesection'] or frame:getParent.args['section'] or local bsection = args['section'] or if tsection ~= and bsection ~= then if tsection ~= bsection then return -- Return an empty string if sections don't match end end local root = mw.html.create local templatestyles = frame:extensionTag .. frame:extensionTag root:wikitext(templatestyles) local flagYesno = yesno(args.flag) local showFlags = flagYesno ~= false local noFlagIcons = not showFlags local fillBlanks = yesno(args.fill_blanks) local generateLinks = yesno(args.generate_links) local solidCell = yesno(args.solid_cell) or args.solid_cell 'gray' local baselink = frame:getParent:getTitle local currentPageTitle = mw.title.getCurrentTitle.fullText if currentPageTitle true local externalNotes = noteListValue -- Process the font size parameter local fontSize if args.font_size then -- Remove trailing '%' if present and convert to number fontSize = tonumber((args.font_size:gsub('%s*%%$', ))) if fontSize then fontSize = math.max(fontSize, 85) -- Ensure font size is at least 85 end end -- Process flag parameter to determine flag template and variant local flagTemplate = 'fbaicon' local flagSize = args.flag_size if showFlags then if args.flag and args.flag ~= and not flagYesno then flagTemplate = args.flag:gsub('^Template:', ) if not templateExists(flagTemplate) then flagTemplate = 'flag icon' end end if flagSize and not flagSize:match('px$') then flagSize = flagSize .. 'px' end end -- Determine whether line should be displayed local showCountry = args.show_country local function shouldShowRow(team1Icon, team2Icon) if not showCountry or noFlagIcons then return true end return team1Icon showCountry end local legs = 2 if args.legs then if yesno(args.legs) '1' then legs = 0 else legs = tonumber(args.legs) and math.max(tonumber(args.legs), 2) or 2 end end local teamWidth = (tonumber(args['team_width']) and args['team_width'] .. 'px') or '250px' local scoreWidth = (tonumber(args['score_width']) and args['score_width'] .. 'px') or '80px' local boldWinner = args.bold_winner "FBR" local isHA = yesno(args.h_a) or (isFBRStyle and legs false local disableSmallText = yesno(args.small_text) true local disableNoWrap = noWrapValue local tableClass = 'wikitable sports-series' local doCollapsed = yesno(args.collapsed) if doCollapsed then tableClass = tableClass .. ' mw-collapsible mw-collapsed' end if yesno(args.center_table) and not doCollapsed then tableClass = tableClass .. ' center-table' end if fontSize then table:css('font-size', fontSize .. '%') end -- Create the table element local table = root:tag('table') :addClass(tableClass) :cssText(tableStyle) if args.id then table:attr('id', args.id) -- Optional id parameter to allow anchor to table end if noWrap then table:attr('data-nowrap', 'y') elseif not disableNoWrap then table:attr('data-nowrap', 'n') end -- Add a caption to table if the "caption" parameter is passed if args.caption then table:tag('caption'):wikitext(args.caption) end -- Count number of columns local colCount = 3 + legs -- Add a title row above column headings if the "title" parameter is passed if args.title then local titleRow = table:tag('tr'):addClass('title-row') titleRow:tag('th') :attr('colspan', colCount) :attr('scope', 'colgroup') :wikitext(args.title) end -- Create the header row with team and score columns local header = table:tag('tr') local defaultTeam1 = isHA and 'Home team' or 'Team 1' local defaultTeam2 = isHA and 'Away team' or 'Team 2' header:tag('th'):attr('scope', 'col'):css('width', teamWidth):wikitext(args['team1'] or defaultTeam1) header:tag('th'):attr('scope', 'col'):css('width', scoreWidth):wikitext(args['aggregate'] or legs -- Add columns for each leg if applicable if legs > 0 then for leg = 1, legs do local legHeading = args['leg' .. leg] -- Check if "legN" parameter is present if not legHeading then if args.leg_prefix then legHeading = yesno(args.leg_prefix) and ('Leg ' .. leg) or (args.leg_prefix .. ' ' .. leg) elseif args.leg_suffix and not yesno(args.leg_suffix) then legHeading = ordinal(leg) .. ' ' .. args.leg_suffix else legHeading = ordinal(leg) .. ' leg' end end header:tag('th'):attr('scope', 'col'):css('width', scoreWidth):wikitext(legHeading) end end local step = (noFlagIcons and 3 or 5) + legs -- Determine the step size based on the presence of flag icons local i = 1 while anyParameterPresent(i, step, args) do local rowIndex = math.floor((i - 1) / step) + 1 local aggNote = args['note_agg_' .. rowIndex] local headingParam = args['heading' .. rowIndex] local team1, team2, aggregateScore, aggregateEndText, legEndText, team1Icon, team2Icon, team1Variant, team2Variant local team1Winner, team2Winner, manualBold, manualColor, isDraw = false, false, false, false, false local leg1Score, leg2Score = false, false -- Process rows from input team1 = args[i] if noFlagIcons then aggregateScore = args[i+1] team2 = args[i+2] else team1Icon, team1Variant = processIcon(args[i+1]) aggregateScore = args[i+2] team2 = args[i+3] team2Icon, team2Variant = processIcon(args[i+4]) end -- Check if the line should be shown based on both teams if shouldShowRow(team1Icon, team2Icon) then -- Add a heading above a given row in the table if headingParam and not showCountry then local headingRow = table:tag('tr'):addClass('heading-row') headingRow:tag('td') :attr('colspan', colCount) :wikitext('' .. headingParam .. '') end local row = table:tag('tr') -- Name the 1st/2nd leg scores for two-legged ties if legs -- Clean the aggregate score local cleanAggregate = cleanScore(aggregateScore) aggregateScore, aggFormat = processScore(aggregateScore) -- Format anchor links for aggregate score local aggParen = cleanAggregate:match("%(.*%(") local aggSpan = (disableNoWrap or (not noWrap and not disableNoWrap and aggParen)) aggregateScore, aggregateEndText = format_and_extract_score(aggregateScore, aggSpan) aggregateEndText, notes = processNote(frame, notes, 'agg', aggNote, aggregateEndText, rowIndex, rand_val, noteGroup) if generateLinks and legs local skipAutoWinner = legs -- Determine the winning team on aggregate team1, team2, team1Winner, team2Winner, manualBold, manualColor, isDraw, aggregateScore = determineWinner(cleanAggregate, team1, team2, boldWinner, colorWinner, aggregateScore, isFBRStyle, legs, leg1Score, leg2Score, disableAwayGoals, skipAutoWinner, aggFormat) -- Function to create flag template parameters local function getFlagParams(icon, variant) local params = if flagSize then params.size = flagSize end return params end -- Generate text to display for each team local team1Text = noFlagIcons and (team1 or ) or ((team1Icon ~= "" and team1Icon ~= nil) and ((team1 or ) .. ' ' .. expandTemplate(frame, flagTemplate, getFlagParams(team1Icon, team1Variant))) or (team1 or )) local team2Text = noFlagIcons and (team2 or ) or ((team2Icon ~= "" and team2Icon ~= nil) and (expandTemplate(frame, flagTemplate, getFlagParams(team2Icon, team2Variant)) .. ' ' .. (team2 or )) or (team2 or )) -- When set by user, adds blank flag placeholder next to team names if fillBlanks and showFlags then local flagDimensions = flagSize or "25x17px" local placeholderFlag = string.format(' "" then team2Text = placeholderFlag .. ' ' .. team2Text end end local aggregateContent if not disableSmallText and skipAutoWinner then aggregateContent = ' -- Create aggregate score cell with conditional styling local aggregateClass = if isFBRStyle and legs -- Create rows for aggregate score and team names, bolded if set by user row:tag('td'):addClass(team1Winner and (colorWinner or manualColor) and 'winner' or nil):wikitext((team1Winner and (boldWinner or manualBold) and team1Text ~= ) and ('' .. team1Text .. '') or team1Text) row:tag('td'):addClass(aggregateClass ~= and aggregateClass or nil):wikitext(aggregateContent) row:tag('td'):addClass(team2Winner and (colorWinner or manualColor) and 'winner' or nil):wikitext((team2Winner and (boldWinner or manualBold) and team2Text ~= ) and ('' .. team2Text .. '') or team2Text) -- Add columns for each leg score if applicable if legs > 0 then for leg = 1, legs do local legIndex = i + 4 + leg + (noFlagIcons and -2 or 0) local legScore = args[legIndex] local legNote = args['note_leg' .. leg .. '_' .. rowIndex] if legScore ~= "nil" then if legScore if legScore ~= "null" then -- Format anchor links for leg scores local cleanLeg = cleanScore(legScore) local legFormat legScore, legFormat = processScore(legScore) local legParen = cleanLeg:match("%(.*%(") local legSpan = (disableNoWrap or (not noWrap and not disableNoWrap and legParen)) legScore, legEndText = format_and_extract_score(legScore, legSpan) legEndText, notes = processNote(frame, notes, 'leg' .. leg, legNote, legEndText, rowIndex, rand_val, noteGroup) if generateLinks and not aggregateContent:lower:find("bye") then if leg 2 then legScore = cleanAndGenerateLinks(team1, team2, legScore, true) end end if legFormat 'both' then legScore = '' .. legScore .. '' end if legFormat 'both' then legScore = '' .. legScore .. '' end local legContent if not disableSmallText and legScore ~= and checkSmallText(legScore) then legContent = ' i = i + step end -- Generate footer text local footerText = createFooter(frame, notes, noteGroup, isFBRStyle, displayNotes, externalNotes, legs) root:wikitext(footerText) local tableCode = tostring(root) -- Rewrite anchor links for the entire table if baselink ~= then tableCode = mw.ustring.gsub(tableCode, '(%[%[)(#[^%[%]]*%|)', '%1' .. baselink .. '%2') end local escapedTitle = currentPageTitle:gsub("([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1") local titlePattern = '%[%[' .. escapedTitle .. '(#[^%[%]]*%|)' tableCode = mw.ustring.gsub(tableCode, titlePattern, '%1') return tableCodeend return p0 then return pos end pos = pos + 1 end return nil end
0) or displayNotes or (next(notes) ~= nil)
0 then divContent:wikitext("Legend: Blue = home team win; Yellow = draw; Red = away team win.") end
0) and "
Notes:" or "Notes:") end'string' and noteId:match('^"table_note') then table.insert(noteDefinitions, frame:extensionTag) end end
0 then footer = footer .. tostring(hiddenRefs) else footer = tostring(hiddenRefs) end else local reflistArgs = footer = footer .. frame:expandTemplate end end
'grey' or args.solid_cell
baselink then baselink = end local notes = local noteGroup = args.note_group or 'lower-alpha' local noteListValue = yesno(args.note_list) local displayNotes = noteListValue
false math.randomseed(os.clock * 10^8) -- Initialize random number generator local rand_val = math.random
showCountry or team2Icon
false or args.legs
nil or yesno(args.bold_winner, true) local colorWinner = yesno(args.color_winner) local matchesStyle = args.matches_style local isFBRStyle = matchesStyle and matchesStyle:upper
0) local disableAwayGoals = yesno(args.away_goals)
false local noWrapValue = yesno(args.nowrap) local noWrap = noWrapValue
false local aggFormat
0 and 'Score' or 'Agg.
Tooltip Aggregate score') header:tag('th'):attr('scope', 'col'):css('width', teamWidth):wikitext(args['team2'] or defaultTeam2)2 then if noFlagIcons then leg1Score = args[i+3] leg2Score = args[i+4] else leg1Score = args[i+5] leg2Score = args[i+6] end end
0 then aggregateScore = cleanAndGenerateLinks(team1, team2, aggregateScore, false) end
0 and aggregateScore ~= and checkSmallText(aggregateScore)
"" then team1Text = team1Text .. ' ' .. placeholderFlag end if not team2Icon or team2Icon
0 then if team1Winner then aggregateClass = 'fbr-home-win' elseif team2Winner then aggregateClass = 'fbr-away-win' elseif isDraw then aggregateClass = 'draw' end elseif isDraw then aggregateClass = 'draw' end if not disableNoWrap and (not noWrap and aggParen) then aggregateClass = (aggregateClass ~= and aggregateClass .. ' ' or ) .. 'allow-wrap' end
"null" then if solidCell then row:tag('td'):addClass('solid-cell') else legScore = '—' end end
1 then legScore = cleanAndGenerateLinks(team1, team2, legScore, false) elseif leg
'bold' or legFormat
'italic' or legFormat