local p = {}
local state = {
entries = {},
pathCell = {},
crossCell = {},
skipPath = {},
shift = {},
hascross = {},
teamsPerMatch = {},
rlegs = {},
maxlegs = {},
byes = {},
hide = {},
matchgroup = {},
headerindex = {},
masterindex = 1,
}
local config = {
autolegs = false,
nowrap = true,
autocol = false,
seeds = true,
forceseeds = false,
boldwinner = false,
aggregate = false,
paramstyle = "indexed",
colspacing = 5,
height = 0,
minc = 1,
c = 1, -- number of rounds
r = nil, -- rows (set later)
}
--========================
-- Localized globals for performance
--========================
local str_byte, str_char, str_find, str_format, str_gmatch, str_gsub, str_match =
string.byte, string.char, string.find, string.format, string.gmatch, string.gsub, string.match
local t_insert, t_sort, t_concat = table.insert, table.sort, table.concat
local m_max, m_min, m_ceil = math.max, math.min, math.ceil
local tonumber, tostring = tonumber, tostring
local pairs, ipairs, type = pairs, ipairs, type
local mw_html_create = mw.html.create
--========================
-- Color configuration
--========================
local COLORS = {
-- Cell backgrounds
cell_bg_light = 'var(--background-color-neutral-subtle,#f8f9fa)',
cell_bg_dark = 'var(--background-color-neutral,#eaecf0)',
text_color = 'var(--color-base,#202122)',
path_line_color = 'gray',
cell_border = 'var(--border-color-base,#a2a9b1)',
}
--========================
-- Core Helpers
--========================
--[[
Helper Index:
1. Basic checks: isempty, notempty, yes, no
2. Arg fetch: bargs
3. String: toChar, split, unboldParenthetical
4. Style: cellBorder, getWidth
]]
-- Basic truthy/empty checks
local function isempty(s) return s==nil or s=='' end
local function notempty(s) return s~=nil and s~='' end
local function yes(val) return val == 'y' or val == 'yes' end
local function no(val) return val == 'n' or val == 'no' end
-- Argument fetcher
local function bargs(s) -- reads pargs, fargs
return pargs[s] or fargs[s]
end
-- String helpers
local function toChar(num) -- pure
return str_char(str_byte("a")+num-1)
end
local function unboldParenthetical(text) -- pure, heavy string ops
if isempty(text) then return text end
local STYLE_NORMAL = '<span style="font-weight:normal">%s</span>'
local PLACEHOLDER_PREFIX = '__WIKILINK__'
-- Step 1: Extract and temporarily replace wikilinks
local counter = 0
local placeholders = {}
text = text:gsub('%[%[(.-)%]%]', function(link)
counter = counter + 1
local key = PLACEHOLDER_PREFIX .. counter .. '__'
placeholders[key] = link
return key
end)
-- Step 2: Wrap parenthetical (...) and bracketed [...] text in normal-weight span
text = text:gsub("(%b())", function(match)
return STYLE_NORMAL:format(match)
end)
text = text:gsub("(%b[])", function(match)
return STYLE_NORMAL:format(match)
end)
-- Step 3: Restore wikilinks
for key, link in pairs(placeholders) do
text = text:gsub(key, '[[' .. link .. ']]')
end
return text
end
local function split(str,delim,tonum) -- pure
result = {};
local a = "[^"..t_concat(delim).."]+"
for w in str:gmatch(a) do
if tonum==true then
t_insert(result, tonumber(w));
else
t_insert(result, w);
end
end
return result;
end
-- Style & dimension helpers
local function cellBorder(b) -- pure
return b[1]..'px '..b[2]..'px '..b[3]..'px '..b[4]..'px'
end
local function getWidth(ctype, default) -- reads pargs, fargs
local result = bargs(ctype..'-width')
if isempty(result) then return default end
if tonumber(result)~=nil then return result..'px' end
return result
end
local function parseArgs(frame, config)
fargs, pargs = frame.args, frame:getParent().args
config.r = tonumber(fargs.rows) or ''
config.c = tonumber(fargs.rounds) or 1
local maxc = tonumber(pargs.maxrounds) or tonumber(pargs.maxround) or ''
config.minc = tonumber(pargs.minround) or 1
if notempty(maxc) then config.c = maxc end
config.autocol = yes(fargs.autocol)
config.colspacing = tonumber(fargs['col-spacing']) or 5
config.height = bargs('height') or 0
config.boldwinner = bargs('boldwinner') or ''
config.forceseeds = yes(bargs('seeds'))
config.seeds = not no(bargs('seeds'))
config.aggregate = yes(bargs('aggregate'))
config.autolegs = yes(bargs('autolegs'))
config.paramstyle = (bargs('paramstyle') == 'numbered') and 'numbered' or 'indexed'
config.nowrap = not no(pargs.nowrap)
end
--========================
-- Bracket State Checks
--========================
local function isBlankEntry(col,row,ctype) -- reads entries
if isempty(state.entries[col][row]) then return true end
if isempty(state.entries[col][row]['team']) and isempty(state.entries[col][row]['text']) then return true end
return false
end
local function showSeeds(j,i) -- reads entries, teamsPerMatch, forceseeds
local showseed=false
if config.forceseeds or notempty(state.entries[j][i]['seed']) then
showseed=true
else
for k=1,state.teamsPerMatch[j]-1 do
if notempty(state.entries[j][i+2*k]) and state.entries[j][i]['group']==state.entries[j][i+2*k]['group'] and notempty(state.entries[j][i+2*k]['seed']) then
showseed=true
end
if notempty(state.entries[j][i-2*k]) and state.entries[j][i]['group']==state.entries[j][i-2*k]['group'] and notempty(state.entries[j][i-2*k]['seed']) then
showseed=true
end
end
end
return showseed
end
local function isRoundHidden(j,i,headerindex)
if notempty(state.entries[j][i]['pheader']) then
state.hide[j][state.entries[j][i]['headerindex']] = false
end
local row = i+1
repeat
if not isBlankEntry(j,row) then
state.hide[j][state.entries[j][i]['headerindex']] = false
end
row = row+1
until (state.entries[j][row]~=nil and state.entries[j][row]['ctype']=='header') or row>config.r
end
local function teamLegs(j,i) -- reads entries, rlegs, autolegs
local legs = state.rlegs[j]
if notempty(state.entries[j][i]['legs']) then
legs = tonumber(state.entries[j][i]['legs'])
end
if config.autolegs then
local l=1
repeat l=l+1
until isempty(state.entries[j][i]['score'][l])
legs = l-1
end
return legs
end
-- Determine whether the "round" after header at (j,i) is empty (used for bye detection)
local function roundIsEmpty(j, i) -- reads entries, isBlankEntry
-- start scanning rows after the header cell at i
local row = i + 1
repeat
if not isBlankEntry(j, row) then
return false
end
row = row + 1
until (state.entries[j][row] ~= nil and state.entries[j][row]['ctype'] == 'header') or row > config.r
return true
end
-- Default header text when none is provided
local function defaultHeaderText(j, headerindex)
if headerindex == 1 then
if j == config.c then
return 'Final'
elseif j == config.c - 1 then
return 'Semifinals'
elseif j == config.c - 2 then
return 'Quarterfinals'
else
return 'Round ' .. j
end
else
return 'Lower round ' .. j
end
end
local function noPaths(j,i)
local result = true
local cols = 2
if state.hascross[j]==true then
cols = 3
end
for k=1,cols do
for n=1,4 do
if state.pathCell[j][i][k][1][n]~=0 then
result = false
return result
end
end
end
if state.hascross[j]==true and (state.crossCell[j][i]['left'][1]==1 or state.crossCell[j][i]['right'][1]==1) then
result = false
return result
end
return result
end
--========================
-- Rendering (HTML / Table Building)
--========================
local function Cell(tbl, j, i, opts)
opts = opts or {}
local cell = tbl:tag('td')
-- classes first (optional helper)
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
if notempty(opts.border) then cell:css('border', opts.border) end
if notempty(opts.borderWidth) then cell:css('border-width', cellBorder(opts.borderWidth)) end
-- restore per-cell background override (this is what RD*-shade needs)
if notempty(opts.bg) then cell:css('background-color', opts.bg) end
if notempty(opts.align) then cell:css('text-align', opts.align) end
if opts.padding and opts.padding ~= '' then cell:css('padding', opts.padding) end
if opts.weight == 'bold' then cell:css('font-weight', 'bold') end
if notempty(opts.text) then cell:wikitext(opts.text) end
if state.entries[j] and state.entries[j][i] and state.entries[j][i].shade_is_rd then
cell:css('color', '#202122') -- keep original behavior
end
return cell
end
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
local opts = {
classes = classes, -- <-- gives border-style & color
colspan = colspan,
rowspan = 2,
borderWidth = {0, 0, 1, 1}, -- same logic as before, you still mutate this
-- bg only when overriding via RD*-shade; otherwise classes handle background
text = (l and tostring(state.entries[j][i][k][l])) or
unboldParenthetical(state.entries[j][i][k]),
weight = ((l == nil and state.entries[j][i].weight == 'bold')
or state.entries[j][i].score.weight[l] == 'bold') and 'bold' or nil,
}
-- Team cell specifics
if k == 'team' and teamLegs(j, i) == 0 then
opts.borderWidth[2] = 1
end
if state.entries[j][i].position == 'top' then
opts.borderWidth[1] = 1
end
if l == teamLegs(j, i) or l == 'agg' or k == 'seed' then
opts.borderWidth[2] = 1
end
-- Bold winner (unchanged logic)
if (l == nil and state.entries[j][i].weight == 'bold')
or state.entries[j][i].score.weight[l] == 'bold' then
opts.weight = 'bold'
end
-- Text content
if l == nil then
opts.text = unboldParenthetical(state.entries[j][i][k])
else
opts.text = tostring(state.entries[j][i][k][l])
end
return Cell(tbl, j, i, opts)
end
-- Compute standard colspan for a single "entry" cell in column j
local function getEntryColspan(j) -- reads maxlegs, seeds, aggregate
local colspan = state.maxlegs[j] + 2
if not config.seeds then
colspan = colspan - 1
end
if (config.aggregate and state.maxlegs[j] > 1) or state.maxlegs[j] == 0 then
colspan = colspan + 1
end
return colspan
end
-- Handle cases where the entry is nil (absent) or explicitly a blank placeholder.
-- Returns true if this function produced the appropriate cell (so caller should return)
local function handleEmptyOrNilEntry(tbl, j, i)
local entry_colspan = getEntryColspan(j)
-- nil entry: produce a spanning blank cell if appropriate
if state.entries[j][i] == nil then
-- If previous entry exists or this is the first row, create a rowspan covering following blank rows
if state.entries[j][i - 1] ~= nil or i == 1 then
local rowspan = 0
local row = i
repeat
rowspan = rowspan + 1
row = row + 1
until state.entries[j][row] ~= nil or row > config.r
-- produce an empty cell with the computed rowspan/colspan
local opts = {
rowspan = rowspan,
colspan = entry_colspan,
text = nil
}
Cell(tbl, j, i, opts)
return true
else
-- do nothing (cell intentionally omitted)
return true
end
end
-- explicit 'blank' ctype: do nothing (no visible content)
if state.entries[j][i]['ctype'] == 'blank' then
return true
end
return false
end
-- Insert a header cell
local function insertHeader(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
-- Empty cell helper
local function emptyCell()
return Cell(tbl, j, i, {
rowspan = 2,
colspan = entry_colspan
})
end
-- If entire round is a bye & header exists, skip rendering (return blank cell)
if state.byes[j][entry.headerindex] and roundIsEmpty(j, i) then
return emptyCell()
end
-- If this header is hidden, render an empty spanning cell
if state.hide[j][entry.headerindex] then
return emptyCell()
end
-- Fill default header text if not set
if isempty(entry.header) then
entry.header = defaultHeaderText(j, entry.headerindex)
end
local classes = { 'brk-td', 'brk-b', 'brk-center' }
if isempty(entry.shade) then
-- no custom shade → use the default background class
table.insert(classes, 'brk-bgD')
end
-- Render the actual header cell
return Cell(tbl, j, i, {
rowspan = 2,
colspan = entry_colspan,
text = entry.header,
align = 'center',
borderWidth = {1,1,1,1},
classes = classes,
-- custom RD*-shade overrides the class background
bg = notempty(entry.shade) and entry.shade or nil
})
end
-- Insert a team cell (seed, team, and scores)
local function insertTeam(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
-- If this team belongs to a 'bye' header and is blank, render empty cell
if (state.byes[j][entry.headerindex] and isBlankEntry(j, i)) or state.hide[j][entry.headerindex] then
return Cell(tbl, j, i, {
rowspan = 2,
colspan = entry_colspan
})
end
local legs = teamLegs(j, i)
local team_colspan = state.maxlegs[j] - legs + 1
if config.aggregate and legs == 1 and state.maxlegs[j] > 1 then
team_colspan = team_colspan + 1
end
if state.maxlegs[j] == 0 then
team_colspan = team_colspan + 1
end
-- Seed column (if enabled). Either render the seed cell or fold it into the team colspan.
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 cell (may span multiple score columns)
teamCell(tbl, 'team', j, i, nil, team_colspan)
-- Score cells (one per leg)
for l = 1, legs do
teamCell(tbl, 'score', j, i, l)
end
-- Aggregate score column (if configured)
if config.aggregate and legs > 1 then
teamCell(tbl, 'score', j, i, 'agg')
end
end
-- Insert a text cell
local function insertText(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
Cell(tbl, j, i, {
rowspan = 2,
colspan = entry_colspan,
text = entry.text,
})
end
-- Insert a group cell (spans several columns)
local function insertGroup(tbl, j, i, entry)
local span = state.entries[j][i].colspan
local colspan = 0
-- sum entry widths per column in the span
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 each adjacent pair
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,
text = entry.group,
align = 'center'
})
end
-- Insert a line cell (visual path markers)
local function insertLine(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
local borderWidth = {0, 0, 0, 0}
borderWidth[3] = 2 * state.pathCell[j - 1][i + 1][3][1][3]
local cell = Cell(tbl, j, i, {
rowspan = 2,
colspan = entry_colspan,
text = entry.text,
borderWidth = borderWidth
})
-- add default line color via class (keeps output tiny)
cell:addClass('brk-line')
return cell
end
-- Insert a line2 cell (alternative path styling)
local function insertLine2(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
local borderWidth = {0, 0, 0, 0}
borderWidth[1] = 2 * state.pathCell[j - 1][i][3][1][1]
local cell = Cell(tbl, j, i, {
rowspan = 2,
colspan = entry_colspan,
text = entry.text,
borderWidth = borderWidth
})
-- add default line color via class (keeps output tiny)
cell:addClass('brk-line')
return cell
end
local INSERTORS = {
header = insertHeader,
team = insertTeam,
text = insertText,
group = insertGroup,
line = insertLine,
line2 = insertLine2
}
local function insertEntry(tbl, j, i)
if handleEmptyOrNilEntry(tbl, j, i) 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
-- Draw a single path cell in the bracket table
local function generatePathCell(tbl, j, i, k, bg, rowspan)
local colData = state.pathCell[j][i][k]
local color = colData.color
-- Skip center cell if there is no cross path
if not state.hascross[j] and k == 2 then
return
end
local cell = tbl:tag('td')
-- Rowspan if this cell spans multiple rows
if rowspan ~= 1 then
cell:attr('rowspan', rowspan)
end
-- Background shading for middle column (cross path visuals)
if notempty(bg) and k == 2 then
cell:css('background', bg)
:css('transform', 'translate(-1px)')
end
-- Draw border if any segment has nonzero width
local borders = colData[1]
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
-- Insert all path cells for a given position in the bracket
local function insertPath(tbl, j, i)
if state.skipPath[j][i] then
return
end
local colspan, rowspan = 2, 1
local bg = ''
local cross = { '', '' }
-- Detect vertical merging (rowspan) for repeated paths
if i < config.r then
local function repeatedPath(a)
if a > config.r - 1 or state.skipPath[j][a] then
return false
end
for k = 1, 3 do
for n = 1, 4 do
if state.pathCell[j][i][k][1][n] ~= state.pathCell[j][a][k][1][n] then
return false
end
end
end
return true
end
if repeatedPath(i) then
local row = i
repeat
if row ~= i and repeatedPath(row) then
state.skipPath[j][row] = true
end
rowspan = rowspan + 1
row = row + 1
until row > config.r or not repeatedPath(row)
rowspan = rowspan - 1
end
end
-- Skip if the previous row has cross path connections
if i > 1 and (state.crossCell[j][i - 1].left[1] == 1 or state.crossCell[j][i - 1].right[1] == 1) then
return
end
-- Handle cross paths
if state.hascross[j] then
colspan = 3
if state.crossCell[j][i].left[1] == 1 or state.crossCell[j][i].right[1] == 1 then
rowspan = 2
if state.crossCell[j][i].left[1] == 1 then
cross[1] = 'linear-gradient(to top right, transparent calc(50% - 1px),'
.. state.crossCell[j][i].left[2] .. ' calc(50% - 1px),'
.. state.crossCell[j][i].left[2] .. ' calc(50% + 1px), transparent calc(50% + 1px))'
end
if state.crossCell[j][i].right[1] == 1 then
cross[2] = 'linear-gradient(to bottom right, transparent calc(50% - 1px),'
.. state.crossCell[j][i].right[2] .. ' calc(50% - 1px),'
.. state.crossCell[j][i].right[2] .. ' calc(50% + 1px), transparent calc(50% + 1px))'
end
end
-- Combine left/right gradient layers if both exist
if notempty(cross[1]) and notempty(cross[2]) then
cross[1] = cross[1] .. ','
end
bg = cross[1] .. cross[2]
end
-- Generate three cells (left, middle, right) for this row
for k = 1, 3 do
generatePathCell(tbl, j, i, k, bg, rowspan)
end
end
-- ===================
-- Cluster 4 – Data Population & Parameters
-- ===================
local function paramNames(cname, j, i, l)
-- Helpers
local function getArg(key)
return bargs(key) or ''
end
local function getPArg(key)
return pargs[key] or ''
end
local function tryBoth(base, name, idx, suffix)
suffix = suffix or ''
local val = getArg(base .. '-' .. name .. idx .. suffix)
if isempty(val) then
val = getArg(base .. '-' .. name .. str_format('%02d', idx) .. suffix)
end
return val
end
-- Round names
local rname = {
{ 'RD' .. j, getArg('RD' .. j .. '-altname') or 'RD' .. j },
{ 'RD' .. j .. toChar(state.entries[j][i].headerindex),
getArg('RD' .. j .. toChar(state.entries[j][i].headerindex) .. '-altname')
or 'RD' .. j .. toChar(state.entries[j][i].headerindex) }
}
-- Base name and indices
local name = { cname, getArg(cname .. '-altname') or cname }
local index = { state.entries[j][i].index, state.entries[j][i].altindex }
local result = {}
if cname == 'header' then
if state.entries[j][i].headerindex == 1 then
for _, base in ipairs({ rname[1], rname[2] }) do
for k = 2, 1, -1 do
result[#result+1] = getArg(base[k])
end
end
else
for k = 2, 1, -1 do
result[#result+1] = getArg(rname[2][k])
end
end
elseif cname == 'pheader' then
if state.entries[j][i].headerindex == 1 then
for _, base in ipairs({ rname[1], rname[2] }) do
for k = 2, 1, -1 do
result[#result+1] = getPArg(base[k])
end
end
else
for k = 2, 1, -1 do
result[#result+1] = getPArg(rname[2][k])
end
end
elseif cname == 'score' then
local bases = { rname[2][2], rname[2][1], rname[1][2], rname[1][1] }
local idxs = { index[2], index[2], index[1], index[1] }
for n = 1, 4 do
if l == 1 then
result[#result+1] = tryBoth(bases[n], name[1], idxs[n])
end
result[#result+1] = tryBoth(bases[n], name[1], idxs[n], '-' .. l)
end
elseif cname == 'shade' then
for k = 2, 1, -1 do
local base = (state.entries[j][i].headerindex == 1) and rname[1][k] or rname[2][k]
result[#result+1] = getArg(base .. '-' .. name[1])
end
result[#result+1] = getArg('RD-shade')
result[#result+1] = COLORS.cell_bg_dark
elseif cname == 'text' then
local bases = { rname[2][2], rname[2][1], rname[1][2], rname[1][1] }
local idxs = { index[2], index[2], index[1], index[1] }
local names = { name[2], name[1] }
for ni = 1, 2 do
for n = 1, 4 do
result[#result+1] = tryBoth(bases[n], names[ni], idxs[n])
end
end
else -- default case
local bases = { rname[2][2], rname[2][1], rname[1][2], rname[1][1] }
local idxs = { index[2], index[2], index[1], index[1] }
for n = 1, 4 do
result[#result+1] = tryBoth(bases[n], name[1], idxs[n])
end
end
-- Return first non-empty
for _, val in ipairs(result) do
if notempty(val) then
return val
end
end
return ''
end
local function indexedParams(j)
for i=1,config.r do
if state.entries[j][i]~=nil then
if state.entries[j][i]['ctype']=='team' then
local legs = state.rlegs[j]
if config.forceseeds then
state.entries[j][i]['seed'] = bargs(masterindex) or ''
masterindex = masterindex+1
end
state.entries[j][i]['team'] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
state.entries[j][i]['legs'] = paramNames('legs',j,i)
state.entries[j][i]['score'] = {}
state.entries[j][i]['weight'] = 'normal'
state.entries[j][i]['score']['weight'] = {}
if notempty(state.entries[j][i]['legs']) then
legs = tonumber(state.entries[j][i]['legs'])
end
for l=1,legs do
state.entries[j][i]['score'][l] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
state.entries[j][i]['score']['weight'][l] = 'normal'
end
if config.aggregate and legs>1 then
state.entries[j][i]['score']['agg'] = bargs(masterindex) or ''
masterindex = masterindex+1
state.entries[j][i]['score']['weight']['agg'] = 'normal'
end
end
if state.entries[j][i]['ctype']=='header' then
state.entries[j][i]['header'] = paramNames('header',j,i)
state.entries[j][i]['pheader'] = paramNames('pheader',j,i)
state.entries[j][i]['shade'] = paramNames('shade',j,i)
end
if state.entries[j][i]['ctype']=='text' then
state.entries[j][i]['text'] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
end
if state.entries[j][i]['ctype']=='group' then
state.entries[j][i]['group'] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
end
if state.entries[j][i]['ctype'] == 'line' and state.entries[j][i]['hastext']==true then
state.entries[j][i]['text'] = bargs(masterindex) or ''
masterindex = masterindex+1
end
end
end
end
local function assignTeamParams(j, i)
local legs = state.rlegs[j]
state.entries[j][i].seed = paramNames('seed', j, i)
state.entries[j][i].team = paramNames('team', j, i)
state.entries[j][i].legs = paramNames('legs', j, i)
state.entries[j][i].score = { weight = {} }
state.entries[j][i].weight = 'normal'
if notempty(state.entries[j][i].legs) then
legs = tonumber(state.entries[j][i].legs)
end
if config.autolegs then
local l = 1
repeat
state.entries[j][i].score[l] = paramNames('score', j, i, l)
state.entries[j][i].score.weight[l] = 'normal'
l = l + 1
until isempty(paramNames('score', j, i, l))
legs = l - 1
else
for l = 1, legs do
state.entries[j][i].score[l] = paramNames('score', j, i, l)
state.entries[j][i].score.weight[l] = 'normal'
end
end
if config.aggregate and legs > 1 then
state.entries[j][i].score.agg = paramNames('score', j, i, 'agg')
state.entries[j][i].score.weight.agg = 'normal'
end
end
local function assignHeaderParams(j, i)
state.entries[j][i].header = paramNames('header', j, i)
state.entries[j][i].pheader = paramNames('pheader', j, i)
state.entries[j][i].shade = paramNames('shade', j, i)
-- Mark if the shade came from any RD*-shade param
local shadeParamsToCheck = {
'RD'..j..'-shade',
'RD'..j..toChar(state.entries[j][i].headerindex)..'-shade',
'RD-shade'
}
state.entries[j][i].shade_is_rd = false
for _, pname in ipairs(shadeParamsToCheck) do
if notempty(bargs(pname)) and state.entries[j][i].shade == bargs(pname) then
state.entries[j][i].shade_is_rd = true
break
end
end
end
local function assignTextParams(j, i)
state.entries[j][i].text = paramNames('text', j, i)
end
local function assignGroupParams(j, i)
state.entries[j][i].group = paramNames('group', j, i)
end
local function assignLineTextParams(j, i)
state.entries[j][i].text = paramNames('text', j, i)
end
local function assignParams()
masterindex = 1
local maxcol = 1
local byerows = 1
local hiderows = 1
local function updateMaxCol(j, i)
if config.autocol and not isBlankEntry(j, i) then
maxcol = m_max(maxcol, j)
end
end
local function updateByerows(j, i)
if state.entries[j][i] and not state.hide[j][state.entries[j][i].headerindex] then
if not state.byes[j][state.entries[j][i].headerindex] or
(state.byes[j][state.entries[j][i].headerindex] and not isBlankEntry(j, i)) then
byerows = m_max(byerows, i)
end
end
end
for j = config.minc, config.c do
-- Set legs for this column
state.rlegs[j] = tonumber(bargs('RD'..j..'-legs')) or tonumber(bargs('legs')) or 1
if notempty(bargs('RD'..j..'-legs')) or bargs('legs') then
config.autolegs = false
end
if config.paramstyle == 'numbered' then
indexedParams(j)
else
for i = 1, config.r do
if state.entries[j][i] ~= nil then
local ctype = state.entries[j][i].ctype
if ctype == 'team' then
assignTeamParams(j, i)
elseif ctype == 'header' then
assignHeaderParams(j, i)
elseif ctype == 'text' then
assignTextParams(j, i)
elseif ctype == 'group' then
assignGroupParams(j, i)
elseif ctype == 'line' and state.entries[j][i].hastext == true then
assignLineTextParams(j, i)
end
end
updateMaxCol(j, i)
end
end
-- Round hiding check
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'header' then
isRoundHidden(j, i)
end
updateByerows(j, i)
end
end
-- Adjust row count if some rounds are byes or hidden
for j = config.minc, config.c do
for k = 1, state.headerindex[j] do
if state.byes[j][k] or state.hide[j][k] then
config.r = byerows + 1
end
end
end
-- Adjust column count automatically
if config.autocol then
config.c = maxcol
end
end
local function getHide(j,headerindex)
state.hide[j] = {}
for k=1,state.headerindex[j] do
if bargs('RD'..j..toChar(k)..'-hide')=='yes' or bargs('RD'..j..toChar(k)..'-hide')=='y' then
state.hide[j][k]=true
end
end
end
local function getByes(j,headerindex)
state.byes[j] = {}
for k=1,state.headerindex[j] do
if bargs('byes')=='yes' or bargs('byes')=='y' then
state.byes[j][k]=true
elseif tonumber(bargs('byes')) then
if j<=tonumber(bargs('byes')) then
state.byes[j][k]=true
end
else
state.byes[j][k]=false
end
if bargs('RD'..j..'-byes')=='yes' or bargs('RD'..j..'-byes')=='y' then
state.byes[j][k]=true
elseif bargs('RD'..j..'-byes')=='no' or bargs('RD'..j..'-byes')=='n' then
state.byes[j][k]=false
end
if bargs('RD'..j..toChar(k)..'-byes')=='yes' or bargs('RD'..j..toChar(k)..'-byes')=='y' then
state.byes[j][k]=true
elseif bargs('RD'..j..'-byes')=='no' or bargs('RD'..j..'-byes')=='n' then
state.byes[j][k]=false
end
end
end
local function getAltIndices()
local teamindex=1
local textindex=1
local groupindex=1
for j=config.minc,config.c do
state.headerindex[j]=0
for i=1,config.r do
if state.entries[j][i]==nil and i==1 then
state.headerindex[j]=state.headerindex[j]+1
end
if state.entries[j][i]~=nil then
if state.entries[j][i]['ctype'] == 'header' then
state.entries[j][i]['altindex'] = state.headerindex[j]
teamindex=1
textindex=1
state.headerindex[j]=state.headerindex[j]+1
elseif state.entries[j][i]['ctype'] == 'team' then
state.entries[j][i]['altindex'] = teamindex
teamindex=teamindex+1
elseif state.entries[j][i]['ctype'] == 'text' then
state.entries[j][i]['altindex'] = textindex
textindex=textindex+1
elseif state.entries[j][i]['ctype'] == 'group' then
state.entries[j][i]['altindex'] = groupindex
groupindex=groupindex+1
elseif state.entries[j][i]['ctype'] == 'line' and state.entries[j][i]['hastext']==true then
state.entries[j][i]['altindex'] = textindex
textindex=textindex+1
end
state.entries[j][i]['headerindex'] = state.headerindex[j]
end
end
getByes(j,headerindex)
getHide(j,headerindex)
end
end
local function matchGroups() -- mutates matchgroup, entries
for j=config.minc,config.c do
state.matchgroup[j]={}
for i=1,config.r do
if state.entries[j][i]~= nil and state.entries[j][i]['ctype']=='team' then
state.matchgroup[j][i]=m_ceil(state.entries[j][i]['index']/state.teamsPerMatch[j])
state.entries[j][i]['group'] = m_ceil(state.entries[j][i]['index']/state.teamsPerMatch[j])
end
end
end
end
local function boldWinner() -- mutates entries, matchgroup; heavy loops
-- Helper: compare one team's score to its opponent for a given leg
local function boldScore(j, i, l)
local e = state.entries[j][i]
if not e or e.ctype ~= 'team' then return 'normal' end
-- Extract numeric score
local rawscore = e.score[l] or ''
local myscore = tonumber(rawscore:match('^%d+'))
if not myscore then return 'normal' end
-- Find all opponent scores for the same match group
local compscores = {}
for oppIndex, groupId in pairs(state.matchgroup[j]) do
if groupId == state.matchgroup[j][i] and oppIndex ~= i then
local theirraw = state.entries[j][oppIndex].score[l] or ''
local theirscore = tonumber(theirraw:match('^%d+'))
if not theirscore then return 'normal' end
t_insert(compscores, theirscore)
end
end
-- If any opponent score >= mine, not bold
for _, v in ipairs(compscores) do
if myscore <= v then return 'normal' end
end
-- Track wins
if l ~= 'agg' then
e.wins = e.wins + 1
else
e.aggwins = 1
end
return 'bold'
end
-- Helper: decide if an entire team entry should be bolded
local function boldTeam(j, i, useAggregate)
local e = state.entries[j][i]
local winsKey = useAggregate and 'aggwins' or 'wins'
local legs = teamLegs(j, i)
if not useAggregate then
-- Bold if majority of legs won
if e[winsKey] > legs / 2 then return 'bold' end
-- Additional checks depending on autolegs
local checkFn = config.autolegs and notempty or function(val) return not isempty(val) end
for l = 1, legs do
local scoreVal = e.score[l]
if not checkFn(scoreVal) or str_find(scoreVal or '', "nbsp") then
return 'normal'
end
end
end
-- Compare wins to opponent
for oppIndex, groupId in pairs(state.matchgroup[j]) do
if groupId == state.matchgroup[j][i] and oppIndex ~= i then
if e[winsKey] <= tonumber(state.entries[j][oppIndex][winsKey]) then
return 'normal'
end
end
end
return 'bold'
end
-- Reset win counts
for j = config.minc, config.c do
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
state.entries[j][i].wins = 0
state.entries[j][i].aggwins = 0
end
end
-- Determine bold for scores
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
local legs = teamLegs(j, i)
for l = 1, legs do
state.entries[j][i].score.weight[l] = boldScore(j, i, l)
end
if config.aggregate and legs > 1 then
state.entries[j][i].score.weight.agg = boldScore(j, i, 'agg')
end
end
end
-- Determine bold for entire team
for i = 1, config.r do
if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
local useAggregate = config.aggregate and teamLegs(j, i) > 1
state.entries[j][i].weight = boldTeam(j, i, useAggregate)
end
end
end
end
local function updateMaxLegs()
for j = config.minc, config.c do
state.maxlegs[j] = state.rlegs[j]
for i = 1, config.r do
if notempty(state.entries[j][i]) then
if notempty(state.entries[j][i].legs) then
state.maxlegs[j] = m_max(state.rlegs[j], state.entries[j][i].legs)
end
if config.autolegs then
local l = 1
repeat l = l + 1
until isempty(state.entries[j][i].score) or isempty(state.entries[j][i].score[l])
state.maxlegs[j] = m_max(state.maxlegs[j], l - 1)
end
end
end
end
end
-- ========================
-- Path Parsing & Drawing
-- ========================
local function parsePaths(j)
local result={}
local str = fargs['col'..j..'-col'..(j+1)..'-paths'] or ''
for val in str:gsub("%s+","")
:gsub(",",", ")
:gsub("%S+","\0%0\0")
:gsub("%b()", function(s) return s:gsub("%z","") end)
:gmatch("%z(.-)%z") do
local array = split(val:gsub("%s+",""):gsub("%)",""):gsub("%(",""),{"-"})
for k,_ in pairs(array) do
array[k] = split(array[k],{","})
end
if notempty(array[2]) then
for m=1,#array[2] do
array[3] = {}
array[2][m] = split(array[2][m],{":"})
array[3][m] = array[2][m][2]
array[2][m] = array[2][m][1]
end
for n=1,#array[1] do
for m=1,#array[2] do
t_insert(result,{tonumber(array[1][n]),tonumber(array[2][m]),['color']=array[3][m]})
end
end
end
end
return result
end
local function isPathHidden(j,i,start,stop)
local result=false
if notempty(state.entries[j][start-1]) and (state.byes[j][state.entries[j][start-1]['headerindex']] and isBlankEntry(j,start-1) and isBlankEntry(j,start+1) or state.hide[j][state.entries[j][start-1]['headerindex']]) then
if bargs('show-bye-paths')~='y' and bargs('show-bye-paths')~='yes' then
result=true
end
end
if notempty(state.entries[j+1][stop-1]) and (state.byes[j+1][state.entries[j+1][stop-1]['headerindex']] and isBlankEntry(j+1,stop-1) and isBlankEntry(j+1,stop+1) or state.hide[j+1][state.entries[j+1][stop-1]['headerindex']])then
if bargs('show-bye-paths')~='y' and bargs('show-bye-paths')~='yes' then
result=true
end
end
if bargs('RD'..j..'-RD'..(j+1)..'-path')=='n' or bargs('RD'..j..'-RD'..(j+1)..'-path')=='no' or bargs('RD'..j..'-RD'..(j+1)..'-path')=='0' then
if notempty(state.entries[j][start-1]) and state.entries[j][start-1]['headerindex']==1 then
result=true
end
end
return result
end
-- ===============
-- Path helpers
-- ===============
-- Helper: write a segment to a path cell
local function setPathCell(j, row, colIndex, borderIndex, value, color)
state.pathCell[j][row][colIndex][1][borderIndex] = value
state.pathCell[j][row][colIndex].color = color
end
local function setMultipleCells(j, row, colIndexes, borderIndex, value, color)
for _, colIndex in ipairs(colIndexes) do
state.pathCell[j][row][colIndex][1][borderIndex] = value
state.pathCell[j][row][colIndex].color = color
end
end
-- ===== Path drawing =====
-- Handle straight path (start == stop)
local function handleStraightPath(j, start, color, straightpaths)
t_insert(straightpaths, { start, color })
end
-- Handle downward paths (start < stop)
local function handleDownwardPath(j, start, stop, cross, color)
if stop > config.r then return end
setPathCell(j, start + 1, 1, 1, 1, color)
if cross == 0 then
if state.hascross[j] then
setPathCell(j, start + 1, 2, 1, 1, color)
for i = start + 1, stop do
setPathCell(j, i, 2, 2, 1, color)
end
else
for i = start + 1, stop do
setPathCell(j, i, 1, 2, 1, color)
end
end
else
state.crossCell[j][cross].left = { 1, color }
for i = start + 1, cross - 1 do
setPathCell(j, i, 1, 2, 1, color)
end
for i = cross + 2, stop do
setPathCell(j, i, 2, 2, 1, color)
end
end
setPathCell(j, stop, 3, 3, 1, color)
end
-- Handle upward paths (start > stop)
local function handleUpwardPath(j, start, stop, cross, color)
if start > config.r then return end
setPathCell(j, stop + 1, 3, 1, 1, color)
if cross == 0 then
if state.hascross[j] then
for i = stop + 1, start do
setPathCell(j, i, 2, 2, 1, color)
end
setPathCell(j, start, 2, 3, 1, color)
else
for i = stop + 1, start do
setPathCell(j, i, 1, 2, 1, color)
end
end
else
state.crossCell[j][cross].right = { 1, color }
for i = stop + 1, cross - 1 do
setPathCell(j, i, 2, 2, 1, color)
end
for i = cross + 2, start do
setPathCell(j, i, 1, 2, 1, color)
end
end
setPathCell(j, start, 1, 3, 1, color)
end
-- ===== Thickness adjustments =====
-- Thicken start==stop paths
local function thickenStraightPaths(j, straightpaths)
for _, sp in ipairs(straightpaths) do
local i, color = sp[1], sp[2]
if i > config.r then break end
if state.pathCell[j][i][1][1][3] == 0 then
-- Set all three columns' bottom border to 1
setMultipleCells(j, i, {1, 2, 3}, 3, 1, color)
-- Set next row's top border to 0.5 if empty
if state.pathCell[j][i+1][1][1][1] == 0 then
setMultipleCells(j, i+1, {1, 2, 3}, 1, 0.5, color)
end
elseif state.pathCell[j][i+1][1][1][1] == 0 then
-- Set next row's top border to 1
setMultipleCells(j, i+1, {1, 3}, 1, 1, color)
if state.hascross[j] then
setMultipleCells(j, i+1, {2}, 1, 1, color)
end
end
end
end
-- Adjust path thickness for outpaths (thicken/thin transitions)
local function adjustOutpaths(j, outpaths)
for _, op in ipairs(outpaths) do
local i = op[1]
-- skip if this is the last row (i+1 would be out of range)
if i < config.r then
local top1, top2 = state.pathCell[j][i+1][1], state.pathCell[j][i+1][2]
local bottom1, bottom2 = state.pathCell[j][i][1], state.pathCell[j][i][2]
-- Thinning: strong bottom to empty top
if bottom1[1][3] == 1 and top1[1][1] == 0 then
top1[1][1] = 0.5 * bottom1[1][3]
top2[1][1] = 0.5 * bottom2[1][3]
top1.color = bottom1.color
top2.color = bottom2.color
-- Thickening: empty bottom to strong top
elseif bottom1[1][3] == 0 and top1[1][1] == 1 then
bottom1[1][3] = top1[1][1]
bottom2[1][3] = top2[1][1]
top1[1][1] = 0.5 * bottom1[1][3]
top2[1][1] = 0.5 * bottom2[1][3]
bottom1.color = top1.color
bottom2.color = top2.color
end
end
end
end
-- Thin double-in paths
local function thinDoubleInPaths(j, inpaths)
for _, ip in ipairs(inpaths) do
local i = ip[1]
-- skip if this is the last row (i+1 would be out of range)
if i < config.r then
local curr3 = state.pathCell[j][i][3]
local next3 = state.pathCell[j][i+1][3]
if curr3[1][3] == 1 and next3[1][1] == 1 and curr3.color == next3.color then
next3[1][1] = 0.5 * curr3[1][3]
end
end
end
end
-- ===== Cross-column connections =====
-- Connect straight paths between adjacent columns
local function connectStraightPaths()
for j = config.minc, config.c - 1 do
for i = 1, config.r - 1 do
local straightpath = false
-- Check if the top entry in next column is blank or a bye
local topEntry = state.entries[j+1][i-1]
local isTopBlankOrBye =
(topEntry == nil or (state.byes[j+1][topEntry.headerindex] and isBlankEntry(j+1, i-1)))
if isTopBlankOrBye then
local currPC, nextPC = state.pathCell[j], state.pathCell[j+1]
local currEntry, nextEnt = state.entries[j], state.entries[j+1]
local cond1 = (currPC[i][3][1][3] ~= 0 and nextPC[i][1][1][3] ~= 0)
local cond2 = (currPC[i+1][3][1][1] ~= 0 and nextPC[i+1][1][1][1] ~= 0)
if cond1 or cond2 then
if nextPC[i][1][1][3] == nextPC[i][3][1][3]
and nextPC[i+1][1][1][1] == nextPC[i+1][3][1][1] then
straightpath = true
end
-- Copy left path data from current col to next col
nextPC[i][1][1][3] = currPC[i][3][1][3]
nextPC[i+1][1][1][1] = currPC[i+1][3][1][1]
nextPC[i][2][1][3] = currPC[i][3][1][3]
nextPC[i+1][2][1][1] = currPC[i+1][3][1][1]
-- Update entries to represent connecting lines
nextEnt[i-1] = { ctype = 'line' }
nextEnt[i] = { ctype = 'blank' }
if notempty(nextEnt[i+1]) then
nextEnt[i+1].ctype = 'line2'
else
nextEnt[i+1] = { ctype = 'line2' }
end
nextEnt[i+2] = { ctype = 'blank' }
-- If perfectly straight path, mirror left values to right
if straightpath then
nextPC[i][3][1][3] = nextPC[i][1][1][3]
nextPC[i+1][3][1][1] = nextPC[i+1][1][1][1]
end
end
end
end
end
end
local function getPaths()
local paths = {}
-- Step 1: Determine which columns have cross paths
for j = config.minc, config.c - 1 do
state.hascross[j] = notempty(fargs['col' .. j .. '-col' .. (j + 1) .. '-cross'])
end
-- Step 2: Process each column
for j = config.minc, config.c - 1 do
paths[j] = parsePaths(j)
-- Initialize per-column path data
state.pathCell[j], state.crossCell[j], state.skipPath[j] = {}, {}, {}
for i = 1, config.r do
state.pathCell[j][i] = {}
for k = 1, 3 do
state.pathCell[j][i][k] = { {0, 0, 0, 0}, color = COLORS.path_line_color }
end
state.crossCell[j][i] = {
left = {0, COLORS.path_line_color},
right = {0, COLORS.path_line_color}
}
state.skipPath[j][i] = false
end
-- Prepare cross location list
local crossloc = split((fargs['col' .. j .. '-col' .. (j + 1) .. '-cross'] or '')
:gsub("%s+", ""), {","}, true)
local hasCrossLoc = notempty(crossloc[1])
if state.shift[j] ~= 0 and hasCrossLoc then
for n = 1, #crossloc do
crossloc[n] = crossloc[n] + state.shift[j]
end
end
-- Temporary holders for later thickening/thinning
local straightpaths, outpaths, inpaths = {}, {}, {}
-- Step 3: Process each path in the current column
for _, path in ipairs(paths[j]) do
local startRow = 2 * (path[1] + state.shift[j]) + (state.teamsPerMatch[j] - 2)
local stopRow = 2 * (path[2] + state.shift[j + 1]) + (state.teamsPerMatch[j + 1] - 2)
-- Build cross rows
local crossRows = {0}
if hasCrossLoc then
for n = 1, #crossloc do
crossRows[n] = 2 * crossloc[n] + (state.teamsPerMatch[j] - 2)
end
end
-- Find applicable cross row
local cross = 0
for _, cr in ipairs(crossRows) do
if (startRow < stopRow and cr < stopRow and cr > startRow)
or (startRow > stopRow and cr > stopRow and cr < startRow) then
cross = cr
end
end
local color = path.color or COLORS.path_line_color
t_insert(outpaths, { startRow, color })
t_insert(inpaths, { stopRow, color })
if not isPathHidden(j, i, startRow, stopRow) then
if startRow == stopRow then
handleStraightPath(j, startRow, color, straightpaths)
elseif startRow < stopRow then
handleDownwardPath(j, startRow, stopRow, cross, color)
else
handleUpwardPath(j, startRow, stopRow, cross, color)
end
end
end
-- Step 4: Apply thickness adjustments
thickenStraightPaths(j, straightpaths)
adjustOutpaths(j, outpaths)
thinDoubleInPaths(j, inpaths)
end
-- Step 5: Connect straight paths across columns
connectStraightPaths()
end
local function getGroups()
-- Helper: true if cell (or the next cell) is empty or blank text
local function isBlankOrBlankText(j, i)
if state.entries[j][i] == nil then
if state.entries[j][i + 1] == nil then
return true
elseif state.entries[j][i + 1].ctype == 'text' and isBlankEntry(j, i + 1) then
return true
end
elseif state.entries[j][i].ctype == 'text' and isBlankEntry(j, i) then
return true
end
return false
end
for j = config.minc, config.c - 1 do
-- Only handle standard 2-team matches (same as original)
if state.teamsPerMatch[j] == 2 then
local groupIndex = 0
for i = 1, config.r - 1 do
if state.pathCell[j][i][3][1][3] == 1 or state.pathCell[j][i + 1][3][1][1] == 1 then
groupIndex = groupIndex + 1
if isBlankOrBlankText(j, i) then
local k = config.minc - 1
repeat
-- guard state.entries[j-k] existence before indexing
if state.entries[j - k] and state.entries[j - k][i + 1]
and state.entries[j - k][i + 1].ctype == 'text'
and isBlankEntry(j - k, i + 1) then
state.entries[j - k][i + 2] = nil
end
-- create blank placeholders as in original logic
if state.entries[j - k] == nil then state.entries[j - k] = {} end
state.entries[j - k][i] = { ctype = 'blank' }
state.entries[j - k][i+1] = { ctype = 'blank' }
if k > 0 and noPaths(j - k, i) then
state.skipPath[j - k][i] = true
state.skipPath[j - k][i+1] = true
end
k = k + 1
until k > j - 1 or not isBlankOrBlankText(j - k, i) or not noPaths(j - k, i)
k = k - 1
if state.entries[j - k] == nil then state.entries[j - k] = {} end
state.entries[j - k][i] = {
ctype = 'group',
index = groupIndex,
colspan = k + 1
}
state.entries[j - k][i + 1] = { ctype = 'blank' }
state.entries[j - k][i].group = bargs('RD' .. j .. '-group' .. groupIndex)
end
end
end
end
end
end
local function getCells()
local DEFAULT_TPM = 2
local maxrow = 1
local colentry = {}
local hasNoHeaders = true
-- Phase 1: Determine header presence and teamsPerMatch
for j = config.minc, config.c do
if notempty(fargs["col" .. j .. "-headers"]) then
hasNoHeaders = false
end
state.teamsPerMatch[j] =
tonumber(fargs["RD" .. j .. "-teams-per-match"]) or
tonumber(fargs["col" .. j .. "-teams-per-match"]) or
tonumber(fargs["teams-per-match"]) or
DEFAULT_TPM
state.maxtpm = m_max(state.maxtpm, state.teamsPerMatch[j])
end
-- Phase 2: Build colentry for each column
for j = config.minc, config.c do
state.entries[j] = {}
state.shift[j] = tonumber(bargs("RD" .. j .. "-shift")) or tonumber(bargs("shift")) or 0
colentry[j] = {
split((fargs["col" .. j .. "-headers"] or ""):gsub("%s+", ""), {","}, true),
split((fargs["col" .. j .. "-matches"] or ""):gsub("%s+", ""), {","}, true),
split((fargs["col" .. j .. "-lines"] or ""):gsub("%s+", ""), {","}, true),
split((fargs["col" .. j .. "-text"] or ""):gsub("%s+", ""), {","}, true),
}
if hasNoHeaders and fargs["noheaders"] ~= "y" and fargs["noheaders"] ~= "yes" then
t_insert(colentry[j][1], 1)
end
end
-- Ctype mapping for colentry positions
local CTYPE_MAP = { "header", "team", "line", "text", "group" }
-- Helper functions for each ctype
local function populateTeam(j, rowIndex, n)
if state.entries[j][rowIndex - 1] == nil and state.entries[j][rowIndex - 2] == nil then
state.entries[j][rowIndex - 2] = { ctype = "text", index = n }
state.entries[j][rowIndex - 1] = { ctype = "blank" }
end
state.entries[j][rowIndex] = { ctype = "team", index = state.teamsPerMatch[j] * n - (state.teamsPerMatch[j] - 1), position = "top" }
state.entries[j][rowIndex + 1] = { ctype = "blank" }
for m = 2, state.teamsPerMatch[j] do
local idx = state.teamsPerMatch[j] * n - (state.teamsPerMatch[j] - m)
state.entries[j][rowIndex + 2 * (m - 1)] = { ctype = "team", index = idx }
state.entries[j][rowIndex + 2 * (m - 1) + 1] = { ctype = "blank" }
end
end
local function populateText(j, rowIndex, index)
state.entries[j][rowIndex] = { ctype = "text", index = index }
state.entries[j][rowIndex + 1] = { ctype = "blank" }
end
local function populateLine(j, rowIndex)
state.entries[j][rowIndex] = { ctype = "line" }
state.entries[j][rowIndex + 1] = { ctype = "blank" }
state.entries[j][rowIndex + 2] = { ctype = "line2" }
state.entries[j][rowIndex + 3] = { ctype = "blank" }
end
local function populateGroup(j, rowIndex, n)
state.entries[j][rowIndex] = { ctype = "group", index = n }
state.entries[j][rowIndex + 1] = { ctype = "blank" }
end
local function populateDefault(j, rowIndex, n)
state.entries[j][rowIndex] = { ctype = "header", index = n, position = "top" }
state.entries[j][rowIndex + 1] = { ctype = "blank" }
end
-- Phase 3: Populate entries for each column
for j = config.minc, config.c do
local textindex = 0
for k, positions in ipairs(colentry[j]) do
t_sort(positions)
local ctype = CTYPE_MAP[k]
for n = 1, #positions do
if state.shift[j] ~= 0 and positions[n] > 1 then
positions[n] = positions[n] + state.shift[j]
end
local rowIndex = 2 * positions[n] - 1
maxrow = m_max(rowIndex + 2 * state.teamsPerMatch[j] - 1, maxrow)
if ctype == "team" then
populateTeam(j, rowIndex, n)
textindex = n
elseif ctype == "text" then
populateText(j, rowIndex, textindex + n)
elseif ctype == "line" then
populateLine(j, rowIndex)
elseif ctype == "group" then
populateGroup(j, rowIndex, n)
else
populateDefault(j, rowIndex, n)
end
end
end
end
if isempty(config.r) then
config.r = maxrow
end
end
--=== Build HTML output ===--
local function buildTable()
local frame = mw.getCurrentFrame()
local div = mw_html_create('div'):css('overflow', 'auto')
-- Load TemplateStyles from the Template namespace
div:wikitext(frame:extensionTag('templatestyles', '', {
src = 'Module:Build bracket/styles.css'
}))
if config.height ~= 0 then div:css('height', config.height) end
local tbl = mw_html_create('table'):addClass('brk')
if config.nowrap then tbl:addClass('brk-nw') end
-- Invisible header row for column widths
tbl:tag('tr'):css('visibility', 'collapse')
tbl:tag('td'):css('width', '1px')
for j = config.minc, config.c do
if config.seeds then tbl:tag('td'):css('width', getWidth('seed', '25px')) end
tbl:tag('td'):css('width', getWidth('team', '150px'))
local scoreWidth = getWidth('score', '25px')
if state.maxlegs[j] == 0 then
tbl:tag('td'):css('width', scoreWidth)
else
for l = 1, state.maxlegs[j] do
tbl:tag('td'):css('width', scoreWidth)
end
end
if config.aggregate and state.maxlegs[j] > 1 then
tbl:tag('td'):css('width', getWidth('agg', scoreWidth))
end
if j ~= config.c then
if state.hascross[j] then
tbl:tag('td'):css('width', config.colspacing - 3 .. 'px'):css('padding-left', '4px')
tbl:tag('td'):css('padding-left', '5px'):css('width', '5px')
tbl:tag('td'):css('width', config.colspacing - 3 .. 'px'):css('padding-right', '2px')
else
tbl:tag('td'):css('width', config.colspacing - 3 .. 'px'):css('padding-left', '4px')
tbl:tag('td'):css('width', config.colspacing - 3 .. 'px'):css('padding-right', '2px')
end
end
end
-- Table rows
for i = 1, config.r do
local row = tbl:tag('tr')
row:tag('td'):css('height', '11px')
for j = config.minc, config.c do
insertEntry(row, j, i)
if j ~= config.c then insertPath(row, j, i) end
end
end
div:wikitext(tostring(tbl))
return tostring(div)
end
--=== Main function ===--
function p.main(frame)
frame:extensionTag('templatestyles', '', { src = 'Module:Build bracket/styles.css' })
-- Reset state and config for each run
state = {
entries = {},
pathCell = {},
crossCell = {},
skipPath = {},
shift = {},
hascross = {},
teamsPerMatch = {},
rlegs = {},
maxlegs = {},
byes = {},
hide = {},
matchgroup = {},
headerindex = {},
masterindex = 1,
maxtpm = 1, -- used later
}
config = {
autolegs = false,
nowrap = true,
autocol = false,
seeds = true,
forceseeds = false,
boldwinner = false,
aggregate = false,
paramstyle = "indexed",
colspacing = 5,
height = 0,
minc = 1,
c = 1, -- number of rounds
r = nil, -- rows (set later)
}
parseArgs(frame, config)
-- === Data processing phases ===
getCells(state, config)
getAltIndices(state, config)
assignParams(state, config)
matchGroups(state, config)
if yes(config.boldwinner) or config.boldwinner == 'high' then
boldWinner(state, config)
end
getPaths(state, config)
if config.minc == 1 then
getGroups(state, config)
end
updateMaxLegs(state, config)
-- === Output ===
return buildTable(state, config)
end
return p