Difference between revisions of "Module:Limitation status list"
(fix) |
(Changing order to match the F/L list header in each article) |
||
(42 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, | ||
− | + | 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 | ||
Line 67: | 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 79: | 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 | ||
− | + | 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 | ||
− | + | self.localColumnHeading = nil | |
− | + | self.localColumnProperty = nil | |
− | |||
− | |||
end | end | ||
Line 105: | 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 | + | local unreleased = options and options:match('unreleased::true') and true or false |
− | local options | + | 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 125: | Line 169: | ||
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 139: | 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 162: | 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 172: | 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 195: | 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 208: | Line 345: | ||
-- @return string | -- @return string | ||
function List:renderNavigation() | function List:renderNavigation() | ||
− | -- | + | -- Strip disambiguation text from the medium's page name |
− | + | local mediumName = mw.text.split(self.medium, ' %(')[1] | |
− | |||
− | |||
− | local currText = self | + | 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 ' ' | + | local prevText = isFilled(self.prev) and ('← [[' .. self.prev .. '|' .. self:pageNameToShortName(self.prev) .. ']]') or ' ' |
− | local nextText = self.next and ('[[' .. self. | + | local nextText = isFilled(self.next) and ('[[' .. self.next .. '|' .. self:pageNameToShortName(self.next) .. ']] →') or ' ' |
− | local output = '<div style=" | + | local output = '<div class="toccolours" style="clear: both; display: flex; margin-bottom: .5em;">' |
− | output = output .. '<div style="flex: 1 | + | output = output .. '<div style="flex: 1; text-align: left;">' .. prevText .. '</div>' |
− | output = output .. '<div style="flex: 1 | + | output = output .. '<div style="flex: 1; text-align: center;">' .. currText .. '</div>' |
− | output = output .. '<div style="flex: 1 | + | 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 227: | 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 | + | -- e.g. "March 2020 Lists (Duel Links)" -> "March 2020" |
− | -- e.g. "OCG April 2014 Lists" -> "April 2014 | + | -- e.g. "OCG April 2014 Lists" -> "April 2014" |
-- @param pageName string | -- @param pageName string | ||
-- @return string | -- @return string | ||
Line 234: | 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 | + | 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 257: | 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 .. '|' .. | + | 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') | ||
Line 279: | Line 448: | ||
end | end | ||
row:tag('td'):wikitext(cardTypeLink) | row:tag('td'):wikitext(cardTypeLink) | ||
− | row:tag('td'):addClass(statusClass):wikitext( | + | 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 292: | 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
Module:Limitation status list is implemented by Template:Limitation list
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 ' '
local nextText = isFilled(self.next) and ('[[' .. self.next .. '|' .. self:pageNameToShortName(self.next) .. ']] →') or ' '
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