Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Bracket: Difference between revisions

From eSportsAmaze
No edit summary
No edit summary
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- ================================================================
-- ================================================================
-- Module:Bracket v2.0
-- Module:Bracket v3.0 (Smart Nodes, Game Links, Info Popups)
-- 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 p     = {}
local html = mw.html
local html = mw.html
local cargo = mw.ext.cargo
local cargo = mw.ext.cargo


-- ── Helpers ──────────────────────────────────────────────────────
local function esc(s) return s and s:gsub("\\", "\\\\"):gsub("'", "\\'") or "" end
 
local function esc(s)
    if not s then return "" end
    return s:gsub("\\","\\\\"):gsub("'","\\'")
end
 
local function clean(s)
local function clean(s)
     if not s then return nil end
     if not s then return nil end
Line 23: Line 13:
end
end


-- Fetch team logo HTML (light/dark aware, matches your existing pattern)
-- Smarter Date Formatter (Handles YYYY-MM-DD and YYYY-MM-DD HH:MM)
local function formatDate(d)
    if not d or d == "" then return "" end
    local year, month, day, hour, min = string.match(d, "(%d%d%d%d)%-(%d%d)%-(%d%d)[T%s]*(%d*)%:*(%d*)")
    if year then
        local months = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}
        local dateStr = tonumber(day) .. "-" .. months[tonumber(month)]
        if hour and hour ~= "" then
            dateStr = dateStr .. " " .. hour .. ":" .. (min ~= "" and min or "00")
        end
        return dateStr
    end
    return d
end
 
local teamLogoCache = {}
local teamLogoCache = {}
local function getTeamLogo(teamName, size)
local function getTeamLogo(teamName)
     if not teamName or teamName == "" then return nil end
     if not teamName or teamName == "" or teamName == "TBD" then return "" end
    size = size or "20px"
     if teamLogoCache[teamName] then return teamLogoCache[teamName] end
     if teamLogoCache[teamName] then
   
        local t = teamLogoCache[teamName]
    local rows = cargo.query("Teams","image,image_dark",{where="name='"..esc(teamName).."'", limit=1})
        if not t then return nil end
    local t = (rows and #rows > 0) and rows[1] or false
        local light = t.image or ""
    if not t then  
        local dark  = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
         teamLogoCache[teamName] = ""
        if light == "" then return nil end
        return ""  
         return "[[File:"..light.."|"..size.."|link=|class=logo-lightmode bk-team-logo]]"
            .. "[[File:"..dark .."|"..size.."|link=|class=logo-darkmode bk-team-logo]]"
     end
     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 light = t.image or ""
     local dark = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
     local dark = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
     if light == "" then return nil end
     if light == "" then  
     return "[[File:"..light.."|"..size.."|link=|class=logo-lightmode bk-team-logo]]"
        teamLogoCache[teamName] = ""
         .. "[[File:"..dark .."|"..size.."|link=|class=logo-darkmode bk-team-logo]]"
        return ""
    end
      
    local htmlStr = "[[File:"..light.."|18px|link=|class=logo-lightmode bk-team-logo]]"
         .. "[[File:"..dark .."|18px|link=|class=logo-darkmode bk-team-logo]]"
    teamLogoCache[teamName] = htmlStr
    return htmlStr
end
end


-- Fetch match data from Cargo BracketMatch table
-- Returns: { team1, score1, team2, score2, winner, bo, date, casters, vod }
local function fetchMatch(event, matchID)
local function fetchMatch(event, matchID)
    if not event or event == "" then return {} end
     local rows = cargo.query("BracketMatch",
     local rows = cargo.query("BracketMatch",
         "team1,score1,team2,score2,winner,bo,match_date,casters,vod,notes,short1,short2",
         "team1,score1,team2,score2,winner,bo,match_date,casters,vod,notes,short1,short2",
         { where = "event='"..esc(event).."' AND match_id='"..esc(matchID).."'",
         { where = "event='"..esc(event).."' AND match_id='"..esc(matchID).."'", limit = 1 })
          limit = 1 })
     if rows and #rows > 0 then return rows[1] end
     if rows and #rows > 0 then return rows[1] end
     return {}
     return {}
end
end


-- Merge cargo data with inline template args (inline wins if set)
local function mergeMatchData(cargoData, args, prefix)
local function mergeMatchData(cargoData, args, prefix)
     local d = {}
     local d = {}
    d.label  = clean(args[prefix.."label"])
     d.team1  = clean(args[prefix.."team1"])  or clean(cargoData.team1)  or "TBD"
     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.team2  = clean(args[prefix.."team2"])  or clean(cargoData.team2)  or "TBD"
    -- short names: used for display in bracket card; falls back to full team name
     d.short1  = clean(args[prefix.."short1"])  or clean(cargoData.short1)  or d.team1
     d.short1  = clean(args[prefix.."short1"])  or clean(cargoData.short1)  or d.team1
     d.short2  = clean(args[prefix.."short2"])  or clean(cargoData.short2)  or d.team2
     d.short2  = clean(args[prefix.."short2"])  or clean(cargoData.short2)  or d.team2
Line 79: Line 80:
end
end


-- ── Match Card Renderer ───────────────────────────────────────────
-- ── NEW MODERN MATCH CARD ──
-- Used by ALL bracket types. Renders one match box.
local function renderMatchCard(d, matchID, defaultLabel, winTo, loseTo, target, isDrop, extraClass, game)
-- winner: '1' or '2' (or team name matching team1/team2)
    local box = html.create('div'):addClass('bk-match')
-- matchID: used for popup data attribute
        :attr('id', 'match-' .. matchID)
-- label: optional label above card (e.g. "Upper Final")
   
    if winTo and winTo ~= "" then box:attr('data-target-win', 'match-' .. winTo) end
    if loseTo and loseTo ~= "" then box:attr('data-target-lose', 'match-' .. loseTo) end
    if target and target ~= "" then box:attr('data-target', 'match-' .. target) end
    if isDrop then box:attr('data-drop', 'true') end
    if extraClass then box:addClass(extraClass) end


local function renderMatchCard(container, d, matchID, label, extraClass)
    local win1 = (d.winner == "1" or d.winner == d.team1) and d.team1 ~= "TBD"
     local wrap = container:tag('div'):addClass('bk-match-wrap')
     local win2 = (d.winner == "2" or d.winner == d.team2) and d.team2 ~= "TBD"


     -- Header row: match label (left) + BO badge (right) on same line
     -- Top Bar (Entire bar is now the clickable trigger)
     local headerRow = wrap:tag('div'):addClass('bk-header-row')
     local top = box:tag('div'):addClass('bk-match-top')
     if label and label ~= "" then
      :attr('data-matchid', matchID) -- Ready for your future Match Page logic
        headerRow:tag('span'):addClass('bk-match-label-inline'):wikitext(label)
      :attr('data-team1', d.team1):attr('data-team2', d.team2)
     end
      :attr('data-score1', d.score1):attr('data-score2', d.score2)
     if d.bo and d.bo ~= "" then
      :attr('data-bo', d.bo):attr('data-date', d.date)
        headerRow:tag('span'):addClass('bk-bo-badge'):wikitext('Bo'..d.bo)
      :attr('data-casters', d.casters):attr('data-vod', d.vod):attr('data-notes', d.notes)
    end
      :attr('title', 'Click for Match Details')
   
     -- The Label
    top:tag('span'):addClass('bk-match-label')
      :wikitext(d.label or defaultLabel or matchID)
      
     -- Meta Info (ONLY Date)
    local metaStr = formatDate(d.date)
    top:tag('span'):addClass('bk-match-meta'):wikitext(metaStr)


     local matchBox = wrap:tag('div'):addClass('bk-match')
    -- Teams Wrapper
    if extraClass and extraClass ~= "" then matchBox:addClass(extraClass) end
     local teamsWrap = box:tag('div'):addClass('bk-teams-wrap')


    -- Determine winner
     local linkPrefix = ""
     local win1 = (d.winner == "1" or d.winner == d.team1) and d.team1 ~= "TBD"
    if game and game ~= "" and game ~= "BGMI" then linkPrefix = game .. "/Teams/" end
    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
     -- Team 1
     local row1 = matchBox:tag('div'):addClass('bk-team')
     local row1 = teamsWrap:tag('div'):addClass('bk-team')
     if win1 then row1:addClass('bk-win') end
     if win1 then row1:addClass('bk-win') elseif win2 then row1:addClass('bk-lose') end
    if not win1 and win2 then row1:addClass('bk-lose') end
     row1:wikitext(getTeamLogo(d.team1))
     local logo1 = getTeamLogo(d.team1, "18px")
     local t1Link = d.team1 ~= "TBD" and "[[" .. linkPrefix .. d.team1 .. "|" .. d.short1 .. "]]" or "TBD"
     local nameCell1 = row1:tag('div'):addClass('bk-team-name')
     row1:tag('span'):addClass('bk-team-name'):wikitext(t1Link)
    if logo1 then nameCell1:wikitext(logo1) end
     row1:tag('span'):addClass('bk-score'):wikitext(d.score1 ~= "" and d.score1 or "-")
    if d.team1 ~= "TBD" then
        -- link uses full team name, display uses short name
        nameCell1:tag('span'):wikitext("[["..d.team1.."|"..d.short1.."]]")
    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
     -- Team 2
     local row2 = matchBox:tag('div'):addClass('bk-team')
     local row2 = teamsWrap:tag('div'):addClass('bk-team')
     if win2 then row2:addClass('bk-win') end
     if win2 then row2:addClass('bk-win') elseif win1 then row2:addClass('bk-lose') end
    if not win2 and win1 then row2:addClass('bk-lose') end
     row2:wikitext(getTeamLogo(d.team2))
     local logo2 = getTeamLogo(d.team2, "18px")
     local t2Link = d.team2 ~= "TBD" and "[[" .. linkPrefix .. d.team2 .. "|" .. d.short2 .. "]]" or "TBD"
     local nameCell2 = row2:tag('div'):addClass('bk-team-name')
     row2:tag('span'):addClass('bk-team-name'):wikitext(t2Link)
    if logo2 then nameCell2:wikitext(logo2) end
     row2:tag('span'):addClass('bk-score'):wikitext(d.score2 ~= "" and d.score2 or "-")
    if d.team2 ~= "TBD" then
        nameCell2:tag('span'):wikitext("[["..d.team2.."|"..d.short2.."]]")
    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
     return box
end
end


-- ════════════════════════════════════════════════════════════════
-- ================================================================
-- FORMAT 1: SINGLE ELIMINATION
-- FORMAT: SINGLE ELIMINATION
-- ════════════════════════════════════════════════════════════════
-- ================================================================
 
local function renderSingleElim(args, event, teamCount, game)
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))
     local rounds = math.floor(math.log(teamCount) / math.log(2))
    if 2^rounds ~= teamCount then
     local suffixNames = { "Grand Final", "Semifinals", "Quarterfinals", "Round of 16", "Round of 32" }
        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 scroll = html.create('div'):addClass('bk-scroll')
     local wrapper = scroll:tag('div'):addClass('bk-wrapper bk-single-elim')
     local wrapper = scroll:tag('div'):addClass('bk-wrapper')


     for r = 1, rounds do
     for r = 1, rounds do
         local col = wrapper:tag('div'):addClass('bk-col bk-col-'..r)
         local col = wrapper:tag('div'):addClass('bk-col')
         col:tag('div'):addClass('bk-col-header'):wikitext(roundNames[r])
        local pos = rounds - r + 1
         col:tag('div'):addClass('bk-col-header'):wikitext(args["r"..r.."name"] or suffixNames[pos] or ("Round "..r))


         local matchCount = teamCount / (2^r)
         local matchCount = teamCount / (2^r)
Line 191: Line 156:
             local cd = fetchMatch(event, matchID)
             local cd = fetchMatch(event, matchID)
             local d  = mergeMatchData(cd, args, matchID)
             local d  = mergeMatchData(cd, args, matchID)
           
            local target = ""
            local isFinal = (r == rounds)
            if not isFinal then
                local nextMatchNum = math.ceil(m / 2)
                target = "R"..(r+1).."M"..nextMatchNum
            end


             local spacer = col:tag('div'):addClass('bk-spacer')
             local card = renderMatchCard(d, matchID, "Match "..m, nil, nil, target, false, isFinal and "bk-final-match" or "", game)
            renderMatchCard(spacer, d, matchID, nil, "has-connector")
            col:node(card)
         end
         end
     end
     end
     return tostring(scroll)
     return tostring(scroll)
end
end


-- ════════════════════════════════════════════════════════════════
-- ================================================================
-- FORMAT 2: DOUBLE ELIMINATION
-- FORMAT: CUSTOM MANUAL BRACKET
-- Standard: Upper Bracket + Lower Bracket + Grand Final
-- ================================================================
-- ════════════════════════════════════════════════════════════════
local function renderCustom(args, event, game)
 
     local scroll = html.create('div'):addClass('bk-scroll')
local function renderDoubleElim(args, event, teamCount)
     local wrapper = scroll:tag('div'):addClass('bk-wrapper')
    -- Supports 4, 8, 16 teams
      
    local ubRounds  -- Upper bracket rounds before GF
     local cols = tonumber(args.columns) or 2
    local lbRounds  -- Lower bracket rounds
     for c = 1, cols do
 
         local col = wrapper:tag('div'):addClass('bk-col')
    if teamCount == 4 then
          
        ubRounds = 2; lbRounds = 2
         local colName = clean(args["col"..c.."name"])
    elseif teamCount == 8 then
        if colName and colName:lower() ~= "none" then
        ubRounds = 3; lbRounds = 4
             col:tag('div'):addClass('bk-col-header'):wikitext(colName)
    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
    end
       
 
        local matchesStr = args["col"..c.."_matches"]
    -- ── Grand Final ──
         if matchesStr then
    local gfSection = wrapper:tag('div'):addClass('bk-section bk-gf-section')
            for mID in string.gmatch(matchesStr, '([^,]+)') do
    gfSection:tag('div'):addClass('bk-section-title bk-gf-title'):wikitext('Grand Final')
                mID = clean(mID)
    local gfRow = gfSection:tag('div'):addClass('bk-row bk-gf-row')
                 local cd = fetchMatch(event, mID)
 
                 local d  = mergeMatchData(cd, args, mID)
    -- Game 1 (UB winner vs LB winner)
                 local winTo = args[mID.."_win_to"]
    local gf1 = fetchMatch(event, "GF1")
                 local loseTo = args[mID.."_lose_to"]
    local gf1d = mergeMatchData(gf1, args, "GF1")
                 local target = args[mID.."_target"]
    local gf1col = gfRow:tag('div'):addClass('bk-col bk-col-gf')
                 local isDrop = args[mID.."_drop"] == "true"  
    gf1col:tag('div'):addClass('bk-col-header'):wikitext(args.gf1name or "Grand Final")
               
    local sp1 = gf1col:tag('div'):addClass('bk-spacer')
                 col:node(renderMatchCard(d, mID, args[mID.."_label"], winTo, loseTo, target, isDrop, "", game))
    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
         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)
     return tostring(scroll)
end
end


-- ════════════════════════════════════════════════════════════════
-- ================================================================
-- MAIN ENTRY POINT
-- MAIN ENTRY
-- ════════════════════════════════════════════════════════════════
-- ================================================================
 
function p.main(frame)
function p.main(frame)
     local args = (frame:getParent() and frame:getParent().args) or frame.args
     local args = (frame:getParent() and frame:getParent().args) or frame.args
 
     local format = (clean(args.format) or "single"):lower()
     local format   = clean(args.format) or "single"
     local event = clean(args.event) or ""
     local event    = clean(args.event) or ""
     local game = clean(args.game) or "" -- Pass the game prefix (e.g. "Honor of Kings")
     local teamCount = tonumber(args.teams) or 8
     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')
     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
     local bracketHTML


    format = format:lower()
     if format == "single" then
     if format == "single" or format == "se" then
         bracketHTML = renderSingleElim(args, event, teamCount, game)
         bracketHTML = renderSingleElim(args, event, teamCount)
     elseif format == "custom" then
    elseif format == "double" or format == "de" then
         bracketHTML = renderCustom(args, event, game)
        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
     else
         bracketHTML = "<div class='bk-error'>Unknown bracket format: <b>"..format.."</b>. "
         bracketHTML = "<div style='color:red; padding:20px'>Format '"..format.."' is currently being upgraded to v3.0. Use 'single' or 'custom' for now.</div>"
            .."Valid: single, double, gsl, rr, swiss</div>"
     end
     end


     root:wikitext(bracketHTML)
     root:wikitext(bracketHTML)


     -- Modal built with mw.html (avoids MediaWiki sanitizer stripping raw HTML strings)
     -- MODAL POPUP
     local overlay = root:tag('div')
     local overlay = root:tag('div'):addClass('bk-modal-overlay')
        :addClass('bk-modal-overlay')
        :css('display','none')
 
     local modal = overlay:tag('div'):addClass('bk-modal')
     local modal = overlay:tag('div'):addClass('bk-modal')
 
     modal:tag('span'):addClass('bk-modal-close'):attr('role','button'):wikitext('&#10005;')
    -- 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('&#10005;')
 
     local mheader = modal:tag('div'):addClass('bk-modal-header')
     local mheader = modal:tag('div'):addClass('bk-modal-header')
     local mteams = mheader:tag('div'):addClass('bk-modal-teams')
     local mteams = mheader:tag('div'):addClass('bk-modal-teams')
     mteams:tag('span'):addClass('bk-modal-t1')
     mteams:tag('span'):addClass('bk-modal-t1')
     mteams:tag('span'):addClass('bk-modal-vs'):wikitext('vs')
     mteams:tag('span'):addClass('bk-modal-vs'):wikitext('vs')
     mteams:tag('span'):addClass('bk-modal-t2')
     mteams:tag('span'):addClass('bk-modal-t2')
     local mscore = mheader:tag('div'):addClass('bk-modal-score')
     local mscore = mheader:tag('div'):addClass('bk-modal-score')
     mscore:tag('span'):addClass('bk-modal-s1')
     mscore:tag('span'):addClass('bk-modal-s1')
     mscore:tag('span'):addClass('bk-modal-dash'):wikitext('&#8211;')
     mscore:tag('span'):addClass('bk-modal-dash'):wikitext('&#8211;')
     mscore:tag('span'):addClass('bk-modal-s2')
     mscore:tag('span'):addClass('bk-modal-s2')
     local mmeta = modal:tag('div'):addClass('bk-modal-meta')
     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-bo')
Line 622: Line 249:


     return tostring(root)
     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
end


return p
return p

Latest revision as of 03:39, 4 April 2026

Documentation for this module may be created at Module:Bracket/doc

-- ================================================================
-- Module:Bracket v3.0 (Smart Nodes, Game Links, Info Popups)
-- ================================================================
local p = {}
local html = mw.html
local cargo = mw.ext.cargo

local function esc(s) return s and s:gsub("\\", "\\\\"):gsub("'", "\\'") or "" 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

-- Smarter Date Formatter (Handles YYYY-MM-DD and YYYY-MM-DD HH:MM)
local function formatDate(d)
    if not d or d == "" then return "" end
    local year, month, day, hour, min = string.match(d, "(%d%d%d%d)%-(%d%d)%-(%d%d)[T%s]*(%d*)%:*(%d*)")
    if year then
        local months = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}
        local dateStr = tonumber(day) .. "-" .. months[tonumber(month)]
        if hour and hour ~= "" then
            dateStr = dateStr .. " " .. hour .. ":" .. (min ~= "" and min or "00")
        end
        return dateStr
    end
    return d 
end

local teamLogoCache = {}
local function getTeamLogo(teamName)
    if not teamName or teamName == "" or teamName == "TBD" then return "" end
    if teamLogoCache[teamName] then return teamLogoCache[teamName] 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
    if not t then 
        teamLogoCache[teamName] = ""
        return "" 
    end
    
    local light = t.image or ""
    local dark = (t.image_dark and t.image_dark ~= "") and t.image_dark or light
    if light == "" then 
        teamLogoCache[teamName] = ""
        return "" 
    end
    
    local htmlStr = "[[File:"..light.."|18px|link=|class=logo-lightmode bk-team-logo]]"
        .. "[[File:"..dark .."|18px|link=|class=logo-darkmode bk-team-logo]]"
    teamLogoCache[teamName] = htmlStr
    return htmlStr
end

local function fetchMatch(event, matchID)
    if not event or event == "" then return {} end
    local rows = cargo.query("BracketMatch",
        "team1,score1,team2,score2,winner,bo,match_date,casters,vod,notes,short1,short2",
        { where = "event='"..esc(event).."' AND match_id='"..esc(matchID).."'", limit = 1 })
    if rows and #rows > 0 then return rows[1] end
    return {}
end

local function mergeMatchData(cargoData, args, prefix)
    local d = {}
    d.label   = clean(args[prefix.."label"])
    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.short1  = clean(args[prefix.."short1"])  or clean(cargoData.short1)  or d.team1
    d.short2  = clean(args[prefix.."short2"])  or clean(cargoData.short2)  or d.team2
    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

-- ── NEW MODERN MATCH CARD ──
local function renderMatchCard(d, matchID, defaultLabel, winTo, loseTo, target, isDrop, extraClass, game)
    local box = html.create('div'):addClass('bk-match')
        :attr('id', 'match-' .. matchID)
    
    if winTo and winTo ~= "" then box:attr('data-target-win', 'match-' .. winTo) end
    if loseTo and loseTo ~= "" then box:attr('data-target-lose', 'match-' .. loseTo) end
    if target and target ~= "" then box:attr('data-target', 'match-' .. target) end
    if isDrop then box:attr('data-drop', 'true') end
    if extraClass then box:addClass(extraClass) end

    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"

    -- Top Bar (Entire bar is now the clickable trigger)
    local top = box:tag('div'):addClass('bk-match-top')
       :attr('data-matchid', matchID) -- Ready for your future Match Page logic
       :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):attr('data-notes', d.notes)
       :attr('title', 'Click for Match Details')
    
    -- The Label
    top:tag('span'):addClass('bk-match-label')
       :wikitext(d.label or defaultLabel or matchID)
    
    -- Meta Info (ONLY Date)
    local metaStr = formatDate(d.date)
    top:tag('span'):addClass('bk-match-meta'):wikitext(metaStr)

    -- Teams Wrapper
    local teamsWrap = box:tag('div'):addClass('bk-teams-wrap')

    local linkPrefix = ""
    if game and game ~= "" and game ~= "BGMI" then linkPrefix = game .. "/Teams/" end

    -- Team 1
    local row1 = teamsWrap:tag('div'):addClass('bk-team')
    if win1 then row1:addClass('bk-win') elseif win2 then row1:addClass('bk-lose') end
    row1:wikitext(getTeamLogo(d.team1))
    local t1Link = d.team1 ~= "TBD" and "[[" .. linkPrefix .. d.team1 .. "|" .. d.short1 .. "]]" or "TBD"
    row1:tag('span'):addClass('bk-team-name'):wikitext(t1Link)
    row1:tag('span'):addClass('bk-score'):wikitext(d.score1 ~= "" and d.score1 or "-")

    -- Team 2
    local row2 = teamsWrap:tag('div'):addClass('bk-team')
    if win2 then row2:addClass('bk-win') elseif win1 then row2:addClass('bk-lose') end
    row2:wikitext(getTeamLogo(d.team2))
    local t2Link = d.team2 ~= "TBD" and "[[" .. linkPrefix .. d.team2 .. "|" .. d.short2 .. "]]" or "TBD"
    row2:tag('span'):addClass('bk-team-name'):wikitext(t2Link)
    row2:tag('span'):addClass('bk-score'):wikitext(d.score2 ~= "" and d.score2 or "-")

    return box
end

-- ================================================================
-- FORMAT: SINGLE ELIMINATION
-- ================================================================
local function renderSingleElim(args, event, teamCount, game)
    local rounds = math.floor(math.log(teamCount) / math.log(2))
    local suffixNames = { "Grand Final", "Semifinals", "Quarterfinals", "Round of 16", "Round of 32" }
    
    local scroll = html.create('div'):addClass('bk-scroll')
    local wrapper = scroll:tag('div'):addClass('bk-wrapper')

    for r = 1, rounds do
        local col = wrapper:tag('div'):addClass('bk-col')
        local pos = rounds - r + 1
        col:tag('div'):addClass('bk-col-header'):wikitext(args["r"..r.."name"] or suffixNames[pos] or ("Round "..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 target = ""
            local isFinal = (r == rounds)
            if not isFinal then
                local nextMatchNum = math.ceil(m / 2)
                target = "R"..(r+1).."M"..nextMatchNum
            end

            local card = renderMatchCard(d, matchID, "Match "..m, nil, nil, target, false, isFinal and "bk-final-match" or "", game)
            col:node(card)
        end
    end
    return tostring(scroll)
end

-- ================================================================
-- FORMAT: CUSTOM MANUAL BRACKET
-- ================================================================
local function renderCustom(args, event, game)
    local scroll = html.create('div'):addClass('bk-scroll')
    local wrapper = scroll:tag('div'):addClass('bk-wrapper')
    
    local cols = tonumber(args.columns) or 2
    for c = 1, cols do
        local col = wrapper:tag('div'):addClass('bk-col')
        
        local colName = clean(args["col"..c.."name"])
        if colName and colName:lower() ~= "none" then
            col:tag('div'):addClass('bk-col-header'):wikitext(colName)
        end
        
        local matchesStr = args["col"..c.."_matches"]
        if matchesStr then
            for mID in string.gmatch(matchesStr, '([^,]+)') do
                mID = clean(mID)
                local cd = fetchMatch(event, mID)
                local d  = mergeMatchData(cd, args, mID)
                local winTo = args[mID.."_win_to"]
                local loseTo = args[mID.."_lose_to"]
                local target = args[mID.."_target"] 
                local isDrop = args[mID.."_drop"] == "true" 
                
                col:node(renderMatchCard(d, mID, args[mID.."_label"], winTo, loseTo, target, isDrop, "", game))
            end
        end
    end
    return tostring(scroll)
end

-- ================================================================
-- MAIN ENTRY
-- ================================================================
function p.main(frame)
    local args = (frame:getParent() and frame:getParent().args) or frame.args
    local format = (clean(args.format) or "single"):lower()
    local event = clean(args.event) or ""
    local game = clean(args.game) or "" -- Pass the game prefix (e.g. "Honor of Kings")
    local teamCount = tonumber(args.teams) or 8

    local root = html.create('div'):addClass('bk-root')
    local bracketHTML

    if format == "single" then
        bracketHTML = renderSingleElim(args, event, teamCount, game)
    elseif format == "custom" then
        bracketHTML = renderCustom(args, event, game)
    else
        bracketHTML = "<div style='color:red; padding:20px'>Format '"..format.."' is currently being upgraded to v3.0. Use 'single' or 'custom' for now.</div>"
    end

    root:wikitext(bracketHTML)

    -- MODAL POPUP
    local overlay = root:tag('div'):addClass('bk-modal-overlay')
    local modal = overlay:tag('div'):addClass('bk-modal')
    modal:tag('span'):addClass('bk-modal-close'):attr('role','button'):wikitext('&#10005;')
    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('&#8211;')
    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

return p