Module:Sandbox/Pbrks
Jump to navigation
Jump to search
---
--- Build Bracket (refactor skeleton)
--- Goal: separate data phases, minimize mutation, clarify responsibilities.
--- This is a FIRST PASS skeleton wired to p.main(frame).
--- We'll port functionality in phases while keeping the public API.
---
local p = {}
-- =====================
-- Localized stdlib
-- =====================
local str_gsub, str_match, str_find = string.gsub, string.match, string.find
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 and mw.html and mw.html.create
-- =====================
-- Small utilities (pure)
-- =====================
local function isempty(s) return s == nil or s == '' end
local function notempty(s) return s ~= nil and s ~= '' end
local function yesish(v)
v = (tostring(v or ''):lower())
return v == 'y' or v == 'yes' or v == 'true' or v == '1'
end
local function noish(v)
v = (tostring(v or ''):lower())
return v == 'n' or v == 'no' or v == 'false' or v == '0'
end
local function toChar(num) return string.char(string.byte('a') + num - 1) end
-- =====================
-- Types (documentation only)
-- =====================
-- Config = {
-- c:number, r:number|nil, minc:number,
-- autocol:boolean, autolegs:boolean,
-- seeds:boolean, forceseeds:boolean,
-- boldwinner_mode:'off'|'high'|'low', boldwinner_aggonly:boolean,
-- aggregate:boolean, aggregate_mode:'off'|'manual'|'sets'|'score',
-- colspacing:number, height:number, nowrap:boolean,
-- paramstyle:'indexed'|'numbered',
-- }
--
-- Grid[j][i] = Cell where Cell has a stable shape:
-- { ctype='header'|'team'|'text'|'line'|'group'|'blank',
-- index:number?, altindex:number?, headerindex:number?,
-- position:'top'|nil,
-- team:string?, seed:string?,
-- legs:number?,
-- score={ [1]=string?, [2]=string?, agg=string?, weight=table }?,
-- weight:'bold'|'normal'|nil,
-- pheader:string?, header:string?, shade:string?, shade_is_rd:boolean?,
-- group:string?, colspan:number?,
-- borderWidth:{number,number,number,number}? ,
-- border:'top'|'bottom'|'both'|nil,
-- }
--
-- Derived = {
-- rlegs = { [j]=number },
-- maxlegs = { [j]=number },
-- teamsPerMatch = { [j]=number },
-- headercount = { [j]=number },
-- hide = { [j] = { [headerindex]=true } },
-- byes = { [j] = { [headerindex]=true } },
-- matchgroup = { [j] = { [i]=groupId } },
-- pathCell, crossCell, skipPath, hascross, shift,
-- }
-- =====================
-- Phase 0. Arg access
-- =====================
local function makeArgReaders(frame)
local fargs = frame.args or {}
local pargs = (frame.getParent and frame:getParent().args) or {}
local function bargs(k) return pargs[k] or fargs[k] end
return fargs, pargs, bargs
end
-- =====================
-- Phase 1. Parse args → Config (pure)
-- =====================
local function parseConfig(fargs, pargs, bargs)
local cfg = {
r = tonumber(fargs.rows) or nil,
c = tonumber(fargs.rounds) or 1,
minc = tonumber(pargs.minround) or 1,
autocol = yesish(fargs.autocol),
colspacing = tonumber(fargs['col-spacing']) or 5,
height = bargs('height') or 0,
-- seeds
forceseeds = yesish(pargs.seeds),
seeds = not noish(pargs.seeds),
-- boldwinner
boldwinner_mode = 'off',
boldwinner_aggonly = false,
-- aggregate
aggregate = false,
aggregate_mode = 'off',
autolegs = yesish(pargs.autolegs),
paramstyle = (bargs('paramstyle') == 'numbered') and 'numbered' or 'indexed',
nowrap = not noish(pargs.nowrap),
deprecation_category = 'Pages using Build Bracket with deprecated parameters',
}
-- rounds override via maxround(s)
local maxc = tonumber(pargs.maxrounds) or tonumber(pargs.maxround)
if maxc then cfg.c = maxc end
-- boldwinner legacy switch mapping
do
local bw = (bargs('boldwinner') or ''):lower()
if bw == 'low' then cfg.boldwinner_mode = 'low'
elseif bw == 'high' or yesish(bw) then cfg.boldwinner_mode = 'high'
elseif bw == 'aggregate' or bw == 'agg' or bw == 'aggregate-high' or bw == 'agg-high' then
cfg.boldwinner_mode = 'high'; cfg.boldwinner_aggonly = true
elseif bw == 'aggregate-low' or bw == 'agg-low' then
cfg.boldwinner_mode = 'low'; cfg.boldwinner_aggonly = true
end
if yesish(bargs('boldwinner-aggregate-only')) then cfg.boldwinner_aggonly = true end
end
-- aggregate mode
do
local aval = (bargs('aggregate') or ''):lower()
if aval == 'sets' or aval == 'legs' then
cfg.aggregate_mode = 'sets'; cfg.aggregate = true
elseif aval == 'score' then
cfg.aggregate_mode = 'score'; cfg.aggregate = true
elseif yesish(aval) then
cfg.aggregate_mode = 'manual'; cfg.aggregate = true
else
cfg.aggregate_mode = 'off'; cfg.aggregate = false
end
end
return cfg
end
-- =====================
-- Phase 2. Build initial grid (headers/teams/lines/text) → Grid
-- NOTE: This mirrors the original getCells(), but returns a new grid and a
-- small Derived stub. We keep arg names compatible (colX-headers, etc.).
-- =====================
local function splitCSVInts(s)
if isempty(s) then return {} end
local out = {}
s = s:gsub('%s+', '')
for n in s:gmatch('[^,]+') do
local v = tonumber(n)
if v then t_insert(out, v) end
end
return out
end
local function buildInitialGrid(cfg, fargs, pargs, bargs)
local grid = {}
local derived = {
teamsPerMatch = {},
shift = {},
maxtpm = 1,
}
local DEFAULT_TPM = 2
-- detect explicit headers
local hasHeaders = false
for j = cfg.minc, cfg.c do
if not isempty(fargs['col'..j..'-headers']) then hasHeaders = true end
end
-- pass 1: get tpm per round and track maxtpm
for j = cfg.minc, cfg.c do
local tpm = tonumber(fargs['RD'..j..'-teams-per-match'])
or tonumber(fargs['col'..j..'-teams-per-match'])
or tonumber(fargs['teams-per-match'])
or DEFAULT_TPM
derived.teamsPerMatch[j] = tpm
if tpm > (derived.maxtpm or 1) then derived.maxtpm = tpm end
end
-- pass 2: populate skeleton cells
local maxrow = 1
for j = cfg.minc, cfg.c do
grid[j] = {}
derived.shift[j] = tonumber(bargs('RD'..j..'-shift')) or tonumber(bargs('shift')) or 0
local headers = splitCSVInts(fargs['col'..j..'-headers'] or '')
local matches = splitCSVInts(fargs['col'..j..'-matches'] or '')
local lines = splitCSVInts(fargs['col'..j..'-lines'] or '')
local texts = splitCSVInts(fargs['col'..j..'-text'] or '')
-- Auto-insert a header if no explicit headers passed anywhere and noheaders flag not set.
if not hasHeaders and (fargs['noheaders'] ~= 'y' and fargs['noheaders'] ~= 'yes') then
t_insert(headers, 1)
end
local function mark(i, cell)
grid[j][i] = cell
end
local function populateHeader(pos)
local i = 2 * (pos + (derived.shift[j] or 0)) - 1
mark(i, { ctype='header', index=#headers, position='top' })
mark(i+1, { ctype='blank' })
maxrow = m_max(maxrow, i+1)
end
local function populateTeamGroup(n)
local start = 2 * (n + (derived.shift[j] or 0)) - 1
-- ensure a text row before first team block
if grid[j][start-1] == nil and grid[j][start-2] == nil then
mark(start-2, { ctype='text', index = n })
mark(start-1, { ctype='blank' })
end
-- top team
mark(start, { ctype='team', index = derived.teamsPerMatch[j]*n - (derived.teamsPerMatch[j]-1), position='top' })
mark(start + 1, { ctype='blank' })
-- remaining teams in match
for m=2, derived.teamsPerMatch[j] do
local idx = derived.teamsPerMatch[j]*n - (derived.teamsPerMatch[j]-m)
local row = start + 2*(m-1)
mark(row, { ctype='team', index=idx })
mark(row + 1, { ctype='blank' })
end
maxrow = m_max(maxrow, start + 2*derived.teamsPerMatch[j]-1)
end
local function populateLine(pos)
local i = 2 * (pos + (derived.shift[j] or 0)) - 1
mark(i, { ctype='line', border='bottom' })
mark(i+1, { ctype='blank' })
mark(i+2, { ctype='line', border='top' })
mark(i+3, { ctype='blank' })
maxrow = m_max(maxrow, i+3)
end
local function populateText(pos, textindex)
local i = 2 * (pos + (derived.shift[j] or 0)) - 1
mark(i, { ctype='text', index=textindex })
mark(i+1, { ctype='blank' })
maxrow = m_max(maxrow, i+1)
end
-- Sorted application keeps row math simple
table.sort(headers); table.sort(matches); table.sort(lines); table.sort(texts)
local textindexCounter = 0
for _,pos in ipairs(headers) do populateHeader(pos) end
for n,_ in ipairs(matches) do populateTeamGroup(n) end
for _,pos in ipairs(lines) do populateLine(pos) end
for k,_ in ipairs(texts) do textindexCounter = textindexCounter + 1; populateText(texts[k], #matches + textindexCounter) end
end
-- finalize row count
if cfg.r == nil then
local mr = 1
for j = cfg.minc, cfg.c do
for i, _ in pairs(grid[j]) do if i > mr then mr = i end end
end
cfg.r = mr
end
return grid, derived
end
-- =====================
-- Phase 3+. Placeholders to port next
-- =====================
local function computeAltIndices(grid, cfg, fargs, pargs, bargs)
-- TODO: port getAltIndices() but return: headercount[j], and set .altindex/.headerindex on cells (pure-ISH)
local headercount = {}
for j = cfg.minc, cfg.c do
headercount[j] = 0
local teamindex, textindex, groupindex = 1, 1, 1
local row = grid[j]
if row and row[1] == nil then headercount[j] = headercount[j] + 1 end
for i=1, cfg.r do
local e = row and row[i]
if e then
if e.ctype == 'header' then e.altindex = headercount[j]; teamindex, textindex = 1, 1; headercount[j] = headercount[j] + 1
elseif e.ctype == 'team' then e.altindex = teamindex; teamindex = teamindex + 1
elseif e.ctype == 'text' then e.altindex = textindex; textindex = textindex + 1
elseif e.ctype == 'group' then e.altindex = groupindex; groupindex = groupindex + 1 end
e.headerindex = headercount[j]
end
end
end
return headercount
end
local function computeHideAndByes(grid, cfg, bargs, headercount)
-- Lua 5.1-safe (no goto). Port of getHide/getByes and a basic empty-round hide pass.
local hide, byes = {}, {}
for j = cfg.minc, cfg.c do
hide[j], byes[j] = {}, {}
for k=1,(headercount[j] or 0) do
-- hide flags
local h = bargs('RD'..j..toChar(k)..'-hide')
hide[j][k] = (h == 'y' or h == 'yes') and true or false
-- byes: global, RDj, RDja (last writer wins, matching original precedence)
local v = false
local globalByes = bargs('byes')
if yesish(globalByes) then v = true
elseif tonumber(globalByes) and j <= tonumber(globalByes) then v = true end
local rd = bargs('RD'..j..'-byes'); if yesish(rd) then v = true elseif noish(rd) then v = false end
local rda = bargs('RD'..j..toChar(k)..'-byes'); if yesish(rda) then v = true end
byes[j][k] = v
end
end
-- helper
local function isBlankEntry(j,i)
local col = grid[j]; local e = col and col[i]
if not e then return true end
if e.ctype == 'team' then return (e.team == nil or e.team == '') and (e.text == nil or e.text == '') end
if e.ctype == 'text' then return (e.text == nil or e.text == '') end
return false
end
-- hide empty-header rounds (no goto; nest conditions)
for j = cfg.minc, cfg.c do
for i=1, cfg.r do
local e = grid[j] and grid[j][i]
if e and e.ctype == 'header' then
local hidx = e.headerindex
if hidx then
local row = i+1; local any=false
while row <= cfg.r do
local rce = grid[j][row]
if rce and rce.ctype == 'header' then break end
if rce and rce.ctype ~= 'blank' and not isBlankEntry(j,row) then any=true; break end
row = row + 1
end
if not any then hide[j][hidx] = hide[j][hidx] or true end
end
end
end
end
return hide, byes
end
-- Assign parameters (seed/team/score/header/text/group/line text)
local function assignParams(grid, cfg, fargs, pargs, bargs, derived)
-- TODO: port paramNames() logic; for now, minimal pass to keep structure.
return grid -- unchanged placeholder
end
-- Aggregates & bolding (pure)
local function computeAggregatesAndBold(grid, cfg, derived)
-- TODO: port computeAggregate() and boldWinner() in a pure style.
end
-- Paths (pure data) and post-processing merges
local function computePaths(grid, cfg, fargs, pargs, bargs, derived)
-- TODO: port getPaths() and connect logic but return pathCell/crossCell/skipPath/hascross; do not mutate grid here.
derived.pathCell, derived.crossCell, derived.skipPath, derived.hascross = {}, {}, {}, {}
return derived
end
local function updateMaxLegs(grid, cfg, derived)
-- TODO: port updateMaxLegs() against current grid entries
derived.rlegs, derived.maxlegs = derived.rlegs or {}, derived.maxlegs or {}
for j = cfg.minc, cfg.c do
derived.rlegs[j] = derived.rlegs[j] or 1
derived.maxlegs[j] = derived.maxlegs[j] or derived.rlegs[j]
end
end
-- =====================
-- Rendering (impure, view-only, no surprises)
-- =====================
local COLORS = {
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)'
}
local function cellBorder(b) return (b[1]..'px '..b[2]..'px '..b[3]..'px '..b[4]..'px') end
local function renderTable(grid, cfg, derived)
local frame = mw.getCurrentFrame()
local div = mw_html_create('div'):css('overflow','auto')
div:wikitext(frame:extensionTag('templatestyles','',{ src='Module:Build bracket/styles.css' }))
if cfg.height ~= 0 then div:css('height', cfg.height) end
local tbl = mw_html_create('table'):addClass('brk')
if cfg.nowrap then tbl:addClass('brk-nw') end
-- invisible header row for col widths (placeholder; widths will depend on derived.maxlegs)
tbl:tag('tr'):css('visibility','collapse'):tag('td'):css('width','1px')
-- TODO: set width cells using derived.maxlegs/aggregate/seeds just like original
for i=1, cfg.r do
local row = tbl:tag('tr')
row:tag('td'):css('height','11px')
for j=cfg.minc, cfg.c do
local e = grid[j] and grid[j][i]
local td = row:tag('td')
if e then
td:wikitext(e.ctype or ''):css('font-size','smaller'):css('color','#666')
else
td:wikitext('')
end
-- TODO: replace with real insertors once phases are fully ported
end
end
div:wikitext(tostring(tbl))
return tostring(div)
end
-- =====================
-- Deprecation cats (kept simple for now)
-- =====================
local function emitDeprecationCats()
local title = mw.title.getCurrentTitle()
if not title or title.namespace ~= 0 then return '' end
return ''
end
-- =====================
-- MAIN
-- =====================
function p.main(frame)
-- Styles once
frame:extensionTag('templatestyles','',{ src='Module:Build bracket/styles.css' })
-- Phase 0/1
local fargs, pargs, bargs = makeArgReaders(frame)
local cfg = parseConfig(fargs, pargs, bargs)
-- Phase 2
local grid, derived = buildInitialGrid(cfg, fargs, pargs, bargs)
-- Phase 3+
derived.headercount = computeAltIndices(grid, cfg, fargs, pargs, bargs)
derived.hide, derived.byes = computeHideAndByes(grid, cfg, bargs, derived.headercount)
grid = assignParams(grid, cfg, fargs, pargs, bargs, derived)
computeAggregatesAndBold(grid, cfg, derived)
computePaths(grid, cfg, fargs, pargs, bargs, derived)
updateMaxLegs(grid, cfg, derived)
-- Output
local out = renderTable(grid, cfg, derived)
return tostring(out) .. emitDeprecationCats()
end
return p