From b46ef5c9d28c0b48b66e634760ffc57b88e0b58d Mon Sep 17 00:00:00 2001 From: hjpalpha Date: Tue, 12 Sep 2023 15:18:21 +0200 Subject: [PATCH] rework --- components/match2/commons/matches_table.lua | 366 ------------------ .../matches_table_starcraft.lua | 15 - .../matches_table/commons/matches_table.lua | 366 ++++++++++++++++++ .../commons/matches_table_custom.lua | 20 + 4 files changed, 386 insertions(+), 381 deletions(-) delete mode 100644 components/match2/commons/matches_table.lua delete mode 100644 components/match2/commons/starcraft_starcraft2/matches_table_starcraft.lua create mode 100644 components/matches_table/commons/matches_table.lua create mode 100644 components/matches_table/commons/matches_table_custom.lua diff --git a/components/match2/commons/matches_table.lua b/components/match2/commons/matches_table.lua deleted file mode 100644 index 62943f55b72..00000000000 --- a/components/match2/commons/matches_table.lua +++ /dev/null @@ -1,366 +0,0 @@ ---- --- @Liquipedia --- wiki=commons --- page=Module:MatchesTable --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - -local MatchesTable = {} - -local Class = require('Module:Class') -local Countdown = require('Module:Countdown') -local HiddenSort = require('Module:HiddenSort') -local Logic = require('Module:Logic') -local Lua = require('Module:Lua') -local String = require('Module:StringUtils') -local Table = require('Module:Table') - -local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper', {requireDevIfEnabled = true}) - -local OpponentLibraries = require('Module:OpponentLibraries') -local Opponent = OpponentLibraries.Opponent -local OpponentDisplay = OpponentLibraries.OpponentDisplay - -local UTC = ' UTC' -local TBD = 'TBD' -local DEFAULT_TBD_IDENTIFIER = 'tbd' -local WINNER_LEFT = 1 -local WINNER_RIGHT = 2 -local SCORE_STATUS = 'S' -local DO_FLIP = true -local NO_FLIP = false -local LEFT_SIDE_OPPONENT = 'Left' -local RIGHT_SIDE_OPPONENT = 'Right' -local DEFAULT_QUERY_LIMIT = 1000 - -local _args -local _matchHeader -local _currentId - --- If run in abbreviated roundnames mode -local ABBREVIATIONS = { - ["Upper Bracket"] = "UB", - ["Lower Bracket"] = "LB", -} - -function MatchesTable.run(args) - _args = args or {} - - local data = mw.ext.LiquipediaDB.lpdb('match2', { - limit = tonumber(_args.limit) or DEFAULT_QUERY_LIMIT, - offset = 0, - order = 'date asc', - conditions = MatchesTable._buildConditions(), - }) - - local output = mw.html.create('table') - :addClass('wikitable wikitable-striped sortable match-card') - :node(MatchesTable._header()) - - if type(data[1]) == 'table' then - for _, match in ipairs(data) do - output:node(MatchesTable._row(match)) - end - end - - return mw.html.create('div') - :addClass('table-responsive') - :css('margin-bottom', '10px') - :node(output) -end - -function MatchesTable._buildConditions() - local tournament = _args.tournament or mw.title.getCurrentTitle().prefixedText - tournament = string.gsub(tournament,'%s','_') - - local lpdbConditions = '[[pagename::' .. tournament .. ']] AND [[date::>1970-01-01]]' - if _args.sdate then - lpdbConditions = lpdbConditions .. ' AND ([[date::>' .. _args.sdate .. ']] OR [[date::' .. _args.sdate .. ']])' - end - if _args.edate then - lpdbConditions = lpdbConditions .. ' AND ([[date::<' .. _args.edate .. ']] OR [[date::' .. _args.edate .. ']])' - end - if _args.section then - lpdbConditions = lpdbConditions .. ' AND [[match2bracketdata_sectionheader::' .. _args.matchsection .. ']]' - end - if _args.matchsection then - lpdbConditions = lpdbConditions .. ' AND [[extradata_matchsection::' .. _args.matchsection .. ']]' - end - - return lpdbConditions -end - -function MatchesTable._header() - local header = mw.html.create('tr') - :addClass('HeaderRow') - :node(mw.html.create('th') - :addClass('divCell') - :attr('data-sort-type','isoDate') - :wikitext('Date') - ) - - if _args.hideround ~= 'true' then - local cell = mw.html.create('th') - :addClass('divCell') - :wikitext('Round') - if not _args.sortround then - cell:addClass('unsortable') - end - header:node(cell) - end - - header - :node(mw.html.create('th') - :addClass('divCell') - :wikitext('Opponent') - ) - :node(mw.html.create('th') - :addClass('divCell') - :css('width','50') - :wikitext('Score') - ) - :node(mw.html.create('th') - :addClass('divCell') - :wikitext('vs. Opponent') - ) - - return header -end - -function MatchesTable._row(match) - local matchHeader = match.match2bracketdata.header or match.extradata.matchsection - if String.isEmpty(matchHeader) then - --if we are in the same matchGroup just use the previous _matchHeader - if _currentId == match.match2bracketid then - matchHeader = _matchHeader - end - --if we do not have a matchHeader yet try: - -- 1) the title (in case it is a matchlist) - -- 2) the sectionheader - -- 3) fallback to the previous _matchHeader - -- last one only applies if we are in a new matchGroup due to it already being used before else - if String.isEmpty(matchHeader) then - matchHeader = string.gsub(match.match2bracketdata.title or '', '%s[mM]atches', '') - if String.isEmpty(matchHeader) then - matchHeader = match.match2bracketdata.sectionheader - if String.isEmpty(matchHeader) then - matchHeader = _matchHeader - end - end - end - end - if String.isEmpty(matchHeader) then - matchHeader = ' ' - end - _currentId = match.match2bracketid - - if type(matchHeader) == 'string' then - --if the header is a default bracket header we need to convert it to proper display text - matchHeader = DisplayHelper.expandHeader(matchHeader) - end - _matchHeader = matchHeader - - if Logic.readBool(_args.shortedroundnames) then - --for default headers in brackets the 3rd entry is the shortest, so use that - --for non default (i.e. custom) entries it might not be set - --so use the first entry as a fallback - matchHeader = matchHeader[3] - or MatchesTable._applyCustomAbbreviations(matchHeader[1]) - else - matchHeader = matchHeader[1] - end - - local row = mw.html.create('tr') - :addClass('Match') - - local dateCell = mw.html.create('td') - :addClass('Date') - - local dateDisplay - if Logic.readBool(match.dateexact) then - local countdownArgs = {} - if (not Logic.readBool(match.finished)) and Logic.readBool(_args.countdown) then - countdownArgs = match.stream or {} - countdownArgs.rawcountdown = 'true' - else - countdownArgs.rawdatetime = 'true' - end - countdownArgs.date = match.date .. _UTC - dateDisplay = Countdown._create(countdownArgs) - elseif _args.dateexact == 'true' then - dateCell - :css('text-align', 'center') - :css('font-style', 'italic') - dateDisplay = 'To be announced' - else - dateDisplay = mw.language.new('en'):formatDate('F j, Y', match.date) - end - - dateCell - :node(HiddenSort.run(match.date)) - :node(dateDisplay) - - row:node(dateCell) - - if not Logic.readBool(_args.hideround) then - local roundCell = mw.html.create('td') - :addClass('Round') - if String.isNotEmpty(matchHeader) then - roundCell:wikitext(matchHeader) - end - row:node(roundCell) - end - - row:node( - MatchesTable._buildOpponent( - match.match2opponents[1], - DO_FLIP, - LEFT_SIDE_OPPONENT - ) - ) - - row:node(MatchesTable.score(match)) - - row:node( - MatchesTable._buildOpponent( - match.match2opponents[2], - NO_FLIP, - RIGHT_SIDE_OPPONENT - ) - ) - - return row -end - -function MatchesTable._applyCustomAbbreviations(matchHeader) - for long, short in pairs(_ABBREVIATIONS) do - matchHeader = matchHeader:gsub(long, short) - end -end - -function MatchesTable._buildOpponent(opponent, flip, side) - opponent = Opponent.fromMatch2Record(opponent) - - local opponentDisplay - if MatchesTable.opponentIsTbdOrEmpty(opponent) then - opponentDisplay = mw.html.create('i') - :wikitext(TBD) - else - opponentDisplay = OpponentDisplay.InlineOpponent{ - opponent = opponent, - teamStyle = 'short', - flip = flip, - } - end - - return mw.html.create('td') - :addClass('Team' .. side) - :node(opponentDisplay) -end - --- overridable value -MatchesTable.tbdIdentifier = DEFAULT_TBD_IDENTIFIER -function MatchesTable.opponentIsTbdOrEmpty(opponent) - local firstPlayer = (opponent.players or {})[1] or {} - - local listToCheck = { - string.lower(firstPlayer.pageName or opponent.name or ''), - string.lower(firstPlayer.displayName or ''), - string.lower(opponent.template or ''), - } - - return Table.includes(listToCheck, MatchesTable.tbdIdentifier) - or Table.all(listToCheck, function(_, value) return String.isEmpty(value) end) -end - -function MatchesTable.score(match) - local scoreCell = mw.html.create('td') - :addClass('Score') - - local scoreDisplay = 'vs.' - if - Logic.readBool(match.finished) or ( - Logic.readBool(match.dateexact) and - os.time() >= MatchesTable._parseDateTime(match.date) - ) - then - scoreDisplay = MatchesTable.scoreDisplay(match) - end - - if (tonumber(match.bestof) or 0) > 0 then - return scoreCell - :node(mw.html.create('div') - :css('line-height', '1.1') - :node(scoreDisplay) - ) - :node(mw.html.create('div') - :css('font-size', '75%') - :css('padding-bottom', '1px') - :wikitext('(') - :node(MatchesTable._bestof(match.bestof)) - :wikitext(')') - ) - end - - return scoreCell:wikitext(scoreDisplay) -end - -function MatchesTable.scoreDisplay(match) - return MatchesTable.getOpponentScore( - match.match2opponents[1], - match.winner == WINNER_LEFT - ) .. ':' .. MatchesTable.getOpponentScore( - match.match2opponents[2], - match.winner == WINNER_RIGHT - ) -end - -function MatchesTable.getOpponentScore(opponent, isWinner) - local score - if opponent.status == SCORE_STATUS then - score = tonumber(opponent.score) - if score == -1 then - score = 0 - end - else - score = opponent.status or '' - end - if isWinner then - score = '' .. score .. '' - end - - return score -end - -function MatchesTable._parseDateTime(str) - local year, month, day, hour, minutes, seconds - = str:match("(%d%d%d%d)-?(%d?%d?)-?(%d?%d?) (%d?%d?):(%d?%d?):(%d?%d?)$") - - -- Adjust time based on server timezone offset from UTC - local offset = os.time(os.date("*t") --[[@as osdate]]) - os.time(os.date("!*t") --[[@as osdate]]) - -- create time - this will take our UTC timestamp and put it into localtime without converting - local localTime = os.time{ - year = year, - month = month, - day = day, - hour = hour, - min = minutes, - sec = seconds - } - - return localTime + offset -- "Convert" back to UTC -end - -function MatchesTable._bestof(value) - value = tonumber(value) - if not value then - return nil - end - - return mw.html.create('abbr') - :attr('title', 'Best of ' .. value) - :wikitext('Bo' .. value) -end - -return Class.export(MatchesTable) diff --git a/components/match2/commons/starcraft_starcraft2/matches_table_starcraft.lua b/components/match2/commons/starcraft_starcraft2/matches_table_starcraft.lua deleted file mode 100644 index 89500849c69..00000000000 --- a/components/match2/commons/starcraft_starcraft2/matches_table_starcraft.lua +++ /dev/null @@ -1,15 +0,0 @@ ---- --- @Liquipedia --- wiki=commons --- page=Module:MatchesTable/Starcraft --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - -local Lua = require('Module:Lua') -local CustomMatchesTable = Lua.import('Module:MatchesTable', {requireDevIfEnabled = true}) - -CustomMatchesTable.OpponentDisplay = Lua.import('Module:OpponentDisplay/Starcraft', {requireDevIfEnabled = true}) -CustomMatchesTable.Opponent = Lua.import('Module:Opponent/Starcraft', {requireDevIfEnabled = true}) - -return CustomMatchesTable diff --git a/components/matches_table/commons/matches_table.lua b/components/matches_table/commons/matches_table.lua new file mode 100644 index 00000000000..6e64c8760b0 --- /dev/null +++ b/components/matches_table/commons/matches_table.lua @@ -0,0 +1,366 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:MatchesTable +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Abbreviation = require('Module:Abbreviation') +local Array = require('Module:Array') +local Class = require('Module:Class') +local Countdown = require('Module:Countdown') +local HiddenSort = require('Module:HiddenSort') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Table = require('Module:Table') + +local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper', {requireDevIfEnabled = true}) + +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent +local OpponentDisplay = OpponentLibraries.OpponentDisplay + +local Condition = require('Module:Condition') +local ConditionTree = Condition.Tree +local ConditionNode = Condition.Node +local Comparator = Condition.Comparator +local BooleanOperator = Condition.BooleanOperator +local ColumnName = Condition.ColumnName + +local UTC = ' UTC' +local TBD = 'TBD' +local WINNER_LEFT = 1 +local WINNER_RIGHT = 2 +local SCORE_STATUS = 'S' +local DO_FLIP = true +local NO_FLIP = false +local LEFT_SIDE_OPPONENT = 'Left' +local RIGHT_SIDE_OPPONENT = 'Right' +local DEFAULT_QUERY_LIMIT = 1000 + +-- If run in abbreviated roundnames mode +local ABBREVIATIONS = { + ["Upper Bracket"] = "UB", + ["Lower Bracket"] = "LB", +} + +---@class MatchesTable +---@operator call(table): MatchesTable +---@field args table +---@field config table +---@field currentId string? +---@field currentMatchHeader string[]? +local MatchesTable = Class.new(function(self, args) self:init(args) end) + +---@param args any +---@return MatchesTable +function MatchesTable:init(args) + args = args or {} + self.args = args + + args.tournament = args.tournament or mw.title.getCurrentTitle().prefixedText + + self.config = { + limit = tonumber(args.limit) or DEFAULT_QUERY_LIMIT, + startDate = args.sdate, + endDate = args.edate, + section = args.section, + matchSection = args.matchsection, + showRound = not Logic.readBool(args.hideround), + sortRound = Logic.readBool(args.sortround), + showCountdown = Logic.readBool(args.countdown), + onlyShowExactDates = Logic.readBool(args.dateexact), + shortenRoundNames = Logic.readBool(args.shortedroundnames), + pages = Array.map(Array.extractValues( + Table.filterByKey(args, function(key) return key:find('^tournament%d-$') end) + ), function(page) return (page:gsub(' ', '_')) end), + } + + return self +end + +---@return MatchesTable +function MatchesTable:build() + local matches = mw.ext.LiquipediaDB.lpdb('match2', { + limit = self.config.limit, + order = 'date asc', + conditions = self:buildConditions(), + }) + + if type(matches[1]) == 'table' then + self.matches = matches + end + + return self +end + +---@return string +function MatchesTable:buildConditions() + local config = self.config + + local conditions = ConditionTree(BooleanOperator.all) + :add{ConditionNode(ColumnName('date'), Comparator.gt, '1970-01-01')} + + local pageConditions = ConditionTree(BooleanOperator.any) + for _, page in pairs(config.pages --[[@as string[] ]]) do + pageConditions:add{ConditionNode(ColumnName('pagename'), Comparator.eq, page)} + end + conditions:add(pageConditions) + + if config.startDate then + pageConditions:add(ConditionTree(BooleanOperator.any):add{ + ConditionNode(ColumnName('date'), Comparator.eq, config.startDate), + ConditionNode(ColumnName('date'), Comparator.gt, config.startDate), + }) + end + + if config.endDate then + pageConditions:add(ConditionTree(BooleanOperator.any):add{ + ConditionNode(ColumnName('date'), Comparator.eq, config.endDate), + ConditionNode(ColumnName('date'), Comparator.lt, config.endDate), + }) + end + + if config.matchSection then + pageConditions:add{ConditionNode(ColumnName('extradata_matchsection'), Comparator.eq, config.matchSection)} + end + + if config.section then + pageConditions:add{ConditionNode(ColumnName('match2bracketdata_sectionheader'), Comparator.eq, config.section)} + end + + return conditions:toString() +end + +---@return Html? +function MatchesTable:create() + if not self.matches then return end + + self.output = mw.html.create('table') + :addClass('wikitable wikitable-striped sortable match-card') + :node(self:header()) + + Array.forEach(self.matches, function(match, matchIndex) return self:row(match) end) + + return mw.html.create('div') + :addClass('table-responsive') + :css('margin-bottom', '10px') + :node(self.output) +end + +---@return Html +function MatchesTable:header() + local header = mw.html.create('tr') + :addClass('HeaderRow') + :node(mw.html.create('th') + :addClass('divCell') + :attr('data-sort-type','isoDate') + :wikitext('Date') + ) + + if self.config.showRound then + header:tag('th') + :addClass('divCell') + :addClass(not self.config.sortRound and 'unsortable' or nil) + :wikitext('Round') + end + + return header + :tag('th'):addClass('divCell'):wikitext('Opponent'):done() + :tag('th'):addClass('divCell'):css('width','50'):wikitext('Score'):done() + :tag('th'):addClass('divCell'):wikitext('vs. Opponent'):done() +end + +---@param match table +---@return Html +function MatchesTable:dateDisplay(match) + local dateCell = mw.html.create('td') + :addClass('Date') + :node(HiddenSort.run(match.date)) + + if Logic.readBool(match.dateexact) then + local countdownArgs = {} + if self.config.showCountdown and not Logic.readBool(match.finished) then + countdownArgs = match.stream or {} + countdownArgs.rawcountdown = true + else + countdownArgs.rawdatetime = true + end + countdownArgs.date = match.date .. UTC + return dateCell:wikitext(Countdown._create(countdownArgs)) + elseif self.config.onlyShowExactDates then + return dateCell + :css('text-align', 'center') + :css('font-style', 'italic') + :wikitext('To be announced') + end + + return dateCell:wikitext(mw.language.new('en'):formatDate('F j, Y', match.date)) +end + +---@param match table +function MatchesTable:row(match) + local matchHeader = self:determineMatchHeader(match) + + local row = mw.html.create('tr') + :addClass('Match') + :node(self:dateDisplay(match)) + + if self.config.showRound then + row:tag('td'):addClass('Round'):wikitext(matchHeader) + end + + row + :node(MatchesTable._buildOpponent(match.match2opponents[1], DO_FLIP, LEFT_SIDE_OPPONENT)) + :node(MatchesTable.score(match)) + :node(MatchesTable._buildOpponent(match.match2opponents[2], NO_FLIP, RIGHT_SIDE_OPPONENT)) + + self.output:node(row) +end + +---@param match table +---@return string +function MatchesTable:determineMatchHeader(match) + local matchHeader = Logic.emptyOr( + match.match2bracketdata.header or match.extradata.matchsection, + self.currentId == match.match2bracketid and self.currentMatchHeader or nil, + --if we do not have a matchHeader yet try: + -- 1) the title (in case it is a matchlist) + -- 2) the sectionheader + -- 3) fallback to the previous _matchHeader + -- last one only applies if we are in a new matchGroup due to it already being used before else + Logic.emptyOr( + string.gsub(match.match2bracketdata.title or '', '%s[mM]atches', ''), + match.match2bracketdata.sectionheader, + self.currentMatchHeader or ' ' + )) + + + --if the header is a default bracket header we need to convert it to proper display text + local headerArray DisplayHelper.expandHeader(matchHeader) + + self.currentId = match.match2bracketid + self.currentMatchHeader = headerArray + + if self.config.shortenRoundNames then + return headerArray[3] or MatchesTable._applyCustomAbbreviations(headerArray[1]) + end + + return headerArray[1] +end + +---@param matchHeader any +---@return string +function MatchesTable._applyCustomAbbreviations(matchHeader) + for long, short in pairs(ABBREVIATIONS) do + matchHeader = matchHeader:gsub(long, short) + end + + return matchHeader +end + +---@param opponent standardOpponent +---@param flip boolean +---@param side string +---@return Html +function MatchesTable._buildOpponent(opponent, flip, side) + local opponentCell = mw.html.create('td'):addClass('Team' .. side) + + opponent = Opponent.fromMatch2Record(opponent) + + if Opponent.isTbd(opponent) or Opponent.isEmpty(opponent) then + return opponentCell:tag('i'):wikitext(TBD):done() + end + + return opponentCell:node(OpponentDisplay.InlineOpponent{ + opponent = opponent, + teamStyle = 'short', + flip = flip, + }) +end + +---@param match table +---@return Html +function MatchesTable.score(match) + local scoreCell = mw.html.create('td') + :addClass('Score') + + local scoreDisplay = (Logic.readBool(match.finished) or ( + Logic.readBool(match.dateexact) and + os.time() >= MatchesTable._parseDateTime(match.date) + )) and MatchesTable.scoreDisplay(match) or 'vs' + + if (tonumber(match.bestof) or 0) <= 0 then + return scoreCell:wikitext(scoreDisplay) + end + + return scoreCell + :tag('div'):css('line-height', '1.1'):node(scoreDisplay):done() + :tag('div') + :css('font-size', '75%') + :css('padding-bottom', '1px') + :wikitext('(') + :node(MatchesTable._bestof(match.bestof)) + :wikitext(')') + :done() +end + +---@param match table +---@return string +function MatchesTable.scoreDisplay(match) + return MatchesTable.getOpponentScore( + match.match2opponents[1], + match.winner == WINNER_LEFT + ) .. ':' .. MatchesTable.getOpponentScore( + match.match2opponents[2], + match.winner == WINNER_RIGHT + ) +end + +---@param opponent any +---@param isWinner any +---@return (string|number)? +function MatchesTable.getOpponentScore(opponent, isWinner) + local score + if opponent.status == SCORE_STATUS then + score = tonumber(opponent.score) + score = score == -1 and 0 or score + else + score = opponent.status or '' + end + if isWinner then + return '' .. score .. '' + end + + return score +end + +---@param str string +---@return integer +function MatchesTable._parseDateTime(str) + local year, month, day, hour, minutes, seconds + = str:match("(%d%d%d%d)-?(%d?%d?)-?(%d?%d?) (%d?%d?):(%d?%d?):(%d?%d?)$") + + -- Adjust time based on server timezone offset from UTC + local offset = os.time(os.date("*t") --[[@as osdateparam]]) - os.time(os.date("!*t") --[[@as osdateparam]]) + -- create time - this will take our UTC timestamp and put it into localtime without converting + local localTime = os.time{ + year = year, + month = month, + day = day, + hour = hour, + min = minutes, + sec = seconds + } + + return localTime + offset -- "Convert" back to UTC +end + +---@param value string|number +---@return string +function MatchesTable._bestof(value) + return Abbreviation.make('Bo' .. value, 'Best of ' .. value) --[[@as string]] +end + +return Class.export(MatchesTable) diff --git a/components/matches_table/commons/matches_table_custom.lua b/components/matches_table/commons/matches_table_custom.lua new file mode 100644 index 00000000000..76ade37f1f2 --- /dev/null +++ b/components/matches_table/commons/matches_table_custom.lua @@ -0,0 +1,20 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:MatchesTable/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local MatchesTable = Lua.import('Module:MatchesTable', {requireDevIfEnabled = true}) + +local CustomMatchesTable = {} + +function CustomMatchesTable.run(args) + return MatchesTable(args):build():create() +end + +return Class.export(CustomMatchesTable) \ No newline at end of file