local p = {}
local entries = {}
local pathCell = {}
local crossCell = {}
local skipPath = {}
local shift = {}
local hascross = {}
local teams_per_match = {}
local rlegs = {}
local maxlegs = {}
local autolegs
local byes = {}
local hide = {}
local matchgroup = {}
local nowrap
local autocol
local seeds
local forceseeds
local boldwinner
local aggregate
local paramstyle
local masterindex
--========================
-- 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 = 'var(--border-color-base,#a2a9b1)',
cell_border = 'var(--border-color-base,#a2a9b1)',
}
local function isempty(s)
return s==nil or s==''
end
local function notempty(s)
return s~=nil and s~=''
end
local function bargs(s)
return pargs[s] or fargs[s]
end
local function toChar(num)
return string.char(string.byte("a")+num-1)
end
local function unboldParenthetical(text)
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)
result = {};
local a = "[^"..table.concat(delim).."]+"
for w in str:gmatch(a) do
if tonum==true then
table.insert(result, tonumber(w));
else
table.insert(result, w);
end
end
return result;
end
local function getWidth(ctype, default)
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 matchGroups()
for j=minc,c do
matchgroup[j]={}
for i=1,r do
if entries[j][i]~= nil and entries[j][i]['ctype']=='team' then
matchgroup[j][i]=math.ceil(entries[j][i]['index']/teams_per_match[j])
entries[j][i]['group'] = math.ceil(entries[j][i]['index']/teams_per_match[j])
end
end
end
end
local function teamLegs(j,i)
local legs = rlegs[j]
if notempty(entries[j][i]['legs']) then
legs = tonumber(entries[j][i]['legs'])
end
if autolegs then
local l=1
repeat l=l+1
until isempty(entries[j][i]['score'][l])
legs = l-1
end
return legs
end
local function boldWinner()
-- Helper: compare one team's score to its opponent for a given leg
local function boldScore(j, i, l)
local e = 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(matchgroup[j]) do
if groupId == matchgroup[j][i] and oppIndex ~= i then
local theirraw = entries[j][oppIndex].score[l] or ''
local theirscore = tonumber(theirraw:match('^%d+'))
if not theirscore then return 'normal' end
table.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 = 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 = 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 string.find(scoreVal or '', "nbsp") then
return 'normal'
end
end
end
-- Compare wins to opponent
for oppIndex, groupId in pairs(matchgroup[j]) do
if groupId == matchgroup[j][i] and oppIndex ~= i then
if e[winsKey] <= tonumber(entries[j][oppIndex][winsKey]) then
return 'normal'
end
end
end
return 'bold'
end
-- Reset win counts
for j = minc, c do
for i = 1, r do
if entries[j][i] and entries[j][i].ctype == 'team' then
entries[j][i].wins = 0
entries[j][i].aggwins = 0
end
end
-- Determine bold for scores
for i = 1, r do
if entries[j][i] and entries[j][i].ctype == 'team' then
local legs = teamLegs(j, i)
for l = 1, legs do
entries[j][i].score.weight[l] = boldScore(j, i, l)
end
if aggregate and legs > 1 then
entries[j][i].score.weight.agg = boldScore(j, i, 'agg')
end
end
end
-- Determine bold for entire team
for i = 1, r do
if entries[j][i] and entries[j][i].ctype == 'team' then
local useAggregate = aggregate and teamLegs(j, i) > 1
entries[j][i].weight = boldTeam(j, i, useAggregate)
end
end
end
end
local function isBlankEntry(col,row,ctype)
if isempty(entries[col][row]) then return true end
if isempty(entries[col][row]['team']) and isempty(entries[col][row]['text']) then return true end
return false
end
local function showSeeds(j,i)
local showseed=false
if forceseeds or notempty(entries[j][i]['seed']) then
showseed=true
else
for k=1,teams_per_match[j]-1 do
if notempty(entries[j][i+2*k]) and entries[j][i]['group']==entries[j][i+2*k]['group'] and notempty(entries[j][i+2*k]['seed']) then
showseed=true
end
if notempty(entries[j][i-2*k]) and entries[j][i]['group']==entries[j][i-2*k]['group'] and notempty(entries[j][i-2*k]['seed']) then
showseed=true
end
end
end
return showseed
end
local function cellBorder(b)
return b[1]..'px '..b[2]..'px '..b[3]..'px '..b[4]..'px'
end
local function Cell(tbl,j,i,rowspan,colspan,text,align,border,border_width,bg,padding,weight,nwrap)
local cell = tbl:tag('td')
if colspan~=1 then
cell:attr('colspan',colspan)
end
if rowspan~=1 then
cell:attr('rowspan',rowspan)
end
if notempty(border) then
cell:css('border',border)
end
if notempty(border_width) then
cell:css('border-width',cellBorder(border_width))
end
if notempty(bg) then
cell:css('background-color',bg)
end
if notempty(align) then
cell:css('text-align',align)
end
cell:css('padding','0em 0.3em')
if weight=='bold' then
cell:css('font-weight',weight)
end
if notempty(text) then
cell:wikitext(text)
end
cell:css('color', COLORS.text_color)
return cell
end
local function teamCell(tbl,k,j,i,l,colspan)
local bg = COLORS.cell_bg_dark
local align
local padding
local weight
local text
local nwrap
local b={0,0,1,1}
if k=='seed' or k=='score' then
align='center'
end
if k~='seed' then
bg=COLORS.cell_bg_light
end
if k=='team' then
padding='0.3em'
if teamLegs(j,i)==0 then
b[2]=1
end
end
if entries[j][i]['position']=='top' then
b[1]=1
end
if l==teamLegs(j,i) or l=='agg' or k=='seed' then
b[2]=1
end
if (l==nil and entries[j][i]['weight']=='bold') or entries[j][i]['score']['weight'][l]=='bold' then
weight='bold'
end
if l==nil then
text=unboldParenthetical(entries[j][i][k])
else
text=tostring(entries[j][i][k][l])
end
return Cell(tbl,j,i,2,colspan,text,align,'solid ' .. COLORS.cell_border,b,bg,padding,weight,nwrap)
end
-- Compute standard colspan for a single "entry" cell in column j
local function getEntryColspan(j)
local colspan = maxlegs[j] + 2
if not seeds then
colspan = colspan - 1
end
if (aggregate and maxlegs[j] > 1) or maxlegs[j] == 0 then
colspan = colspan + 1
end
return colspan
end
-- Determine whether the "round" after header at (j,i) is empty (used for bye detection)
local function roundIsEmpty(j, i)
-- 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 (entries[j][row] ~= nil and entries[j][row]['ctype'] == 'header') or row > r
return true
end
-- Default header text when none is provided
local function defaultHeaderText(j, headerindex)
if headerindex == 1 then
if j == c then
return 'Final'
elseif j == c - 1 then
return 'Semifinals'
elseif j == c - 2 then
return 'Quarterfinals'
else
return 'Round ' .. j
end
else
return 'Lower round ' .. j
end
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 entries[j][i] == nil then
-- if previous entry exists or this is the first row, create a rowspan covering following blank rows
if entries[j][i - 1] ~= nil or i == 1 then
local rowspan = 0
local row = i
repeat
rowspan = rowspan + 1
row = row + 1
until entries[j][row] ~= nil or row > r
-- produce an empty cell with the computed rowspan/colspan
Cell(tbl, j, i, rowspan, entry_colspan)
return true
else
-- do nothing (cell intentionally omitted)
return true
end
end
-- explicit 'blank' ctype: do nothing (no visible content)
if 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)
-- If entire round is a bye & header exists, skip rendering (return blank cell)
if byes[j][entry.headerindex] and roundIsEmpty(j, i) then
return Cell(tbl, j, i, 2, entry_colspan)
end
-- If this header is hidden, render an empty spanning cell
if hide[j][entry.headerindex] then
return Cell(tbl, j, i, 2, entry_colspan)
end
-- fill default header text if not set
if isempty(entry.header) then
if entry.headerindex == 1 then
entry.header = defaultHeaderText(j, entry.headerindex)
else
entry.header = defaultHeaderText(j, entry.headerindex)
end
end
return Cell(tbl, j, i, 2, entry_colspan, entry.header, 'center', '1px solid ' .. COLORS.cell_border, nil, entry.shade)
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 (byes[j][entry.headerindex] and isBlankEntry(j, i)) or hide[j][entry.headerindex] then
return Cell(tbl, j, i, 2, entry_colspan)
end
local legs = teamLegs(j, i)
local team_colspan = maxlegs[j] - legs + 1
if aggregate and legs == 1 and maxlegs[j] > 1 then
team_colspan = team_colspan + 1
end
if maxlegs[j] == 0 then
team_colspan = team_colspan + 1
end
-- Seed column (if seeds enabled). Either render the seed cell or fold it into the team colspan.
if 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 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, 2, entry_colspan, entry.text, nil, nil, nil, nil, '0.3em')
end
-- Insert a group cell (spans several columns)
local function insertGroup(tbl, j, i, entry)
local colspan = 0
for m = j, entries[j][i]['colspan'] + j - 1 do
colspan = colspan + maxlegs[m] + 2
if not seeds then colspan = colspan - 1 end
if (aggregate and maxlegs[m] > 1) or maxlegs[m] == 0 then
colspan = colspan + 1
end
end
colspan = colspan + 2 * (entries[j][i]['colspan'] - 1)
return Cell(tbl, j, i, 2, colspan, entry.group, 'center')
end
-- Insert a line cell (visual path markers)
local function insertLine(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
local b = {0, 0, 0, 0}
-- Thicken based on pathCell values (mirrors original logic)
b[3] = 2 * pathCell[j - 1][i + 1][3][1][3]
return Cell(tbl, j, i, 2, entry_colspan, entry.text, nil, 'solid ' .. COLORS.cell_border, b)
end
-- Insert a line2 cell (alternative path styling)
local function insertLine2(tbl, j, i, entry)
local entry_colspan = getEntryColspan(j)
local b = {0, 0, 0, 0}
b[1] = 2 * pathCell[j - 1][i][3][1][1]
return Cell(tbl, j, i, 2, entry_colspan, entry.text, nil, 'solid ' .. COLORS.cell_border, b)
end
-- The new compact dispatcher for a single entry
local function insertEntry(tbl, j, i)
-- Early handling for nil / blank entries (will generate empty spanning cells when necessary)
if handleEmptyOrNilEntry(tbl, j, i) then return end
-- Now we know an actual entry exists
local entry = entries[j][i]
if not entry then return end
local ctype = entry['ctype']
if ctype == 'header' then
return insertHeader(tbl, j, i, entry)
elseif ctype == 'team' then
return insertTeam(tbl, j, i, entry)
elseif ctype == 'text' then
return insertText(tbl, j, i, entry)
elseif ctype == 'group' then
return insertGroup(tbl, j, i, entry)
elseif ctype == 'line' then
return insertLine(tbl, j, i, entry)
elseif ctype == 'line2' then
return insertLine2(tbl, j, i, entry)
else
-- Unknown ctype: fallback to a blank spanning cell
return Cell(tbl, j, i, 2, getEntryColspan(j))
end
end
local function isRoundHidden(j,i,headerindex)
if notempty(entries[j][i]['pheader']) then
hide[j][entries[j][i]['headerindex']] = false
end
local row = i+1
repeat
if not isBlankEntry(j,row) then
hide[j][entries[j][i]['headerindex']] = false
end
row = row+1
until (entries[j][row]~=nil and entries[j][row]['ctype']=='header') or row>r
end
local function paramNames(cname, j, i, l)
-- Helper: safely get a parameter by key
local function getParam(key)
return bargs(key) or ''
end
-- Build round name variants
local roundBase = 'RD' .. j
local roundAlt = bargs(roundBase .. '-altname') or roundBase
local roundLetter = 'RD' .. j .. toChar(entries[j][i].headerindex)
local roundLetterAlt = bargs(roundLetter .. '-altname') or roundLetter
-- Build common arrays
local rname = { {roundBase, roundAlt}, {roundLetter, roundLetterAlt} }
local name = { cname, bargs(cname .. '-altname') or cname }
local index = { entries[j][i].index, entries[j][i].altindex }
local result = {}
if cname == 'header' then
if entries[j][i].headerindex == 1 then
for _, base in ipairs({rname[1], rname[2]}) do
for k = 2, 1, -1 do
table.insert(result, getParam(base[k]))
end
end
else
for k = 2, 1, -1 do
table.insert(result, getParam(rname[2][k]))
end
end
elseif cname == 'pheader' then
if entries[j][i].headerindex == 1 then
for _, base in ipairs({rname[1], rname[2]}) do
for k = 2, 1, -1 do
table.insert(result, pargs[base[k]] or '')
end
end
else
for k = 2, 1, -1 do
table.insert(result, pargs[rname[2][k]] or '')
end
end
elseif cname == 'score' then
for m = 2, 1, -1 do
for k = 2, 1, -1 do
if l == 1 then
table.insert(result, getParam(rname[m][k] .. '-' .. name[1] .. index[m])
or getParam(rname[m][k] .. '-' .. name[1] .. '0' .. index[m]))
end
table.insert(result, getParam(rname[m][k] .. '-' .. name[1] .. index[m] .. '-' .. l)
or getParam(rname[m][k] .. '-' .. name[1] .. '0' .. index[m] .. '-' .. l))
end
end
elseif cname == 'shade' then
for k = 2, 1, -1 do
local roundKey = (entries[j][i].headerindex == 1) and rname[1][k] or rname[2][k]
table.insert(result, getParam(roundKey .. '-' .. name[1]))
end
table.insert(result, getParam('RD-shade'))
table.insert(result, COLORS.cell_bg_dark)
elseif cname == 'text' then
for n = 2, 1, -1 do
for m = 2, 1, -1 do
for k = 2, 1, -1 do
table.insert(result, getParam(rname[m][k] .. '-' .. name[n] .. index[m])
or getParam(rname[m][k] .. '-' .. name[n] .. '0' .. index[m]))
end
end
end
else -- Default case
for m = 2, 1, -1 do
for k = 2, 1, -1 do
table.insert(result, getParam(rname[m][k] .. '-' .. name[1] .. index[m])
or getParam(rname[m][k] .. '-' .. name[1] .. '0' .. index[m]))
end
end
end
-- Return first non-empty result
for _, val in ipairs(result) do
if notempty(val) then
return val
end
end
return ''
end
local function indexedParams(j)
for i=1,r do
if entries[j][i]~=nil then
if entries[j][i]['ctype']=='team' then
local legs = rlegs[j]
if forceseeds then
entries[j][i]['seed'] = bargs(masterindex) or ''
masterindex = masterindex+1
end
entries[j][i]['team'] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
entries[j][i]['legs'] = paramNames('legs',j,i)
entries[j][i]['score'] = {}
entries[j][i]['weight'] = 'normal'
entries[j][i]['score']['weight'] = {}
if notempty(entries[j][i]['legs']) then
legs = tonumber(entries[j][i]['legs'])
end
for l=1,legs do
entries[j][i]['score'][l] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
entries[j][i]['score']['weight'][l] = 'normal'
end
if aggregate and legs>1 then
entries[j][i]['score']['agg'] = bargs(masterindex) or ''
masterindex = masterindex+1
entries[j][i]['score']['weight']['agg'] = 'normal'
end
end
if entries[j][i]['ctype']=='header' then
entries[j][i]['header'] = paramNames('header',j,i)
entries[j][i]['pheader'] = paramNames('pheader',j,i)
entries[j][i]['shade'] = paramNames('shade',j,i)
end
if entries[j][i]['ctype']=='text' then
entries[j][i]['text'] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
end
if entries[j][i]['ctype']=='group' then
entries[j][i]['group'] = bargs(tostring(masterindex)) or ''
masterindex = masterindex+1
end
if entries[j][i]['ctype'] == 'line' and entries[j][i]['hastext']==true then
entries[j][i]['text'] = bargs(masterindex) or ''
masterindex = masterindex+1
end
end
end
end
local function assignTeamParams(j, i)
local legs = rlegs[j]
entries[j][i].seed = paramNames('seed', j, i)
entries[j][i].team = paramNames('team', j, i)
entries[j][i].legs = paramNames('legs', j, i)
entries[j][i].score = { weight = {} }
entries[j][i].weight = 'normal'
if notempty(entries[j][i].legs) then
legs = tonumber(entries[j][i].legs)
end
if autolegs then
local l = 1
repeat
entries[j][i].score[l] = paramNames('score', j, i, l)
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
entries[j][i].score[l] = paramNames('score', j, i, l)
entries[j][i].score.weight[l] = 'normal'
end
end
if aggregate and legs > 1 then
entries[j][i].score.agg = paramNames('score', j, i, 'agg')
entries[j][i].score.weight.agg = 'normal'
end
end
local function assignHeaderParams(j, i)
entries[j][i].header = paramNames('header', j, i)
entries[j][i].pheader = paramNames('pheader', j, i)
entries[j][i].shade = paramNames('shade', j, i)
end
local function assignTextParams(j, i)
entries[j][i].text = paramNames('text', j, i)
end
local function assignGroupParams(j, i)
entries[j][i].group = paramNames('group', j, i)
end
local function assignLineTextParams(j, i)
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 autocol and not isBlankEntry(j, i) then
maxcol = math.max(maxcol, j)
end
end
local function updateByerows(j, i)
if entries[j][i] and not hide[j][entries[j][i].headerindex] then
if not byes[j][entries[j][i].headerindex] or
(byes[j][entries[j][i].headerindex] and not isBlankEntry(j, i)) then
byerows = math.max(byerows, i)
end
end
end
for j = minc, c do
-- Set legs for this column
rlegs[j] = tonumber(bargs('RD'..j..'-legs')) or tonumber(bargs('legs')) or 1
if notempty(bargs('RD'..j..'-legs')) or bargs('legs') then
autolegs = false
end
if paramstyle == 'numbered' then
indexedParams(j)
else
for i = 1, r do
if entries[j][i] ~= nil then
local ctype = 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 entries[j][i].hastext == true then
assignLineTextParams(j, i)
end
end
updateMaxCol(j, i)
end
end
-- Round hiding check
for i = 1, r do
if entries[j][i] and 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 = minc, c do
for k = 1, headerindex[j] do
if byes[j][k] or hide[j][k] then
r = byerows + 1
end
end
end
-- Adjust column count automatically
if autocol then
c = maxcol
end
end
local function getHide(j,headerindex)
hide[j] = {}
for k=1,headerindex[j] do
if bargs('RD'..j..toChar(k)..'-hide')=='yes' or bargs('RD'..j..toChar(k)..'-hide')=='y' then
hide[j][k]=true
end
end
end
local function getByes(j,headerindex)
byes[j] = {}
for k=1,headerindex[j] do
if bargs('byes')=='yes' or bargs('byes')=='y' then
byes[j][k]=true
elseif tonumber(bargs('byes')) then
if j<=tonumber(bargs('byes')) then
byes[j][k]=true
end
else
byes[j][k]=false
end
if bargs('RD'..j..'-byes')=='yes' or bargs('RD'..j..'-byes')=='y' then
byes[j][k]=true
elseif bargs('RD'..j..'-byes')=='no' or bargs('RD'..j..'-byes')=='n' then
byes[j][k]=false
end
if bargs('RD'..j..toChar(k)..'-byes')=='yes' or bargs('RD'..j..toChar(k)..'-byes')=='y' then
byes[j][k]=true
elseif bargs('RD'..j..'-byes')=='no' or bargs('RD'..j..'-byes')=='n' then
byes[j][k]=false
end
end
end
local function getAltIndices()
local teamindex=1
local textindex=1
local groupindex=1
for j=minc,c do
headerindex[j]=0
for i=1,r do
if entries[j][i]==nil and i==1 then
headerindex[j]=headerindex[j]+1
end
if entries[j][i]~=nil then
if entries[j][i]['ctype'] == 'header' then
entries[j][i]['altindex'] = headerindex[j]
teamindex=1
textindex=1
headerindex[j]=headerindex[j]+1
elseif entries[j][i]['ctype'] == 'team' then
entries[j][i]['altindex'] = teamindex
teamindex=teamindex+1
elseif entries[j][i]['ctype'] == 'text' then
entries[j][i]['altindex'] = textindex
textindex=textindex+1
elseif entries[j][i]['ctype'] == 'group' then
entries[j][i]['altindex'] = groupindex
groupindex=groupindex+1
elseif entries[j][i]['ctype'] == 'line' and entries[j][i]['hastext']==true then
entries[j][i]['altindex'] = textindex
textindex=textindex+1
end
entries[j][i]['headerindex'] = headerindex[j]
end
end
getByes(j,headerindex)
getHide(j,headerindex)
end
end
local function noPaths(j,i)
local result = true
local cols = 2
if hascross[j]==true then
cols = 3
end
for k=1,cols do
for n=1,4 do
if pathCell[j][i][k][1][n]~=0 then
result = false
return result
end
end
end
if hascross[j]==true and (crossCell[j][i]['left'][1]==1 or crossCell[j][i]['right'][1]==1) then
result = false
return result
end
return result
end
-- Draw a single path cell in the bracket table
local function generatePathCell(tbl, j, i, k, bg, rowspan)
local colData = pathCell[j][i][k]
local color = colData.color
-- Skip center cell if there is no cross path
if not 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 skipPath[j][i] then
return
end
local colspan, rowspan = 2, 1
local bg = ''
local cross = { '', '' }
-- Detect vertical merging (rowspan) for repeated paths
if i < r then
local function repeatedPath(a)
if a > r - 1 or skipPath[j][a] then
return false
end
for k = 1, 3 do
for n = 1, 4 do
if pathCell[j][i][k][1][n] ~= 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
skipPath[j][row] = true
end
rowspan = rowspan + 1
row = row + 1
until row > r or not repeatedPath(row)
rowspan = rowspan - 1
end
end
-- Skip if the previous row has cross path connections
if i > 1 and (crossCell[j][i - 1].left[1] == 1 or crossCell[j][i - 1].right[1] == 1) then
return
end
-- Handle cross paths
if hascross[j] then
colspan = 3
if crossCell[j][i].left[1] == 1 or crossCell[j][i].right[1] == 1 then
rowspan = 2
if crossCell[j][i].left[1] == 1 then
cross[1] = 'linear-gradient(to top right, transparent calc(50% - 1px),'
.. crossCell[j][i].left[2] .. ' calc(50% - 1px),'
.. crossCell[j][i].left[2] .. ' calc(50% + 1px), transparent calc(50% + 1px))'
end
if crossCell[j][i].right[1] == 1 then
cross[2] = 'linear-gradient(to bottom right, transparent calc(50% - 1px),'
.. crossCell[j][i].right[2] .. ' calc(50% - 1px),'
.. 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
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
table.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(entries[j][start-1]) and (byes[j][entries[j][start-1]['headerindex']] and isBlankEntry(j,start-1) and isBlankEntry(j,start+1) or hide[j][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(entries[j+1][stop-1]) and (byes[j+1][entries[j+1][stop-1]['headerindex']] and isBlankEntry(j+1,stop-1) and isBlankEntry(j+1,stop+1) or hide[j+1][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(entries[j][start-1]) and entries[j][start-1]['headerindex']==1 then
result=true
end
end
return result
end
local function getPaths()
-- ===== Small atomic helpers =====
-- Helper: write a segment to a path cell
local function setPathCell(j, row, colIndex, borderIndex, value, color)
pathCell[j][row][colIndex][1][borderIndex] = value
pathCell[j][row][colIndex].color = color
end
local function setMultipleCells(j, row, colIndexes, borderIndex, value, color)
for _, colIndex in ipairs(colIndexes) do
pathCell[j][row][colIndex][1][borderIndex] = value
pathCell[j][row][colIndex].color = color
end
end
-- ===== Path drawing =====
-- Handle straight path (start == stop)
local function handleStraightPath(j, start, color, straightpaths)
table.insert(straightpaths, { start, color })
end
-- Handle downward paths (start < stop)
local function handleDownwardPath(j, start, stop, cross, color)
if stop > r then return end
setPathCell(j, start + 1, 1, 1, 1, color)
if cross == 0 then
if 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
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 > r then return end
setPathCell(j, stop + 1, 3, 1, 1, color)
if cross == 0 then
if 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
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 > r then break end
if 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 pathCell[j][i+1][1][1][1] == 0 then
setMultipleCells(j, i+1, {1, 2, 3}, 1, 0.5, color)
end
elseif 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 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 < r then
local top1, top2 = pathCell[j][i+1][1], pathCell[j][i+1][2]
local bottom1, bottom2 = pathCell[j][i][1], 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 < r then
local curr3 = pathCell[j][i][3]
local next3 = 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 = minc, c - 1 do
for i = 1, r - 1 do
local straightpath = false
-- Check if the top entry in next column is blank or a bye
local topEntry = entries[j+1][i-1]
local isTopBlankOrBye =
(topEntry == nil or (byes[j+1][topEntry.headerindex] and isBlankEntry(j+1, i-1)))
if isTopBlankOrBye then
local currPC, nextPC = pathCell[j], pathCell[j+1]
local currEntry, nextEnt = entries[j], 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
-- ===== Main logic =====
local paths = {}
-- Step 1: Determine which columns have cross paths
for j = minc, c - 1 do
hascross[j] = notempty(fargs['col' .. j .. '-col' .. (j + 1) .. '-cross'])
end
-- Step 2: Process each column
for j = minc, c - 1 do
paths[j] = parsePaths(j)
-- Initialize per-column path data
pathCell[j], crossCell[j], skipPath[j] = {}, {}, {}
for i = 1, r do
pathCell[j][i] = {}
for k = 1, 3 do
pathCell[j][i][k] = { {0, 0, 0, 0}, color = COLORS.path_line_color }
end
crossCell[j][i] = {
left = {0, COLORS.path_line_color},
right = {0, COLORS.path_line_color}
}
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 shift[j] ~= 0 and hasCrossLoc then
for n = 1, #crossloc do
crossloc[n] = crossloc[n] + 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] + shift[j]) + (teams_per_match[j] - 2)
local stopRow = 2 * (path[2] + shift[j + 1]) + (teams_per_match[j + 1] - 2)
-- Build cross rows
local crossRows = {0}
if hasCrossLoc then
for n = 1, #crossloc do
crossRows[n] = 2 * crossloc[n] + (teams_per_match[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
table.insert(outpaths, { startRow, color })
table.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 entries[j][i] == nil then
if entries[j][i + 1] == nil then
return true
elseif entries[j][i + 1].ctype == 'text' and isBlankEntry(j, i + 1) then
return true
end
elseif entries[j][i].ctype == 'text' and isBlankEntry(j, i) then
return true
end
return false
end
for j = minc, c - 1 do
-- Only handle standard 2-team matches (same as original)
if teams_per_match[j] == 2 then
local groupIndex = 0
for i = 1, r - 1 do
if pathCell[j][i][3][1][3] == 1 or pathCell[j][i + 1][3][1][1] == 1 then
groupIndex = groupIndex + 1
if isBlankOrBlankText(j, i) then
local k = minc - 1
repeat
-- guard entries[j-k] existence before indexing
if entries[j - k] and entries[j - k][i + 1]
and entries[j - k][i + 1].ctype == 'text'
and isBlankEntry(j - k, i + 1) then
entries[j - k][i + 2] = nil
end
-- create blank placeholders as in original logic
if entries[j - k] == nil then entries[j - k] = {} end
entries[j - k][i] = { ctype = 'blank' }
entries[j - k][i+1] = { ctype = 'blank' }
if k > 0 and noPaths(j - k, i) then
skipPath[j - k][i] = true
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 entries[j - k] == nil then entries[j - k] = {} end
entries[j - k][i] = {
ctype = 'group',
index = groupIndex,
colspan = k + 1
}
entries[j - k][i + 1] = { ctype = 'blank' }
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 teams_per_match
for j = minc, c do
if notempty(fargs["col" .. j .. "-headers"]) then
hasNoHeaders = false
end
teams_per_match[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
maxtpm = math.max(maxtpm, teams_per_match[j])
end
-- Phase 2: Build colentry for each column
for j = minc, c do
entries[j] = {}
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
table.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 entries[j][rowIndex - 1] == nil and entries[j][rowIndex - 2] == nil then
entries[j][rowIndex - 2] = { ctype = "text", index = n }
entries[j][rowIndex - 1] = { ctype = "blank" }
end
entries[j][rowIndex] = { ctype = "team", index = teams_per_match[j] * n - (teams_per_match[j] - 1), position = "top" }
entries[j][rowIndex + 1] = { ctype = "blank" }
for m = 2, teams_per_match[j] do
local idx = teams_per_match[j] * n - (teams_per_match[j] - m)
entries[j][rowIndex + 2 * (m - 1)] = { ctype = "team", index = idx }
entries[j][rowIndex + 2 * (m - 1) + 1] = { ctype = "blank" }
end
end
local function populateText(j, rowIndex, index)
entries[j][rowIndex] = { ctype = "text", index = index }
entries[j][rowIndex + 1] = { ctype = "blank" }
end
local function populateLine(j, rowIndex)
entries[j][rowIndex] = { ctype = "line" }
entries[j][rowIndex + 1] = { ctype = "blank" }
entries[j][rowIndex + 2] = { ctype = "line2" }
entries[j][rowIndex + 3] = { ctype = "blank" }
end
local function populateGroup(j, rowIndex, n)
entries[j][rowIndex] = { ctype = "group", index = n }
entries[j][rowIndex + 1] = { ctype = "blank" }
end
local function populateDefault(j, rowIndex, n)
entries[j][rowIndex] = { ctype = "header", index = n, position = "top" }
entries[j][rowIndex + 1] = { ctype = "blank" }
end
-- Phase 3: Populate entries for each column
for j = minc, c do
local textindex = 0
for k, positions in ipairs(colentry[j]) do
table.sort(positions)
local ctype = CTYPE_MAP[k]
for n = 1, #positions do
if shift[j] ~= 0 and positions[n] > 1 then
positions[n] = positions[n] + shift[j]
end
local rowIndex = 2 * positions[n] - 1
maxrow = math.max(rowIndex + 2 * teams_per_match[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(r) then
r = maxrow
end
end
--=== Small helpers for yes/no parsing ===--
local function yes(val) return val == 'y' or val == 'yes' end
local function no(val) return val == 'n' or val == 'no' end
--=== Update max legs per column ===--
local function updateMaxLegs()
for j = minc, c do
maxlegs[j] = rlegs[j]
for i = 1, r do
if notempty(entries[j][i]) then
if notempty(entries[j][i].legs) then
maxlegs[j] = math.max(rlegs[j], entries[j][i].legs)
end
if autolegs then
local l = 1
repeat l = l + 1
until isempty(entries[j][i].score) or isempty(entries[j][i].score[l])
maxlegs[j] = math.max(maxlegs[j], l - 1)
end
end
end
end
end
--=== Build HTML output ===--
local function buildTable()
local div = mw.html.create('div'):css('overflow', 'auto')
if height ~= 0 then div:css('height', height) end
local tbl = mw.html.create('table')
:attr('cellpadding', '0')
:attr('cellspacing', '0')
:css('font-size', '90%')
:css('border-collapse', 'separate')
:css('margin', '1em 2em 0em 1em')
:css('color', COLORS.text_color)
if nowrap then tbl:css('white-space', 'nowrap') end
-- Invisible header row for column widths
tbl:tag('tr'):css('visibility', 'collapse')
tbl:tag('td'):css('width', '1px')
for j = minc, c do
if 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 maxlegs[j] == 0 then
tbl:tag('td'):css('width', scoreWidth)
else
for l = 1, maxlegs[j] do
tbl:tag('td'):css('width', scoreWidth)
end
end
if aggregate and maxlegs[j] > 1 then
tbl:tag('td'):css('width', getWidth('agg', scoreWidth))
end
if j ~= c then
if hascross[j] then
tbl:tag('td'):css('width', colspacing - 3 .. 'px'):css('padding-left', '4px')
tbl:tag('td'):css('padding-left', '5px'):css('width', '5px')
tbl:tag('td'):css('width', colspacing - 3 .. 'px'):css('padding-right', '2px')
else
tbl:tag('td'):css('width', colspacing - 3 .. 'px'):css('padding-left', '4px')
tbl:tag('td'):css('width', colspacing - 3 .. 'px'):css('padding-right', '2px')
end
end
end
-- Table rows
for i = 1, r do
local row = tbl:tag('tr')
row:tag('td'):css('height', '11px')
for j = minc, c do
insertEntry(row, j, i)
if j ~= c then insertPath(row, j, i) end
end
end
div:wikitext(tostring(tbl))
return tostring(div)
end
--=== Main function ===--
function p.main(frame)
fargs, pargs = frame.args, frame:getParent().args
-- Parse basic args
r = tonumber(fargs.rows) or ''
c = tonumber(fargs.rounds) or 1
maxc = tonumber(pargs.maxrounds) or tonumber(pargs.maxround) or ''
minc = tonumber(pargs.minround) or 1
if notempty(maxc) then c = maxc end
autocol = yes(fargs.autocol)
colspacing = tonumber(fargs['col-spacing']) or 5
height = bargs('height') or 0
-- Defaults
maxtpm, seeds, forceseeds, aggregate, autolegs, nowrap = 1, true, false, false, false, true
boldwinner = bargs('boldwinner') or ''
forceseeds = yes(bargs('seeds'))
seeds = not no(bargs('seeds'))
aggregate = yes(bargs('aggregate'))
autolegs = yes(bargs('autolegs'))
paramstyle = (bargs('paramstyle') == 'numbered') and 'numbered' or 'indexed'
nowrap = not no(pargs.nowrap)
-- Data processing
headerindex = {} -- re-init before use
getCells()
getAltIndices()
assignParams()
matchGroups()
if yes(boldwinner) or boldwinner == 'high' then boldWinner() end
getPaths()
if minc == 1 then getGroups() end
updateMaxLegs()
-- Output
return buildTable()
end
return p