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.
- 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:
- 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.
- 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.
- 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.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.