Module:Build bracket/Render

local Render = {}

-- ================================
-- 1) MODULE STATE & BIND INJECTS
-- ================================
-- Upvalues bound in buildTable(...)
local state, config, Helpers, StateChecks

-- Local stdlib aliases
local t_insert = table.insert

-- Helper/function locals (set during bind)
local isempty, notempty, cellBorder, unboldParenthetical
local showSeeds, teamLegs, roundIsEmpty, defaultHeaderText, isBlankEntry

-- =========================
-- 2) CORE <td> CONSTRUCTOR
-- =========================
local function Cell(tbl, j, i, opts)
    opts = opts or {}
    local cell = tbl:tag("td")

    -- classes/attributes
    if opts.classes then
        for _, c in ipairs(opts.classes) do
            cell:addClass(c)
        end
    end
    if opts.colspan and opts.colspan ~= 1 then
        cell:attr("colspan", opts.colspan)
    end
    if opts.rowspan and opts.rowspan ~= 1 then
        cell:attr("rowspan", opts.rowspan)
    end

    -- styling
    if opts.borderWidth then
        cell:css("border-width", cellBorder(opts.borderWidth))
    end
    if opts.weight == "bold" then
        cell:css("font-weight", "bold")
    end
    if opts.bg then
        cell:css("background", opts.bg)
    end
    if opts.color then
        cell:css("color", opts.color)
    end

    -- alignment helpers
    if opts.align == "center" then
        cell:addClass("brk-center")
    elseif opts.align == "right" then
        cell:css("text-align", "right")
    end

    if opts.text then
        cell:wikitext(opts.text)
    end
    return cell
end

-- =====================================
-- 3) ENTRY SIZING (computed per-round)
-- =====================================
local entryColspan = nil
local function getEntryColspan(j)
    return entryColspan and entryColspan[j] or 1
end

-- ==========================
-- 4) TEAM / SCORE CELLS
-- ==========================
local function teamCell(tbl, k, j, i, l, colspan)
    local classes = {"brk-td", "brk-b", (k == "seed") and "brk-bgD" or "brk-bgL"}
    if k == "seed" or k == "score" then
        classes[#classes + 1] = "brk-center"
    end

    -- strict bolding
    local weightFlag
    if k == "team" then
        if state.entries[j][i].weight == "bold" then
            weightFlag = "bold"
        end
    elseif k == "score" and l ~= nil then
        local sc = state.entries[j][i].score
        if sc and sc.weight and sc.weight[l] == "bold" then
            weightFlag = "bold"
        end
    end

    local legs = teamLegs(j, i)
    local opts = {
        classes = classes,
        colspan = colspan,
        rowspan = 2,
        borderWidth = {0, 0, 1, 1},
        weight = weightFlag -- 'bold' or nil
    }

    -- borders
    if k == "team" and legs == 0 then
        opts.borderWidth[2] = 1
    end
    if state.entries[j][i].position == "top" then
        opts.borderWidth[1] = 1
    end
    if l == legs or l == "agg" or k == "seed" then
        opts.borderWidth[2] = 1
    end

    -- text
    local function tostr(x)
        return (x == nil) and "" or tostring(x)
    end
    if l == nil then
        opts.text = unboldParenthetical(tostr(state.entries[j][i][k]))
    else
        local v = state.entries[j][i][k] and state.entries[j][i][k][l]
        opts.text = tostr(v)
    end

    -- ensure seeds inherit team bold without affecting score logic
    if k == "seed" and state.entries[j][i] and state.entries[j][i].weight == "bold" then
        opts.weight = opts.weight or "bold"
    end
    return Cell(tbl, j, i, opts)
end

-- ======================================
-- 5) NIL/BLANK ENTRY HANDLING PER CELL
-- ======================================
local function handleEmptyOrNilEntry(tbl, j, i, R)
    local entry_colspan = getEntryColspan(j)
    local col = state.entries[j] or {}

    -- nil entry: optionally emit spanning blank to keep grid intact
    if col[i] == nil then
        if col[i - 1] ~= nil or i == 1 then
            local rowspan, row = 0, i
            repeat
                rowspan = rowspan + 1
                row = row + 1
            until col[row] ~= nil or row > R
            Cell(tbl, j, i, {rowspan = rowspan, colspan = entry_colspan})
            return true
        else
            return true -- intentionally omitted cell
        end
    end

    if col[i]["ctype"] == "blank" then
        return true
    end
    return false
end

-- ============================
-- 6) ENTRY INSERTORS (ctype)
-- ============================
-- 6.1 Header
local function insertHeader(tbl, j, i, entry)
    local byesJ = state.byes[j]
    local hideJ = state.hide[j]
    local entry_colspan = getEntryColspan(j)

    if (byesJ and byesJ[entry.headerindex] and roundIsEmpty(j, i)) or (hideJ and hideJ[entry.headerindex]) then
        return Cell(tbl, j, i, {rowspan = 2, colspan = entry_colspan})
    end

    if isempty(entry.header) then
        entry.header = defaultHeaderText(j, entry.headerindex)
    end

    local classes = {"brk-td", "brk-b", "brk-center"}
    local useCustomShade = entry.shade_is_rd and not isempty(entry.shade)
    if not useCustomShade then
        t_insert(classes, "brk-bgD")
    end

    local cellOpts = {
        rowspan = 2,
        colspan = entry_colspan,
        text = entry.header,
        classes = classes,
        borderWidth = {1, 1, 1, 1}
    }
    if useCustomShade then
        cellOpts.bg = entry.shade
    end
    return Cell(tbl, j, i, cellOpts)
end

-- 6.2 Team (+seed/+scores/+agg)
local function insertTeam(tbl, j, i, entry)
    local byesJ = state.byes[j]
    local hideJ = state.hide[j]
    local entry_colspan = getEntryColspan(j)
    local maxlegs = state.maxlegs[j] or 1
    local legs = teamLegs(j, i)
    local team_colspan = maxlegs - legs + 1

    -- bye/hidden → reserve footprint
    if ((byesJ and byesJ[entry.headerindex]) and isBlankEntry(j, i)) or (hideJ and hideJ[entry.headerindex]) then
        return Cell(tbl, j, i, {rowspan = 2, colspan = entry_colspan})
    end

    if config.aggregate and legs == 1 and maxlegs > 1 then
        team_colspan = team_colspan + 1
    end
    if maxlegs == 0 then
        team_colspan = team_colspan + 1
    end

    -- seed
    if config.seeds then
        if showSeeds(j, i) == true then
            teamCell(tbl, "seed", j, i)
        else
            team_colspan = team_colspan + 1
        end
    end

    -- team name
    teamCell(tbl, "team", j, i, nil, team_colspan)

    -- scores
    for l = 1, legs do
        teamCell(tbl, "score", j, i, l)
    end

    -- aggregate
    if config.aggregate and legs > 1 then
        teamCell(tbl, "score", j, i, "agg")
    end
end

-- 6.3 Text
local function insertText(tbl, j, i, entry)
    Cell(tbl, j, i, {rowspan = 2, colspan = getEntryColspan(j), text = entry.text})
end

-- 6.4 Group (spans columns)
local function insertGroup(tbl, j, i, entry)
    local span = state.entries[j][i].colspan or 1
    local colspan = 0

    -- sum entry widths per column
    for m = j, j + span - 1 do
        colspan = colspan + state.maxlegs[m] + 2
        if not config.seeds then
            colspan = colspan - 1
        end
        if (config.aggregate and state.maxlegs[m] > 1) or state.maxlegs[m] == 0 then
            colspan = colspan + 1
        end
    end

    -- add path columns between rounds
    for m = j, j + span - 2 do
        colspan = colspan + (state.hascross[m] and 3 or 2)
    end

    return Cell(tbl, j, i, {rowspan = 2, colspan = colspan, classes = {"brk-center"}, text = entry.group or ""})
end

-- 6.5 Line
local function insertLine(tbl, j, i, entry)
    local entry_colspan = getEntryColspan(j)

    local borderWidth = {0, 0, 0, 0}
    if entry.borderWidth then
        borderWidth = entry.borderWidth
    else
        -- derive from left path column
        local wantTop = entry.border == "top" or entry.border == "both"
        local wantBottom = (entry.border == nil) or entry.border == "bottom" or entry.border == "both"

        if wantBottom and state.pathCell[j - 1] and state.pathCell[j - 1][i + 1] then
            borderWidth[3] = 2 * (state.pathCell[j - 1][i + 1][3][1][3] or 0)
        end
        if wantTop and state.pathCell[j - 1] and state.pathCell[j - 1][i] then
            borderWidth[1] = 2 * (state.pathCell[j - 1][i][3][1][1] or 0)
        end
    end

    local cell = Cell(tbl, j, i, {rowspan = 2, colspan = entry_colspan, text = entry.text, borderWidth = borderWidth})
    cell:addClass("brk-line")
    if entry.color then
        cell:css("border-color", entry.color)
    end
    return cell
end

local INSERTORS = {
    header = insertHeader,
    team = insertTeam,
    text = insertText,
    group = insertGroup,
    line = insertLine
}

local function insertEntry(tbl, j, i, R)
    if handleEmptyOrNilEntry(tbl, j, i, R) then
        return
    end
    local entry = state.entries[j][i]
    if not entry then
        return
    end
    local fn = INSERTORS[entry.ctype]
    if fn then
        return fn(tbl, j, i, entry)
    end
    return Cell(tbl, j, i, {rowspan = 2, colspan = getEntryColspan(j)})
end

-- ===================================
-- 7) PATH CELL EMITTERS (between RDs)
-- ===================================
-- Always emit the correct number of <td>s:
-- - 2 lanes when no cross (k=1,3)
-- - 3 lanes when cross (k=1,2,3). The center lane (k=2) is skipped only if no cross.
local function generatePathCell(tbl, j, i, k, bg, rowspan)
    if not state.hascross[j] and k == 2 then
        return
    end -- keep table aligned

    local colData = state.pathCell[j][i][k]
    local borders = (colData and colData[1]) or {0, 0, 0, 0}
    local color = (colData and colData.color) or "transparent"

    local cell = tbl:tag("td")
    if rowspan and rowspan ~= 1 then
        cell:attr("rowspan", rowspan)
    end

    if k == 2 and state.hascross[j] and notempty(bg) then
        cell:css("background", bg):css("transform", "translate(-1px)")
    end

    if borders[1] ~= 0 or borders[2] ~= 0 or borders[3] ~= 0 or borders[4] ~= 0 then
        cell:css("border", "solid " .. color):css(
            "border-width",
            (2 * borders[1]) ..
                "px " .. (2 * borders[2]) .. "px " .. (2 * borders[3]) .. "px " .. (2 * borders[4]) .. "px"
        )
    end
    return cell
end

local function insertPath(tbl, j, i, R)
    if state.skipPath[j][i] then
        return
    end

    local colspan, rowspan = 2, 1
    local bg = ""
    local cross = {"", ""}

    local Pj = state.pathCell[j]
    local Xj = state.crossCell[j]
    local SPj = state.skipPath[j]

    -- vertical merge: extend rowspan down while borders repeat identically
    if i < R then
        local function sameBorders(a)
            if a > R - 1 or SPj[a] then
                return false
            end
            local pi, pa = Pj[i], Pj[a]
            for k = 1, 3 do
                local bi, ba = pi[k][1], pa[k][1]
                if bi[1] ~= ba[1] or bi[2] ~= ba[2] or bi[3] ~= ba[3] or bi[4] ~= ba[4] then
                    return false
                end
            end
            return true
        end
        if sameBorders(i) then
            local row = i
            repeat
                if row ~= i and sameBorders(row) then
                    SPj[row] = true
                end
                rowspan = rowspan + 1
                row = row + 1
            until row > R or not sameBorders(row)
            rowspan = rowspan - 1
        end
    end

    -- avoid double-emitting cross rows (previous row already spans)
    if
        i > 1 and Xj[i - 1] and
            ((Xj[i - 1].left and Xj[i - 1].left[1] == 1) or (Xj[i - 1].right and Xj[i - 1].right[1] == 1))
     then
        return
    end

    -- cross visuals
    if state.hascross[j] then
        colspan = 3
        if Xj[i].left[1] == 1 or Xj[i].right[1] == 1 then
            rowspan = 2
            if Xj[i].left[1] == 1 then
                cross[1] =
                    "linear-gradient(to top right, transparent calc(50% - 1px)," ..
                    Xj[i].left[2] ..
                        " calc(50% - 1px)," .. Xj[i].left[2] .. " calc(50% + 1px), transparent calc(50% + 1px))"
            end
            if Xj[i].right[1] == 1 then
                cross[2] =
                    "linear-gradient(to bottom right, transparent calc(50% - 1px)," ..
                    Xj[i].right[2] ..
                        " calc(50% - 1px)," .. Xj[i].right[2] .. " calc(50% + 1px), transparent calc(50% + 1px))"
            end
        end
        if notempty(cross[1]) and notempty(cross[2]) then
            cross[1] = cross[1] .. ","
        end
        bg = cross[1] .. cross[2]
    end

    -- emit L | (CENTER) | R cells
    for k = 1, 3 do
        generatePathCell(tbl, j, i, k, bg, rowspan)
    end
end

-- =========================================
-- 8) INVISIBLE SCAFFOLDING (LEGACY COMPAT)
-- =========================================
local function emitRowHeight(tr, rowHeightPx) -- left height cell per data row
    tr:tag("td"):css("height", rowHeightPx)
end

-- widths row: fixes widths for seed/team/score(+agg) and path columns
local function emitWidthsRow(tbl, MINC, C, seedW, teamW, scoreW, aggW, pathW, leftPad, rightPad, crossW, crossPad)
    tbl:tag("tr"):css("visibility", "collapse") -- spacer

    local tr = tbl:tag("tr")
    tr:tag("td"):css("width", "1px") -- tiny leading gutter

    for j = MINC, C do
        if config.seeds then
            tr:tag("td"):css("width", seedW)
        end
        tr:tag("td"):css("width", teamW)

        local maxlegs = (state.maxlegs and state.maxlegs[j]) or 1
        if maxlegs <= 0 then
            tr:tag("td"):css("width", scoreW) -- legacy extra column when maxlegs == 0
        else
            for _ = 1, maxlegs do
                tr:tag("td"):css("width", scoreW)
            end
        end
        if config.aggregate and maxlegs > 1 then
            tr:tag("td"):css("width", aggW)
        end

        if j < C then
            if state.hascross and state.hascross[j] then
                tr:tag("td"):css("width", pathW):css("padding-left", leftPad) -- L
                tr:tag("td"):css("width", crossW):css("padding-left", crossPad) -- CENTER
                tr:tag("td"):css("width", pathW):css("padding-right", rightPad) -- R
            else
                tr:tag("td"):css("width", pathW):css("padding-left", leftPad) -- L
                tr:tag("td"):css("width", pathW):css("padding-right", rightPad) -- R
            end
        end
    end
end

-- ==============================
-- 9) PUBLIC: BUILD THE TABLE
-- ==============================
function Render.buildTable(frame, _state, _config, _Helpers, _StateChecks)
    -- Bind upvalues
    state, config, Helpers, StateChecks = _state, _config, _Helpers, _StateChecks

    -- Helpers
    isempty, notempty = Helpers.isempty, Helpers.notempty
    cellBorder = Helpers.cellBorder
    unboldParenthetical = Helpers.unboldParenthetical

    -- State checks
    showSeeds = StateChecks.showSeeds
    teamLegs = StateChecks.teamLegs
    roundIsEmpty = StateChecks.roundIsEmpty
    defaultHeaderText = StateChecks.defaultHeaderText
    isBlankEntry = StateChecks.isBlankEntry

    -- Hot locals
    local MINC, C = config.minc, config.c
    local R0 = tonumber(config.r) or 0
    local entries = state.entries
    local pathCell = state.pathCell
    local crossCell = state.crossCell
    local hide = state.hide or {}
    local byes = state.byes or {}
    local maxlegsArr = state.maxlegs or {}
    local hascross = state.hascross or {}

    -- Respect explicit |rows=|
    local userRowsArg = config._fargs and config._fargs.rows

    -- Is this entry visibly rendered
    local function entryIsVisible(j, i)
        local col = entries[j]
        local e = col and col[i]
        if not e then
            return false
        end

        local ct = e.ctype
        local hidx = e.headerindex
        local hid = (hide[j] and hide[j][hidx]) or false
        if hid then
            return false
        end

        if ct == "team" then
            local bye = (byes[j] and byes[j][hidx]) or false
            if bye and isBlankEntry(j, i) then
                return false
            end
            return true
        elseif ct == "header" then
            local bye = (byes[j] and byes[j][hidx]) or false
            if bye and roundIsEmpty(j, i) then
                return false
            end
            return true -- header shows (defaults applied) unless hidden or bye+empty
        elseif ct == "text" then
            return notempty(e.text)
        elseif ct == "group" then
            return notempty(e.group)
        elseif ct == "line" then
            -- Count visible borders via path scan, not the placeholder cell itself
            return notempty(e.text) -- only count if it actually prints text
        end
        return false
    end

    -- Backward scan for last visually-used row (entries or painted paths)
    local function computeBottomUsedRow(R)
        for i = R, 1, -1 do
            -- Any visible entry on this row?
            for j = MINC, C do
                if entryIsVisible(j, i) then
                    local e = entries[j][i]
                    -- teams occupy a row pair → include the following row if within bounds
                    if e.ctype == "team" then
                        return (i + 1 <= R) and (i + 1) or i
                    else
                        return i
                    end
                end
            end

            -- Any path/cross on this row?
            for j = MINC, C - 1 do
                local Pj = pathCell[j]
                local Xj = crossCell[j]

                -- Cross uses a row pair
                local cc = Xj and Xj[i]
                if cc and ((cc.left and cc.left[1] == 1) or (cc.right and cc.right[1] == 1)) then
                    return (i + 1 <= R) and (i + 1) or i
                end

                -- Any nonzero border on any lane?
                local row = Pj and Pj[i]
                if row then
                    for k = 1, 3 do
                        local cell = row[k]
                        local b = cell and cell[1]
                        if b and ((b[1] or 0) ~= 0 or (b[2] or 0) ~= 0 or (b[3] or 0) ~= 0 or (b[4] or 0) ~= 0) then
                            return i
                        end
                    end
                end
            end
        end
        return 1 -- nothing visible; keep at least one row
    end

    -- Effective number of data rows to emit
    local R_eff = (userRowsArg and userRowsArg ~= "") and R0 or computeBottomUsedRow(R0)

    -- Precompute entryColspan per round
    entryColspan = {}
    for j = MINC, C do
        local ml = maxlegsArr[j] or 1
        local col = ml + 2
        if not config.seeds then
            col = col - 1
        end
        if (config.aggregate and ml > 1) or ml == 0 then
            col = col + 1
        end
        entryColspan[j] = col
    end

    -- Table skeleton
    local tbl = mw.html.create("table"):addClass("brk")
    if config.nowrap then
        tbl:addClass("brk-nw")
    end

    -- Fixed internal row height (do NOT use config.height here)
    local rowHeightPx = "11px"

    -- Column widths (resolve once)
    local getWidth = Helpers.getWidth
    local seedW = (getWidth and getWidth("seed", "25px")) or "25px"
    local teamW = (getWidth and getWidth("team", "150px")) or "150px"
    local scoreW = (getWidth and getWidth("score", "25px")) or "25px"
    local aggRaw = (getWidth and getWidth("agg", nil)) or nil
    local aggW = Helpers.isempty(aggRaw) and scoreW or aggRaw
    local pathW = "2px"
    local crossW = (getWidth and getWidth("cross", "5px")) or "5px"
    local crossPad = (getWidth and getWidth("crosspad", "5px")) or "5px"

    -- between-round spacing split (e.g., 6 → 4px left, 2px right)
    local spacing = tonumber(config.colspacing) or 6
    local leftFrac = math.floor(spacing * 2 / 3)
    local leftPad, rightPad = (leftFrac .. "px"), ((spacing - leftFrac) .. "px")

    -- widths row
    emitWidthsRow(tbl, MINC, C, seedW, teamW, scoreW, aggW, pathW, leftPad, rightPad, crossW, crossPad)

    -- Data rows
    for i = 1, R_eff do
        local tr = tbl:tag("tr")
        emitRowHeight(tr, rowHeightPx) -- left height cell

        for j = MINC, C do
            insertEntry(tr, j, i, R_eff)
            if j < C then
                insertPath(tr, j, i, R_eff)
            end
        end
    end

    -- Wrap with a div that loads TemplateStyles and enables scroll overflow
    local fr = frame or mw.getCurrentFrame()
    local container = mw.html.create("div")

    -- Height now applies to container (numbers → px; units respected)
    local containerHeight = Helpers.toCssLength(config.height, nil)
    if containerHeight then
        container:css("max-height", containerHeight)
    end

    container:css("overflow-x", "auto"):css("overflow-y", "auto")
    container:wikitext(fr:extensionTag("templatestyles", "", {src = "Module:Build bracket/styles.css"}))
    container:node(tbl)

    return tostring(container)
end

-- ============
-- 10) EXPORTS
-- ============
return Render

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.