Module:Bracket: Difference between revisions
From eSportsAmaze
More actions
Esportsamaze (talk | contribs) No edit summary |
Esportsamaze (talk | contribs) No edit summary |
||
| Line 86: | Line 86: | ||
if label and label ~= "" then | if label and label ~= "" then | ||
wrap:tag('div'):addClass('bk-match-label'):wikitext(label) | wrap:tag('div'):addClass('bk-match-label'):wikitext(label) | ||
end | |||
-- BO badge sits ABOVE the card in normal flow | |||
if d.bo and d.bo ~= "" then | |||
wrap:tag('div'):addClass('bk-bo-badge'):wikitext('Bo'..d.bo) | |||
end | end | ||
| Line 109: | Line 114: | ||
if isPlayed then matchBox:addClass('bk-match-played') end | if isPlayed then matchBox:addClass('bk-match-played') end | ||
matchBox:addClass('bk-match-clickable') -- JS hook | matchBox:addClass('bk-match-clickable') -- JS hook | ||
-- Team 1 | -- Team 1 | ||
Revision as of 00:44, 20 March 2026
Documentation for this module may be created at Module:Bracket/doc
-- ================================================================
-- Module:Bracket v2.0
-- Supports: Single Elimination, Double Elimination, GSL, Round Robin, Swiss
-- Data: Cargo-driven (BracketMatch table) + inline template args
-- Match popup: embeds data-* attrs, JS handles modal
-- ================================================================
local p = {}
local html = mw.html
local cargo = mw.ext.cargo
-- ── Helpers ──────────────────────────────────────────────────────
local function esc(s)
if not s then return "" end
return s:gsub("\\","\\\\"):gsub("'","\\'")
end
local function clean(s)
if not s then return nil end
local c = s:gsub("[\r\n]",""):gsub("^%s*(.-)%s*$","%1")
return c ~= "" and c or nil
end
-- Fetch team logo HTML (light/dark aware, matches your existing pattern)
local teamLogoCache = {}
local function getTeamLogo(teamName, size)
if not teamName or teamName == "" then return nil end
size = size or "20px"
if teamLogoCache[teamName] then
local t = teamLogoCache[teamName]
if not t then return nil end
local light = t.image or ""
local dark = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
if light == "" then return nil end
return "[[File:"..light.."|"..size.."|link=|class=logo-lightmode bk-team-logo]]"
.. "[[File:"..dark .."|"..size.."|link=|class=logo-darkmode bk-team-logo]]"
end
local rows = cargo.query("Teams","image,image_dark",
{where="name='"..esc(teamName).."'", limit=1})
local t = (rows and #rows > 0) and rows[1] or false
teamLogoCache[teamName] = t
if not t then return nil end
local light = t.image or ""
local dark = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
if light == "" then return nil end
return "[[File:"..light.."|"..size.."|link=|class=logo-lightmode bk-team-logo]]"
.. "[[File:"..dark .."|"..size.."|link=|class=logo-darkmode bk-team-logo]]"
end
-- Fetch match data from Cargo BracketMatch table
-- Returns: { team1, score1, team2, score2, winner, bo, date, casters, vod }
local function fetchMatch(event, matchID)
local rows = cargo.query("BracketMatch",
"team1,score1,team2,score2,winner,bo,match_date,casters,vod,notes",
{ where = "event='"..esc(event).."' AND match_id='"..esc(matchID).."'",
limit = 1 })
if rows and #rows > 0 then return rows[1] end
return {}
end
-- Merge cargo data with inline template args (inline wins if set)
local function mergeMatchData(cargoData, args, prefix)
local d = {}
d.team1 = clean(args[prefix.."team1"]) or clean(cargoData.team1) or "TBD"
d.team2 = clean(args[prefix.."team2"]) or clean(cargoData.team2) or "TBD"
d.score1 = clean(args[prefix.."score1"]) or clean(cargoData.score1) or ""
d.score2 = clean(args[prefix.."score2"]) or clean(cargoData.score2) or ""
d.winner = clean(args[prefix.."win"]) or clean(cargoData.winner) or ""
d.bo = clean(args[prefix.."bo"]) or clean(cargoData.bo) or ""
d.date = clean(args[prefix.."date"]) or clean(cargoData.match_date) or ""
d.casters = clean(args[prefix.."casters"]) or clean(cargoData.casters) or ""
d.vod = clean(args[prefix.."vod"]) or clean(cargoData.vod) or ""
d.notes = clean(args[prefix.."notes"]) or clean(cargoData.notes) or ""
return d
end
-- ── Match Card Renderer ───────────────────────────────────────────
-- Used by ALL bracket types. Renders one match box.
-- winner: '1' or '2' (or team name matching team1/team2)
-- matchID: used for popup data attribute
-- label: optional label above card (e.g. "Upper Final")
local function renderMatchCard(container, d, matchID, label, extraClass)
local wrap = container:tag('div'):addClass('bk-match-wrap')
if label and label ~= "" then
wrap:tag('div'):addClass('bk-match-label'):wikitext(label)
end
-- BO badge sits ABOVE the card in normal flow
if d.bo and d.bo ~= "" then
wrap:tag('div'):addClass('bk-bo-badge'):wikitext('Bo'..d.bo)
end
local matchBox = wrap:tag('div'):addClass('bk-match')
if extraClass and extraClass ~= "" then matchBox:addClass(extraClass) end
-- Determine winner
local win1 = (d.winner == "1" or d.winner == d.team1) and d.team1 ~= "TBD"
local win2 = (d.winner == "2" or d.winner == d.team2) and d.team2 ~= "TBD"
local isPlayed = (d.score1 ~= "" or d.score2 ~= "") or (d.winner ~= "")
-- Data attrs for popup (JS reads these)
matchBox:attr('data-match-id', matchID)
matchBox:attr('data-team1', d.team1)
matchBox:attr('data-team2', d.team2)
matchBox:attr('data-score1', d.score1)
matchBox:attr('data-score2', d.score2)
matchBox:attr('data-bo', d.bo)
matchBox:attr('data-date', d.date)
matchBox:attr('data-casters', d.casters)
matchBox:attr('data-vod', d.vod)
matchBox:attr('data-notes', d.notes)
if isPlayed then matchBox:addClass('bk-match-played') end
matchBox:addClass('bk-match-clickable') -- JS hook
-- Team 1
local row1 = matchBox:tag('div'):addClass('bk-team')
if win1 then row1:addClass('bk-win') end
if not win1 and win2 then row1:addClass('bk-lose') end
local logo1 = getTeamLogo(d.team1, "18px")
local nameCell1 = row1:tag('div'):addClass('bk-team-name')
if logo1 then nameCell1:wikitext(logo1) end
if d.team1 ~= "TBD" then
nameCell1:tag('span'):wikitext("[["..d.team1.."]]")
else
nameCell1:tag('span'):addClass('bk-tbd'):wikitext("TBD")
end
if win1 then row1:tag('div'):addClass('bk-win-icon'):wikitext('✓') end
local scoreBox1 = row1:tag('div'):addClass('bk-score')
if d.score1 ~= "" then scoreBox1:wikitext(d.score1) end
-- Divider
matchBox:tag('div'):addClass('bk-divider')
-- Team 2
local row2 = matchBox:tag('div'):addClass('bk-team')
if win2 then row2:addClass('bk-win') end
if not win2 and win1 then row2:addClass('bk-lose') end
local logo2 = getTeamLogo(d.team2, "18px")
local nameCell2 = row2:tag('div'):addClass('bk-team-name')
if logo2 then nameCell2:wikitext(logo2) end
if d.team2 ~= "TBD" then
nameCell2:tag('span'):wikitext("[["..d.team2.."]]")
else
nameCell2:tag('span'):addClass('bk-tbd'):wikitext("TBD")
end
if win2 then row2:tag('div'):addClass('bk-win-icon'):wikitext('✓') end
local scoreBox2 = row2:tag('div'):addClass('bk-score')
if d.score2 ~= "" then scoreBox2:wikitext(d.score2) end
return matchBox
end
-- ════════════════════════════════════════════════════════════════
-- FORMAT 1: SINGLE ELIMINATION
-- ════════════════════════════════════════════════════════════════
local function renderSingleElim(args, event, teamCount)
-- teamCount must be power of 2: 4, 8, 16, 32
local rounds = math.floor(math.log(teamCount) / math.log(2))
if 2^rounds ~= teamCount then
return "<div class='bk-error'>Single elimination requires power-of-2 team count (4, 8, 16, 32). Got: "..teamCount.."</div>"
end
local roundNames = {
[1] = args.r1name or "Round 1",
}
-- Auto-name rounds by position from the end
local suffixNames = { "Grand Final", "Semifinal", "Quarterfinal", "Round of 16", "Round of 32" }
for i = rounds, 1, -1 do
local pos = rounds - i + 1 -- 1 = Grand Final
roundNames[i] = args["r"..i.."name"] or suffixNames[pos] or ("Round "..i)
end
local scroll = html.create('div'):addClass('bk-scroll')
local wrapper = scroll:tag('div'):addClass('bk-wrapper bk-single-elim')
for r = 1, rounds do
local col = wrapper:tag('div'):addClass('bk-col bk-col-'..r)
col:tag('div'):addClass('bk-col-header'):wikitext(roundNames[r])
local matchCount = teamCount / (2^r)
for m = 1, matchCount do
local matchID = "R"..r.."M"..m
local cd = fetchMatch(event, matchID)
local d = mergeMatchData(cd, args, matchID)
local spacer = col:tag('div'):addClass('bk-spacer')
renderMatchCard(spacer, d, matchID, nil, "has-connector")
end
end
return tostring(scroll)
end
-- ════════════════════════════════════════════════════════════════
-- FORMAT 2: DOUBLE ELIMINATION
-- Standard: Upper Bracket + Lower Bracket + Grand Final
-- ════════════════════════════════════════════════════════════════
local function renderDoubleElim(args, event, teamCount)
-- Supports 4, 8, 16 teams
local ubRounds -- Upper bracket rounds before GF
local lbRounds -- Lower bracket rounds
if teamCount == 4 then
ubRounds = 2; lbRounds = 2
elseif teamCount == 8 then
ubRounds = 3; lbRounds = 4
elseif teamCount == 16 then
ubRounds = 4; lbRounds = 6
else
return "<div class='bk-error'>Double elimination supports 4, 8, or 16 teams. Got: "..teamCount.."</div>"
end
local scroll = html.create('div'):addClass('bk-scroll')
local wrapper = scroll:tag('div'):addClass('bk-wrapper bk-double-elim')
-- ── Upper Bracket ──
local ubSection = wrapper:tag('div'):addClass('bk-section')
ubSection:tag('div'):addClass('bk-section-title'):wikitext('Upper Bracket')
local ubRow = ubSection:tag('div'):addClass('bk-row')
local ubRoundNames = {}
local ubSuffix = {"Upper Final","Upper Semifinal","Upper Quarterfinal","Upper Round 1"}
for r = ubRounds, 1, -1 do
local pos = ubRounds - r + 1
ubRoundNames[r] = args["ubr"..r.."name"] or ubSuffix[pos] or ("UB Round "..r)
end
for r = 1, ubRounds do
local col = ubRow:tag('div'):addClass('bk-col bk-col-'..r)
col:tag('div'):addClass('bk-col-header'):wikitext(ubRoundNames[r])
local matchCount = teamCount / (2^r)
for m = 1, matchCount do
local matchID = "UBR"..r.."M"..m
local cd = fetchMatch(event, matchID)
local d = mergeMatchData(cd, args, matchID)
local sp = col:tag('div'):addClass('bk-spacer')
renderMatchCard(sp, d, matchID, nil, r < ubRounds and "has-connector" or "")
end
end
-- ── Lower Bracket ──
local lbSection = wrapper:tag('div'):addClass('bk-section bk-lower-section')
lbSection:tag('div'):addClass('bk-section-title bk-lower-title'):wikitext('Lower Bracket')
local lbRow = lbSection:tag('div'):addClass('bk-row')
local lbRoundNames = {}
local lbSuffix = {"Lower Final","Lower Semifinal","Lower Round 4","Lower Round 3","Lower Round 2","Lower Round 1"}
for r = lbRounds, 1, -1 do
local pos = lbRounds - r + 1
lbRoundNames[r] = args["lbr"..r.."name"] or lbSuffix[pos] or ("LB Round "..r)
end
for r = 1, lbRounds do
local col = lbRow:tag('div'):addClass('bk-col bk-col-lb-'..r)
col:tag('div'):addClass('bk-col-header bk-lb-header'):wikitext(lbRoundNames[r])
-- LB match count: alternates between dropping & non-dropping rounds
local matchCount
if r == 1 then
matchCount = teamCount / 4
elseif r % 2 == 1 then
matchCount = math.max(1, teamCount / (2^(math.ceil(r/2)+1)))
else
matchCount = math.max(1, teamCount / (2^(math.ceil(r/2)+1)))
end
matchCount = math.max(1, matchCount)
for m = 1, matchCount do
local matchID = "LBR"..r.."M"..m
local cd = fetchMatch(event, matchID)
local d = mergeMatchData(cd, args, matchID)
local sp = col:tag('div'):addClass('bk-spacer')
renderMatchCard(sp, d, matchID, nil, r < lbRounds and "has-connector" or "")
end
end
-- ── Grand Final ──
local gfSection = wrapper:tag('div'):addClass('bk-section bk-gf-section')
gfSection:tag('div'):addClass('bk-section-title bk-gf-title'):wikitext('Grand Final')
local gfRow = gfSection:tag('div'):addClass('bk-row bk-gf-row')
-- Game 1 (UB winner vs LB winner)
local gf1 = fetchMatch(event, "GF1")
local gf1d = mergeMatchData(gf1, args, "GF1")
local gf1col = gfRow:tag('div'):addClass('bk-col bk-col-gf')
gf1col:tag('div'):addClass('bk-col-header'):wikitext(args.gf1name or "Grand Final")
local sp1 = gf1col:tag('div'):addClass('bk-spacer')
renderMatchCard(sp1, gf1d, "GF1", nil, "")
-- Optional bracket reset
local gf2 = fetchMatch(event, "GF2")
local gf2d = mergeMatchData(gf2, args, "GF2")
if gf2d.team1 ~= "TBD" or gf2d.team2 ~= "TBD" or clean(args.GF2team1) or clean(args.GF2team2) then
local gf2col = gfRow:tag('div'):addClass('bk-col bk-col-gf')
gf2col:tag('div'):addClass('bk-col-header'):wikitext(args.gf2name or "Bracket Reset")
local sp2 = gf2col:tag('div'):addClass('bk-spacer')
renderMatchCard(sp2, gf2d, "GF2", nil, "")
end
return tostring(scroll)
end
-- ════════════════════════════════════════════════════════════════
-- FORMAT 3: GSL / 4-TEAM DOUBLE ELIM
-- Qualifier 1: #1 vs #2 → winner → Final, loser → Qualifier 2
-- Eliminator: #3 vs #4 → winner → Qualifier 2, loser → OUT
-- Qualifier 2: loser(Q1) vs winner(Elim) → winner → Final, loser → OUT
-- Final: winner(Q1) vs winner(Q2) → WINNER
-- ════════════════════════════════════════════════════════════════
local function renderGSL(args, event)
local scroll = html.create('div'):addClass('bk-scroll')
local wrapper = scroll:tag('div'):addClass('bk-wrapper bk-gsl')
-- ── Column 1: Qualifier 1 + Eliminator (run simultaneously) ──
local col1 = wrapper:tag('div'):addClass('bk-col bk-gsl-col1')
col1:tag('div'):addClass('bk-col-header'):wikitext(args.q1name or 'Qualifier 1')
local q1 = fetchMatch(event, "Q1")
local q1d = mergeMatchData(q1, args, "Q1")
local q1sp = col1:tag('div'):addClass('bk-spacer bk-gsl-top')
renderMatchCard(q1sp, q1d, "Q1", nil, "has-connector")
-- Gap between Q1 and Elim
col1:tag('div'):addClass('bk-gsl-gap')
col1:tag('div'):addClass('bk-col-header bk-col-header-2'):wikitext(args.elimname or 'Eliminator')
local elim = fetchMatch(event, "ELIM")
local elimd = mergeMatchData(elim, args, "ELIM")
local elimsp = col1:tag('div'):addClass('bk-spacer bk-gsl-bot')
renderMatchCard(elimsp, elimd, "ELIM", nil, "has-connector")
-- ── Column 2: Qualifier 2 ──
local col2 = wrapper:tag('div'):addClass('bk-col bk-gsl-col2')
col2:tag('div'):addClass('bk-col-header'):wikitext(args.q2name or 'Qualifier 2')
local q2 = fetchMatch(event, "Q2")
local q2d = mergeMatchData(q2, args, "Q2")
local q2sp = col2:tag('div'):addClass('bk-spacer bk-gsl-mid')
renderMatchCard(q2sp, q2d, "Q2", nil, "has-connector")
-- ── Column 3: Final ──
local col3 = wrapper:tag('div'):addClass('bk-col bk-gsl-col3')
col3:tag('div'):addClass('bk-col-header bk-gf-title'):wikitext(args.finalname or 'Grand Final')
local final = fetchMatch(event, "FINAL")
local finald = mergeMatchData(final, args, "FINAL")
local finalsp = col3:tag('div'):addClass('bk-spacer bk-gsl-final')
renderMatchCard(finalsp, finald, "FINAL", nil, "bk-final-match")
-- ── Fate Labels (under the bracket) ──
local legend = scroll:tag('div'):addClass('bk-gsl-legend')
legend:tag('span'):addClass('bk-legend-item bk-legend-adv'):wikitext('✓ Advances to next round')
legend:tag('span'):addClass('bk-legend-item bk-legend-elim'):wikitext('✗ Eliminated')
return tostring(scroll)
end
-- ════════════════════════════════════════════════════════════════
-- FORMAT 4: ROUND ROBIN
-- ════════════════════════════════════════════════════════════════
local function renderRoundRobin(args, event, teamCount)
-- Build team list
local teams = {}
for i = 1, teamCount do
local t = clean(args["team"..i])
if t then table.insert(teams, t) end
end
-- If fewer teams passed as args, try to figure out from BracketMatch
if #teams == 0 then
return "<div class='bk-error'>Round Robin: please pass team1=, team2=, ... up to team"..teamCount.."= arguments.</div>"
end
local n = #teams
local scroll = html.create('div'):addClass('bk-scroll')
local wrapper = scroll:tag('div'):addClass('bk-wrapper bk-rr')
-- ── Results Matrix Table ──
local section = wrapper:tag('div'):addClass('bk-rr-section')
section:tag('div'):addClass('bk-section-title'):wikitext('Match Results')
local tbl = section:tag('table'):addClass('bk-rr-table')
-- Header row
local hdr = tbl:tag('tr')
hdr:tag('th'):addClass('bk-rr-corner'):wikitext('')
for _, t in ipairs(teams) do
local logo = getTeamLogo(t, "16px")
local th = hdr:tag('th'):addClass('bk-rr-th')
if logo then th:wikitext(logo) end
th:tag('span'):wikitext(t)
end
-- Data rows
for i, t1 in ipairs(teams) do
local tr = tbl:tag('tr')
-- Row header
local rowHdr = tr:tag('th'):addClass('bk-rr-row-hdr')
local logo1 = getTeamLogo(t1, "16px")
if logo1 then rowHdr:wikitext(logo1) end
rowHdr:tag('span'):wikitext(t1)
for j, t2 in ipairs(teams) do
if i == j then
tr:tag('td'):addClass('bk-rr-self'):wikitext('—')
else
-- Match ID: RR_i_j (always smaller index first for canonical ID)
local matchID
if i < j then
matchID = "RR"..i.."v"..j
else
matchID = "RR"..j.."v"..i
end
local cd = fetchMatch(event, matchID)
local d = mergeMatchData(cd, args, matchID)
local score
if i < j then
score = d.score1
else
score = d.score2
end
local isWin = (i < j and d.winner == "1") or (i > j and d.winner == "2")
or (i < j and d.winner == t1) or (i > j and d.winner == t1)
local isLose = (i < j and d.winner == "2") or (i > j and d.winner == "1")
or (i < j and d.winner == t2) or (i > j and d.winner == t2)
local td = tr:tag('td'):addClass('bk-rr-cell bk-match-clickable')
:attr('data-match-id', matchID)
:attr('data-team1', d.team1)
:attr('data-team2', d.team2)
:attr('data-score1', d.score1)
:attr('data-score2', d.score2)
:attr('data-bo', d.bo)
:attr('data-date', d.date)
:attr('data-casters', d.casters)
:attr('data-vod', d.vod)
if isWin then td:addClass('bk-rr-win')
elseif isLose then td:addClass('bk-rr-lose')
end
if score ~= "" then td:wikitext(score)
elseif isWin then td:wikitext('W')
elseif isLose then td:wikitext('L')
else td:addClass('bk-rr-pending'):wikitext('·')
end
end
end
end
-- ── Standings Table ──
local standSection = wrapper:tag('div'):addClass('bk-rr-standings')
standSection:tag('div'):addClass('bk-section-title'):wikitext('Standings')
-- Calculate W/L/D from cargo data
local wld = {}
for _, t in ipairs(teams) do
wld[t] = { w = 0, l = 0, d = 0, pts = 0 }
end
for i = 1, n-1 do
for j = i+1, n do
local matchID = "RR"..i.."v"..j
local cd = fetchMatch(event, matchID)
local d = mergeMatchData(cd, args, matchID)
if d.winner ~= "" then
local winner = d.winner
local t1n = teams[i]; local t2n = teams[j]
local actualWinner
if winner == "1" or winner == t1n then actualWinner = t1n
elseif winner == "2" or winner == t2n then actualWinner = t2n
end
if actualWinner then
local loser = actualWinner == t1n and t2n or t1n
if wld[actualWinner] then wld[actualWinner].w = wld[actualWinner].w + 1; wld[actualWinner].pts = wld[actualWinner].pts + 3 end
if wld[loser] then wld[loser].l = wld[loser].l + 1 end
end
end
end
end
-- Sort by points desc
local sorted = {}
for _, t in ipairs(teams) do table.insert(sorted, t) end
table.sort(sorted, function(a,b)
local wa = wld[a] or {pts=0}; local wb = wld[b] or {pts=0}
if wa.pts ~= wb.pts then return wa.pts > wb.pts end
return (wa.w or 0) > (wb.w or 0)
end)
local stbl = standSection:tag('table'):addClass('bk-rr-stand-table')
local shdr = stbl:tag('tr')
shdr:tag('th'):wikitext('#')
shdr:tag('th'):css('text-align','left'):wikitext('Team')
shdr:tag('th'):wikitext('W')
shdr:tag('th'):wikitext('L')
shdr:tag('th'):wikitext('Pts')
for pos, t in ipairs(sorted) do
local wl = wld[t] or {w=0,l=0,pts=0}
local tr = stbl:tag('tr'):addClass('bk-rr-stand-row')
tr:tag('td'):addClass('bk-rr-rank'):wikitext(tostring(pos))
local nameTd = tr:tag('td'):addClass('bk-rr-stand-name')
local logo = getTeamLogo(t, "18px")
if logo then nameTd:wikitext(logo) end
nameTd:tag('span'):wikitext("[["..t.."]]")
tr:tag('td'):addClass('bk-rr-w'):wikitext(tostring(wl.w))
tr:tag('td'):addClass('bk-rr-l'):wikitext(tostring(wl.l))
tr:tag('td'):addClass('bk-rr-pts'):wikitext(tostring(wl.pts))
end
return tostring(scroll)
end
-- ════════════════════════════════════════════════════════════════
-- FORMAT 5: SWISS
-- ════════════════════════════════════════════════════════════════
local function renderSwiss(args, event, teamCount, swissRounds)
swissRounds = swissRounds or tonumber(args.rounds) or 5
local scroll = html.create('div'):addClass('bk-scroll')
local wrapper = scroll:tag('div'):addClass('bk-wrapper bk-swiss')
for r = 1, swissRounds do
local roundSection = wrapper:tag('div'):addClass('bk-swiss-round')
roundSection:tag('div'):addClass('bk-section-title'):wikitext(
args["r"..r.."name"] or ("Round "..r))
local matchCount = math.floor(teamCount / 2)
local matchRow = roundSection:tag('div'):addClass('bk-swiss-matches')
for m = 1, matchCount do
local matchID = "SR"..r.."M"..m
local cd = fetchMatch(event, matchID)
local d = mergeMatchData(cd, args, matchID)
renderMatchCard(matchRow, d, matchID, nil, "")
end
end
return tostring(scroll)
end
-- ════════════════════════════════════════════════════════════════
-- MAIN ENTRY POINT
-- ════════════════════════════════════════════════════════════════
function p.main(frame)
local args = (frame:getParent() and frame:getParent().args) or frame.args
local format = clean(args.format) or "single"
local event = clean(args.event) or ""
local teamCount = tonumber(args.teams) or 8
-- Wrap everything in a root container with the modal placeholder
local root = html.create('div'):addClass('bk-root')
root:attr('data-event', event)
-- Format selector tabs (if multiple formats on same page via JS tabs — optional)
local bracketHTML
format = format:lower()
if format == "single" or format == "se" then
bracketHTML = renderSingleElim(args, event, teamCount)
elseif format == "double" or format == "de" then
bracketHTML = renderDoubleElim(args, event, teamCount)
elseif format == "gsl" or format == "4team" then
bracketHTML = renderGSL(args, event)
elseif format == "rr" or format == "roundrobin" then
bracketHTML = renderRoundRobin(args, event, teamCount)
elseif format == "swiss" then
bracketHTML = renderSwiss(args, event, teamCount)
else
bracketHTML = "<div class='bk-error'>Unknown bracket format: <b>"..format.."</b>. "
.."Valid: single, double, gsl, rr, swiss</div>"
end
root:wikitext(bracketHTML)
-- Modal built with mw.html (avoids MediaWiki sanitizer stripping raw HTML strings)
local overlay = root:tag('div')
:addClass('bk-modal-overlay')
:css('display','none')
local modal = overlay:tag('div'):addClass('bk-modal')
-- Use span instead of button (<button> is stripped by MediaWiki sanitizer)
modal:tag('span')
:addClass('bk-modal-close')
:attr('title','Close')
:attr('role','button')
:attr('tabindex','0')
:wikitext('✕')
local mheader = modal:tag('div'):addClass('bk-modal-header')
local mteams = mheader:tag('div'):addClass('bk-modal-teams')
mteams:tag('span'):addClass('bk-modal-t1')
mteams:tag('span'):addClass('bk-modal-vs'):wikitext('vs')
mteams:tag('span'):addClass('bk-modal-t2')
local mscore = mheader:tag('div'):addClass('bk-modal-score')
mscore:tag('span'):addClass('bk-modal-s1')
mscore:tag('span'):addClass('bk-modal-dash'):wikitext('–')
mscore:tag('span'):addClass('bk-modal-s2')
local mmeta = modal:tag('div'):addClass('bk-modal-meta')
mmeta:tag('div'):addClass('bk-modal-row bk-modal-bo')
mmeta:tag('div'):addClass('bk-modal-row bk-modal-date')
mmeta:tag('div'):addClass('bk-modal-row bk-modal-casters')
mmeta:tag('div'):addClass('bk-modal-row bk-modal-notes')
mmeta:tag('div'):addClass('bk-modal-row bk-modal-vod')
return tostring(root)
end
-- ── Convenience wrappers ──────────────────────────────────────────
function p.single(frame)
local args = (frame:getParent() and frame:getParent().args) or frame.args
args.format = "single"
return p.main(frame)
end
function p.double(frame)
local args = (frame:getParent() and frame:getParent().args) or frame.args
args.format = "double"
return p.main(frame)
end
function p.gsl(frame)
local args = (frame:getParent() and frame:getParent().args) or frame.args
args.format = "gsl"
return p.main(frame)
end
function p.rr(frame)
local args = (frame:getParent() and frame:getParent().args) or frame.args
args.format = "rr"
return p.main(frame)
end
function p.swiss(frame)
local args = (frame:getParent() and frame:getParent().args) or frame.args
args.format = "swiss"
return p.main(frame)
end
return p