Jump to content

Module:RoundN

विकिपीडिया से
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 = {
	esc = {
		bs = require'Module:Escape',--backslash
		comma = {['(%([^,]*),([^%)]*%))'] = '%1|@!#|%2'},--escape commas in ()
	},
	scanPattern = function(self, args, step, mNum)
		self.pattern = nil
		if args[step] then
			self.pattern, self.nonFunc = string.match(self.esc.bs:text(args[step]), '^node_function{(.-)}(.*)')
		end
		if self.pattern then
			for k, v in pairs(self.esc.comma) do
				self.pattern = self.pattern:gsub(k, v)
			end
			self.nonFunc = self.nonFunc and self.esc.bs:undo(self.nonFunc)
			self.pattern = mw.text.split(self.pattern, '%s*,%s*')
			for k, v in ipairs(self.pattern) do
				local func, arg = string.match(v, '^(%w+)%(?([^%)]*)')
				if func and self[func] and self[func].main then
					self.pattern[k] = func
					if arg then
						for x, y in pairs(self.esc.comma) do
							arg = self.esc.bs:undo(arg):gsub(y:gsub('%%%d', ''), x:match('%)([^%(])%(') or x:gsub('\\', ''))
						end
						self[func].arg = self[func].arg or {}
						self[func].arg[mNum] = arg
					end
				end
			end
		end
		return self.pattern
	end,
	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,
		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 nF = p.getNodeFunc()
			local h = nF.helper
			if h.freeCells() == h.info.totalCells 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 = p.colspan
						}
						:wikitext(k == 0 and nF.line.arg[h.info.num] or '')
				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() == helper.info.totalCells then
				tab.r = rowNum[helper.info.row]:tag'td'
					:attr{
						rowspan = 6,
						colspan = p.colspan
					}
				helper.info.occupied.full = true
				return x
			else
				return nil
			end
		end
	},
	orphan = {--sets a flag for skipMatch to be set by p._main
		main = function(x)
			local nF = p.getNodeFunc()
			nF.orphan.num = nF.helper.info.num
			return x
		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 = p.colspan}
		: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 = p.colspan}
end

function cleanPts(s)
	return s and tonumber(s:gsub('<.+>', ''):gsub('%D', ''), 10) or s and math.huge or 0
end

function cleanSum(clean)
	local sum = {0, 0}
	for _, box in ipairs(clean) do
		for team, score in ipairs(box) do
			sum[team] = sum[team] + score
		end
	end
	return unpack(sum)
end

function boldWin(s1, s2)
	return p.bold and s1 ~= s2 and (math[({'min', 'max'})[p.bold]](s1, s2) == s1 and {true} or {[2] = true}) or {}--({{s1 < s2, s1 > s2}, {s1 > s2, s1 < s2}, {}})[p.bold or 3]
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],
			[f[1] and 'text-align' or 'padding'] = f[1] or '0 .6ex'
		}
		:attr{rowspan = 2}
	tab.r:tag(f.bold and 'b' or ''):wikitext(v or '&nbsp;')
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 {}
		local clean = args[key] or params.ifNil
		if clean then
			params.append = params.append or ''
			clean = mw.text.decode(clean):gsub('<!.+>', ''):gsub(params.pattern or '[^%w-;%.]', '') .. params.append
			clean = clean ~= params.append and clean or params.ifNil
		end
		args[key] = params.keepOld and args[key] or clean
		return clean
	end
	p.cols = tonumber(args:clean('columns', {pattern = '%D'}))
	p.tCols = (tonumber(args:clean('final_RDs_excluded', {pattern = '%D'})) or 0) + p.cols
	p.bold = ({low = 1, high = 2})[args:clean('bold_winner')]
	local matchPer = {
		pattern = '%d*per%d+[%-x]%d+',
		vals = '(%d*)per(%d+)([%-x])(%d+)'
	}
	local skipMatch, unBold  = {}, {}--(skip|manualbold)match# to boolean
	for k, _ in pairs(args) do
		local mType, mNum = string.match(k, '^(%l+)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'
			else
				for pattern in args:clean(k, {ifNil = ''}):gfind(matchPer.pattern) do
					local d1, period, op, d2 = pattern:match(matchPer.vals)
					d1 = tonumber(d1) or 1
					d2 = op == '-' and d2 or (d1 + period * (d2 - 1))
					for y = d1, d2, period do
						mType[y] = true
					end
				end
				for _, x in ipairs(mw.text.split(args[k]:gsub(matchPer.pattern, ''):gsub('[;%-%a][;%-%a]+', ';'):match('^;*(.-)[;%-]*$'), ';')) do
					x = mw.text.split(x, '-')
					for y = tonumber(x[1]) or 1, tonumber(x[2] or x[1]) 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',
		'short_brackets',
		--'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 '&nbsp;'
	}
	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 scoreWidth = args['score-width'] and mw.text.split(args:clean('score-width', {pattern = '[^%d;]'}), ';') or {}
	local sp = {--set column widths
		args['team-width'] or 170,
		p.widescore and 40 or 30,
		p.short_brackets and 6 or 15,
		p.short_brackets and 4 or 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 = ''}))
	p.scoreSumBox = args['score-boxes'] and args['score-boxes']:match('%d ?%+ ?sum')
	local sumBox = p.scoreSumBox and 1 or 0
	p.scoreBoxes = (tonumber(args:clean('score-boxes', {pattern = '%D'})) or 1) + sumBox
	p.scoreSumBox = p.scoreBoxes > 0 and p.scoreSumBox or nil
	p.colspan = p.scoreBoxes > 0 and (p.scoreBoxes + 1) or nil
	local nodeArgs = {
		score = p.scoreBoxes - sumBox,
		team = {offset = 1 + p.scoreBoxes - sumBox},
		all = 1 + (1 + p.scoreBoxes - sumBox),
		tableSum = {
			__add = function(v, t)
				local s = v
				for i, n in ipairs(t) do
					s = s + n - (p.scoreSumBox and i > 1 and 1 or 0)
				end
				return s
			end
		}
	}
	nodeArgs.team[1] = 1--constant to be replaced later by new param
	nodeArgs.team[2] = nodeArgs.team[1] + nodeArgs.team.offset
	nodeArgs.blank = setmetatable({}, nodeArgs.tableSum)
	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])
		for s = 1, p.scoreBoxes do
			spacer(#scoreWidth == 1 and scoreWidth[1] or scoreWidth[s] or sp[2])
		end
		if not p.no_column_head then
			head.wt = args['RD' .. k]
				or p.RD[#p.RD + k - p.tCols - 1]
				or ('Round of ' .. math.pow(2, p.tCols - 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.tCols) * 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')
	for c = 1, p.cols do
		RD.tot = RD.tot + (rows / math.pow(2, c)) / 3
		if c > 1 and (c < p.cols or p.template) then
			rowNum[1]:tag'td'
				:attr(c < p.cols and {rowspan = bump, colspan = p.colspan} 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 nil
					)
				)
		end
		local bumps = bump
		RD.top = m.num
		nodeFunc.bridge.lay[c] = {}
		p.span = p.tCols > 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
					m.showBox = setmetatable({1, p.colspan or 1, p.colspan or 1}, nodeArgs.tableSum)
					if nodeFunc:scanPattern(args, step, m.num) then
						nodeFunc.called = {}
						nodeFunc.helper.info = {
							occupied = {{},{}},
							totalCells = 6 * (p.colspan or 1),
							row = RD.r,
							col = c,
							first = RD.top,
							num = m.num,
							gap = p.span
						}
					end
				elseif nodeFunc.pattern and m.phase == 1 then
					if nodeFunc.called.canvas or nodeFunc.called.bridge then
						tab.r:wikitext(nodeFunc.nonFunc)
						step = step + 1
					end
				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 = p.colspan}
					elseif m.phase == 2 then
						m.num = m.num + 1
						step = step + (p.omit_blanks and 0 or nodeArgs.all)
						bumps = bumps + maxSpan(p.span, RD.r, rows)
					end
				elseif m.phase == 0 then
					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
						if not nodeFunc.helper.freeCells() then
							step = step + 1
							m.showBox = nodeArgs.blank
						end
					end
					if m.showBox[1] then
						dpBox(nodeFunc.pattern and nodeFunc.nonFunc or args[step], RD.r, skipMatch[m.num])
						if p.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 = 'Number only visible outside article space (e.g. template) when |numberpreview=yes'}
							end
						end
					end
					if p.colspan then
						m.nonEmpty = {}
						for s = step + 2, step + nodeArgs.team.offset do
							local i = {s, s + nodeArgs.team.offset}
							if args[i[1]] or args[i[2]] then
								table.insert(m.nonEmpty, i)
							end
						end
						if p.scoreSumBox then
							table.insert(m.nonEmpty, {-step, -step - 1})
						end
						if p.bold and m.showBox[2] and m.showBox[3] and not unBold[m.num] then
							m.bold = {
								box = {},
								clean = {}
							}
							for s, i in ipairs(m.nonEmpty) do
								if p.scoreSumBox and s == #m.nonEmpty then
									args[i[1]], args[i[2]] = cleanSum(m.bold.clean)
									m.bold.box[s] = boldWin(args[i[1]], args[i[2]])
								else
									m.bold.clean[s] = {cleanPts(args[i[1]]), cleanPts(args[i[2]])}
									m.bold.box[s] = boldWin(m.bold.clean[s][1], m.bold.clean[s][2])
								end
							end
							m.bold.win = m.bold.box[#m.nonEmpty] or {}
						else
							m.bold = nil
						end
					end
				else
					if m.showBox[m.phase] then
						local base = {color = p.color and c == p.cols and m.phase}
						teamBox(args[step + nodeArgs.team[m.phase]], RD.r, setmetatable(base, {__index = {bold = m.bold and m.bold.win[m.phase]}}))
						if p.colspan then
							if #m.nonEmpty == 0 then
								for s = 1, p.scoreBoxes do
									teamBox('', RD.r, base)
								end
							elseif #m.nonEmpty < p.scoreBoxes then
								tab.r:attr{colspan = 1 + p.scoreBoxes - #m.nonEmpty}
							end
							for s, i in ipairs(m.nonEmpty) do
								teamBox(args[i[m.phase]], RD.r, setmetatable(base, {__index = {'center', bold = m.bold and m.bold.box[s][m.phase]}}))
							end
						end
					end
					if m.phase == 2 then
						if nodeFunc.orphan.num == m.num then
							skipMatch[m.num] = 'orphan'
						end
						step = step + (m.showBox.unchanged and nodeArgs.full or m.showBox)
						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 = p.colspan}
						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.tCols - 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.tCols == p.cols and (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(cleanPts(args[step + 2]), cleanPts(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:clean('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'
				:css{
					['overflow-y'] = 'scroll',
					['max-height'] = tonumber(args.scroll_height, 10) and args.scroll_height .. 'px' or args.scroll_height
				}
				: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