Module:Card

From Yugipedia
Revision as of 20:37, 8 September 2023 by Deltaneos (talk | contribs) (Categories array containing "All cards", but can be extended to include more. Render categories as normal on normal pages. Render as links inside a div on demo pages.)
Jump to: navigation, search

--[[
	Module for creating card articles
	General flow of what happens:
	- Defines an empty `Card` object with default data
	- `Card.card()` is called with data from a template call passed in
		- Calls `Card:new()` to create a new `Card` object with the input data
			- Runs through the "setter" functions to fill the data
		- Runs through an "add row" function for each row in `Card.config.rows`
		- Calls `Card:render()` to draw the final output
			- Calls various other "render" functions to help with creating the output

	This module should not be invoked directly.
	Other modules should import this one, make some changes to configuration.
	The other module will then run through the same flow as described above.
]]--

-- ------------------------------------
-- Import modules
-- ------------------------------------
-- Library of generic functions for helping with Lua tables
TableTools       = require('Module:TableTools')

-- Pseudo-classes for individual elements of cards
-- e.g. `Type:new('Fusion')` returns an object with data on the Fusion type
local EffectType = require('Module:Card/Effect type')
local Locale     = require('Module:Card/Locale')
local Type       = require('Module:Card/Type')



-- ------------------------------------
-- Generic functions
-- * Might move these to a more dedicated module
-- ------------------------------------

-- Generic function for replacing parameters within a string
-- Might move to more dedicated module
-- e.g.
-- Input: `replaceParams('Hi {$name}!', { name = 'Yugi' })`
-- Output: "Hi Yugi!"
--
-- @param message string - Text to perform the replacements on
-- @param params  table  - Map of variables names to the text to replace them.
-- @return string
function replaceParams(message, params)
	for param, replacement in pairs(params) do
		message = string.gsub(message, '{$' .. param .. '}', replacement)
	end

	return message
end

-- Capitalize the first letter in a string
-- Might move to more dedicated module
-- e.g. `ucfirst('abc')` returns "Abc"
-- @param text string
-- @return string
function ucfirst(text)
	return text:sub(1,1):upper() .. text:sub(2)
end



-- ------------------------------------
-- The main structure of the card object
-- - Create the object that contains data on the card itself and configuration options
-- ------------------------------------
-- Create an empty Card object with all the default values
local Card = {
	-- Store all details on the card itself here.
	cardTypes = {},
	property = nil,
	attribute = nil,
	types = {},
	isMonster = nil,
	isNormalMonster = nil,
	isEffectMonster = nil,
	isPendulumMonster = nil,
	level = nil,
	linkArrows = {},
	atk = nil,
	def = nil,
	linkRating = nil,
	limitationText = nil,
	password = nil,
	effectTypes = {},
	locales = {
		en = {
			name = nil,
			lore = nil,
			pendulumEffect = nil,
			previousNames = {}
		}
	},
	customColor = nil,

	-- Params for rendered output
	pageName = mw.title.getCurrentTitle().text,
	categories = { 'All cards' },

	styles = {
		'Module:Card/base.css'
	},
	main = false,
	images = {},
	rows = {},
	prev = nil,
	next = nil,

	-- Flag if the module is only being used for demonstrative purposes.
	demo = false,

	-- Configuration options
	-- Overwrite these in modules that extend this one 
	config = {
		baseClass = nil,
		colorCoded = true,
		defaultImage = 'Back-EN.png',
		enableMainLinks = true,
		footerText = nil,
		icons        = {
			-- Individual (just one image for each item)
			level            = '[[File:CG Star.svg|18px|alt=]]',
			unknownLevel     = '[[File:CG Star Unknown.svg|18px|alt=]]',
			negativeLevel    = '[[File:Negative Star.svg|18px|alt=]]',
			rank             = '[[File:Rank Star.svg|18px|alt=]]',
			unknownRank      = '[[File:Rank Star Unknown.svg|18px|alt=]]',
			unknownAttribute = '[[File:UNKNOWN.svg|28px|alt=]]',

			-- Multiple (contain patterns that represent multiple icons)
			levels     = nil,
			cardTypes  = '[[File:{$cardType_uc}.svg|28px|alt=]]',
			properties = '[[File:{$property}.svg|28px|alt=]]',
			attributes = '[[File:{$attribute}.svg|28px|alt=]]',
			types      = nil,
		},
		-- Languages the card has details to be shown in
		langs = { 'en' },
		-- Rows to show in the rendered output
		-- This is empty and should be set in modules that extend this one.
		rows = {},
		-- The Types that are allowed, split into categories
		-- All Type categories are empty here and should be set in modules that extend this one.
		types = {
			type     = {},
			summon   = {},
			ability  = {},
			tuner    = {},
			pendulum = {},
			maximum  = {},
			token    = {},
			normal   = {},
			effect   = {},
			unknown  = {},
		}
	}
}

-- Create a new card object
-- e.g. `Card:new({ atk = 3000, def = 2500, level = 8, types = 'Dragon / Normal' })`
-- The parameters are most likely to come from template input.
-- This will create an object for a card with all those details.
-- The code will automatically fill in more details based on the input.
-- And it will be possible to call other functions on the Card object that make use of this data.
--
-- @param args table
-- @return Card
function Card:new(args)
	-- Create a new instance of the class with all the default values
	-- Essentially create a clone of the `Card` object defined earlier.
	local c = {}
	setmetatable(c, self)
	self.__index = self

	-- Update the `Type` class, so that it accepts values based on the card's config
	Type:setAllowedValues(self.config.types)

	-- Fill in the new card object with data from the arguments
	c.demo = args.demo or false

	-- The pagename can only be overwitten in demos
	if (c.demo and args.page_name) then
		c.pageName = args.page_name
	end

	c.number = args.number
	c.property = args.property
	c:setCardTypes(args.card_type, args)
	c.property = args.property
	c.attribute = args.attribute
	c:setTypes(args.types)
	c:setLinkArrows(args.link_arrows)
	c.atk = args.atk
	c.def = args.def
	c.level = args.level
	c.rank = args.rank
	c.pendulumScale = args.pendulum_scale
	c.limitationText = args.limitation_text
	c.password = args.password
	c:setEffectTypes(args.effect_types)

	c.locales = Locale:createMany(c.config.langs, args)
	c:setName(args.name)

	c.customColor = args.color
	c:setMainLink(args.main)
	c:setImages(args.image)

	-- Add template specific styles or use the ones from Module:Card
	table.insert(c.styles, args.templatestyles or 'Module:Card/styles.css')

	-- Set parameters other than those supplied above
	-- Classes that inherit from this one can use this to set additional data
	c:setCustomArgs(args)
	return c
end

-- ------------------------------------
-- Setters
-- Functions that add/update information on an instance of the `Card` object based on input 
-- ------------------------------------
-- Set the English name
-- @param name string - The card's name. Default to the page name.
function Card:setName(name)
	-- Check if a name was supplied
	if (name) then
		-- Use the supplied name as the card name
		self.locales.en.name = name
	else
		-- Use the page name before the first parenthesis as the card name.
		self.locales.en.name = mw.text.split(self.pageName, ' %(')[1]
	end
end

-- Set the card types
-- e.g. 'Spell','Trap', 'Virus'
-- It is usually not necessary to set 'Monster', it can be asumed based on other information.
-- Rarely a card can have multiple card types e.g. 'Counter / Token' or 'Spell / Trap'
-- 
-- @param cardTypesString string
-- @param args table
function Card:setCardTypes(cardTypesString, args)
	-- Flag if the card is a monster.
	-- Even if the cardType is not set to "Monster", the card could be a monster
	-- e.g. if cardType is "Token".
	self.isMonster = (cardTypesString == 'Monster' or args.atk or args.def or args.attribute or args.types)
		and true
		or false

	-- If the card type is explicitly stated use that.
	-- Default to "Monster" if the card is a monster.
	if (cardTypesString) then
		self.cardTypes = mw.text.split(cardTypesString, '%s*/%s*')
	elseif (self.isMonster) then
		self.cardTypes = { 'Monster' }
	end
end

-- Set Types-related data by passing in the type string
-- @param typesString string '/'-separated list of types as printed on the card
function Card:setTypes(typesString)
	-- Blank everything and start from a clean slate
	self.isNormalMonster   = false
	self.isEffectMonster   = false
	self.isPendulumMonster = false
	self.types = {}
	
	if (typesString) then
		-- Split the type input by forward slash
		-- Allow spaces at either side of the slash
		-- e.g. 'Pyro / Fusion / Effect' → { 'Pyro', 'Fusion', 'Effect' }
		local typeNames = mw.text.split(typesString, '%s*/%s*')

		-- Set these values if their Types are anywhere in the array
		self.isNormalMonster   = TableTools.inArray(typeNames, 'Normal')
		self.isEffectMonster   = TableTools.inArray(typeNames, 'Effect')
		self.isPendulumMonster = TableTools.inArray(typeNames, 'Pendulum')

		-- Fill the Types table with an object for each type.
		for _, typeName in pairs(typeNames) do
			table.insert(self.types, Type:new(typeName))
		end
	end
end

-- Set the `effectTypes` property by passing the input effect types
-- @param effectTypesString string comma-separated list of effect type names
function Card:setEffectTypes(effectTypesString)
	-- Ensure this starts from a clean slate
	self.effectTypes = {}

	-- If there's nothing to set, end early
	if (not effectTypesString or effectTypesString == '') then return end

	-- Split the effect type input by cammo
	-- Allow spaces at either side of the comma
	local effectTypeNames = mw.text.split(effectTypesString, '%s*,%s*')

	-- Fill the effect types list with an object for each effect type.
	for _, effectTypeName in pairs(effectTypeNames) do
		table.insert(self.effectTypes, EffectType:new(effectTypeName))
	end
end

-- Set the Link Arrows data
-- Doing this will also set the Link Rating equal to the number of arrows
-- @param linkArrowsInput string
function Card:setLinkArrows(linkArrowsInput)
	-- Split the input by comma to form an array
	self.linkArrows = mw.text.split(linkArrowsInput or '', '%s*,%s*')

	-- Link Rating is equal to the number of Link Arrows
	self.linkRating = #self.linkArrows
end

-- Set the main link
-- @param main string - manually specified main link.
--                      The link will determined automatically otherwise.
function Card:setMainLink(main)
	-- If main links are not enabled, "main" is always `false`.
	if (not self.config.enableMainLinks) then
		self.main = false
		return
	end

	-- If the input specifically says not to use a main link, "main" is `false`.
	if (TableTools.inArray({ 'no', 'none', 'false' }, main)) then
		self.main = false
		return
	end

	-- If the main link is specifically provided, use that.
	if (main) then
		self.main = main
		table.insert(self.styles, 'Module:Hatnote/styles.css')
		return
	end

	-- Get the name by, stripping parentheses text from the page name.
	local cardName = mw.text.split(self.pageName, ' %(')[1]

	-- If there's a difference, use the non-paretheses version as the main.
	if (self.pageName ~= cardName) then
		self.main = cardName
		table.insert(self.styles, 'Module:Hatnote/styles.css')
		return
	end

	-- If "main" is not found at this point, assume there is none.
	self.main = false
end

-- Set images data based on the input parameter
-- @param string input
function Card:setImages(input)
	-- Clear existing value
	self.images = {}

	-- Default to backing image if no input
	input = input or self.config.defaultImage

	-- Split input by new line for multiple artworks
	local inputLines = mw.text.split(input, '\n')

	local previousArtwork = nil

	-- For each line (artwork)
	for _, line in pairs(inputLines) do
		-- Ensure each item on the line ends with `;` to help pattern matching
		line = line .. ';'

		-- Content before first `;` is the image
		local image     = mw.text.split(line, ';')[1]

		-- Content after `artwork::` in the params string is the artwork number.
		-- Defaults to 1
		local artwork   = tonumber(line:match('; *artwork::([^;]-) *;') or 1)

		-- Content after `thumb::` in the params string is the thumbnail
		-- Defaults to the full image
		local thumbnail = line:match('; *thumb::([^;]-) *;') or image

		-- Content after `name::` in the params string is the artwork name
		local name      = line:match('; *name::([^;]-) *;')

		-- If the params string contains `current`, mark as the current image
		local isCurrent = line:match('; *current *;') ~= nil

		-- If the artwork has the same base number as its predecessor
		-- its number is .1 higher than it
		if (previousArtwork ~= nil and math.floor(artwork) == math.floor(previousArtwork)) then
			artwork = previousArtwork + .1
		end

		-- Add object to list of images
		table.insert(self.images, {
			image     = image,
			thumbnail = thumbnail,
			artwork   = artwork,
			name      = name,
			isCurrent = isCurrent
		})

		-- Prepare for the next iteration of the loop.
		previousArtwork = artwork
	end
end

-- Function inherited classes can override to set additional parameters
-- @param args table
-- @return void
function Card:setCustomArgs(args)
	
end



-- ------------------------------------
-- Row-adding functions
-- Functions that add key-value pairs to `Card.rows`
-- ------------------------------------
-- Add something to the list of rows to be rendered in the output
-- @param string label - Text to display in the label cell
-- @param string value - Text/icons to display in the data cell
function Card:addRow(label, value)
	if (value and value ~= '') then
		table.insert(self.rows, { label = label, value = value })
	end
end

-- Add row specifically for the Number
function Card:addNumberRow()
	self:addRow('Number', self.number)
end

-- Add a row specifically for the card type to be rendered in the output
function Card:addCardTypeRow()
	if (self.cardTypes == nil or #self.cardTypes == 0) then return end

	local links = {}

	for _, cardType in pairs(self.cardTypes) do
		local page = cardType .. ' Card'
		if     cardType == 'Token'   then page = 'Monster Token'
		elseif cardType == 'Counter' then page = 'Counter' end
	
		table.insert(links, '[[' .. page .. '|' .. cardType .. ']]')
	end

	local icon = self:renderCardTypeIcon() or ''
	
	self:addRow('[[Card type]]', table.concat(links, ' / ') .. ' ' .. icon)
end

-- Add a row specifically for the property to be rendered in the output
function Card:addPropertyRow()
	-- Exit early if no card type or property
	if (self.cardTypes == nil or #self.cardTypes == 0 or self.property == nil) then return end
	
	local text = '[[' .. self.property .. ' ' .. self.cardTypes[1] .. ' Card|' .. self.property .. ']]'
	local icon = self:renderPropertyIcon()
	
	self:addRow('[[Property]]', text .. ' ' .. icon)
end

-- Add a row specifically for the Attribute to be rendered in the output
function Card:addAttributeRow()
	-- If it's not a monster, don't continue
	if (not self.isMonster) then return end

	local text = '[[' .. (self.attribute or '???') .. ']]'
	local icon = self:renderAttributeIcon()

	self:addRow([[Attribute]], text .. ' ' .. icon)
end

-- Add a row specifically for the Type
function Card:addTypeRow()
	-- If it's not a monster, don't continue
	if (not self.isMonster) then return end
	
	self:addRow('[[Type]]s', self:renderTypeString())
end

-- Add a row specifically for the Level to be rendered in the output
function Card:addLevelRow()
	-- Don't show for non-monsters
	if (not self.isMonster) then return end

	-- Don't show for Xyz or Link monsters
	local summonType = self:getSummonType()
	if (TableTools.inArray({'Xyz', 'Link'}, summonType)) then return end

	self:addRow('[[Level]]', (self.level or '???') .. ' ' .. self:renderLevelStars())
end

-- Add a row specifically for the Rank to be rendered in the output
function Card:addRankRow()
	-- Only show for Xyz Monsters
	if (self:getSummonType() ~= 'Xyz') then return end

	self:addRow('[[Rank]]', (self.rank or '???') .. ' ' .. self:renderRankStars())
end

-- Add a row specifically for the Link Arrows
function Card:addLinkArrowsRow()
	-- Not a Link Monster, exit early
	if (self:getSummonType() ~= 'Link') then
		return nil
	end

	-- Cell contains the map showing the different arrows and a textual list
	-- Wrap the two in a flex div to vertically center them
	local cell = mw.html.create('div')
	cell
		:css('display', 'flex')
		:css('align-items', 'center')
		:css('gap', '.25em')
		:wikitext(self:renderLinkMap())
		:tag('div')
		:wikitext(table.concat(self.linkArrows, ', '))

	self:addRow('[[Link Arrow]]s', tostring(cell))
end

-- Add a row specifically for the Pendulum Scale
function Card:addPendulumScaleRow()
	-- Only allow this row for Pendulum Monsters with a Pendulum Scale
	if (not self.isPendulumMonster or self.pendulumScale == nil) then
		return nil
	end

	local icon = '[[File:Pendulum Scale.png|22px|alt=|class=noviewer]]'

	self:addRow('[[Pendulum Scale]]', icon .. ' ' .. self.pendulumScale)
end

-- Add a row specifically for ATK and DEF to be rendered in the output
-- Only shows for monsters that are not Link Monsters
function Card:addAtkDefRow()
	-- If the card is not a monster or is a Link Monster don't show this row.
	if (not self.isMonster or self:getSummonType() == 'Link') then
		return nil
	end

	-- If both values are blank and the cardType is "Token" don't show the row.
	if (not self.atk and not self.def and self:hasCardType('Token')) then
		return nil
	end

	local atk = self.atk or '???'
	local def = self.def or '???'

	self:addRow('[[ATK]] / [[DEF]]', atk .. ' / ' .. def)
end

-- Add a row specifically for ATK and Link to be rendered in the output
-- Only shows for Link Monsters
function Card:addAtkLinkRow()
	if (self:getSummonType() ~= 'Link') then
		return nil
	end

	local atk = self.atk or '???'
	local link = self.linkRating or '???'

	self:addRow('[[ATK]] / [[Link Rating|LINK]]', atk .. ' / ' .. link)
end

-- Add a row specifically for the effect types
function Card:addEffectTypeRow()
	if (#self.effectTypes == 0) then return end

	local linkedEffectTypes = {}

	-- Loop through all the card's effect types
	for _, effectType in pairs(self.effectTypes) do
		-- Create a pipe link and add it to the array.
		local linkedEffectType = ('[[%s|%s]]'):format(effectType.pageName, effectType.name)
		table.insert(linkedEffectTypes, linkedEffectType)
	end

	-- Join all elements in the array, separating them with commas
	local effectTypesString = table.concat(linkedEffectTypes, ', ')

	self:addRow('Effect types', effectTypesString)
end

-- Add a rowspecifically for the lore
function Card:addLoreRow()
	local lore = self.locales.en:getFullLore(self)

	if (lore == nil or lore == '') then return end
	
	local loreHtml = mw.html.create('div')
	loreHtml:attr('class', 'lore')

	if (self.locales.en.pendulumEffect) then
		-- Place Pendulum Effect and monster text in a description list
		local dl = mw.html.create('dl')
		dl:css('margin', '.5em')
		dl:tag('dt'):wikitext("'''Pendulum Effect'''")
		dl:tag('dd'):wikitext(self.locales.en.pendulumEffect)
		dl:tag('dt'):wikitext("'''Monster text'''")
		dl:tag('dd'):wikitext(lore)

		loreHtml:wikitext(tostring(dl))
	else
		-- Place lore in a single paragraph in the 
		loreHtml:tag('p'):wikitext(lore)
	end

	self:addRow(nil, tostring(loreHtml))
end

-- Add row specifically for the Limitation text
function Card:addLimitationTextRow()
	self:addRow('[[Limitation text]]', self.limitationText)
end

-- Add row specifically for the Password
function Card:addPasswordRow()
	self:addRow('[[Password]]', self.password)
end



-- ------------------------------------
-- Getters
-- Functions that retrieve information from the instance of the `Card` object
-- ------------------------------------
-- Get the card's summon Type
-- @return string
function Card:getSummonType()
	-- Exit early, if not a monster
	if (not self.isMonster) then return nil end

	-- loop through the Types until the Summon Type is found
	for _, type in pairs(self.types) do
		if (type.category == 'Summon') then
			return type.name
		end
	end

	-- Summon Type was not found, return `nil`
	return nil
end

-- Get the CSS class for the card's color
-- (Does not include Pendulum)
-- @return string
function Card:getColorClass()
	-- If the card has a custom color, use that
	if (self.customColor) then
		return self.customColor .. '-card'
	end

	-- Counters use the same color as Tokens
	if (self:hasCardType('Counter')) then return 'token-card' end

	-- If the card is not a monster, base the color on the card type
	if (self.cardTypes[1] and self.cardTypes[1] ~= 'Monster') then
		return string.lower(self.cardTypes[1]) .. '-card'
	end

	-- If the card has a Summon type (Fusion, Ritual, etc.),
	-- base the color on that
	local summonType = self:getSummonType()
	if (summonType) then
		-- The different types of Accel Synchro all use the Synchro color
		if (string.find(summonType, 'Accel Synchro')) then
			return 'synchro-card'
		end

		-- Lowercase, replace spaces with dashes, append "-card"
		return string.gsub(string.lower(summonType), ' ', '-') .. '-card'
	end

	-- If the card is an Effect Monster, base the color on that
	if (self.isEffectMonster) then
		return 'effect-card'
	end

	-- If the card is a Normal Monster, base the color on that
	if (self.isNormalMonster) then
		return 'normal-card'
	end

	-- If the color couldn't be determined above, use this as the default
	return 'blank-card'
end

-- Get the variable CSS classes used in the HTML output
-- Includes the main color class and the pendulum color class
-- @return string
function Card:getCssClass()
	local cssClass = ''

	-- If there is a class to be used for all cards, add that
	if (self.config.baseClass) then
		cssClass = cssClass .. self.config.baseClass
	end

	-- If the cards are color-coded, add more classes
	if (self.config.colorCoded) then
		cssClass = cssClass .. self:getColorClass()
	
		if (self.isPendulumMonster) then
			cssClass = cssClass .. ' pendulum-card'
		end
	end
	
	return cssClass
end

-- Check if the card has a specified card type
-- @param cardType string
-- @return boolean
function Card:hasCardType(cardType)
	return TableTools.inArray(self.cardTypes, cardType)
end



-- ------------------------------------
-- Rendering functions
-- Functions that format details from the card in a certain way
-- ------------------------------------
-- Render an icon representing the card type
-- @return string
function Card:renderCardTypeIcon()
	-- If the card is not a Spell or Trap, exit early
	-- Only bother with the first supplied card type.
	-- (Things with multiple card types don't use this icon.)
	if (self.cardTypes[1] ~= 'Spell' and self.cardTypes[1] ~= 'Trap') then
		return ''
	end

	-- String containing a pattern matching the card type icons
	local pattern = self.config.icons.cardTypes

	-- If there's no pattern, don't try to render an icon.
	if (not pattern) then return '' end

	-- Replace variables in the pattern.
	-- e.g. replace '{$cardType}' with 'Spell' if this is a Spell Card.
	local params = {
		cardType    = self.cardTypes[1],
		cardType_uc = string.upper(self.cardTypes[1])
	}

	return replaceParams(pattern, params)
end

-- Render an icon representing the Property
-- @return string
function Card:renderPropertyIcon()
	-- If the card is not a Spell or Trap, exit early.
	-- Only bother with the first card type.
	-- (Things with multiple card types don't use this icon.)
	if (self.cardTypes[1] ~= 'Spell' and self.cardTypes[1] ~= 'Trap') then
		return ''
	end

	-- String containing a pattern matching the property icons
	local pattern = self.config.icons.properties

	-- If there's no pattern, don't try to render an icon.
	if (not pattern) then return '' end

	-- Replace variables in the pattern.
	-- e.g. replace '{$cardType}' with 'Spell' if this is a Spell Card.
	local params = {
		cardType = self.cardTypes[1],
		property = self.property
	}

	return replaceParams(pattern, params)
end

-- Render an icon representing the Attribute
-- @return string
function Card:renderAttributeIcon()
	-- Exit early, if not a monster
	if (not self.isMonster) then return '' end

	if (self.attribute == nil or self.attribute == '???') then
		return self.config.icons.unknownAttribute
	end

	local pattern = self.config.icons.attributes

	-- If there's no pattern, don't try to render an icon.
	if (not pattern) then return '' end

	local params = {
		attribute = self.attribute
	}

	return replaceParams(pattern, params)
end

-- Render the list of Types as a string with links and separators
-- @return string
function Card:renderTypeString()
	-- Exit early, if not a monster
	if (not self.isMonster) then return end

	-- Array containing links to each of the card's Types.
	local linkedTypes = {}

	-- Loop through all the card's Types
	for _, type in pairs(self.types) do
		-- Create a pipe link and add it to the array.
		local linkedType = ('[[%s|%s]]'):format(type.pageName, type.name)
		table.insert(linkedTypes, linkedType)
	end

	-- Join all elements in the array, separating them with slashes.
	return table.concat(linkedTypes, ' / ')
end

-- Render the Level as star icons
-- @return string
function Card:renderLevelStars()
	-- Don't show for non-monsters
	if (not self.isMonster) then return '' end

	-- Don't show for Xyz or Link monsters
	local summonType = self:getSummonType()
	if (TableTools.inArray({'Xyz', 'Link'}, summonType)) then return '' end
	
	local icons = self.config.icons

	-- If unknown Level, just show a single instance of the unknown icon.
	if (self.level == nil or self.level == '?' or self.level == '???') then
		return icons.unknownLevel
	end

	-- Now that unknown is out of the way, this can be cast as a number.
	local level = tonumber(self.level)

	-- Exit early if the Level is 0
	if (level == 0) then return '' end

	-- If there is one icon pattern to cover all Levels use that.
	if (icons.levels ~= nil) then
		return replaceParams(icons.levels, { level = self.level})
	end

	-- Use different icons if Level is positive or negative
	local icon = level >= 0 and icons.level or icons.negativeLevel

	-- Repeat the same icon a number of times equal to the Level/Rank (stars)
	return '<span class="card-stars">' .. 
		string.rep(icon, math.abs(level)) .. 
		'</span>'
end

-- Render the Rank as star icons
-- @return string
function Card:renderRankStars()
	-- Exit early, if not an Xyz Monster
	if (self:getSummonType() ~= 'Xyz') then return '' end

	local icons = self.config.icons

	-- If unknown Level/Rank, just show a single instance of the unknown icon.
	if (self.rank == nil or self.rank == '?' or self.rank == '???') then
		return icons.unknownRank
	end

	-- Exit early if the Rank is 0
	if (self.rank == 0) then return '' end

	-- Repeat the same icon a number of times equal to the Rank
	return '<span class="card-stars">' .. string.rep(icons.rank, self.rank) .. '</span>'
end

-- Render a grid showing the Link Arrows
-- @return string
function Card:renderLinkMap()
	local gridPositions = {
		'Top-Left',    'Top-Center',    'Top-Right',
		'Middle-Left', 'Middle-Center', 'Middle-Right',
		'Bottom-Left', 'Bottom-Center', 'Bottom-Right'
	}

	-- `div` for a 3x3 grid
	local map = mw.html.create('div')
	map
		:css('display', 'inline-grid')
		:css('width', '36px'):css('height', '46px')
		:css('grid-template-rows', '12px 12px 12px')
		:css('grid-template-columns', '10px 15px 10px')

	-- Add an element for each position in the grid
	for _, position in pairs(gridPositions) do
		if (position == 'Middle-Center') then
			-- Nothing in the center square
			map:wikitext('&nbsp;')
		else
			-- Arrow will be highlighted in in the list of Link Arrows
			local active = TableTools.inArray(self.linkArrows, position)

			-- These two images have a different width than the others
			local size = TableTools.inArray({ 'Top-Center', 'Bottom-Center' }, position) and 'x10px' or '10px'

			-- Form the file name
			-- Remove '-' from the position name, add "2" if the position is inactive
			local file = 'LM-' .. (position:gsub('-', '')) ..(not active and '2' or '') .. '.png'
			map:wikitext('[[File:' .. file .. '|' ..size .. '|alt=]]')
		end
	end

	return tostring(map)
end

-- Render the card image section
-- Either a single image or a switcher for multiple images
-- @return string
function Card:renderImages()
	-- If there's only one image, show it
	if (#self.images == 1) then
		return '[[File:' .. self.images[1].image .. '|200px]]'
	end

	-- Create the HTML for the image switcher wrapper
	local imageSwitcher = mw.html.create('div')
	imageSwitcher
		:addClass('switcher-container')
		:css('margin', '0 auto')
		:css('max-width', '200px')
		:css('text-align', 'left')

	-- Add a child element for each image and its label
	for _, image in pairs(self.images) do
		local title = image.name or ('Artwork ' .. image.artwork)
		local imageDisplay = '[[File:' .. image.image .. '|200px|alt=' .. title .. ']]'
		local labelHtml = mw.html.create('div')
		
		labelHtml:attr('class', 'switcher-label'):wikitext(title)
		
		if (image.isCurrent) then
			labelHtml:attr('data-switcher-default', 'true')
		end

		imageSwitcher
			:tag('div')
			:css('margin-bottom', '5px')
			:wikitext(imageDisplay .. tostring(labelHtml))
	end

	return tostring(imageSwitcher)
end

-- Create a TemplateStyles tag
function renderTemplateStyles(page)
	if (page == nil or page == '') then return end
	
	return tostring( mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = page } } )
end

-- Generate the wikitext output
function Card:render()
	local output = ''

	-- Add template styles
	for _, style in pairs(self.styles) do
		output = output .. '\n' .. renderTemplateStyles(style)
	end

	if (self.main) then
		output = output .. '<div role="note" class="hatnote navigation-not-searchable">Main card page: "[[' .. self.main .. ']]"</div>'
	end

	output = output .. '\n<div class="card-table ' .. self:getCssClass() .. '">'

	output = output .. '\n  <div class="heading">' .. self.locales.en.name .. '</div>'

	if (self.locales.ja) then
		output = output .. '\n  <div class="above hlist">' .. self:renderJaNamesList() .. '</div>'
	end

	output = output .. '\n  <div class="card-table-columns">'
	output = output .. '\n    <div class="imagecolumn">'
	output = output .. self:renderImages()
	output = output .. '\n    </div>' -- .imagecolumn

	output = output .. '\n    <div class="infocolumn">'
	output = output .. '\n      <table class="innertable">'

	for _, row in pairs(self.rows) do
		output = output .. '\n<tr>'
		if (row.label) then
			output = output .. '\n  <th scope="row" style="text-align: left;">' .. row.label .. '</th>'
			output = output .. '\n  <td>' .. row.value .. '</td>'
		else
			output = output .. '\n <td colspan="2">' .. row.value .. '</td>'
		end
		output = output .. '\n</tr>'
	end

	output = output .. '\n      </table>'
	output = output .. '\n    </div>' -- .infocolumn

	output = output .. '\n  </div>' -- .card-table-columns

	output = output .. self:renderFooter()

	output = output .. '\n</div>' -- .card-table

	output = output .. self:renderLocalesTable()
	
	output = output .. self:renderCategories()

	return output
end

-- Render the footer section of the card-details output
-- @return string
function Card:renderFooter()
	if (not self.config.footerText or self.config.footerText == '') then return '' end
	
	local output = '<div class="below">'
	if (self.prev or self.next) then
		output = output .. '\n  <div class="chronology">'
		output = output .. '\n    <div class="prev">' .. (self.prev or '') .. '</div>'
		output = output .. '\n    <div class="curr">' .. self.config.footerText .. '</div>'
		output = output .. '\n    <div class="prev">' .. (self.next or '') .. '</div>'
		output = output .. '\n  </div>'
	else
		output = output .. self.config.footerText
	end
	output = output .. '\n</div>'

	return output
end

-- Generate the wikitext for the "other languages" section
-- @return string
function Card:renderLocalesTable()
	-- Needs to have at least two languages
	-- i.e. Needs at least one language other than English
	if (TableTools.size(self.locales) < 2) then return '' end
	
	output = '\n==Other languages=='

	output = output .. '\n<table class="wikitable">'
	output = output .. '\n  <tr>'
	output = output .. '\n    <th scope="col">Language</th>'
	output = output .. '\n    <th scope="col">Name</th>'
	output = output .. '\n    <th scope="col">Card text</th>'
	output = output .. '\n  </tr>'

	-- Iterate through `self.config.langs`, rather than `self.locales`
	-- because "langs" is numerically indexed, so its ordering is respected
	for _, lang in pairs(self.config.langs) do
		-- Only include languages that have recorded data.
		-- Don't include English.
		if (lang ~= 'en' and self.locales[lang]) then
			local locale = self.locales[lang]
			local langCode = locale:getHtmlLang()

			output = output .. '\n<tr>'
			output = output .. '\n  <th scope="row">' .. locale.language.name .. '</th>'
			output = output .. '\n  <td lang="' .. langCode .. '">'
			output = output .. (locale.name or '')
			if (locale.romanizedName) then
				output = output .. '<br />(<i lang="' .. locale:getRomanizedHtmlLang() .. '">' .. locale.romanizedName .. '</i>)'
			end
			output = output .. '</td>'
			output = output .. '\n  <td lang="' .. langCode .. '">' .. locale:getFullLore(self) .. '</td>'
			output = output .. '\n</tr>'
		end
	end

	output = output .. '</table>'

	return output
end

-- Render a description list for the different forms of the Japanese name
-- @return string
function Card:renderJaNamesList()
	if not self.locales.ja then return '' end
	
	local ja = self.locales.ja
	local jaBaseName = ja:getBaseName()
	local jaTopName  = ja:getTopName()

	local list = mw.html.create('dl')

	-- Main Japanese name
	if (ja.name) then
		list:tag('dt'):wikitext('Japanese')
		list:tag('dd'):tag('span'):attr('lang', 'ja-Japn'):wikitext(' ' .. ja.name)
	end

	-- Base Japanese name, if different than main
	if (jaBaseName and jaBaseName ~= ja.name) then
		list:tag('dt'):wikitext('Base')
		list:tag('dd'):tag('span'):attr('lang', 'ja-Japn'):wikitext(' ' .. jaBaseName)
	end

	-- Base rōmaji name, if different than main rōmaji name
	if (ja.baseRomanizedName and ja.baseRomanizedName ~= ja.romanizedName) then
		list:tag('dt'):wikitext("Base ''rōmaji''")
		list:tag('dd'):tag('span'):attr('lang', 'ja-Latn-hepburn'):wikitext(' <i>' .. ja.BaseRomanizedName .. '</i>')
	end

	-- Base translated name, if different than main translated name
	if (ja.baseTranslatedName and ja.baseTranslatedName ~= ja.translatedName) then
		list:tag('dt'):wikitext("Base translated")
		list:tag('dd'):tag('span'):wikitext(' ' .. jaBaseTranslatedName)
	end

	-- Furigana name, if different than main
	if (jaTopName and jaTopName ~= ja.name) then
		list:tag('dt'):wikitext('Furigana')
		list:tag('dd'):tag('span'):attr('lang', 'ja-Hrkt'):wikitext(' ' .. jaTopName)
	end

	-- Rōmaji name
	if (ja.romanizedName) then
		local label = (ja.baseRomanizedName)
			and "Furigana ''rōmaji''"
			or "''Rōmaji''"
		list:tag('dt'):wikitext(label)
		list:tag('dd'):tag('span'):attr('lang', 'ja-Latn-hepburn'):wikitext(' <i>' .. ja.romanizedName .. '</i>')
	end

	-- Translated name
	if (ja.translatedName) then
		local label = (ja.baseTranslatedName)
			and 'Furigana translated'
			or 'Translated'
		list:tag('dt'):wikitext(label)
		list:tag('dd'):tag('span'):wikitext(' ' .. ja.translatedName)
	end

	return tostring(list)
end

-- Render the categories
-- If this is a demo page, show a section with links to each category.
-- For non-demo pages, actually add the categories.
-- @return string
function Card:renderCategories()
	if not self.demo then
		local categoryText = ''

		for _, category in pairs(self.categories) do
			categoryText = categoryText .. '[[Category:' .. category .. ']]'
		end

		return categoryText
	end

	local categoryBlock = mw.html.create('div')
		:attr('class', 'catlinks')
		:wikitext('Categories: ')

	for i, category in pairs(self.categories) do
		if (i ~= 1) then
			categoryBlock:wikitext(' | ' )
		end
		categoryBlock:wikitext('[[:Category:' .. category .. '|' .. category .. ']]')
	end

	return tostring(categoryBlock)
end


-- Function to be invoked by the module
-- Takes all template parameters and returns the HTML
function Card.card(frame)
	-- Get the template parameters
	local args = frame:getParent().args

	-- If a parameter contains an empty string, ignore it
	local normalizedArgs = {}
	for param, value in pairs(args) do
	    if (value and mw.text.trim(value) ~= '') then
	    	normalizedArgs[param] = value
	    end
	end

	-- Create a new card object
	local c = Card:new(normalizedArgs)

	-- For each row configured to show, call the function to add it
	for _, row in pairs(c.config.rows) do
	    c['add' .. ucfirst(row) .. 'Row'](c)
	end

	-- Render the output
	return c:render()
end

return Card