Difference between revisions of "Module:Limitation status list"

From Yugipedia
Jump to: navigation, search
(support for column with Chinese/Japanese/Korean names)
(Changing order to match the F/L list header in each article)
 
(43 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 +
local inArray = require('Module:TableTools').inArray
 +
 
-- Shell object for the list itself
 
-- Shell object for the list itself
 
local List = {
 
local List = {
 
name = nil,
 
name = nil,
 
medium = nil,
 
medium = nil,
 +
format = nil,
 
locality = nil,
 
locality = nil,
 
startDate = nil,
 
startDate = nil,
 +
endDate = nil,
 
prev = nil,
 
prev = nil,
 
next = nil,
 
next = nil,
Line 14: Line 18:
 
-- The name of the SMW property to find the names in
 
-- The name of the SMW property to find the names in
 
localColumnProperty = nil,
 
localColumnProperty = nil,
 +
 +
errors = {}
 
}
 
}
  
Line 27: Line 33:
 
cardType = nil,
 
cardType = nil,
 
status = nil,
 
status = nil,
 +
-- If a card was on the list despite not being released while the list was in effect
 +
unreleased = false,
 +
afterDate = nil,
 
prevStatus = nil,
 
prevStatus = nil,
note = nil
+
prevStatusNote = nil
 +
}
 +
 
 +
local statusDescriptions = {
 +
['Forbidden']    = 'These cards cannot be used in the [[Main Deck]], [[Extra Deck]], or [[Side Deck]].',
 +
['Limited']      = 'A maximum of one copy of each of these cards can be included in the [[Main Deck]], [[Extra Deck]], and [[Side Deck]] combined.',
 +
['Semi-Limited'] = 'A maximum of two copies of each of these cards can be included in the [[Main Deck]], [[Extra Deck]], and [[Side Deck]] combined.',
 +
['Limited 1']    = 'A maximum of one out of all of the following cards can be included in the [[Main Deck]], [[Extra Deck]], [[Side Deck]], and choice of [[Skill Card]] combined.',
 +
['Limited 2']    = 'A maximum of two out of all of the following cards can be included in the [[Main Deck]], [[Extra Deck]], [[Side Deck]], and choice of [[Skill Card]] combined. This may be two copies of the same card or one copy one card and one copy of another.',
 +
['Limited 3']    = 'A maximum of three out of all of the following cards can be included in the [[Main Deck]], [[Extra Deck]], [[Side Deck]], and choice of [[Skill Card]] combined. This can include multiple copies of the same card, as long as the combined total from this list is three or fewer.',
 +
['Unlimited']    = 'Limitations have been removed from the following cards since the last list.'
 +
}
 +
 
 +
-- Mediums that have only been released in Japanese
 +
-- @todo: Come up with solution that doesn't involve hardcoding games in the module
 +
local jaOnlyMediums = {
 +
'Yu-Gi-Oh! Duel Monsters (video game)',
 +
'Yu-Gi-Oh! Rush Duel',
 
}
 
}
  
Line 48: Line 74:
  
 
l:setData(args or {})
 
l:setData(args or {})
 +
 +
l:validateAfterParsing()
  
 
return l
 
return l
Line 57: Line 85:
 
self.name = args.name or mw.title.getCurrentTitle().text
 
self.name = args.name or mw.title.getCurrentTitle().text
 
self.medium = args.medium
 
self.medium = args.medium
 +
self.format = args.format
 
self.locality = args.locality
 
self.locality = args.locality
 
self.startDate = args.start_date
 
self.startDate = args.start_date
 +
self.endDate = args.end_date
 
self.prev = args.prev
 
self.prev = args.prev
 
self.next = args.next
 
self.next = args.next
 +
self:setLocalisationAttributes()
  
 
self.items = {}
 
self.items = {}
Line 66: Line 97:
 
self:addItems('Limited', args.limited)
 
self:addItems('Limited', args.limited)
 
self:addItems('Semi-Limited', args.semi_limited)
 
self:addItems('Semi-Limited', args.semi_limited)
 +
self:addItems('Limited 1', args.limited_1)
 +
self:addItems('Limited 2', args.limited_2)
 +
self:addItems('Limited 3', args.limited_3)
 
self:addItems('Unlimited', args.no_longer_on_list)
 
self:addItems('Unlimited', args.no_longer_on_list)
  
Line 78: Line 112:
 
self.locality == 'Korean'
 
self.locality == 'Korean'
 
) then
 
) then
self.localColumnHeading = self.locality
+
self.localColumnHeading = self.locality .. ' name'
 
self.localColumnProperty = self.locality .. ' name'
 
self.localColumnProperty = self.locality .. ' name'
 +
return
 +
end
  
elseif self.medium == 'OCG' or self.medium == 'Yu-Gi-Oh! Official Card Game' then
+
local isJaOnly = inArray(jaOnlyMediums, self.medium)
self.localColumnHeading = 'Japanese'
+
local isOcg = self.medium == 'OCG' or self.medium == 'Yu-Gi-Oh! Official Card Game'
 +
 
 +
if (isJaOnly or (isOcg and self.locality ~= 'Asian-English')) then
 +
self.localColumnHeading = 'Japanese name'
 
self.localColumnProperty = 'Japanese name'
 
self.localColumnProperty = 'Japanese name'
 +
return
 +
end
  
else
+
self.localColumnHeading = nil
self.localColumnHeading = nil
+
self.localColumnProperty = nil
self.localColumnProperty = nil
 
end
 
 
end
 
end
  
Line 104: Line 143:
 
-- Split the line input by `//` and get data from each piece
 
-- Split the line input by `//` and get data from each piece
 
local lineData = mw.text.split(line, '%s*//%s*')
 
local lineData = mw.text.split(line, '%s*//%s*')
 +
local itemText = lineData[1]
 +
local options = lineData[2]
  
 
-- Extract data from content after the `//`
 
-- Extract data from content after the `//`
local itemText = lineData[1]
+
local unreleased = options and options:match('unreleased::true') and true or false
local options = lineData[2]
+
local afterDate = options and options:match('after::([^;]*)')
 
local prevStatus = options and options:match('prev::([^;]*)')
 
local prevStatus = options and options:match('prev::([^;]*)')
+
local prevStatusNote = options and options:match('prev%-note::([^;]*)')
 +
 
 
-- Create a new list item object
 
-- Create a new list item object
 
local listItem = mw.clone(ListItem)
 
local listItem = mw.clone(ListItem)
  
 
listItem.status = status
 
listItem.status = status
 +
listItem.unreleased = unreleased
 +
listItem.afterDate = afterDate
 
listItem.prevStatus = prevStatus
 
listItem.prevStatus = prevStatus
 +
listItem.prevStatusNote = prevStatusNote
 
listItem.displayText = itemText
 
listItem.displayText = itemText
  
Line 123: Line 168:
 
if isCard then
 
if isCard then
 
local cardData = self:lookupCardData(itemText)
 
local cardData = self:lookupCardData(itemText)
+
 
listItem.card = itemText
+
listItem.cardFound    = cardData and true or false
listItem.cardName = cardData.name
+
-- The original name that was supplied
listItem.localCardName = cardData.localName
+
listItem.card         = itemText
listItem.cardType = cardData.cardType
+
-- The name that SMW found
listItem.displayText = '"[[' .. listItem.card .. '|' .. listItem.cardName .. ']]"'
+
listItem.pageName      = cardData and cardData.pageName  or nil
 +
listItem.cardName     = cardData and cardData.name     or itemText
 +
listItem.localCardName = cardData and cardData.localName or ''
 +
listItem.cardType     = cardData and cardData.cardType
 +
listItem.displayText   = '"[[' .. listItem.card .. '|' .. listItem.cardName .. ']]"'
 
end
 
end
  
Line 138: Line 187:
 
function List:setSmwData()
 
function List:setSmwData()
 
mw.smw.set({
 
mw.smw.set({
['Effective date'] = self.startDate,
+
['Effective date'] = self.startDate, -- deprecated
 +
['Start date']    = self.startDate,
 +
['End date']      = self.endDate,
 
['Medium']        = self.medium,
 
['Medium']        = self.medium,
 
['Release']        = self.medium,
 
['Release']        = self.medium,
 +
['Format']        = self.format or self.locality or self.medium,
 
['Locality']      = self.locality,
 
['Locality']      = self.locality,
 
['Page type']      = 'Status list'
 
['Page type']      = 'Status list'
Line 161: Line 213:
 
local queryParams = {
 
local queryParams = {
 
'[[' .. pageName  .. ']]',
 
'[[' .. pageName  .. ']]',
 +
'? = pageName#',
 
'?English name = name',
 
'?English name = name',
'?Card type#  = cardType',
+
'?Card type#  = cardType'
 
}
 
}
  
 +
-- If there is a column for another language
 +
-- also query for the card's name in that language
 
if (self.localColumnProperty) then
 
if (self.localColumnProperty) then
 
table.insert(queryParams, '?' .. self.localColumnProperty .. ' = localName')
 
table.insert(queryParams, '?' .. self.localColumnProperty .. ' = localName')
Line 171: Line 226:
 
local cardData = mw.smw.ask(queryParams)
 
local cardData = mw.smw.ask(queryParams)
  
return cardData and cardData[1] or {}
+
return cardData and cardData[1] or nil
 +
end
 +
 
 +
-- Validate the data after it has been parsed
 +
-- Checks for errors and adds them to the `errors` attribute
 +
function List:validateAfterParsing()
 +
for i, item in pairs(self.items) do
 +
-- If not a card, go to the next iteration of the loop.
 +
-- (Any validation after this line is only applicable to cards.)
 +
if (not item.card) then break end
 +
 
 +
-- Check if card data was found for the looked up name.
 +
if (item.cardFound == false) then
 +
table.insert(self.errors, 'Failed to look up details for card: <code>' .. item.card .. '</code>.')
 +
 
 +
-- If the card wasn't found, move on to the next list item.
 +
-- No need to perform the next few checks.
 +
break
 +
end
 +
 
 +
-- Check if a card is listed more than once
 +
for i2, item2 in pairs(self.items) do
 +
-- `i < i2`: Only look at records that come after this one,
 +
-- so the same error isn't reported again for the duplicate(s)
 +
if (i < i2 and item.pageName == item2.pageName) then
 +
table.insert(self.errors, '<code>' .. item.pageName .. '</code> is listed more than once.')
 +
 +
-- Duplicate found, so no need to keep looking.
 +
break
 +
end
 +
end
 +
 
 +
-- Check if the supplied name doesn't match the found page name
 +
if (item.card ~= item.pageName) then
 +
table.insert(self.errors, 'The supplied page name, <code>' .. item.card .. '</code>, does not match the looked-up page name, <code>' .. item.pageName .. '</code>.')
 +
end
 +
 
 +
if (item.status == 'Unlimited' and not item.prevStatus) then
 +
table.insert(self.errors, '<code>' .. item.card .. '</code> has been removed from the list, but does not specify its previous status.')
 +
end
 +
end
 
end
 
end
  
Line 194: Line 289:
 
-- @return string
 
-- @return string
 
function List:render()
 
function List:render()
 +
self:renderDisplayTitle()
 +
 
local output = ''
 
local output = ''
 +
output = output .. self:renderErrors()
 
output = output .. self:renderNavigation()
 
output = output .. self:renderNavigation()
 
output = output .. self:renderStatusList('Forbidden')
 
output = output .. self:renderStatusList('Forbidden')
 
output = output .. self:renderStatusList('Limited')
 
output = output .. self:renderStatusList('Limited')
 
output = output .. self:renderStatusList('Semi-Limited')
 
output = output .. self:renderStatusList('Semi-Limited')
 +
output = output .. self:renderStatusList('Limited 1')
 +
output = output .. self:renderStatusList('Limited 2')
 +
output = output .. self:renderStatusList('Limited 3')
 
output = output .. self:renderStatusList('Unlimited', 'No longer on list')
 
output = output .. self:renderStatusList('Unlimited', 'No longer on list')
+
output = output .. self:renderCategories()
 +
 
 +
return output
 +
end
 +
 
 +
-- Format the page name, if necessary
 +
function List:renderDisplayTitle()
 +
local italicTitle = nil
 +
local pageName    = mw.title.getCurrentTitle().text
 +
-- Medium without the word "Yu-Gi-Oh!"
 +
local mediumShort = mw.text.trim(replacePlain(self.medium, 'Yu-Gi-Oh!', ''), '%s')
 +
 
 +
-- If the medium or a shortened version of it is mentioned in the page name italicize it
 +
if pageName:find(self.medium) then
 +
italicTitle = replacePlain(pageName, self.medium, '<i>' .. self.medium .. '</i>')
 +
elseif pageName:find(mediumShort) then
 +
italicTitle = replacePlain(pageName, mediumShort, '<i>' .. mediumShort .. '</i>')
 +
end
 +
 
 +
-- Use the italicized name as the page name
 +
if italicTitle then
 +
mw.getCurrentFrame():callParserFunction('DISPLAYTITLE', italicTitle)
 +
end
 +
end
 +
 
 +
function List:renderErrors()
 +
-- Exit early with empty string if there are no errors.
 +
if (#self.errors == 0) then return '' end;
 +
 
 +
local output = '<div style="color: #d33;">'
 +
output = output .. '\nThis list contains the following issues:\n'
 +
 
 +
for _, error in pairs(self.errors) do
 +
output = output .. '\n* ' ..  error .. ''
 +
end
 +
 
 +
output = output .. '\n</div>'
 +
 
 
return output
 
return output
 
end
 
end
Line 207: Line 345:
 
-- @return string
 
-- @return string
 
function List:renderNavigation()
 
function List:renderNavigation()
-- If prev and next are both empty, don't show a navigation menu
+
-- Strip disambiguation text from the medium's page name
if (not isFilled(self.prev) and not isFilled(self.next)) then
+
local mediumName = mw.text.split(self.medium, ' %(')[1]
return ''
 
end
 
  
local currText = self:pageNameToShortName(self.name)
+
local currText = (self.locality or '') .. ' <i>' .. mediumName .. '</i> ' .. (self.format or '') .. ' Forbidden & Limited Lists'
local prevText = self.prev and ('← [[' .. self.prev .. '|' .. self:pageNameToShortName(self.prev) .. ']]') or '&nbsp;'
+
local prevText = isFilled(self.prev) and ('← [[' .. self.prev .. '|' .. self:pageNameToShortName(self.prev) .. ']]') or '&nbsp;'
local nextText = self.next and ('[[' .. self.prev .. '|' .. self:pageNameToShortName(self.next) .. ']] →') or '&nbsp;'
+
local nextText = isFilled(self.next) and ('[[' .. self.next .. '|' .. self:pageNameToShortName(self.next) .. ']] →') or '&nbsp;'
  
local output = '<div style="border: 1px solid #aaa; display: flex; margin-bottom: .5em;">'
+
local output = '<div class="toccolours" style="clear: both; display: flex; margin-bottom: .5em;">'
output = output .. '<div style="flex: 1; padding: .25em; text-align: left;">' .. prevText .. '</div>'
+
output = output .. '<div style="flex: 1; text-align: left;">' .. prevText .. '</div>'
output = output .. '<div style="flex: 1; padding: .25em; text-align: center;">' .. currText .. '</div>'
+
output = output .. '<div style="flex: 1; text-align: center;">' .. currText .. '</div>'
output = output .. '<div style="flex: 1; padding: .25em; text-align: right;">' .. nextText .. '</div>'
+
output = output .. '<div style="flex: 1; text-align: right;">' .. nextText .. '</div>'
 
output = output .. '</div>'
 
output = output .. '</div>'
 +
 +
if isFilled(self.startDate) and isFilled(self.endDate) then
 +
output = output .. '<p style="font-size: .9em; text-align: center;"><b>Effective</b>: '
 +
output = output .. self.startDate .. ' – ' ..  self.endDate
 +
output = output .. '</p>'
 +
end
  
 
return output
 
return output
Line 226: Line 368:
  
 
-- Convert a page name to a short name
 
-- Convert a page name to a short name
-- e.g. "March 2020 Lists (Duel Links)" -> "March 2020 Lists"
+
-- e.g. "March 2020 Lists (Duel Links)" -> "March 2020"
-- e.g. "OCG April 2014 Lists" -> "April 2014 Lists"
+
-- e.g. "OCG April 2014 Lists" -> "April 2014"
 
-- @param pageName string
 
-- @param pageName string
 
-- @return string
 
-- @return string
Line 233: Line 375:
 
-- Strip out disambiguation text
 
-- Strip out disambiguation text
 
local shortName = mw.text.split(pageName, ' %(')[1]
 
local shortName = mw.text.split(pageName, ' %(')[1]
+
 
 
-- Remove mention of the medium
 
-- Remove mention of the medium
shortName = shortName:gsub(self.medium, '')
+
shortName = replacePlain(shortName, self.medium, '')
 +
 
 +
-- Remove the word 'Lists'
 +
shortName = replacePlain(shortName, ' Lists', '')
  
 
-- Clear any leading/trailing spaces
 
-- Clear any leading/trailing spaces
 
return mw.text.trim(shortName)
 
return mw.text.trim(shortName)
 +
end
 +
 +
function List:renderCategories()
 +
-- Don't render any categories if this is outside of the main namespace
 +
if (isFilled(mw.title.getCurrentTitle().nsText)) then return '' end
 +
 +
local output = '[[Category:' .. (self.locality or '') .. ' ' .. (self.medium or '') .. ' ' .. (self.format or '') .. ' Forbidden & Limited Lists]]'
 +
 +
if (#self.errors > 0) then
 +
output = output .. '[[Category:Pages with validation errors]]'
 +
end
 +
 +
return output
 
end
 
end
  
Line 256: Line 414:
  
 
local headingRow = list:tag('tr')
 
local headingRow = list:tag('tr')
headingRow:tag('th'):wikitext('Card')
+
headingRow:tag('th'):attr('scope', 'col'):wikitext('Card')
 
if self.localColumnHeading then
 
if self.localColumnHeading then
headingRow:tag('th'):wikitext(self.localColumnHeading)
+
headingRow:tag('th'):attr('scope', 'col'):wikitext(self.localColumnHeading)
 
end
 
end
headingRow:tag('th'):wikitext('Card type')
+
headingRow:tag('th'):attr('scope', 'col'):wikitext('Card type')
headingRow:tag('th'):wikitext('Status')
+
headingRow:tag('th'):attr('scope', 'col'):wikitext('Status')
headingRow:tag('th'):wikitext('Changes')
+
headingRow:tag('th'):attr('scope', 'col'):wikitext('Changes')
  
 
for _, item in pairs(items) do
 
for _, item in pairs(items) do
 
local cardTypeLink = item.cardType
 
local cardTypeLink = item.cardType
and ('[[' .. item.cardType .. '|' .. string.gsub(item.cardType, ' Card', '') .. ']]')
+
and ('[[' .. item.cardType .. '|' .. replacePlain(item.cardType, ' Card', '') .. ']]')
 
or ''
 
or ''
  
 
-- Form the CSS class that styles the cells in the status column
 
-- Form the CSS class that styles the cells in the status column
local statusClass = 'status-' .. (item.status):lower()
+
local statusClass = 'status-' .. (item.status:gsub(' ', '-')):lower()
 +
 
 +
local statusText = '[[' .. item.status .. ']]'
 +
 +
if item.afterDate then
 +
statusText = statusText .. ' (after: ' .. item.afterDate .. ')';
 +
end
 +
 
 +
-- If the card was unreleased, add an extra bit of text to explain that.
 +
if item.unreleased then
 +
local explainText = 'This card was not released' .. (self.locality and (' in ' .. self.locality) or '') .. ' while this list was in effect.'
 +
statusText = statusText .. ' (<span class="explain" title="' .. explainText .. '">Unreleased</span>)'
 +
end
  
 
local row = list:tag('tr')
 
local row = list:tag('tr')
 
row:tag('td'):wikitext(item.displayText)
 
row:tag('td'):wikitext(item.displayText)
 
if self.localColumnHeading then
 
if self.localColumnHeading then
headingRow:tag('td'):wikitext(item.localName)
+
row:tag('td'):wikitext(item.localCardName)
 
end
 
end
 
row:tag('td'):wikitext(cardTypeLink)
 
row:tag('td'):wikitext(cardTypeLink)
row:tag('td'):addClass(statusClass):wikitext('[[' .. item.status .. ']]')
+
row:tag('td'):addClass(statusClass):wikitext(statusText)
row:tag('td'):wikitext(item.prevStatus and 'was [[' .. item.prevStatus .. ']]' or '')
+
row:tag('td'):wikitext(
 +
(item.prevStatus and 'was [[' .. item.prevStatus .. ']]' or '') ..
 +
(item.prevStatusNote or '')
 +
)
 
end
 
end
  
return tostring(heading) .. tostring(list)
+
local description = statusDescriptions[status] or ''
 +
 
 +
return tostring(heading) .. description .. tostring(list)
 
end
 
end
  
Line 291: Line 466:
 
function isFilled(value)
 
function isFilled(value)
 
return value ~= nil and value ~= ''
 
return value ~= nil and value ~= ''
 +
end
 +
 +
-- Replace all occurances of a string with another
 +
-- Unlike `string.gsub`, do not treat the searched term as a pattern.
 +
--
 +
-- @param source  str - The text to perform the replacement in
 +
-- @param search  str - The text to search for
 +
-- @param replace str - The text to replace the searched text with
 +
-- @return str
 +
function replacePlain(source, search, replace)
 +
-- Escape any patterns in the search term
 +
search = mw.ustring.gsub(search, '([%(%)%.%%%+%-%*%?%[%^%$%]])', '%%%1')
 +
 +
-- Replace all instances of the search term, with the replace term
 +
return mw.ustring.gsub(source, search, replace)
 
end
 
end
  
 
return List
 
return List

Latest revision as of 01:39, 6 May 2024

local inArray = require('Module:TableTools').inArray

-- Shell object for the list itself
local List = {
	name = nil,
	medium = nil,
	format = nil,
	locality = nil,
	startDate = nil,
	endDate = nil,
	prev = nil,
	next = nil,
	items = {},

	-- If there is a column for non-English names
	-- The text to display in the column heading
	localColumnHeading = nil,
	-- The name of the SMW property to find the names in
	localColumnProperty = nil,

	errors = {}
}

-- Shell object for an item in the list
local ListItem = {
	-- Link to card page. `nil` if not a specific card
	card = nil,
	-- Name of the card page, if applicable
	cardName = nil,
	localCardName = nil,
	-- Final text to display in the cell
	displayText = nil,
	cardType = nil,
	status = nil,
	-- If a card was on the list despite not being released while the list was in effect
	unreleased = false,
	afterDate = nil,
	prevStatus = nil,
	prevStatusNote = nil
}

local statusDescriptions = {
	['Forbidden']    = 'These cards cannot be used in the [[Main Deck]], [[Extra Deck]], or [[Side Deck]].',
	['Limited']      = 'A maximum of one copy of each of these cards can be included in the [[Main Deck]], [[Extra Deck]], and [[Side Deck]] combined.',
	['Semi-Limited'] = 'A maximum of two copies of each of these cards can be included in the [[Main Deck]], [[Extra Deck]], and [[Side Deck]] combined.',
	['Limited 1']    = 'A maximum of one out of all of the following cards can be included in the [[Main Deck]], [[Extra Deck]], [[Side Deck]], and choice of [[Skill Card]] combined.',
	['Limited 2']    = 'A maximum of two out of all of the following cards can be included in the [[Main Deck]], [[Extra Deck]], [[Side Deck]], and choice of [[Skill Card]] combined. This may be two copies of the same card or one copy one card and one copy of another.',
	['Limited 3']    = 'A maximum of three out of all of the following cards can be included in the [[Main Deck]], [[Extra Deck]], [[Side Deck]], and choice of [[Skill Card]] combined. This can include multiple copies of the same card, as long as the combined total from this list is three or fewer.',
	['Unlimited']    = 'Limitations have been removed from the following cards since the last list.'
}

-- Mediums that have only been released in Japanese
-- @todo: Come up with solution that doesn't involve hardcoding games in the module
local jaOnlyMediums = {
	'Yu-Gi-Oh! Duel Monsters (video game)',
	'Yu-Gi-Oh! Rush Duel',
}

-- Main function that gets invoked
-- Create a new list from the user input, then render it
function List.list(frame)
	-- Get the template parameters
	local args = frame:getParent().args

	local l = List:new(args)
	return l:render()
end

-- Create a new instance of a list
-- @param args table
-- @return List
function List:new(args)
	local l = mw.clone(List)

	l:setData(args or {})

	l:validateAfterParsing()

	return l
end

-- Main function for setting data for the list
-- @param args table - data from the user input
function List:setData(args)
	self.name = args.name or mw.title.getCurrentTitle().text
	self.medium = args.medium
	self.format = args.format
	self.locality = args.locality
	self.startDate = args.start_date
	self.endDate = args.end_date
	self.prev = args.prev
	self.next = args.next
	self:setLocalisationAttributes()

	self.items = {}
	self:addItems('Forbidden', args.forbidden)
	self:addItems('Limited', args.limited)
	self:addItems('Semi-Limited', args.semi_limited)
	self:addItems('Limited 1', args.limited_1)
	self:addItems('Limited 2', args.limited_2)
	self:addItems('Limited 3', args.limited_3)
	self:addItems('Unlimited', args.no_longer_on_list)

	self:setSmwData()
end

function List:setLocalisationAttributes()
	if (
		self.locality == 'Simplified Chinese' or
		self.locality == 'Traditional Chinese' or
		self.locality == 'Japanese' or
		self.locality == 'Korean'
	) then
		self.localColumnHeading = self.locality .. ' name'
		self.localColumnProperty = self.locality .. ' name'
		return
	end

	local isJaOnly = inArray(jaOnlyMediums, self.medium)
	local isOcg = self.medium == 'OCG' or self.medium == 'Yu-Gi-Oh! Official Card Game'

	if (isJaOnly or (isOcg and self.locality ~= 'Asian-English')) then
		self.localColumnHeading = 'Japanese name'
		self.localColumnProperty = 'Japanese name'
		return
	end

	self.localColumnHeading = nil
	self.localColumnProperty = nil
end

-- Add items based on user input
-- @param status string
-- @param listInput string
function List:addItems(status, listInput)
	-- Exit early if there is no input
	if not isFilled(listInput) then return end

	-- Split the input by new line and loop through each line
	local listItems = mw.text.split(listInput, '\n')

	for _, line in pairs(listItems) do
		-- Split the line input by `//` and get data from each piece
		local lineData = mw.text.split(line, '%s*//%s*')
		local itemText = lineData[1]
		local options = lineData[2]

		-- Extract data from content after the `//`
		local unreleased = options and options:match('unreleased::true') and true or false
		local afterDate = options and options:match('after::([^;]*)')
		local prevStatus = options and options:match('prev::([^;]*)')
		local prevStatusNote = options and options:match('prev%-note::([^;]*)')

		-- Create a new list item object
		local listItem = mw.clone(ListItem)

		listItem.status = status
		listItem.unreleased = unreleased
		listItem.afterDate = afterDate
		listItem.prevStatus = prevStatus
		listItem.prevStatusNote = prevStatusNote
		listItem.displayText = itemText

		-- Assume it's a card if there's no manual link syntax
		local isCard = not itemText:find('%[%[')

		-- If it's a card, look up the card's data and fill in more of the attributes
		if isCard then
			local cardData = self:lookupCardData(itemText)

			listItem.cardFound     = cardData and true or false
			-- The original name that was supplied
			listItem.card          = itemText
			-- The name that SMW found
			listItem.pageName      = cardData and cardData.pageName  or nil
			listItem.cardName      = cardData and cardData.name      or itemText
			listItem.localCardName = cardData and cardData.localName or ''
			listItem.cardType      = cardData and cardData.cardType
			listItem.displayText   = '"[[' .. listItem.card .. '|' .. listItem.cardName .. ']]"'
		end

		-- Add the card to the list
		table.insert(self.items, listItem)
	end
end

function List:setSmwData()
	mw.smw.set({
		['Effective date'] = self.startDate, -- deprecated
		['Start date']     = self.startDate,
		['End date']       = self.endDate,
		['Medium']         = self.medium,
		['Release']        = self.medium,
		['Format']         = self.format or self.locality or self.medium,
		['Locality']       = self.locality,
		['Page type']      = 'Status list'
	})

	-- Go through each list item to add a subobject
	for _, item in pairs(self.items) do
		-- If it's not a card, don't store any data
		if (not item.card) then return end

		mw.smw.subobject({
			'List contains = ' .. item.card,
			'Status        = ' .. item.status,
		})
	end
end

-- Find data on a card by suppyling its name
function List:lookupCardData(pageName)
	local queryParams = {
		'[[' .. pageName  .. ']]',
		'? = pageName#',
		'?English name = name',
		'?Card type#   = cardType'
	}

	-- If there is a column for another language
	-- also query for the card's name in that language
	if (self.localColumnProperty) then
		table.insert(queryParams, '?' .. self.localColumnProperty .. ' = localName')
	end

	local cardData = mw.smw.ask(queryParams)

	return cardData and cardData[1] or nil
end

-- Validate the data after it has been parsed
-- Checks for errors and adds them to the `errors` attribute
function List:validateAfterParsing()
	for i, item in pairs(self.items) do
		-- If not a card, go to the next iteration of the loop.
		-- (Any validation after this line is only applicable to cards.)
		if (not item.card) then break end

		-- Check if card data was found for the looked up name.
		if (item.cardFound == false) then
			table.insert(self.errors, 'Failed to look up details for card: <code>' .. item.card .. '</code>.')

			-- If the card wasn't found, move on to the next list item.
			-- No need to perform the next few checks.
			break
		end

		-- Check if a card is listed more than once
		for i2, item2 in pairs(self.items) do
			-- `i < i2`: Only look at records that come after this one,
			-- so the same error isn't reported again for the duplicate(s)
			if (i < i2 and item.pageName == item2.pageName) then
				table.insert(self.errors, '<code>' .. item.pageName .. '</code> is listed more than once.')
				
				-- Duplicate found, so no need to keep looking.
				break
			end
		end

		-- Check if the supplied name doesn't match the found page name
		if (item.card ~= item.pageName) then
			table.insert(self.errors, 'The supplied page name, <code>' .. item.card .. '</code>, does not match the looked-up page name, <code>' .. item.pageName .. '</code>.')
		end

		if (item.status == 'Unlimited' and not item.prevStatus) then
			table.insert(self.errors, '<code>' .. item.card .. '</code> has been removed from the list, but does not specify its previous status.')
		end
	end
end

-- Filter a list's `cards` attribute by a status
-- @return table
function List:getItemsByStatus(status)
	-- New array to contain list items with just the specified status
	local filteredItems = {}

	-- Loop through the list of all cards
	for _, item in pairs(self.items) do
		-- If the item has the specified status, add it to the new array
		if (item.status == status) then
			table.insert(filteredItems, item)
		end
	end

	return filteredItems
end

-- Render the overall output
-- @return string
function List:render()
	self:renderDisplayTitle()

	local output = ''
	output = output .. self:renderErrors()
	output = output .. self:renderNavigation()
	output = output .. self:renderStatusList('Forbidden')
	output = output .. self:renderStatusList('Limited')
	output = output .. self:renderStatusList('Semi-Limited')
	output = output .. self:renderStatusList('Limited 1')
	output = output .. self:renderStatusList('Limited 2')
	output = output .. self:renderStatusList('Limited 3')
	output = output .. self:renderStatusList('Unlimited', 'No longer on list')
	output = output .. self:renderCategories()

	return output
end

-- Format the page name, if necessary
function List:renderDisplayTitle()
	local italicTitle = nil
	local pageName    = mw.title.getCurrentTitle().text
	-- Medium without the word "Yu-Gi-Oh!"
	local mediumShort = mw.text.trim(replacePlain(self.medium, 'Yu-Gi-Oh!', ''), '%s')

	-- If the medium or a shortened version of it is mentioned in the page name italicize it
	if pageName:find(self.medium) then
		italicTitle = replacePlain(pageName, self.medium, '<i>' .. self.medium .. '</i>')
	elseif pageName:find(mediumShort) then
		italicTitle = replacePlain(pageName, mediumShort, '<i>' .. mediumShort .. '</i>')
	end

	-- Use the italicized name as the page name
	if italicTitle then
		mw.getCurrentFrame():callParserFunction('DISPLAYTITLE', italicTitle)
	end
end

function List:renderErrors()
	-- Exit early with empty string if there are no errors.
	if (#self.errors == 0) then return '' end;

	local output = '<div style="color: #d33;">'
	output = output .. '\nThis list contains the following issues:\n'

	for _, error in pairs(self.errors) do
		output = output .. '\n* ' ..  error .. ''
	end

	output = output .. '\n</div>'

	return output
end

-- Render the navigation section
-- @return string
function List:renderNavigation()
	-- Strip disambiguation text from the medium's page name
	local mediumName = mw.text.split(self.medium, ' %(')[1]

	local currText = (self.locality or '') .. ' <i>' .. mediumName .. '</i> ' .. (self.format or '') .. ' Forbidden & Limited Lists'
	local prevText = isFilled(self.prev) and ('← [[' .. self.prev .. '|' .. self:pageNameToShortName(self.prev) .. ']]') or '&nbsp;'
	local nextText = isFilled(self.next) and ('[[' .. self.next .. '|' .. self:pageNameToShortName(self.next) .. ']] →') or '&nbsp;'

	local output = '<div class="toccolours" style="clear: both; display: flex; margin-bottom: .5em;">'
	output = output .. '<div style="flex: 1; text-align: left;">' .. prevText .. '</div>'
	output = output .. '<div style="flex: 1; text-align: center;">' .. currText .. '</div>'
	output = output .. '<div style="flex: 1; text-align: right;">' .. nextText .. '</div>'
	output = output .. '</div>'

	if isFilled(self.startDate) and isFilled(self.endDate) then
		output = output .. '<p style="font-size: .9em; text-align: center;"><b>Effective</b>: '
		output = output .. self.startDate .. ' – ' ..  self.endDate
		output = output .. '</p>'
	end

	return output
end

-- Convert a page name to a short name
-- e.g. "March 2020 Lists (Duel Links)" -> "March 2020"
-- e.g. "OCG April 2014 Lists" -> "April 2014"
-- @param pageName string
-- @return string
function List:pageNameToShortName(pageName)
	-- Strip out disambiguation text
	local shortName = mw.text.split(pageName, ' %(')[1]

	-- Remove mention of the medium
	shortName = replacePlain(shortName, self.medium, '')

	-- Remove the word 'Lists'
	shortName = replacePlain(shortName, ' Lists', '')

	-- Clear any leading/trailing spaces
	return mw.text.trim(shortName)
end

function List:renderCategories()
	-- Don't render any categories if this is outside of the main namespace
	if (isFilled(mw.title.getCurrentTitle().nsText)) then return '' end

	local output = '[[Category:' .. (self.locality or '') .. ' ' .. (self.medium or '') .. ' ' .. (self.format or '') .. ' Forbidden & Limited Lists]]'

	if (#self.errors > 0) then
		output = output .. '[[Category:Pages with validation errors]]'
	end

	return output
end

-- Render a section with a list fora given status
-- @param status string
-- @param heading status - The text to appear in the heading, if different than the status
-- @return string
function List:renderStatusList(status, heading)
	-- Get all list items for the given status
	local items = self:getItemsByStatus(status)

	-- If there are no cards, return empty string and end early
	if (#items == 0) then return '' end

	local heading = mw.html.create('h2'):wikitext(heading or status)
	local list = mw.html.create('table'):addClass('wikitable sortable')

	local headingRow = list:tag('tr')
	headingRow:tag('th'):attr('scope', 'col'):wikitext('Card')
	if self.localColumnHeading then
		headingRow:tag('th'):attr('scope', 'col'):wikitext(self.localColumnHeading)
	end
	headingRow:tag('th'):attr('scope', 'col'):wikitext('Card type')
	headingRow:tag('th'):attr('scope', 'col'):wikitext('Status')
	headingRow:tag('th'):attr('scope', 'col'):wikitext('Changes')

	for _, item in pairs(items) do
		local cardTypeLink = item.cardType
			and ('[[' .. item.cardType .. '|' .. replacePlain(item.cardType, ' Card', '') .. ']]')
			or ''

		-- Form the CSS class that styles the cells in the status column
		local statusClass = 'status-' .. (item.status:gsub(' ', '-')):lower()

		local statusText = '[[' .. item.status .. ']]'
		
		if item.afterDate then
			statusText = statusText .. ' (after: ' .. item.afterDate .. ')';
		end

		-- If the card was unreleased, add an extra bit of text to explain that.
		if item.unreleased then
			local explainText = 'This card was not released' .. (self.locality and (' in ' .. self.locality) or '') .. ' while this list was in effect.'
			statusText = statusText .. ' (<span class="explain" title="' .. explainText .. '">Unreleased</span>)'
		end

		local row = list:tag('tr')
		row:tag('td'):wikitext(item.displayText)
		if self.localColumnHeading then
			row:tag('td'):wikitext(item.localCardName)
		end
		row:tag('td'):wikitext(cardTypeLink)
		row:tag('td'):addClass(statusClass):wikitext(statusText)
		row:tag('td'):wikitext(
			(item.prevStatus and 'was [[' .. item.prevStatus .. ']]' or '') ..
			(item.prevStatusNote or '')
		)
	end

	local description = statusDescriptions[status] or ''

	return tostring(heading) .. description .. tostring(list)
end

-- Check if something has been filled out
-- i.e. check that it's neither `nil` nor empty string
-- @param value mixed
-- @return bool
function isFilled(value)
	return value ~= nil and value ~= ''
end

-- Replace all occurances of a string with another
-- Unlike `string.gsub`, do not treat the searched term as a pattern.
--
-- @param source  str - The text to perform the replacement in
-- @param search  str - The text to search for 
-- @param replace str - The text to replace the searched text with
-- @return str
function replacePlain(source, search, replace)
	-- Escape any patterns in the search term
	search = mw.ustring.gsub(search, '([%(%)%.%%%+%-%*%?%[%^%$%]])', '%%%1')

	-- Replace all instances of the search term, with the replace term
	return mw.ustring.gsub(source, search, replace)
end

return List