Module:Featured cards

From Yugipedia
Revision as of 23:57, 4 February 2024 by Deltaneos (talk | contribs) (This gets overwritten by `nil` later on if no quantity is specified. `nil` is a better default anyway, as not specifying a quantity doesn't necessarily mean there was exactly 1.)
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.
]]--

TableTools = require('Module:TableTools')

-- @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 lists            table     Lists of cards grouped by card type
local FeaturedCards = {
	character     = nil,
	characterName = nil,
	lists         = {}
}

-- @field card             string     Page name of the card
-- @field cardName         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 ListItem = {
	card          = nil,
	cardName      = nil,
	quantity      = nil,
	isDebut       = false,
	mentionedOnly = false,
	dubOnly       = false
}

local cardTypes = {
	monsters = { link = 'Monster Card', text = 'Monster Cards' },
	spells   = { link = 'Spell Card',   text = 'Spell Cards'   },
	traps    = { link = 'Trap Card',    text = 'Trap Cards'    },
}


-- Convert a wikitext unordered list (bulleted list) to an array (i.e. table)
-- e.g. `'\n* A\n* B\n* C'` -> { 'A', 'B', 'C' }`
--
-- @param text string unordered list in wikitext
-- @return table
local function ulToArray(text)
	-- No text, return empty table
	if (not text) then return {} end

	-- remove initial `*` and spacing
	local trimmedText = mw.text.trim(text, '* \n')

	-- If the text is empty or only contained whitespace, return empty table 
	if (trimmedText == '') then return {} end

	-- Split by `*`, surrounded in optional spaces
	return mw.text.split(trimmedText, '%s*\*%s*')
end

-- Parse data from a wikitext link
-- e.g. `'[[Metal Fiend Token (manga)|Metal Devil]]'` -> `{ link = 'Metal Fiend Token (manga)', text = 'Metal Devil' }`
-- @param linkInput string
-- @return table
local function parseLink(linkInput)
	--[[
	Pattern for a link in wikitext
	- any amount of characters (text before)
	- two opering square brackets
	- any number of non-pipe characters <-- first capture group
	- optional pipe character
	- optional amount of any characters <-- second capture group
	- two closing square brackets
	- any amount of characters (text after)
	]]--
	local pattern = '.*%[%[([^|]*)|?(.*)%]%].*'

	-- Use the first capture group a the link
	local link = linkInput:gsub(pattern, '%1')
	
	-- Use the second capture group (characterss after the pipe) as the text
	-- If empty, default to the same as the link text
	local text = linkInput:gsub(pattern, '%2')
	text = (text ~= '') and text or link

	return { link = link, text = text }
end

-- Get arguments from a line
-- e.g. `'[[Mystical Moon]] // debut; mentioned; dub; qty::2'`
-- becomes `{ 'debut', 'mentioned', 'dub', ['qty'] = 2 }
--
-- @param lineText string
-- @return table
local function getLineArgs(lineText)
	-- Get text after the `//`
	local argsText = mw.text.split(lineText, '%s*//%s*')[2] or ''

	-- Split by semicolons surrounded by any amount of spaces
	-- e.g. { 'debut', 'mentioned', 'dub', 'qty::2' }
	local argsArr = mw.text.split(argsText, '%s*;%s*')

	local pattern = '(.*)::(.*)'
	
	local args = {}

	for _, arg in pairs(argsArr) do
		local param, value = arg:match(pattern)
		
		if (not param) then
			table.insert(args, arg)
		else
			args[param] = value
		end
	end

	return args
end

-- Convert an unordered (bulleted) list to a table of cards
-- @param listInput string
-- @return table<ListItem>
local function ulToCardList(listInput)
	local listItems = {}

	-- Loop through each list item
	for _, itemInput in pairs(ulToArray(listInput)) do
		local listItem = mw.clone(ListItem)

		-- Parse data from the list item
		local link     = parseLink(itemInput)
		local lineArgs = getLineArgs(itemInput)

		listItem.card     = link.link
		listItem.cardName = link.text
		listItem.isDebut       = TableTools.inArray(lineArgs, 'debut')
		listItem.mentionedOnly = TableTools.inArray(lineArgs, 'mentioned')
		listItem.dubOnly       = TableTools.inArray(lineArgs, 'dub')
		listItem.quantity      = lineArgs['qty']

		table.insert(listItems, listItem)
	end

	return listItems
end

-- 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 featured cards list
-- @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)

	fc.character     = args.character
	fc.characterName = coalesce(args.characterName, args.character)

	return fc
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')

	-- 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

	-- Collapsible section containing the card lists
	local listsHtml = container:tag('div')
	listsHtml:addClass('featured-cards-lists mw-collapsible-content')
	listsHtml:css({ ['display'] = 'flex', ['flex-wrap'] = 'wrap', ['width'] = '100%' })
	listsHtml:wikitext(self:renderList('monsters'))
	listsHtml:wikitext(self:renderList('spells'))
	listsHtml:wikitext(self:renderList('traps'))

	return tostring(container)
end

-- Render one of the card lists as HTML
-- @param listName string
-- @return string
function FeaturedCards:renderList(listName)
	local cards = self.lists[listName]

	if (not cards or #cards == 0) then return '' end

	local cardType = cardTypes[listName]
	
	local container = mw.html.create('div')
	container:addClass('featured-cards-list')
	container:css('flex', 1)
	container:tag('p'):wikitext('<b>[[' .. cardType.link .. '|' .. cardType.text .. ']]</b>')
	
	local ul = container:tag('ul')

	for _, card in pairs(cards) do
		local li = ul:tag('li'):wikitext(card:renderLine())
	end

	return tostring(container)
end

function ListItem:renderLine()
	-- Start the line with the linked card name
	local lineText = '[[' .. self.card .. '|' .. self.cardName .. ']]'

	-- 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()
	
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 fc = FeaturedCards:new({ 
		character     = args['character'],
		characterName = args['character_name']
	})
	fc.lists.monsters = ulToCardList(args.monsters)
	fc.lists.spells   = ulToCardList(args.spells)
	fc.lists.traps    = ulToCardList(args.traps)
	
	return fc:render()
end

return FeaturedCards