Edit the documentation or categories for this module. This module has an i18n file.
local util_args = require('Module:ArgsUtil')
local util_cargo = require('Module:CargoUtil')
local util_esports = require('Module:EsportsUtil')
local util_map = require("Module:MapUtil")
local util_table = require('Module:TableUtil')
local util_text = require('Module:TextUtil')
local util_tournament = require("Module:TournamentUtil")
local util_vars = require('Module:VarsUtil')
local bracket_wiki = require('Module:Bracket/Wiki') -- wiki localization per game
local i18n = require('Module:i18nUtil')
local m_team = require('Module:Team')
local lang = mw.getLanguage('en')
local ROWS_PER_TEAM = 6
local ROWS_PER_TITLE = 2
local ROWS_PER_HLINE = 1
local ROUNDWIDTH = 12
local LINEWIDTH = '3em'
local SCOREWIDTH = 2
local TEAM_STYLE = nil -- constant from args
local SHOW_BESTOF = false
local CURRENT_ROUND = nil -- index used in too many places to pass in every function signature
local PRINT_TITLES = true
local p = {}
local h = {}
function p.main(frame)
local tpl_args = util_args.merge()
i18n.init('Bracket')
h.setConstants(tpl_args)
-- use require instead of loadData so that we can use next() and #
local settings
local function assignBracket()
settings = require('Module:Bracket/'.. tpl_args[1])
end
if not tpl_args[1] then
error(i18n.print('error_noDefinition'))
elseif pcall(assignBracket) then
-- pass
else
error(i18n.print('error_invalidInput', tpl_args[1]))
end
local args = h.processArgs(tpl_args)
h.processSettings(settings, args)
if util_args.castAsBool(args.cargo) then
h.addCargoData(args, settings)
end
return h.makeOutput(args, settings)
end
function h.setConstants(tpl_args)
TEAM_STYLE = tpl_args.teamstyle
SHOW_BESTOF = util_args.castAsBool(tpl_args.show_bestof)
PRINT_TITLES = not util_args.castAsBool(tpl_args.notitle)
end
function h.processArgs(tpl_args)
-- format tpl_args
local args = {}
for k, v in pairs(tpl_args) do
if type(k) ~= 'string' then
-- pass
elseif k:find('R%d+M%d+_.*_') then
-- team-specific arg
local r, m, val, team = k:match('R(%d+)M(%d+)_(.*)_(%d+)')
r = tonumber(r)
m = tonumber(m)
h.initializeMatch(args, r, m)
args[r][m]['team' .. team][val] = h.castArg(val, v)
elseif k:find('R%d+M%d+_.*') then
-- match-specific arg
local r, m, val = k:match('R(%d+)M(%d+)_(.*)')
r = tonumber(r)
m = tonumber(m)
h.initializeMatch(args, r, m)
args[r][m][val] = h.castArg(val, v)
elseif k:find('R%d+_') then
-- round-specific arg
local r, val = k:match('R(%d+)_(.*)')
r = tonumber(r)
h.initializeMatch(args, r)
args[r][val] = v
else
-- global arg
args[k] = v
end
end
h.addImpliedArgs(args)
return args
end
function h.castArg(val, v)
if val == 'team' then return m_team.teamlinkname(v) end
if val == 'bye' then return util_args.castAsBool(v) end
if val == 'winner' or val == 'bestof' then return tonumber(v) end
if val == 'winners' then return h.castWinnersArg(v) end
if val == 'score' then return h.castScoreArg(v) end
return v
end
function h.castWinnersArg(v)
return util_map.split(v, nil, tonumber)
end
function h.castScoreArg(v)
return util_map.split(v, nil, h.castPartScoreArg)
end
function h.castPartScoreArg(str)
return tonumber(str) or str
end
function h.initializeMatch(args, r, m)
if not args[r] then
args[r] = {}
end
if not args[r][m] and m then
args[r][m] = { team1 = {}, team2 = {} }
end
end
function h.addImpliedArgs(args)
for round, roundData in pairs(args) do
if type(round) == 'number' then
for match, matchData in pairs(roundData) do
if type(match) == 'number' then
h.addImpliedArgsToMatch(matchData)
end
end
end
end
end
function h.addImpliedArgsToMatch(matchData)
if matchData.class == 'qualified' then
matchData.label = i18n.print('qualMatch')
elseif matchData.class == 'relegated' then
matchData.label = i18n.print('relMatch')
end
for i, v in ipairs({ 'team1', 'team2' }) do
if matchData[v] then
h.addImpliedArgsToTeam(matchData[v], matchData, i)
end
end
end
function h.addImpliedArgsToTeam(team, matchData, i)
team.teamfinal = team.teamfinal or team.team
team.iswinner = matchData.winner == i
team.bestof = matchData.bestof
local function mapWinners(winner)
return winner == i
end
if matchData.winners then
team.iswinners = util_map.copy(matchData.winners, mapWinners)
end
end
function h.processSettings(settings, args)
-- in theory this could be done in the settings module before returning but
-- this way the code is a bit more hidden from users editing stuff
-- and also this makes the settings module closer to a read-only table that you
-- import (and clone) here which i guess is nice?
-- tbh im not sure if this was the right way to do it tho
for r, col in ipairs(settings) do
local m = #col.matches
while m >= 1 do
-- need to iterate backwards bc we'll delete third-place matches if hidden
local match = col.matches[m]
local lines = col.lines and col.lines[m]
if lines and lines.reseed then
lines.class = lines.class:format(lang:lc(args.reseed or 'reseeding'))
end
if match.argtoshow then
if not util_args.castAsBool(args[match.argtoshow]) then
if col.matches[m+1] then
col.matches[m+1].above = (col.matches[m+1].above or 0) + (match.above or 0) + 6
end
table.remove(col.matches,m)
end
end
m = m - 1
end
end
end
-- cargo
function h.addCargoData(args, settings)
local overviewPage = util_esports.getOverviewPage(args.page)
local data = h.doCargoQuery(overviewPage)
if #data == 0 then
return
end
local processed = h.processCargoData(data)
h.addProcessedToArgs(args, settings, processed, overviewPage)
end
function h.doCargoQuery(page)
local query = {
tables = {
'MatchSchedule=MS',
'Teams=Teams1',
'TournamentRosters=Ros1',
'Teams=Teams2',
'TournamentRosters=Ros2',
},
join = {
'MS.Team1=Teams1._pageName',
'MS.PageAndTeam1=Ros1.PageAndTeam',
'MS.Team2=Teams2._pageName',
'MS.PageAndTeam2=Ros2.PageAndTeam',
},
fields = h.getFields(),
where = ('MS.OverviewPage="%s"'):format(page),
}
return util_cargo.queryAndCast(query)
end
function h.getFields()
local fields = {
'MS.Team1',
'MS.Team2',
'MS.Team1Final',
'MS.Team2Final',
'MS.Winner [number]',
'MS.Player1',
'MS.Player2',
'MS.FF [number]',
'MS.Team1Score [number]',
'MS.Team2Score [number]',
'MS.Tab',
'MS.N_MatchInTab',
'MS.MatchId', -- idk what this is doing here
'MS.BestOf [number]',
}
if util_tournament.isInternational() then
fields[#fields+1] = 'COALESCE(Ros1.Region, Teams1.Region)=Team1Region'
fields[#fields+1] = 'COALESCE(Ros2.Region,Teams2.Region)=Team2Region'
end
return fields
end
function h.processCargoData(data)
local processed = {}
for _, row in ipairs(data) do
util_esports.setScoreDisplays(row)
if not row.Tab then
error(i18n.print('error_noTabDefined', row.MatchId))
elseif not row.MatchId then
error(i18n.print('error_noMatchIdDefined'))
end
processed[('%s_%s'):format(row.Tab,row.N_MatchInTab)] = {
winner = row.Winner,
team1 = h.getTeamCargoFromRow(row, 1),
team2 = h.getTeamCargoFromRow(row, 2),
}
end
return processed
end
function h.getTeamCargoFromRow(row, i)
local function field(field)
return row['Team' .. i .. field]
end
local ret = {
score = field('ScoreDisplay') and { field('ScoreDisplay') },
team = field(''),
teamfinal = field('Final'),
player = row['Player' .. i],
iswinner = row.Winner == i,
bestof = row.BestOf,
region = field('Region'),
}
return ret
end
function h.addProcessedToArgs(args, settings, processed, overviewPage)
for r, col in ipairs(settings) do
h.initializeMatch(args, r)
local title = args[r] and args[r].title or col.matches.title or ''
for m, _ in ipairs(col.matches) do
h.initializeMatch(args, r, m)
local argmatch = args[r] and args[r][m]
if argmatch and argmatch.cargomatch then
h.addMatchCargoToMatch(argmatch, processed[argmatch.cargomatch])
else
-- the matchid does NOT include page number in it
local matchid = ('%s_%s'):format(title, m)
if not argmatch then
h.initializeMatch(args, r, m)
argmatch = args[r][m]
end
h.addMatchCargoToMatch(argmatch, processed[matchid])
end
end
end
end
function h.addMatchCargoToMatch(argMatch, cargoDataMatch)
if not cargoDataMatch then return end
-- allow arg data to overwrite cargo data always if applicable
argMatch.winner = argMatch.winner or cargoDataMatch.winner
for _, team in ipairs({ 'team1', 'team2' }) do
for k, v in pairs(cargoDataMatch[team]) do
argMatch[team][k] = argMatch[team][k] or v
end
end
end
-- print
function h.makeOutput(args, settings)
local output = mw.html.create()
if settings.togglers then
h.printAllBrackets(args, settings, output)
else
h.printBracket(args, settings, output:tag('div'), {})
end
return output
end
function h.printAllBrackets(args, settings, output)
local toggleN = util_vars.setGlobalIndex('BracketToggler')
local togglers = h.makeTogglerButtons(settings.togglers, toggleN)
local tblRound1 = h.printNextBracketDiv(output, toggleN, 1)
h.printBracket(args, settings, tblRound1, togglers)
local tableList = { tblRound1 }
for i, toggle in ipairs(settings.togglers) do
h.setupNextToggle(settings, args, togglers, toggle, i)
local tbl = h.printNextBracketDiv(output, toggleN, i + 1)
h.printBracket(args, toggle.bracket, tbl, togglers)
tableList[#tableList+1] = tbl
end
h.setTableHidden(tableList, args.initround)
end
function h.setupNextToggle(settings, args, togglers, toggle, i)
h.fixColumnLabelsForToggle(settings, toggle.bracket, i)
table.remove(args, 1)
table.remove(togglers, 1)
h.processSettings(toggle.bracket, args)
end
function h.fixColumnLabelsForToggle(settings, bracket, i)
for k, col in ipairs(bracket) do
col.matches.title = settings[k + i].matches.title
end
end
function h.printNextBracketDiv(output, toggleN, i)
local div = output:tag('div')
:addClass(h.allToggleClass(toggleN, false))
:addClass(h.roundToggleClass(toggleN, i, false))
return div
end
function h.allToggleClass(n, isAttr)
local dot = isAttr and '.' or ''
return ('%sbracket-toggle-allrounds-%s'):format(dot, n)
end
function h.roundToggleClass(n, i, isAttr)
local dot = isAttr and '.' or ''
return ('%sbracket-toggle-round-%s-%s'):format(dot, n, i)
end
function h.makeTogglerButtons(togglers, n)
local tbl = {}
tbl[1] = h.makeToggler(n, 1)
for i, _ in ipairs(togglers) do
if i == #togglers then
tbl[#tbl+1] = h.makeLastToggler(n)
else
-- first add 1 because we already did 1 from the default bracket
tbl[#tbl+1] = h.makeToggler(n, i + 1)
end
end
return tbl
end
function h.makeToggler(n, i)
local div = mw.html.create('div')
:addClass('bracket-toggler')
:wikitext('[')
div:tag('span')
:addClass('alwaysactive-toggler')
:attr('data-toggler-hide', h.allToggleClass(n, true))
:attr('data-toggler-show', h.roundToggleClass(n, i + 1, true))
:wikitext('x')
div:wikitext(']')
return div
end
function h.makeLastToggler(n)
local div = mw.html.create('div')
:addClass('bracket-toggler')
div:tag('span')
:addClass('alwaysactive-toggler')
:attr('data-toggler-hide', h.allToggleClass(n, true))
:attr('data-toggler-show', h.roundToggleClass(n, 1, true))
:wikitext('<<')
return div
end
function h.setTableHidden(tableList, initround)
initround = tonumber(initround or 1) or 1
for k, tbl in ipairs(tableList) do
if k ~= initround then
tbl:addClass('toggle-section-hidden')
end
end
end
function h.printBracket(args, settings, tbl, togglers)
tbl:addClass('bracket-grid')
:css({
['grid-template-columns'] = h.getGTC(settings, args),
['grid-template-rows'] = h.getGTR(settings)
})
for r, col in ipairs(settings) do
CURRENT_ROUND = 'round' .. (r - 1)
h.addLinesColumn(tbl, col.lines)
CURRENT_ROUND = 'round' .. r
h.addMatchesColumn(tbl, args, col.matches, r, togglers[r])
end
return tbl
end
function h.getGTC(settings, args)
local scores = {}
for round, col in ipairs(settings) do
scores[round] = args[round] and tonumber(args[round].extendedseries or '') or col.extendedseries or 1
end
local firstcol = settings[1].lines and next(settings[1].lines)
local firstwidth = firstcol and LINEWIDTH or '0'
return h.getCustomGTC(scores, args.roundwidth, args.roundminwidth, firstwidth)
end
function h.getCustomGTC(scores, roundwidth, minwidth, firstwidth)
local linewidth = minwidth and ' minmax(2em,3em) ' or ' 3em '
roundwidth = h.getRoundwidth(roundwidth)
minwidth = h.parseWidth(minwidth) or roundwidth
local widths = {}
for k, v in ipairs(scores) do
local min = (SCOREWIDTH * (v - 1) + minwidth)
local max = (SCOREWIDTH * (v - 1) + roundwidth)
widths[#widths+1] = ('minmax(%sem, %sem)'):format(min, max)
end
return firstwidth .. ' ' .. table.concat(widths, linewidth)
end
function h.getRoundwidth(roundwidth)
if not roundwidth then return ROUNDWIDTH end
return h.parseWidth(roundwidth)
end
function h.parseWidth(width)
if not width then return nil end
return tonumber(width:gsub('em','') or '')
end
function h.getGTR(settings)
local max = 0
for _, col in ipairs(settings) do
local total = 0
for _, match in ipairs(col.matches) do
total = total + (match.above or 0)
if match.display == 'match' then
total = total + ROWS_PER_TEAM
elseif match.display == 'hline' then
total = total + ROWS_PER_HLINE
end
end
if total > max then
max = total
end
end
if PRINT_TITLES then max = max + ROWS_PER_TITLE end
return ('repeat(%s,var(--grid-row-height))'):format(max)
end
function h.addLinesColumn(tbl, lineData)
if not lineData then return end
for m, row in ipairs(lineData) do
if m == 1 and PRINT_TITLES then
h.addBracketLine(tbl, row, 2)
else
h.addBracketLine(tbl, row, 0)
end
end
end
function h.addBracketLine(tbl, linerow, extra)
if linerow.above + extra > 0 then
local div = tbl:tag('div')
:addClass('bracket-line')
:addClass(CURRENT_ROUND)
:cssText(('grid-row:span %s;'):format(linerow.above + extra))
end
tbl:tag('div')
:addClass('bracket-line')
:addClass(linerow.class)
:addClass(CURRENT_ROUND)
:cssText(('grid-row:span %s;'):format(linerow.height))
end
function h.addMatchesColumn(tbl, args, data, r, toggler)
if PRINT_TITLES then
local title = args[r] and args[r].title or data.title or ''
h.makeTitle(tbl, title, toggler)
end
for m, row in ipairs(data) do
local game = args[r] and args[r][m] or { team1 = {}, team2 = {} }
if row.above then
h.printSpacer(tbl, row.above)
end
if row.display == 'match' then
h.makeMatch(tbl, game, not args.nolabels and row.label)
elseif row.display == 'hline' then
h.makeHorizontalCell(tbl)
end
end
end
function h.makeTitle(tbl, text, toggler)
local outerdiv = tbl:tag('div')
:addClass('bracket-grid-header')
:addClass(CURRENT_ROUND)
local innerdiv = outerdiv:tag('div')
:addClass('bracket-header-content')
:wikitext(text)
if toggler then
innerdiv:node(toggler)
end
end
function h.makeHorizontalCell(tbl)
tbl:tag('div')
:addClass('bracket-spacer')
:addClass('horizontal')
:addClass(CURRENT_ROUND)
end
function h.makeMatch(tbl, game, label)
if game.label then label = game.label end
h.printSpacer(tbl, nil, label)
h.printTeam(tbl, game, game.team1)
h.printTeam(tbl, game, game.team2)
h.printSpacer(tbl, nil, nil)
end
function h.printSpacer(tbl, above, label)
local div = tbl:tag('div')
:addClass('bracket-spacer')
:addClass(CURRENT_ROUND)
if label then
div:wikitext(label)
end
if above then
div:cssText(('grid-row:span %s;'):format(above))
end
end
function h.printTeam(tbl, game, data)
local line = tbl:tag('div')
:addClass('bracket-team')
:addClass(CURRENT_ROUND)
:addClass(game.class)
if not data.bye then
util_esports.addTeamHighlighter(line, data.player or data.teamfinal or data.team)
end
if data.iswinner then
line:addClass('bracket-winner')
end
local team = line:tag('div')
:addClass('bracket-team-name')
if data.free then
team:wikitext(data.free)
elseif data.bye then
team:wikitext('BYE')
line:addClass('bracket-bye')
else
bracket_wiki.teamDisplay(team, data, TEAM_STYLE)
end
h.printScore(line, data)
end
function h.printScore(line, data)
if SHOW_BESTOF and not data.score then
h.printBestof(line, data)
return
end
for i, v in ipairs(data.score or { '' }) do
local div = line:tag('div')
:addClass('bracket-team-points')
:wikitext((not data.bye and v) or (data.bye and '-') or '')
if data.iswinner then
div:addClass('bracket-score-winner')
elseif data.iswinners and data.iswinners[i] then
div:addClass('bracket-score-loser')
end
end
end
function h.printBestof(line, data)
if not SHOW_BESTOF then return end
if data.bye then return end
local div = line:tag('div')
:addClass('bracket-team-points')
:addClass('bracket-team-bestof')
if data.bestof then
div:wikitext(i18n.print('bestof', data.bestof))
end
end
return p