diff --git a/lua/spec/snapshots/teamcard_legacy.png b/lua/spec/snapshots/teamcard_legacy.png new file mode 100644 index 00000000000..55592f4bac5 Binary files /dev/null and b/lua/spec/snapshots/teamcard_legacy.png differ diff --git a/lua/spec/teamcard_legacy_spec.lua b/lua/spec/teamcard_legacy_spec.lua new file mode 100644 index 00000000000..79a46b73db0 --- /dev/null +++ b/lua/spec/teamcard_legacy_spec.lua @@ -0,0 +1,614 @@ +--- Triple Comment to Enable our LLS Plugin +describe('TeamCard Legacy', function() + describe('parseQualifier', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('returns nil for nil input', function() + assert.is_nil(LegacyTeamCard.parseQualifier(nil)) + end) + + it('parses plain text as method=qual type=other', function() + local q = LegacyTeamCard.parseQualifier('Foo Bar') + assert.are_same({method = 'qual', type = 'other', text = 'Foo Bar'}, q) + end) + + it('detects "Invited" as method=invite', function() + local q = LegacyTeamCard.parseQualifier('Invited') + assert.are_same({method = 'invite', type = 'other', text = 'Invited'}, q) + end) + + it('detects "invite" case-insensitively', function() + local q = LegacyTeamCard.parseQualifier('invite via league') + assert.are_equal('invite', q.method) + assert.are_equal('other', q.type) + assert.are_equal('invite via league', q.text) + end) + + it('parses internal link as method=qual type=tournament when tournament resolves', function() + local stubTournament = stub(require('Module:Tournament'), 'getTournament', + function() return {pageName = 'Foo_Bar/2022'} end) + local q = LegacyTeamCard.parseQualifier('[[Foo_Bar/2022|Qualifier]]') + assert.are_same({method = 'qual', type = 'tournament', page = 'Foo_Bar/2022', text = 'Qualifier'}, q) + stubTournament:revert() + end) + + it('parses internal link as method=qual type=internal when tournament does not resolve', function() + local stubTournament = stub(require('Module:Tournament'), 'getTournament', function() return nil end) + local q = LegacyTeamCard.parseQualifier('[[Some_Page|Some Text]]') + assert.are_same({method = 'qual', type = 'internal', page = 'Some_Page', text = 'Some Text'}, q) + stubTournament:revert() + end) + + it('parses external link as method=qual type=external', function() + local q = LegacyTeamCard.parseQualifier('[https://foo.bar Foo Bar]') + assert.are_same({method = 'qual', type = 'external', url = 'https://foo.bar', text = 'Foo Bar'}, q) + end) + + it('handles relative internal link', function() + local stubTournament = stub(require('Module:Tournament'), 'getTournament', function() return nil end) + local q = LegacyTeamCard.parseQualifier('[[/Qualifier|Qual]]') + assert.are_equal('internal', q.type) + -- exact page resolved relative to current page; check it begins with the current page name + assert.is_truthy(q.page) + stubTournament:revert() + end) + end) + + describe('mapPlayer basic mapping', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('reads display from positional', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'Faker'}, 'p1', nil) + assert.are_equal('Faker', p[1]) + end) + + it('reads link', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'Faker', p1link = 'Lee Sang-hyeok'}, 'p1', nil) + assert.are_equal('Lee Sang-hyeok', p.link) + end) + + it('reads flag', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'Faker', p1flag = 'kr'}, 'p1', nil) + assert.are_equal('kr', p.flag) + end) + + it('prefers flag_o over flag', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'Faker', p1flag = 'kr', p1flag_o = 'us'}, 'p1', nil) + assert.are_equal('us', p.flag) + end) + + it('reads team override', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1team = 'oldTeam'}, 'p1', nil) + assert.are_equal('oldTeam', p.team) + end) + + it('reads id', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1id = 'faker-id'}, 'p1', nil) + assert.are_equal('faker-id', p.id) + end) + + it('reads faction', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1faction = 'p'}, 'p1', nil) + assert.are_equal('p', p.faction) + end) + + it('reads race as faction fallback', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1race = 'z'}, 'p1', nil) + assert.are_equal('z', p.faction) + end) + + it('reads pos as role', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1pos = 'top'}, 'p1', nil) + assert.are_equal('top', p.role) + end) + end) + + describe('mapPlayer status & trophies', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('sums wins and winsc into trophies', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1wins = '2', p1winsc = '1'}, 'p1', nil) + assert.are_equal(3, p.trophies) + end) + + it('trophies nil when neither set', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X'}, 'p1', nil) + assert.is_nil(p.trophies) + end) + + it('passes joindate/leavedate through', function() + local p = LegacyTeamCard.mapPlayer( + {p1 = 'X', p1joindate = '2025-01-01', p1leavedate = '2025-12-01'}, 'p1', nil) + assert.are_equal('2025-01-01', p.joindate) + assert.are_equal('2025-12-01', p.leavedate) + end) + + it('reads played true', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1played = 'true'}, 'p1', nil) + assert.is_true(p.played) + end) + + it('reads result as played fallback', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1result = 'true'}, 'p1', nil) + assert.is_true(p.played) + end) + + it('dnp forces played=false even if result=true', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1result = 'true', p1dnp = 'true'}, 'p1', nil) + assert.is_false(p.played) + end) + + it('pNsub sets status=sub', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1sub = 'true'}, 'p1', nil) + assert.are_equal('sub', p.status) + end) + + it('pNleave sets status=former', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1leave = 'true'}, 'p1', nil) + assert.are_equal('former', p.status) + end) + + it('pNleave overrides pNsub', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1sub = 'true', p1leave = 'true'}, 'p1', nil) + assert.are_equal('former', p.status) + end) + + it('pNsub=true + pNdnp=true: status=sub and played=false', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1sub = 'true', p1dnp = 'true'}, 'p1', nil) + assert.are_equal('sub', p.status) + assert.is_false(p.played) + end) + + it('pNleave=true + pNdnp=true: status=former and played=false', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', p1leave = 'true', p1dnp = 'true'}, 'p1', nil) + assert.are_equal('former', p.status) + assert.is_false(p.played) + end) + end) + + describe('mapPlayer source groups', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('source group s sets status=sub', function() + local p = LegacyTeamCard.mapPlayer({s1 = 'X'}, 's1', 's') + assert.are_equal('sub', p.status) + end) + + it('source group f sets status=former', function() + local p = LegacyTeamCard.mapPlayer({f1 = 'X'}, 'f1', 'f') + assert.are_equal('former', p.status) + end) + + it('source group s + subdnpdefault sets played=false when no result', function() + local p = LegacyTeamCard.mapPlayer( + {s1 = 'X', subdnpdefault = 'true'}, 's1', 's') + assert.is_false(p.played) + assert.are_equal('sub', p.status) + end) + + it('source group s + subdnpdefault + explicit result keeps played=true', function() + local p = LegacyTeamCard.mapPlayer( + {s1 = 'X', s1result = 'true', subdnpdefault = 'true'}, 's1', 's') + assert.is_true(p.played) + end) + + it('main group with noVarDefault leaves played untouched', function() + local p = LegacyTeamCard.mapPlayer({p1 = 'X', noVarDefault = 'true'}, 'p1', nil) + assert.is_nil(p.played) + end) + + it('source group s with noVarDefault and no result sets played=false', function() + local p = LegacyTeamCard.mapPlayer({s1 = 'X', noVarDefault = 'true'}, 's1', 's') + assert.is_false(p.played) + end) + + it('source group f with noVarDefault leaves played untouched', function() + local p = LegacyTeamCard.mapPlayer({f1 = 'X', noVarDefault = 'true'}, 'f1', 'f') + assert.is_nil(p.played) + end) + + it('source group s + pNsub=true: status=sub (redundant, no conflict)', function() + local p = LegacyTeamCard.mapPlayer({s1 = 'X', s1sub = 'true'}, 's1', 's') + assert.are_equal('sub', p.status) + end) + end) + + describe('mapCoach', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('coach defaults role to coach', function() + local c = LegacyTeamCard.mapCoach({c1 = 'Score'}, 'c1', nil) + assert.are_equal('coach', c.role) + assert.are_equal('staff', c.type) + end) + + it('coach with cNpos overrides role', function() + local c = LegacyTeamCard.mapCoach({c1 = 'Score', c1pos = 'head coach'}, 'c1', nil) + assert.are_equal('head coach', c.role) + end) + + it('scN source group sets status=sub', function() + local c = LegacyTeamCard.mapCoach({sc1 = 'Mata'}, 'sc1', 'sc') + assert.are_equal('coach', c.role) + assert.are_equal('sub', c.status) + end) + + it('fcN source group sets status=former', function() + local c = LegacyTeamCard.mapCoach({fc1 = 'kkOma'}, 'fc1', 'fc') + assert.are_equal('coach', c.role) + assert.are_equal('former', c.status) + end) + + it('cNsub sets status=sub', function() + local c = LegacyTeamCard.mapCoach({c1 = 'X', c1sub = 'true'}, 'c1', nil) + assert.are_equal('sub', c.status) + end) + + it('wins+winsc sum to trophies', function() + local c = LegacyTeamCard.mapCoach({c1 = 'X', c1wins = '1', c1winsc = '2'}, 'c1', nil) + assert.are_equal(3, c.trophies) + end) + + it('flag_o wins over flag', function() + local c = LegacyTeamCard.mapCoach({c1 = 'X', c1flag = 'kr', c1flag_o = 'us'}, 'c1', nil) + assert.are_equal('us', c.flag) + end) + end) + + describe('mapPlayers enumeration', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('enumerates main players', function() + local players = LegacyTeamCard.mapPlayers({p1 = 'A', p2 = 'B', p3 = 'C'}) + assert.are_equal(3, #players) + assert.are_equal('A', players[1][1]) + assert.are_equal('B', players[2][1]) + end) + + it('appends sN players with status=sub', function() + local players = LegacyTeamCard.mapPlayers({p1 = 'A', s1 = 'B'}) + assert.are_equal(2, #players) + assert.is_nil(players[1].status) + assert.are_equal('sub', players[2].status) + end) + + it('appends fN players with status=former', function() + local players = LegacyTeamCard.mapPlayers({p1 = 'A', f1 = 'B'}) + assert.are_equal('former', players[2].status) + end) + + it('reads t2p* bucketed by t2type', function() + local players = LegacyTeamCard.mapPlayers({p1 = 'A', t2p1 = 'B', t2type = 'sub'}) + assert.are_equal('sub', players[2].status) + end) + + it('t2type=staff promotes t2p* to type=staff', function() + local players = LegacyTeamCard.mapPlayers({p1 = 'A', t2p1 = 'B', t2type = 'staff'}) + assert.are_equal('staff', players[2].type) + end) + + it('dedups t2p* against s* by pageName, t2p* wins', function() + local players = LegacyTeamCard.mapPlayers({ + p1 = 'Faker', + s1 = 'Pawn', s1link = 'Pawn (Korean)', + t2p1 = 'Pawn (player)', t2p1link = 'Pawn (Korean)', t2type = 'sub', + }) + local pawnCount = 0 + for _, p in ipairs(players) do + if p.link == 'Pawn (Korean)' then pawnCount = pawnCount + 1 end + end + assert.are_equal(1, pawnCount) + end) + end) + + describe('mapCoaches enumeration', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('enumerates main coaches', function() + local coaches = LegacyTeamCard.mapCoaches({c1 = 'A', c2 = 'B'}) + assert.are_equal(2, #coaches) + assert.are_equal('coach', coaches[1].role) + assert.are_equal('coach', coaches[2].role) + end) + + it('appends scN as sub coaches', function() + local coaches = LegacyTeamCard.mapCoaches({c1 = 'A', sc1 = 'B'}) + assert.are_equal('sub', coaches[2].status) + end) + + it('reads t2c* with t2type', function() + local coaches = LegacyTeamCard.mapCoaches({c1 = 'A', t2c1 = 'B', t2type = 'former'}) + assert.are_equal('former', coaches[2].status) + end) + end) + + describe('mapCard', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('uses link over team for template', function() + local card = LegacyTeamCard.mapCard({team = 'Display', link = 'Real Team'}) + assert.are_equal('Real Team', card[1]) + end) + + it('falls back to team if no link', function() + local card = LegacyTeamCard.mapCard({team = 'Solo Team'}) + assert.are_equal('Solo Team', card[1]) + end) + + it('team2/team3 populate contenders with all three teams', function() + local card = LegacyTeamCard.mapCard({ + team = 'A', team2 = 'B', team3 = 'C', + }) + assert.is_nil(card[1]) + assert.are_same({'A', 'B', 'C'}, card.contenders) + end) + + it('contender uses link if present', function() + local card = LegacyTeamCard.mapCard({ + team = 'A', link = 'AReal', + team2 = 'B', link2 = 'BReal', + }) + assert.are_same({'AReal', 'BReal'}, card.contenders) + end) + + it('qualification built from qualifier', function() + local card = LegacyTeamCard.mapCard({team = 'A', qualifier = 'Invited'}) + assert.are_equal('invite', card.qualification.method) + end) + + it('notes and inotes both populate notes list', function() + local card = LegacyTeamCard.mapCard({team = 'A', notes = 'note A', inotes = 'note B'}) + assert.are_equal(2, #card.notes) + assert.are_equal('note A', card.notes[1][1]) + assert.are_equal('note B', card.notes[2][1]) + end) + + it('aliases reads alsoknownas first then aliases', function() + assert.are_equal('foo;bar', + LegacyTeamCard.mapCard({team = 'A', alsoknownas = 'foo;bar', aliases = 'wrong'}).aliases) + assert.are_equal('only-aliases', + LegacyTeamCard.mapCard({team = 'A', aliases = 'only-aliases'}).aliases) + end) + + it('combines players and coaches into one list', function() + local card = LegacyTeamCard.mapCard({team = 'A', p1 = 'P', c1 = 'C'}) + assert.are_equal('P', card.players[1][1]) + assert.are_equal('C', card.players[2][1]) + assert.are_equal('staff', card.players[2].type) + end) + + it('sets import=false unconditionally', function() + local card = LegacyTeamCard.mapCard({team = 'A', import = 'true'}) + assert.is_false(card.import) + end) + end) + + describe('toggle folding', function() + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('folds zero toggles to defaults', function() + local f = LegacyTeamCard._foldToggles({}) + assert.is_false(f.showPlayerInfo) + assert.are_equal(0, f.extraPlayers) + assert.are_same({}, f.notes) + end) + + it('playerinfo=true sets showPlayerInfo', function() + local f = LegacyTeamCard._foldToggles({{playerinfo = 'true'}}) + assert.is_true(f.showPlayerInfo) + end) + + it('sums p_extra', function() + local f = LegacyTeamCard._foldToggles({{p_extra = '2'}, {p_extra = '3'}}) + assert.are_equal(5, f.extraPlayers) + end) + + it('collects notes in order, skipping empty', function() + local f = LegacyTeamCard._foldToggles({{note = 'first'}, {note = ''}, {note = 'second'}}) + assert.are_same({'first', 'second'}, f.notes) + end) + end) + + describe('run — partition and malformed', function() + local Template = require('Module:Template') + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + local function stashAll(entries) + for _, e in ipairs(entries) do + Template.stashReturnValue(e, 'LegacyTeamCard') + end + end + + it('with no stash, returns empty render without error', function() + local out = LegacyTeamCard.run() + assert.is_truthy(out) + end) + + it('with malformed structure (no header, just cards), adds tracking category and renders', function() + local TPParser = require('Module:TeamParticipants/Parse/Wiki') + local stubParseMalformed = stub(TPParser, 'parseWikiInput', function() + return {participants = {}} + end) + local addCategory = stub(mw.ext.TeamLiquidIntegration, 'add_category', function() end) + stashAll({ + {team = 'A', __source = 'card'}, + {team = 'B', __source = 'card'}, + }) + local out = LegacyTeamCard.run() + assert.is_truthy(out) + assert.stub(addCategory).was.called_with('Pages with malformed Legacy TeamCard structure') + addCategory:revert() + stubParseMalformed:revert() + end) + end) + + describe('run — render and post-render side effects', function() + local Template = require('Module:Template') + local PageVariableNamespace = require('Module:PageVariableNamespace') + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('passes minimumplayers = defaultRowNumber + extraRows + sum(p_extra)', function() + local TPParser = require('Module:TeamParticipants/Parse/Wiki') + local captured + local stubParse = stub(TPParser, 'parseWikiInput', function(args) + captured = args + return {participants = {}, expectedPlayerCount = tonumber(args.minimumplayers)} + end) + + Template.stashReturnValue({__source = 'header'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'toggle', p_extra = '2'}, 'LegacyTeamCard') + Template.stashReturnValue( + {__source = 'card', team = 'A', defaultRowNumber = '5', extraRows = '1'}, 'LegacyTeamCard') + + LegacyTeamCard.run() + assert.are_equal('8', tostring(captured.minimumplayers)) + + stubParse:revert() + end) + + it('sets externalControlsRendered after render', function() + local TPParser = require('Module:TeamParticipants/Parse/Wiki') + local teamParticipantsVars = PageVariableNamespace('TeamParticipants') + teamParticipantsVars:set('externalControlsRendered', '') + + local stubParse2 = stub(TPParser, 'parseWikiInput', function() + return {participants = {}} + end) + + Template.stashReturnValue({__source = 'header'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'card', team = 'A'}, 'LegacyTeamCard') + + LegacyTeamCard.run() + assert.are_equal('true', teamParticipantsVars:get('externalControlsRendered')) + + stubParse2:revert() + end) + + it('second block does not render controls strip', function() + local TPParser = require('Module:TeamParticipants/Parse/Wiki') + local teamParticipantsVars = PageVariableNamespace('TeamParticipants') + + local stubParse = stub(TPParser, 'parseWikiInput', function() + return {participants = {}} + end) + + -- First block + teamParticipantsVars:set('externalControlsRendered', '') + Template.stashReturnValue({__source = 'header'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'card', team = 'A'}, 'LegacyTeamCard') + LegacyTeamCard.run() + assert.are_equal('true', teamParticipantsVars:get('externalControlsRendered')) + + -- Second block: flag is already set, run() must still succeed + Template.stashReturnValue({__source = 'header'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'card', team = 'B'}, 'LegacyTeamCard') + local secondOutput = LegacyTeamCard.run() + assert.is_truthy(secondOutput) + + stubParse:revert() + end) + + it('forces store=false outside mainspace', function() + local TPParser = require('Module:TeamParticipants/Parse/Wiki') + local Controller = require('Module:TeamParticipants/Controller') + local Repository = require('Module:TeamParticipants/Repository') + + local stubParse3 = stub(TPParser, 'parseWikiInput', function() + return {participants = {{}}} + end) + local stubImport = stub(Controller, 'importParticipants', function() end) + local stubFill = stub(Controller, 'fillIncompleteRosters', function() end) + local stubEnrich = stub(Controller, 'enrichPlayerDates', function() end) + local stubSetVars = stub(Repository, 'setPageVars', function() end) + local saveSpy = stub(Repository, 'save', function() end) + local namespaceStub = stub(require('Module:Namespace'), 'isMain', function() return false end) + + Template.stashReturnValue({__source = 'header'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'card', team = 'A'}, 'LegacyTeamCard') + LegacyTeamCard.run() + assert.stub(saveSpy).was_not_called() + + namespaceStub:revert() + saveSpy:revert() + stubSetVars:revert() + stubEnrich:revert() + stubFill:revert() + stubImport:revert() + stubParse3:revert() + end) + end) + + describe('run — preprocessCard hook', function() + local Template = require('Module:Template') + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + it('applies preprocessCard to each card before mapping', function() + local TPParser = require('Module:TeamParticipants/Parse/Wiki') + local captured + local stubParse = stub(TPParser, 'parseWikiInput', function(args) + captured = args + return {participants = {}, expectedPlayerCount = 0} + end) + + Template.stashReturnValue({__source = 'header'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'card', sub1 = 'X'}, 'LegacyTeamCard') + + LegacyTeamCard.run({ + preprocessCard = function(card) + if card.sub1 then card.p1 = card.sub1; card.p1sub = 'true'; card.sub1 = nil end + return card + end, + }) + + -- The first opponent in captured should now have p1='X' as a sub-status player. + assert.are_equal('X', captured[1].players[1][1]) + assert.are_equal('sub', captured[1].players[1].status) + + stubParse:revert() + end) + end) + + describe('integration', function() + it('renders a representative legacy block via Module:Template stash', function() + local TeamTemplateMock = require('wikis.commons.Mock.TeamTemplate') + TeamTemplateMock.setUp() + local LpdbQuery = stub(mw.ext.LiquipediaDB, 'lpdb', function() return {} end) + local LpdbPlacementStore = stub(mw.ext.LiquipediaDB, 'lpdb_placement', function() end) + + local Template = require('Module:Template') + local LegacyTeamCard = require('Module:TeamCard/Legacy') + + Template.stashReturnValue({__source = 'toggle', playerinfo = 'true', p_extra = '1'}, 'LegacyTeamCard') + Template.stashReturnValue({__source = 'header', cols = '4'}, 'LegacyTeamCard') + Template.stashReturnValue({ + __source = 'card', + team = 'Team Liquid', + defaultRowNumber = '5', + subdnpdefault = 'true', + p1 = 'alexis', + p2 = 'dodonut', + p3 = 'meL', + p4 = 'Noia', + p5 = 'sarah', + s1 = 'sub-player', s1result = 'true', + c1 = 'Coach Name', + qualifier = 'Invited', + }, 'LegacyTeamCard') + Template.stashReturnValue({ + __source = 'card', + team = 'TBD', + team2 = 'mouz', + team3 = 'bds', + defaultRowNumber = '5', + qualifier = '[[Qualifier/2025|Qualifier]]', + }, 'LegacyTeamCard') + + GoldenTest('teamcard_legacy', tostring(LegacyTeamCard.run()), + [[]]) + + LpdbQuery:revert() + LpdbPlacementStore:revert() + TeamTemplateMock.tearDown() + end) + end) +end) diff --git a/lua/test_assets/team_template_data.lua b/lua/test_assets/team_template_data.lua index 6cf84022fb9..474950b426c 100644 --- a/lua/test_assets/team_template_data.lua +++ b/lua/test_assets/team_template_data.lua @@ -49,6 +49,18 @@ local data = { ['2023-01-26'] = 'heroic 2023', ['2024-02-20'] = 'heroic 2024', }, + -- tbd subtemplate + ['tbd'] = { + bracketname = "TBD", + image = "", + imagedark = "", + legacyimage = "", + legacyimagedark = "", + name = "TBD", + page = "TBD", + shortname = "TBD", + templatename = "tbd", + }, -- bds subtemplates ['bds esport old'] = { bracketname = "BDS Esport", diff --git a/lua/wikis/commons/TeamCard/Legacy.lua b/lua/wikis/commons/TeamCard/Legacy.lua new file mode 100644 index 00000000000..84d88458aa4 --- /dev/null +++ b/lua/wikis/commons/TeamCard/Legacy.lua @@ -0,0 +1,408 @@ +--- +-- @Liquipedia +-- page=Module:TeamCard/Legacy +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local Logic = Lua.import('Module:Logic') +local Namespace = Lua.import('Module:Namespace') +local PageVariableNamespace = Lua.import('Module:PageVariableNamespace') +local Template = Lua.import('Module:Template') +local Tournament = Lua.import('Module:Tournament') + +local TeamParticipantsController = Lua.import('Module:TeamParticipants/Controller') + +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local WidgetUtil = Lua.import('Module:Widget/Util') + +local teamParticipantsVars = PageVariableNamespace('TeamParticipants') + +local LegacyTeamCard = {} + +---@param entries table[] +---@return table[], table?, table[] +local function partitionStash(entries) + local toggles, header, cards = {}, nil, {} + local sawHeader = false + Array.forEach(entries, function(entry) + local source = entry.__source or 'card' + if source == 'toggle' then + table.insert(toggles, entry) + elseif source == 'header' then + if sawHeader then + table.insert(cards, entry) + else + header = entry + sawHeader = true + end + else + table.insert(cards, entry) + end + end) + return toggles, header, cards +end + +---@param opts table? Optional table; supports `preprocessCard` hook. +---@return Widget +function LegacyTeamCard.run(opts) + opts = opts or {} + local preprocessCard = opts.preprocessCard or function(args) return args end + + local entries = Template.retrieveReturnValues('LegacyTeamCard') + local toggles, header, cardEntries = partitionStash(entries) + + if not header and #cardEntries > 0 then + mw.ext.TeamLiquidIntegration.add_category('Pages with malformed Legacy TeamCard structure') + end + + local toggleFolded = LegacyTeamCard._foldToggles(toggles) + + local defaultRows, extraRows = 0, 0 + Array.forEach(cardEntries, function(card) + defaultRows = tonumber(card.defaultRowNumber) or defaultRows + extraRows = tonumber(card.extraRows) or extraRows + end) + + local tpArgs = { + minimumplayers = defaultRows + extraRows + toggleFolded.extraPlayers, + showplayerinfo = toggleFolded.showPlayerInfo and 'true' or nil, + } + Array.forEach(cardEntries, function(card) + table.insert(tpArgs, LegacyTeamCard.mapCard(preprocessCard(card))) + end) + + if not Namespace.isMain() then + tpArgs.store = 'false' + end + + local display = TeamParticipantsController.fromTemplate(tpArgs) + teamParticipantsVars:set('externalControlsRendered', 'true') + + local notesWidget + if #toggleFolded.notes > 0 then + notesWidget = HtmlWidgets.Div{ + classes = {'team-participant__notes'}, + children = Array.interleave(toggleFolded.notes, HtmlWidgets.Br{}), + } + end + + return HtmlWidgets.Fragment{children = WidgetUtil.collect(notesWidget, display)} +end + +---@param rawQualifier string|table|nil +---@return {method: string, type: string, page: string?, url: string?, text: string?}? +function LegacyTeamCard.parseQualifier(rawQualifier) + if type(rawQualifier) == 'table' then + rawQualifier = rawQualifier[1] + end + if not rawQualifier or rawQualifier == '' then + return nil + end + + local trimmed = mw.text.trim(rawQualifier) + local method = trimmed:lower():match('^invited?') and 'invite' or 'qual' + + local text, internalLink, externalLink = LegacyTeamCard._parseQualifierLink(rawQualifier) + + if internalLink then + local tournament = Tournament.getTournament(internalLink) + return { + method = method, + type = tournament and 'tournament' or 'internal', + page = internalLink, + text = text, + } + elseif externalLink then + return { + method = method, + type = 'external', + url = externalLink, + text = text, + } + else + return {method = method, type = 'other', text = text} + end +end + +-- Port of Module:TeamCard/Qualifier (and Module:TeamCard/Storage._parseQualifier). +---@private +---@param rawQualifier string +---@return string?, string?, string? # (linkText, internalLink, externalLink) +function LegacyTeamCard._parseQualifierLink(rawQualifier) + local cleanQualifier = rawQualifier:gsub('%[', ''):gsub('%]', '') + if cleanQualifier:find('|') then + local parts = mw.text.split(cleanQualifier, '|', true) + local link, displayName = parts[1], parts[2] + if link:sub(1, 1) == '/' then + link = mw.title.getCurrentTitle().fullText .. link + end + link = link:gsub(' ', '_') + return displayName, link, nil + elseif rawQualifier:sub(1, 1) == '[' then + local parts = mw.text.split(cleanQualifier, ' ', true) + local link = parts[1] + table.remove(parts, 1) + return table.concat(parts, ' '), nil, link + else + return rawQualifier, nil, nil + end +end + +---@param tcArgs table +---@param prefix string +---@param sourceGroup nil|'s'|'f' -- nil for main p*, 's' for substitute source, 'f' for former +---@return table +function LegacyTeamCard.mapPlayer(tcArgs, prefix, sourceGroup) + local wins = tonumber(tcArgs[prefix .. 'wins']) + local winsc = tonumber(tcArgs[prefix .. 'winsc']) + local trophies + if wins or winsc then + trophies = (wins or 0) + (winsc or 0) + end + + local explicitPlayResult = Logic.readBoolOrNil(tcArgs[prefix .. 'played'] + or tcArgs[prefix .. 'result']) + local played = explicitPlayResult + if Logic.readBool(tcArgs[prefix .. 'dnp']) then + played = false + end + + local status + if Logic.readBool(tcArgs[prefix .. 'leave']) then + status = 'former' + elseif Logic.readBool(tcArgs[prefix .. 'sub']) then + status = 'sub' + elseif sourceGroup == 's' then + status = 'sub' + elseif sourceGroup == 'f' then + status = 'former' + end + + -- Default-DNP rules (only when no explicit played/result and no explicit dnp). + if explicitPlayResult == nil and not Logic.readBool(tcArgs[prefix .. 'dnp']) then + if sourceGroup == 's' and (Logic.readBool(tcArgs.subdnpdefault) or Logic.readBool(tcArgs.noVarDefault)) then + played = false + end + end + + return { + [1] = tcArgs[prefix], + link = tcArgs[prefix .. 'link'], + flag = tcArgs[prefix .. 'flag_o'] or tcArgs[prefix .. 'flag'], + team = tcArgs[prefix .. 'team'], + id = tcArgs[prefix .. 'id'], + faction = tcArgs[prefix .. 'faction'] or tcArgs[prefix .. 'race'], + role = tcArgs[prefix .. 'pos'], + trophies = trophies, + joindate = tcArgs[prefix .. 'joindate'], + leavedate = tcArgs[prefix .. 'leavedate'], + played = played, + status = status, + } +end + +---@param tcArgs table +---@param prefix string +---@param sourceGroup nil|'sc'|'fc' -- nil for main c*, 'sc' for sub-coach source, 'fc' for former-coach source +---@return table +function LegacyTeamCard.mapCoach(tcArgs, prefix, sourceGroup) + local wins = tonumber(tcArgs[prefix .. 'wins']) + local winsc = tonumber(tcArgs[prefix .. 'winsc']) + local trophies + if wins or winsc then + trophies = (wins or 0) + (winsc or 0) + end + + local role = tcArgs[prefix .. 'pos'] or 'coach' + + local status + if Logic.readBool(tcArgs[prefix .. 'leave']) then + status = 'former' + elseif Logic.readBool(tcArgs[prefix .. 'sub']) then + status = 'sub' + elseif sourceGroup == 'sc' then + status = 'sub' + elseif sourceGroup == 'fc' then + status = 'former' + end + + return { + [1] = tcArgs[prefix], + link = tcArgs[prefix .. 'link'], + flag = tcArgs[prefix .. 'flag_o'] or tcArgs[prefix .. 'flag'], + team = tcArgs[prefix .. 'team'], + role = role, + type = 'staff', + trophies = trophies, + status = status, + } +end + +local DEFAULT_MAX_PLAYER_INDEX = 10 +local MAX_COACH_INDEX = 10 + +local TN_TYPE_DEFAULTS = {t2 = 'sub', t3 = 'former'} + +---@param tcArgs table +---@param prefix string +---@param maxIndex integer +---@return integer[] +local function indicesPresent(tcArgs, prefix, maxIndex) + return Array.filter(Array.range(1, maxIndex), function(i) + return Logic.isNotEmpty(tcArgs[prefix .. i]) + end) +end + +---@param value string? +---@return string +local function normalizeKey(value) + if Logic.isEmpty(value) then return '' end + return value:gsub(' ', '_'):lower() +end + +---@param tcArgs table +---@return table[] +function LegacyTeamCard.mapPlayers(tcArgs) + local players = {} + local indexByKey = {} + local maxPlayerIndex = tonumber(tcArgs.maxPlayers) or DEFAULT_MAX_PLAYER_INDEX + + local function add(person, allowOverwrite) + local key = normalizeKey(person.link or person[1]) + if key ~= '' and indexByKey[key] then + if allowOverwrite then + players[indexByKey[key]] = person + end + return + end + table.insert(players, person) + if key ~= '' then + indexByKey[key] = #players + end + end + + Array.forEach(indicesPresent(tcArgs, 'p', maxPlayerIndex), function(i) + add(LegacyTeamCard.mapPlayer(tcArgs, 'p' .. i, nil), false) + end) + Array.forEach(indicesPresent(tcArgs, 's', maxPlayerIndex), function(i) + add(LegacyTeamCard.mapPlayer(tcArgs, 's' .. i, 's'), false) + end) + Array.forEach(indicesPresent(tcArgs, 'f', maxPlayerIndex), function(i) + add(LegacyTeamCard.mapPlayer(tcArgs, 'f' .. i, 'f'), false) + end) + + Array.forEach({'t2', 't3'}, function(tab) + local tabType = (tcArgs[tab .. 'type'] or TN_TYPE_DEFAULTS[tab]):lower() + local sourceGroup + if tabType == 'sub' then sourceGroup = 's' + elseif tabType == 'former' then sourceGroup = 'f' + else sourceGroup = nil end + + Array.forEach(indicesPresent(tcArgs, tab .. 'p', maxPlayerIndex), function(i) + local person = LegacyTeamCard.mapPlayer(tcArgs, tab .. 'p' .. i, sourceGroup) + if tabType == 'staff' then + person.type = 'staff' + end + add(person, true) + end) + end) + + return players +end + +---@param tcArgs table +---@return table[] +function LegacyTeamCard.mapCoaches(tcArgs) + local coaches = {} + + Array.forEach(indicesPresent(tcArgs, 'c', MAX_COACH_INDEX), function(i) + table.insert(coaches, LegacyTeamCard.mapCoach(tcArgs, 'c' .. i, nil)) + end) + Array.forEach(indicesPresent(tcArgs, 'sc', MAX_COACH_INDEX), function(i) + table.insert(coaches, LegacyTeamCard.mapCoach(tcArgs, 'sc' .. i, 'sc')) + end) + Array.forEach(indicesPresent(tcArgs, 'fc', MAX_COACH_INDEX), function(i) + table.insert(coaches, LegacyTeamCard.mapCoach(tcArgs, 'fc' .. i, 'fc')) + end) + + Array.forEach({'t2', 't3'}, function(tab) + local tabType = (tcArgs[tab .. 'type'] or TN_TYPE_DEFAULTS[tab]):lower() + local sourceGroup + if tabType == 'sub' then sourceGroup = 'sc' + elseif tabType == 'former' then sourceGroup = 'fc' + else sourceGroup = nil end + + Array.forEach(indicesPresent(tcArgs, tab .. 'c', MAX_COACH_INDEX), function(i) + table.insert(coaches, LegacyTeamCard.mapCoach(tcArgs, tab .. 'c' .. i, sourceGroup)) + end) + end) + + return coaches +end + +---@param tcArgs table +---@return table -- TP opponent arg +function LegacyTeamCard.mapCard(tcArgs) + local card = {} + + local hasContenders = Logic.isNotEmpty(tcArgs.team2) or Logic.isNotEmpty(tcArgs.team3) + if hasContenders then + card.contenders = {} + Array.forEach({{'team', 'link'}, {'team2', 'link2'}, {'team3', 'link3'}}, function(pair) + local teamArg, linkArg = pair[1], pair[2] + local value = tcArgs[linkArg] or tcArgs[teamArg] + if Logic.isNotEmpty(value) then + table.insert(card.contenders, value) + end + end) + else + card[1] = tcArgs.link or tcArgs.team + end + + if Logic.isNotEmpty(tcArgs.qualifier) then + card.qualification = LegacyTeamCard.parseQualifier(tcArgs.qualifier) + end + + local notes = {} + if Logic.isNotEmpty(tcArgs.notes) then + table.insert(notes, {[1] = tcArgs.notes, highlighted = false}) + end + if Logic.isNotEmpty(tcArgs.inotes) then + table.insert(notes, {[1] = tcArgs.inotes, highlighted = false}) + end + if #notes > 0 then card.notes = notes end + + card.date = tcArgs.date + card.aliases = tcArgs.alsoknownas or tcArgs.aliases + card.import = false + + local players = LegacyTeamCard.mapPlayers(tcArgs) + Array.extendWith(players, LegacyTeamCard.mapCoaches(tcArgs)) + card.players = players + + return card +end + +---@private +---@param toggleEntries table[] +---@return {showPlayerInfo: boolean, extraPlayers: integer, notes: string[]} +function LegacyTeamCard._foldToggles(toggleEntries) + local result = {showPlayerInfo = false, extraPlayers = 0, notes = {}} + Array.forEach(toggleEntries, function(entry) + if Logic.readBool(entry.playerinfo) then + result.showPlayerInfo = true + end + local extra = tonumber(entry.p_extra) + if extra then result.extraPlayers = result.extraPlayers + extra end + if Logic.isNotEmpty(entry.note) then + table.insert(result.notes, entry.note) + end + end) + return result +end + +return LegacyTeamCard