Module:RoundN
Appearance
local p = {
RD = {
'Quarter Finals',
'Semi Finals',
'Final',
'Third Place'
},
reuseStr = {},
--always call this like so: (p.reuseStr.name or p:saveStr('name', 'string' .. 'string' .. etc))
--this avoids repeating the concatenation operation with each loop
saveStr = function(self, name, concatString)
self.reuseStr[name] = concatString
return concatString
end,
cleaned = {}
}
local rowNum, head = {}, {}
local tab = mw.html.create'table'
local nodeFunc = {
helper = {
freeCells = function()
local v = p.getNodeFunc().helper.info
local count = 0
if not v.occupied.full then
for _, x in ipairs(v.occupied) do
for _, y in ipairs(x) do
count = count + 1
end
end
v.occupied.full = v.totalCells == count
end
if v.occupied.full then
return false
end
return v.totalCells - count
end,
--[[showBox = function()
return {}
end,]]
top = function()--node is top of fork if top is 0
return (p.getNodeFunc().helper.info.num - p.getNodeFunc().helper.info.first) % 2
end
},
line = {--this node is omitted and replaced with a line
main = function(x)
local h = p.getNodeFunc().helper
if h.freeCells() == 12 then
local top = h.top()
for k = 0, 1 do
rowNum[h.info.row + k * 4]:tag'td'
:css(
top == 0 and 'border-top' or 'border-bottom',
k ~= top and p.reuseStr.solid or nil
)
:attr{
rowspan = k == 0 and 4 or 2,
colspan = 2
}
end
h.info.occupied.full = true
else
return nil
end
return x
end
},
bridge = {--Draw a line to the neighboring node in the same column that is not connected to the current node
main = function(x)
local nF = p.getNodeFunc()
local v = nF.helper.info
nF.bridge.lay[v.col][v.num - v.first + 1 + (nF.helper.top() == 1 and 1 or -1)] = true
return x
end,
lay = {}
},
canvas = {--Merges all cells in node. Content will be the next parameter.
main = function(x)
local helper = p.getNodeFunc().helper
if helper.freeCells() == 12 then
tab.r = rowNum[helper.info.row]:tag'td'
:attr{
rowspan = 6,
colspan = 2
}
helper.info.occupied.full = true
return x
else
return nil
end
end
},
skipAllowed = {--table of supported node functions when node is skipped (i.e. by skipmatch)
bridge = true,
canvas = true
}
}
function p.getNodeFunc()
return nodeFunc
end
--Provides a convenient naming shortcut up to {{#invoke:RoundN|N512}} = {{invoke:RoundN|main|columns = 9}}
for c = 1, 9 do
local N = math.pow(2, c)
p['N' .. N] = function(frame)
return p.main(frame.args, c)
end
p['n' .. N] = p['N' .. N]--to make case insensitive
end
function newRow(bodyRow)
tab.r = tab:tag'tr'
tab.r:tag'td'
:css(p.flex_tree.css)
:wikitext(p.flex_tree.wt)
if bodyRow then
table.insert(rowNum, bodyRow, tab.r)
end
end
function drawHead(text, row3rd)
(row3rd and rowNum[row3rd]:tag'td':attr{rowspan = 2}
or head.row:tag'td')
:attr{colspan = 2}
:css{
['text-align'] = 'center',
border = '1px solid #aaa',
background = '#f2f2f2'
}
:wikitext(text)
end
function spacer(width)
tab.r:tag'td'
:attr{width = width}
:wikitext(p.no_column_head and '' or ' ')
end
function dpBox(v, r)
tab.r = rowNum[r]:tag'td'
:wikitext(v or p.flex_tree.wt)
:attr{rowspan = 2, colspan = 2}
end
function boldWin(s1, s2, manual)
if p.bold and not manual then
function cleanScore(s)
return s and tonumber(string.gsub(string.gsub(s, '<.+>', ''), '%D', ''), 10) or s and 0 or math.huge
end
s1 = cleanScore(s1)
s2 = cleanScore(s2)
return ({{s1 < s2, s1 > s2}, {s1 > s2, s1 < s2}})[p.bold]
end
return {}
end
function maxSpan(span, start, rows)
return math.min(span, math.max(0, rows - start + 1))
end
function teamBox(v, r, f)
tab.oldR = tab.r
tab.r = rowNum[r]:tag'td'
:css{
border = '1px solid #aaa',
background = ({'gold', 'silver', '#C96', '#f9f9f9'})[f.color or 4],
['text-align'] = f[1]
}
:attr{rowspan = 2}
tab.r:tag(f.bold and 'b' or ''):wikitext(' ' .. (v or ''))
end
function p._main(args, frame)
function args:clean(key, params)--prevent html comments from breaking named args and reduces repeat concatenation
params = params or {}
if params.checkClean and p.cleaned[key] then
return p.cleaned[key]
end
p.cleaned[key] = (args[key] or params.ifNil) and
(string.gsub(string.gsub(args[key] and
mw.text.decode(args[key]) or params.ifNil or '',
'<!.+>', ''),
params.pattern or '[^%w-;]', '') .. (params.append or '')
)
or nil
p.cleaned[key] = p.cleaned[key] ~= '' and p.cleaned[key] or params.ifNil
args[key] = params.keepOld and (args[key] or params.ifNil) or p.cleaned[key]
local clean = p.cleaned[key]
p.cleaned[key] = params.checkClean and p.cleaned[key] or nil
return clean
end
p.cols = tonumber(args:clean('columns', {pattern = '%D'}))
p.bold = ({low = 1, high = 2})[args:clean('bold_winner')]
local skipMatch, unBold = {}, {}--(skip|manualbold)match# to boolean
for k, _ in pairs(args) do
local mType, mNum = string.match(k, '^(%a+)match(%d*)$')
mType, mNum = ({skip = skipMatch, manualbold = unBold})[mType], tonumber(mNum)
if mType then
if mNum then
mType[mNum] = args:clean(k) == 'yes' or args[k] == 'true'
elseif ({['-']=1,[';']=1})[string.match(args:clean(k), '%d([-;])%d')] then--match ranges when no skipmatch
for _, x in ipairs(mw.text.split(args[k], ';')) do
local m = mw.text.split(x, '-')
for y = tonumber(m[1] or x) or 1, tonumber(m[2] or m[1] or x) or 0 do
mType[y] = true
end
end
end
end
end
for _, v in ipairs({--more args to boolean
'widescore',
'template',
'color',
'3rdplace',
'omit_blanks',
'scroll_head_unlock',
'previewnumbers',
'flex_tree',
'no_column_head'
--[['one_per_branch']]
}) do
if args[v] and (p[v] == nil or type(p[v]) == 'boolean') then
p[v] = args:clean(v) == 'yes' or args[v] == 'true'
end
end
p.flex_tree = {
css = p.flex_tree and {} or {['font-size'] = '50%'},
wt = p.flex_tree and '' or ' '
}
tab
:cssText(
(args.scroll_height and 'padding' or 'margin') .. ':1em 2em 1em 1em;border:0;font-size:90%;'
.. (args.style or '')
)
:attr{cellpadding = 0, cellspacing = 0}
if not p.no_column_head then--headings row
newRow()
head.row = tab.r
newRow()
else
tab.r = tab:tag'tr'
tab.r:tag'td'
end
local sp = {--set column widths
args['team-width'] or 170,
args['score-width'] or (p.widescore and 40) or 30,
15,
20
}
if p.template then
p.template = mw.title.new(args.name)
p.templateFixedName = (p.template.namespace == 0 and 'Template:' or '') .. p.template.fullText
end
p.template = p.template and mw.title.new(args:clean('name', {pattern = ''}))
for k = 1, p.cols do
if k > 1 then
spacer(sp[3])
spacer(sp[4])
if not p.no_column_head then
head.row:tag'td':attr{colspan = 2}
end
end
spacer(sp[1])
spacer(sp[2])
if not p.no_column_head then
head.wt = args['RD' .. k]
or p.RD[#p.RD + k - p.cols - 1]
or ('Round of ' .. math.pow(2, p.cols - k + 1))
drawHead(
k == 1 and p.template and mw.getCurrentFrame():expandTemplate{
title = 'tnavbar-header',
args = {head.wt, p.templateFixedName}
} or head.wt
)
end
end
local step, bump, RD, m, rows = 1, 0, {tot = 0}, {num = 1, phase = 0}, math.pow(2, p.cols) * 3--Begin body row output
tab.line = {--reduces concats
{
[true] = args:clean('line_px', {ifNil = 3, append = 'px'}),
[false] = 0
},
args.line_px .. ' ' .. args.line_px
}
for r = 1, rows do
newRow(r)
end
p.reuseStr.solid = p.reuseStr.solid or p:saveStr('solid', tab.line[1][true] .. ' solid')
--prep work for set_scrolling_anchors
--[[f args.set_scrolling_anchors then
args:clean('set_scrolling_anchors', {pattern = '%D'})
args.set_scroll_anchors = math.max(tonumber(args.set_scroll_anchors, 10), 1)
end]]
for c = 1, p.cols do
RD.tot = RD.tot + (rows / math.pow(2, c)) / 3
--prep for set_scrolling_anchors
--tab.anchor = args.set_scroll_anchors and c > args.set_scroll_anchors
if c > 1 and (c < p.cols or p.template) then
rowNum[1]:tag'td'
:attr(c < p.cols and {rowspan = bump, colspan = 2} or {})
:wikitext(p.flex_tree.wt
.. (p.no_column_head and p.template and c == p.cols and
mw.getCurrentFrame():expandTemplate{
title = 'tnavbar-header',
args = {'', p.templateFixedName}
}
or ''
)
--prep work for set_scrolling_anchors
--[[.. (tab.anchors and
('')
or ''
)]]
)
end
local bumps = bump
RD.top = m.num
nodeFunc.bridge.lay[c] = {}
p.span = p.cols > c and (bump * 2) or math.max((bump - 1) / 2, 2)
for r = 1, rows, 2 do
RD.r = r + bumps
if rowNum[RD.r] and m.num <= RD.tot then
if m.phase == 0 then
nodeFunc.pattern = string.match(args:clean(step, {keepOld = true, ifNil = '', pattern = '[^{}_,%w]'}), 'node_function{([^}]*)')
if nodeFunc.pattern then
nodeFunc.pattern = nodeFunc.pattern and mw.text.split(nodeFunc.pattern, '[%s,]+')
nodeFunc.called = {}
nodeFunc.helper.info = {
occupied = {{},{}},
totalCells = 12,--constant to be replaced by var when one_per_branch implemented
row = RD.r,
col = c,
first = RD.top,
num = m.num,
gap = p.span
}
end
elseif nodeFunc.pattern and nodeFunc.called.canvas and m.phase == 1 then
tab.r:wikitext(args[step])
step = step + 1
end
if skipMatch[m.num] then
if m.phase == 0 then
if nodeFunc.pattern then
for x, y in ipairs(nodeFunc.pattern) do
if nodeFunc.skipAllowed[y] then
nodeFunc.called[y] = nodeFunc[y].main(x)
end
end
end
local start = RD.r + (nodeFunc.pattern and nodeFunc.called.canvas and 6 or 0)
rowNum[start]:tag'td':attr{rowspan = maxSpan((start > RD.r and 0 or 6) + bump * 2, start, rows), colspan = 2}
elseif m.phase == 2 then
m.num = m.num + 1
step = step + (p.omit_blanks and 0 or 5)
bumps = bumps + maxSpan(p.span, RD.r, rows)
end
elseif m.phase == 0 then
m.showBox = {1, 2, --[[]]2}--[[not p.one_per_branch and 2 or nil}]]
if nodeFunc.pattern then
for x, y in ipairs(nodeFunc.pattern) do
if nodeFunc[y] and nodeFunc[y].main then
nodeFunc.called[y] = nodeFunc[y].main(x)
end
end
step = step + 1
if not nodeFunc.helper.freeCells() then
m.showBox = {}
end
end
if m.showBox[1] then
dpBox(args[step], RD.r, skipMatch[m.num])
if args.previewnumbers then
p.namespace = p.namespace or mw.title.getCurrentTitle().namespace
if p.namespace ~= 0 then
tab.r:tag'div'
:css{
float = 'left',
border = '1px solid red',
padding = '0 .5ex',
['color'] = 'red'
}
:wikitext(m.num)
:attr{title = 'This number is only visible outside artcle space (e.g. template) when |numberpreview=yes'}
end
end
end
m.bold = boldWin(args[step + 2], args[step + 4], not (m.showBox[2] or m.showBox[3]) or unBold[m.num])
else
if m.showBox[m.phase] then
teamBox(args[step + (m.phase - 1) * 2 + 1], RD.r, {color = p.color and c == p.cols and m.phase, bold = m.bold[m.phase]})
teamBox(args[step + (m.phase - 1) * 2 + 2], RD.r, {'center', color = p.color and c == p.cols and m.phase, bold = m.bold[m.phase]})
end
--not yet implemented
--[[if p.one_per_branch and m.showBox[2] then
m.phase = 2
tab.oldR:attr{rowspan = 4}
tab.r:attr{rowspan = 4}
bumps = bumps + 2
end]]
if m.phase == 2 then
step = step + (m.showBox[1] or 0) + (m.showBox[2] or 0) + (m.showBox[3] or 0)
m.num = m.num + 1
if bump > 0 and rowNum[RD.r + 2] and not (nodeFunc.pattern and nodeFunc.called.canvas) then
bumps = bumps + p.span
rowNum[RD.r + 2]:tag'td':attr{rowspan = p.span, colspan = 2}
end
r = r + bump
end
end
m.phase = (m.phase + 1) % 3
end
end
if p.cols > c then--draw lines to next round
p.unit = bump + 3
bump = (rows - rows / math.pow(2, c)) / math.pow(2, p.cols - c)
bumps = p.unit + 1
rowNum[1]
:tag'td':attr{rowspan = bumps}
:tag'td'
:attr{rowspan = bump + 4}
:css(nodeFunc.bridge.lay[c][0] and
{['border-right'] = p.reuseStr.solid}
or {}
)
RD.n = 0
for r = bumps + 1, rows, p.unit * 2 do
tab.r = rowNum[r]:tag'td'
local interval = ((r - bumps - 1) / (p.unit * 2)) % 4
if interval % 2 == 0 then
--RD.t and RD.t2 control whether lines are drawn
RD.t = RD.t2 or skipMatch[RD.tot + RD.n / 2 + 1] and 3 or ((skipMatch[RD.top] and 1 or 0) + (skipMatch[RD.top + 1] and 2 or 0))
RD.n = RD.n + 2
RD.t2 = skipMatch[RD.tot + RD.n / 2 + 1] and 3 or ((skipMatch[RD.top + RD.n] and 1 or 0) + (skipMatch[RD.top + RD.n + 1] and 2 or 0))
if RD.t == 0 then
tab.r
:attr{rowspan = maxSpan(p.unit * 2, r, rows)}
:css(skipMatch[RD.tot + RD.n / 2] and {} or {
border = p.reuseStr.solid,
['border-left'] = 0
})
else
tab.r
:attr{rowspan = maxSpan(p.unit, r, rows)}
:cssText(RD.t == 2 and
(p.reuseStr.topRight or p:saveStr('topRight', 'border-width:' .. tab.line[2] .. ' 0 0;border-style:solid'))
or RD.t == 1 and (nodeFunc.bridge.lay[c][RD.n - 2] and
(p.reuseStr.right or p:saveStr('right', ';border-right:' .. p.reuseStr.solid))
or 'vertical-align:bottom'
)
or nil
)
:node(RD.t == 1 and interval > 0 and not nodeFunc.bridge.lay[c][RD.n - 2] and
mw.html.create'div'
:css{height = tab.line[1][true], ['border-right'] = p.reuseStr.solid}
)
rowNum[r + p.unit]:tag'td'
:attr{rowspan = maxSpan(p.unit, r + p.unit, rows)}
:cssText(RD.t == 1 and
(p.reuseStr.bttmRght or p:saveStr('bttmRght', 'border-width:0 ' .. tab.line[2] .. ' 0;border-style:solid'))
or RD.t == 2 and (nodeFunc.bridge.lay[c][RD.n + 2] and
(p.reuseStr.right or p:saveStr('right', ';border-right:' .. p.reuseStr.solid))
or 'vertical-align:top'
)
or nil
)
:node(RD.t == 2 and interval ~= 2 and not nodeFunc.bridge.lay[c][RD.n + 2] and
mw.html.create'div'
:css{height = tab.line[1][true], ['border-right'] = p.reuseStr.solid}
)
end
RD.t = {
RD.t < 3,
rowNum[r + p.unit * 5] and RD.t2 < 3 or false
}
rowNum[r + p.unit]:tag'td'
:attr{rowspan = maxSpan(p.unit * 4, r + p.unit, rows)}
:css(interval == 0 and (RD.t[1] or RD.t[2]) and {
['border-width'] = tab.line[1][RD.t[1]] .. ' 0 ' .. tab.line[1][RD.t[2]],
['border-style'] = 'solid'
} or {})
else
tab.r
:attr{rowspan = maxSpan(p.unit * 2, r, rows)}
:css(nodeFunc.bridge.lay[c][RD.n] and
{['border-right'] = p.reuseStr.solid}
or {}
)
end
end
end
end
if p['3rdplace'] or p.cols > 3 and p['3rdplace'] == nil and not p.no_column_head then--begin 3rd place
RD.r = rows / 2 + 4 + math.max(p.span, 3)
for r = rows + 1, RD.r + 7 do
newRow(r)
end
if rowNum[rows + 1] and p.cols > 1 then --if 3rd place extends below bottom cell
rowNum[rows + 1]:tag'td':attr{
rowspan = RD.r + 7 - rows,
colspan = (p.cols - 1) * 4
}
end
drawHead(args.Consol or args['RD' .. (p.cols + 1)] or p.RD[4], RD.r)
dpBox(args[step], RD.r + 2)
m.bold = boldWin(args[step + 2], args[step + 4], unBold[m.num])
for k, v in ipairs({
{4, {color = p.color and 3, bold = m.bold[1]}},
{4, {'center', color = p.color and 3, bold = m.bold[1]}},
{6, {bold = m.bold[2]}},
{6, {'center', bold = m.bold[2]}}
}) do
teamBox(args[step + k], RD.r + v[1], v[2])
end
end
return args.scroll_height and
mw.html.create'div'
:cssText'border-bottom:1px solid #eee;display:inline-block'
:node(not (p.scroll_head_unlock or p.no_column_head) and mw.html.create'div'
:css{
overflow = 'hidden',
height = '3em',
['border-bottom'] = 'inherit',
['margin-right'] = '17px'
}
:node(mw.clone(tab))
)
:tag'div'
:cssText('overflow-y:scroll;max-height:'
.. args.scroll_height .. (string.match(args.scroll_height, '^%d*$') and 'px' or '')
)
:node(not (p.scroll_head_unlock or p.no_column_head) and
tab:css('margin-top', '-3em')
or tab
)
:done()
or tab
end
function p.main(frame, columns)
local args = require'Module:Arguments'.getArgs(frame, {trim = false})
args.columns = args.columns or columns
return p._main(args, frame)
end
return p