Module:Featured cards

From Yugipedia
Revision as of 19:44, 1 April 2024 by Deltaneos (talk | contribs) (moved common functions for parsing input to a separate module)
Jump to: navigation, search
--[[
    Module for rendering a list of cards featured in something
    Typically separate instances of this module are used for each character

    This should be used in articles on chapters, episodes, films, etc.
    This should not be used for Deck lists on character pages.
]]--

local TableTools = require('Module:TableTools')
local InputParser = require('Module:Input parser')

-- Object for the overall collection of cards
-- @field character        string    Page name of the character that the cards belong to
-- @field characterName    string    Name the character went by in this instance
-- @field groups           table<FeaturedCardsGroup>
local FeaturedCards = {
	character     = nil,
	characterName = nil,
	groups        = {},
}

-- Object for a group within the collection (e.g. 'Main Deck' or 'Extra Deck')
-- @field title string
-- @field columns table<FeaturedCardsColumn>
local FeaturedCardsGroup = {
	title = nil,
	columns = {},
}

-- Object for a column within a group e.g. 'Monster Cards' or 'Spell Cards'
-- @field title string
-- @field lists table<FeaturedCardsList>
local FeaturedCardsColumn = {
	title = nil,
	lists = {}
}

-- Object for a list within a column e.g. 'Normal Monsters' or 'Continuous Spells'
-- @field title string
-- @field cards table<FeaturedCard>
local FeaturedCardsList = {
	title = nil,
	cards = {}
}

-- Object for a card within a list
-- @field link             string     Page name of the card
-- @field name             string     Name the card went by in this instance
-- @field quantity         number     Amount of copies of the card that appeared
-- @field isDebut          boolean    If this is the card's first appearance
-- @field mentionedOnly    boolean    If the card was mentioned, but not seen
-- @field dubOnly          boolean    The card appears in the English dub, but not the Japanese version
local FeaturedCard = {
	link          = nil,
	name          = nil,
	quantity      = nil,
	isDebut       = false,
	mentionedOnly = false,
	dubOnly       = false
}

-- Specify a list of values, get the first non-empty one
-- empty means `nil`, empty string or string containing only whitespace
-- e.g. `coalesce(nil, '', '   ', 'something', nil)` -> `'something'`
-- e.g. `name = coalesce(args.name, pagename)`
local function coalesce(...)
	for _, expr in ipairs(arg) do
		if (expr and (type(expr) ~= 'string' or mw.text.trim(expr) ~= '')) then
			return expr
		end
	end

	return nil
end

-- Create a new instance of a FeaturedCards object
-- @param args table
-- @return FeaturedCards
function FeaturedCards:new(args)
	-- Create a new instance of the FeaturedCards object with its default values
	local fc = mw.clone(FeaturedCards)

	-- Populate it with data from the arguments
	fc.character     = args.character
	fc.characterName = coalesce(args.characterName, args.character)

	return fc
end

-- Add a new group to a FeaturedCards instance
-- @param title string
-- @return FeaturedCardsGroup
function FeaturedCards:addGroup(title)
	-- Clone the default object and populate it
	local group = mw.clone(FeaturedCardsGroup)
	group.title = title

	-- Add it to the list of groups
	table.insert(self.groups, group)

	return group
end

-- Add a new column to a FeaturedCardsGroup instance
-- @param title string
-- @return FeaturedCardsColumn
function FeaturedCardsGroup:addColumn(title)
	-- Clone the default object and populate it
	local column = mw.clone(FeaturedCardsColumn)
	column.title = title

	-- Add it to the group's list of columns
	table.insert(self.columns, column)

	return column
end

-- Add a new list to a FeaturedCardsColumn instance
-- @param title      string
-- @param listsInput string
function FeaturedCardsColumn:addList(title, listInput)
	-- Clone the default object and populate it
	local list = mw.clone(FeaturedCardsList)
	list.title = title
	list:addCards(listInput)

	-- Add it to the column's list of lists
	table.insert(self.lists, list)

	return list
end

-- Convert an unordered (bulleted) list to a table of cards
-- @param listInput string
-- @return table<FeaturedCard>
function FeaturedCardsList:addCards(listInput)
	-- Loop through each list item
	for _, cardInput in pairs(InputParser.ulToArray(listInput)) do
		local card = mw.clone(FeaturedCard)

		-- Parse data from the list item
		local link     = InputParser.parseLink(cardInput)
		local lineArgs = InputParser.getLineArgs(cardInput)

		card.link          = link.link
		card.name          = link.text
		card.isDebut       = TableTools.inArray(lineArgs, 'debut')
		card.mentionedOnly = TableTools.inArray(lineArgs, 'mentioned')
		card.dubOnly       = TableTools.inArray(lineArgs, 'dub')

		-- If the link is followed by an "x" or a "×" followed by a number,
		-- use the number as the quantity
		card.quantity      = cardInput:match('%]%]%s+[x×](%d+)')

		table.insert(self.cards, card)
	end
end

-- Get all list items
-- @return table<FeaturedCard>
function FeaturedCards:getAllCards()
	local cards = {}

	for _, group in pairs(self.groups) do
		for _, column in pairs(group.columns) do
			for _, list in pairs(column.lists) do
				for _, card in pairs(list.cards) do
					table.insert(cards, card)
				end
			end
		end
	end

	return cards
end


-- Check if a group has cards
-- @return bool
function FeaturedCardsGroup:hasCards()
	-- Loop through the columns until one with cards is found
	for _, column in pairs(self.columns) do
		if (column:hasCards()) then
			return true
		end
	end

	-- If none are found
	return false
end

-- Check if a column has cards
-- @return bool
function FeaturedCardsColumn:hasCards()
	-- Loop through the lists until one with cards is found
	for _, list in pairs(self.lists) do
		if (list:hasCards()) then
			return true
		end
	end

	-- If none are found
	return false
end

-- Check if a list has cards
-- @return bool
function FeaturedCardsList:hasCards()
	return self.cards and #self.cards > 0
end

-- Check if there are multiple non-empty groups in a `FeaturedCards` instance
-- @return bool
function FeaturedCards:hasMultipleGroups()
	local count = 0

	-- Loop through groups and increase the counter if the group contains cards
	for _, group in pairs(self.groups) do
		if (group:hasCards()) then
			count = count + 1

			-- As soon as a second group with cards is found
			-- this function can return `true`
			if (count > 1) then
				return true
			end
		end
	end

	return false
end

-- Render a loaded instance of the object as HTML
-- @return string
function FeaturedCards:render()
	-- The element that encapsulates the entire HTML
	local container = mw.html.create('div')
	container:addClass('featured-cards toccolours mw-collapsible')
	container:css('margin-bottom', '1em')

	-- The heading element, containing the character name
	local headerHtml = container:tag('div')
	headerHtml:addClass('featured-cards-header')
	headerHtml:css({
		['background-color'] = '#ccf',
		['padding']          = '.2em .3em',
		['text-align']       = 'center'
	})

	if (self.character) then
		headerHtml:wikitext('<b>[[' .. self.character .. '|' .. self.characterName .. ']]</b>')
	else
		-- If `self.character` is empty, don't include a link
		headerHtml:wikitext('<b>' .. (self.characterName or 'Cards') .. '</b>')
	end

	-- The main collapsible section
	local bodyHtml = container:tag('div')
	bodyHtml:addClass('featured-cards-body mw-collapsible-content')
	
	-- Groups will have headers and be collapsible, if there is more than one of them
	local groupsAreCollapsible = self:hasMultipleGroups()

	for _, group in pairs(self.groups) do
		bodyHtml:wikitext(group:render(groupsAreCollapsible))
	end

	return tostring(container)
end

-- Render a loaded instance of a FeaturedCardsGroup
-- @param isCollapsible bool
-- @return string
function FeaturedCardsGroup:render(isCollapsible)
	-- If it has no cards, render as empty string
	if (not self:hasCards()) then return '' end

	local container = mw.html.create('div')

	if (isCollapsible) then
		container:addClass('mw-collapsible')

		local heading = container:tag('div')
		heading:css({['text-align'] = 'center', ['margin-top'] = '.25em', ['padding'] = '.1em', ['background-color'] = '#ddf' })
		heading:wikitext(self.title)
	end

	local body = container:tag('div')
	body:addClass('featured-cards-lists')
	body:css({ ['display'] = 'flex', ['flex-wrap'] = 'wrap', ['width'] = '100%' })

	if (isCollapsible) then
		body:addClass('mw-collapsible-content')
	end

	for _, column in pairs(self.columns) do
		body:wikitext(column:render())
	end

	return tostring(container)
end

-- Render a loaded instance of a FeaturedCardsColumn
-- @return string
function FeaturedCardsColumn:render()
	-- If the column has no cards, render it as an empty string
	if (not self:hasCards()) then return '' end

	local container = mw.html.create('div'):addClass('featured-cards-list'):css('flex', 1)

	-- Add a heading
	container:tag('p'):tag('b'):wikitext(self.title)

	-- Add each list
	for _, list in pairs(self.lists) do
		container:wikitext(list:render())
	end

	return tostring(container)
end

-- Render a loaded instance of a FeaturedCardsList
-- @return string
function FeaturedCardsList:render()
	-- If the list has no cards, render it as an empty string
	if (not self:hasCards()) then return '' end
	
	local container = mw.html.create('div')

	-- Add title if there is one
	if (self.title) then
		container:tag('p'):wikitext(self.title)
	end

	-- Add an unordered list for the cards
	local ul = container:tag('ul')

	for _, card in pairs(self.cards) do
		ul:tag('li'):wikitext(card:render())
	end

	return tostring(container)
end

-- Render the wikitext for displaying a card in a list
-- @return string
function FeaturedCard:render()
	-- Start the line with the linked card name
	local lineText = '[[' .. self.link .. '|' .. self.name .. ']]'

	-- If this is the card's first appearance, italicize it
	if (self.isDebut) then
		lineText = '<i>' .. lineText .. '</i>'
	end

	-- If there is more than one, display the quantity next to it
	if (self.quantity and tonumber(self.quantity) > 1) then
		lineText = lineText .. ' ×' .. self.quantity
	end

	-- See if there's additional text to display in brackets
	local additional = {}

	if self.mentionedOnly then table.insert(additional, 'mentioned') end
	if self.dubOnly       then table.insert(additional, 'dub')       end

	if (#additional > 0) then
		lineText = lineText .. ' (' .. table.concat(additional, ', ') .. ')'
	end

	return lineText
end

-- @todo Set Semantic MediaWiki data
function FeaturedCards:setSmwData()
	local cards = self:getAllCards()

	for _, card in pairs(cards) do
		mw.smw.subobject({
			'Card appearing = ' .. card.link,
			'English name   = ' .. card.name,
			'Owner          = ' .. (self.character     or ''),
			'Owner name     = ' .. (self.characterName or ''),
			'Quantity       = ' .. (card.quantity      or ''),
		})
	end
end

-- Function that can be called via #invoke
-- Create a FeaturedCards instance from template params
-- And render it as HTML
-- @return string
function FeaturedCards.renderFromTemplate(frame)
	local args = frame:getParent().args
	
	local characterLink = InputParser.parseLink(args['character'])

	local fc = FeaturedCards:new({ 
		character     = characterLink.link,
		characterName = characterLink.text,
	})

	local spellName     = args['magic'] and 'Magic' or 'Spell'
	local extraDeckName = (args['magic'] or args['fusion_deck']) and 'Fusion Deck' or 'Extra Deck'

	local mainDeck  = fc:addGroup('[[Main Deck]]')

	local monsterColumn = mainDeck:addColumn('[[Monster Card]]s')
	local spellColumn   = mainDeck:addColumn('[[Spell Card|' .. spellName ..' Card]]')
	local trapColumn    = mainDeck:addColumn('[[Trap Card]]s')
	local otherColumn   = mainDeck:addColumn('Other')

	monsterColumn:addList(nil,                       args['monsters']         )
	monsterColumn:addList('[[Normal Monster]]s',     args['normal_monsters']  )
	monsterColumn:addList('[[Effect Monster]]s',     args['effect_monsters']  )
	monsterColumn:addList('[[Flip monster]]s',       args['flip_monsters']    )
	monsterColumn:addList('[[Toon monster]]s',       args['toon_monsters']    )
	monsterColumn:addList('[[Spirit monster]]s',     args['spirit_monsters']  )
	monsterColumn:addList('[[Union monster]]s',      args['union_monsters']   )
	monsterColumn:addList('[[Gemini monster]]s',     args['gemini_monsters']  )
	monsterColumn:addList('[[Tuner monster]]s',      args['tuner_monsters']  )
	monsterColumn:addList('[[Dark Tuner monster]]s', args['dark_tuner_monsters']  )
	monsterColumn:addList('[[Ritual Monster]]s',     args['ritual_monsters']  )
	monsterColumn:addList('[[Pendulum Monster]]s',   args['pendulum_monsters'])
	monsterColumn:addList('[[Maximum Monster]]s',    args['maxmimum_monsters'])

	spellColumn:addList(nil,                                            args['spells'] or args['magic'])
	spellColumn:addList('[[Normal Spell Card|Normal Spells]]',          args['normal_spells']    )
	spellColumn:addList('[[Equip Spell Cards|Equip Spells]]',           args['equip_spells']     )
	spellColumn:addList('[[Field Spell Cards|Field Spells]]',           args['field_spells']     )
	spellColumn:addList('[[Ritual Spell Cards|Ritual Spells]]',         args['ritual_spells']    )
	spellColumn:addList('[[Continuous Spell Cards|Continuous Spells]]', args['continuous_spells'])
	spellColumn:addList('[[Quick-Play Spell Cards|Quick-Play Spells]]', args['quick_play_spells'])
	spellColumn:addList('[[Link Spell Cards|Link Spells]]',             args['link_spells']      )

	trapColumn:addList(nil,                                          args['traps']           )
	trapColumn:addList('[[Normal Trap Card|Normal Traps]]',          args['normal_traps']    )
	trapColumn:addList('[[Counter Trap Cards|Counter Traps]]',       args['counter_traps']   )
	trapColumn:addList('[[Equip Trap Cards|Equip Traps]]',           args['equip_traps']     )
	trapColumn:addList('[[Field Trap Cards|Field Traps]]',           args['field_traps']     )
	trapColumn:addList('[[Continuous Trap Cards|Continuous Traps]]', args['continuous_traps'])

	otherColumn:addList(nil,                  args['other']    )
	otherColumn:addList('[[Equip Card]]s',    args['equips']   )
	otherColumn:addList('[[Illusion Card]]s', args['illusions'])
	otherColumn:addList('[[Virus Card]]s',    args['viruses']  )
	otherColumn:addList('Unknown',            args['unknown']  )

	local extraDeck = fc:addGroup('[[Extra Deck|' .. extraDeckName .. ']]')

	local fusionColumn  = extraDeck:addColumn(nil)
	local synchroColumn = extraDeck:addColumn(nil)
	local xyzColumn     = extraDeck:addColumn(nil)
	local linkColumn    = extraDeck:addColumn(nil)

	fusionColumn:addList( '[[Fusion Monster]]s',       args['fusion_monsters'])
	synchroColumn:addList('[[Synchro Monster]]s',      args['synchro_monsters'])
	synchroColumn:addList('[[Dark Synchro Monster]]s', args['dark_synchro_monsters'])
	xyzColumn:addList(    '[[Xyz Monster]]s',          args['xyz_monsters'])
	linkColumn:addList(   '[[Link Monster]]s',         args['link_monsters'])

	fc:setSmwData()

	return fc:render()
end

return FeaturedCards