Module:Chapter
Module:Chapter is used for looking up and formatting information on manga chapters.
It is implemented by Template:Chapter and Template:Images by chapter category.
Table data[edit]
The module returns a table, referred to as a Chapter
table in this documentation. It has the following attributes:
- pageName
- The chapter's page name on this site
- seriesLink
- The chapter's series' page name on this site
- seriesName
- The name of the chapter's series
- subseries
- The name of the chapter's subseries (Yu-Gi-Oh! Duelist or Yu-Gi-Oh! Millennium World for relevant chapters of the original Yu-Gi-Oh!)
- arcLink
- The page name of the chapter's arc, if applicable
- arcName
- The name of the chapter's arc, if applicable
- number
- The chapter's number in the series
- subseriesNumber
- The chapter's number in the subseries, if applicable
- chapterWord
- The word used to describe chapters in the series e.g. "Duel" for Yu-Gi-Oh!, "Rank" for Yu-Gi-Oh! ZEXAL, etc. Special chapters may have their own chapter word.
- isSpecial
- If the chapter is a special chapter (one that doesn't follow the standard numbering)
- name
- The English name of the chapter
Table methods[edit]
- Chapter:new(args)
- Look up chapter information for the supplied arguments and return them as an instance of the
Chapter
table. - Arguments may be the series name and chapter number e.g.
{ series = 'Yu-Gi-Oh! GX', number = 5 }
or the page name e.g.{ page = 'Summon the Dark Ruler!!' }
- Chapter:formatAsName()
- Format the instance of the Chapter table as its English name. The name will link to the chapter's page and be enclosed in quotes.
- Chapter:formatAsNumber()
- Format the instance of the Chapter table as its chapter number, if it has one. The number will link to the chapter's page.
- Chapter:formatAsRef()
- Format the instance of the Chapter table as its name in reference format.
- Chapter:getImageCategoryName()
- Get the name that should be used for the category containing images for the chapter
- Chapter:formatImageCategoryNavLink
- Format the instance of the Chapter tale as a link to its image category as the link should be formatted in previous/next links in such a category's navigation menu
- Chapter:getPrev(prev)
- Get the previous chapter in the series as an instance of the
Chapter
table - If the
prev
param is unused, this will automatically get the previous numbered chapter in the series. - If
prev
is a number, this will look up details for the chapter in the same series of that number. - If
prev
is used and not a number, this will look up details for the chapter whose page name matches the value. - Chapter:getNext(next)
- Get the next chapter in the series as an instance of the
Chapter
table - If the
next
param is unused, this will automatically get the next numbered chapter in the series. - If
next
is a number, this will look up details for the chapter in the same series of that number. - If
next
is used and not a number, this will look up details for the chapter whose page name matches the value.
Template methods[edit]
These methods are meant to be invoked by templates whose parameters will be passed into them.
- Chapter.chapter(frame)
- Render a link to a chapter, with various formatting options. See Template:Chapter for more details.
- Chapter.imageCategory(frame)
- Render the page content for a category for images from a given chapter. See Template:Images by chapter category for more details.
local Chapter = {
pageName = nil,
seriesLink = nil,
seriesName = nil,
subseries = nil,
arcLink = nil,
arcName = nil,
number = nil,
subseriesNumber = nil,
chapterWord = nil,
isSpecial = false,
name = nil,
pageNumber = nil
}
-- Check if Semantic MediaWiki is enabled/operational
-- (The module should have some fallbacks prepared if it isn't.)
local smwEnabled = mw.smw and true or false
-- Chapters in the overall numbering for the original Yu-Gi-Oh! series that
-- are considered to be Yu-Gi-Oh! Duelist and Yu-Gi-Oh! Millennium World
-- (Each table contains the first and last chapter number in the subseries.)
local DUELIST_RANGE = { 60, 278 }
local MW_RANGE = { 279, 343 }
local seriesData = {
['Yu-Gi-Oh!'] = { link = 'Yu-Gi-Oh! (manga)', chapterWord = 'Duel' },
['Yu-Gi-Oh! R'] = { link = 'Yu-Gi-Oh! R', chapterWord = 'Duel Round' },
['Yu-Gi-Oh! GX'] = { link = 'Yu-Gi-Oh! GX (manga)', chapterWord = 'chapter' },
['Yu-Gi-Oh! 5D\'s'] = { link = 'Yu-Gi-Oh! 5D\'s (manga)', chapterWord = 'Ride' },
['Yu-Gi-Oh! ZEXAL'] = { link = 'Yu-Gi-Oh! ZEXAL (manga)', chapterWord = 'Rank' },
['Yu-Gi-Oh! D Team ZEXAL'] = { link = 'Yu-Gi-Oh! D-Team ZEXAL', chapterWord = 'chapter' },
['Yu-Gi-Oh! ARC-V'] = { link = 'Yu-Gi-Oh! ARC-V (manga)', chapterWord = 'Scale' },
['Yu-Gi-Oh! ARC-V The Strongest Duelist Yuya!!'] = { link = 'Yu-Gi-Oh! ARC-V The Strongest Duelist Yuya!!', chapterWord = 'chapter' },
['Yu-Gi-Oh! SEVENS Luke! Explosive Supremacy Legend!!'] = { link = 'Yu-Gi-Oh! SEVENS Luke! Explosive Supremacy Legend!!', chapterWord = 'Supremacy' },
['Yu-Gi-Oh! Rush Duel LP'] = { link = 'Yu-Gi-Oh! Rush Duel LP', chapterWord = 'Broadcast' },
['Yu-Gi-Oh! OCG Structures'] = { link = 'Yu-Gi-Oh! OCG Structures', chapterWord = 'chapter' },
['Nururin Charisma! GO! GO! Gokibore!!'] = { link = 'Nururin Charisma! GO! GO! Gokibore!!', chapterWord = 'chapter' },
['Yu-Gi-Oh! GO RUSH!!'] = { link = 'Yu-Gi-Oh! GO RUSH!! (manga)', chapterWord = 'Rush' },
['Yu-Gi-Oh! OCG Stories'] = { link = 'Yu-Gi-Oh! OCG Stories', chapterWord = 'chapter', perArcNumbering = true },
}
-- Capitalize the first letter in a string
-- Might move to more dedicated module
-- e.g. `ucfirst('abc')` returns "Abc"
-- @param text string
-- @return string
local function ucfirst(text)
return text:sub(1,1):upper() .. text:sub(2)
end
-- Create a new instance of a chapter object
-- @param args table (include either [`series` and `number`] OR [`page_name`] to look up the chapter)
-- @return Chapter
function Chapter:new(args)
args = args or {}
-- Create a new instance of a chapter object
local c = mw.clone(Chapter)
c.pageNumber = args['page']
-- If SMW is down, fail somewhat gracefully
if (not smwEnabled) then
c.pageName = c.predictPageName(args)
c.seriesName = args['series']
c.seriesLink = seriesData[args['series']]
and seriesData[args['series']].link
or args['series']
c.arcLink = args['arc'] .. ' (arc)'
c.arcName = args['arc']
c.number = args['number']
c.isSpecial = not args['number']
return c
end
-- Lookup information on the chapter
local chapterData = self.lookup(args)
-- If nothing was found, return the empty object
if (not chapterData.pageName) then
return c
end
-- Get data on the series
local seriesData = seriesData[chapterData.series] or {}
-- Fill in the chapter data
c.pageName = chapterData.pageName
c.name = chapterData.name
c.seriesName = chapterData.series
c.seriesLink = seriesData.link or chapterData.series
c.arcLink = chapterData.arcLink or args.arc
c.arcName = chapterData.arcName or args.arc
c.number = chapterData.number
c.isSpecial = not chapterData.number
if (chapterData.chapterWord) then
-- If the chapter has its own specific chapter word, use that
c.chapterWord = chapterData.chapterWord
elseif (not c.isSpecial) then
-- For non-special chapters use the series' chapter word
-- If there is none, default to 'Chapter'
c.chapterWord = seriesData.chapterWord or 'Chapter'
end
c:setSubseriesData()
return c
end
-- Look up data for a chapter
-- @param args table (include either [`series` and `number`] OR [`page`] to look up the chapter)
-- @return table
function Chapter.lookup(args)
local query = ''
if (args['page_name']) then
query = query .. '[[' .. args['page_name'] .. ']]'
end
if (args['series']) then
query = query .. '[[Chapter series::' .. args['series'] .. ']]'
end
if (args['arc']) then
query = query .. '[[Arc.English name::' .. args['arc'] .. ']]'
end
if (args['number']) then
query = query .. '[[Chapter number::' .. args['number'] .. ']]'
end
local result = mw.smw.ask{
query,
'? = pageName#',
'?Chapter series = series',
'?Arc# = arcLink',
'?Arc.English name = arcName',
'?Chapter number = number',
'?Chapter word# = chapterWord',
'?English name = name',
'?Release date# = releaseDate',
'?Release date = releaseDateFormatted',
}
-- Get the first result. If nothing is found, use an empty table.
result = result and result[1] or {}
-- If the page name was passed into the template, make sure it gets used
-- So if it's a redirect, it gets tracked in Special:WhatLinksHere
if (args['page_name']) then
result.pageName = args['page_name']
end
return result
end
-- Fill in the Yu-Gi-Oh! Duelist or Yu-Gi-Oh! Millennium World information
-- if applicable for Yu-Gi-Oh! chapters
function Chapter:setSubseriesData()
-- Exit early if the series isn't Yu-Gi-Oh!
if self.seriesName ~= 'Yu-Gi-Oh!' then return end
-- If the chapter is in the Duelist range
-- Set the subseries to "Yu-Gi-Oh! Duelist" and calculate its number
if (self.number and self.number >= DUELIST_RANGE[1] and self.number <= DUELIST_RANGE[2]) then
self.subseries = 'Yu-Gi-Oh! Duelist'
self.subseriesNumber = self.number - DUELIST_RANGE[1] + 1
end
-- If the chapter is in the Millennium World range
-- Set the subseries to "Yu-Gi-Oh! Millennium World" and calculate its number
if (self.number and self.number >= MW_RANGE[1] and self.number <= MW_RANGE[2]) then
self.subseries = 'Yu-Gi-Oh! Millennium World'
self.subseriesNumber = self.number - MW_RANGE[1] + 1
end
end
-- Trim whitespace from each string in a table
-- And remove elements that only contain whitespace
--
-- @param table args
-- @return table
local function cleanWhitespace(args)
-- New table for the updated params
-- Trying to update the existing one causes unexpected behaviour when trying
-- to remove a blank value.
local cleaned = {}
-- Go through old table to populate the new table
for param, value in pairs(args) do
-- Clean whitespace if it's a string
if (type(value) == 'string') then
value = mw.text.trim(value)
end
-- Only add to new table if not blank
if (value and value ~= '') then
cleaned[param] = value
end
end
return cleaned
end
-- Convert unnamed args to named args, unless superseded by a named arg
-- @param table args
-- @return table
local function unnamedToNamedArgs(args)
-- If the second unnamed param is 'ref'...
if (args[2] == 'ref') then
args['page_name'] = args['page_name'] or args[1]
args['format'] = args['format'] or args[2]
-- If the second argument is defined and is not numeric
elseif (not tonumber(args[2])) then
args['series'] = args['series'] or args[1]
args['arc'] = args['arc'] or args[2]
args['number'] = args['number'] or args[3]
args['format'] = args['format'] or args[4]
-- Otherwise
else
args['series'] = args['series'] or args[1]
args['number'] = args['number'] or args[2]
args['format'] = args['format'] or args[3]
end
return args
end
-- Convert a shortened series name to a full name
-- e.g. "GX" -> "Yu-Gi-Oh! GX"
-- @param string series
-- @return string
local function toFullSeriesName(series)
-- If the series does not exist unless a 'Yu-Gi-Oh!' prefix is added, add the prefix
if (series and not seriesData[series] and seriesData['Yu-Gi-Oh! ' .. series]) then
series = 'Yu-Gi-Oh! ' .. series
end
return series
end
-- If the args used 'Yu-Gi-Oh! Duelist' / 'Yu-Gi-Oh! Millennium World' as the series
-- Convert the series to 'Yu-Gi-Oh!' and update the number
-- @param table args
-- @return table
local function convertSubseriesArgs(args)
-- If the supplied series is 'Yu-Gi-Oh! Duelist',
-- change to 'Yu-Gi-Oh!' and use overall series numbering
if (args.series and (args.series == 'Yu-Gi-Oh! Duelist' or args.series == 'Duelist')) then
args.series = 'Yu-Gi-Oh!'
-- Get the overall number by adding the offset to the Duelist number.
args.number = args.number and (args.number + DUELIST_RANGE[1] - 1) or nil
end
-- If the supplied series is 'Yu-Gi-Oh! Millennium World',
-- change to 'Yu-Gi-Oh!' and use overall series numbering
if (args.series and (args.series == 'Yu-Gi-Oh! Millennium World' or args.series == 'Millennium World')) then
args.series = 'Yu-Gi-Oh!'
-- Get the overall number by adding the offset to the Millennium World number.
args.number = args.number and (args.number + MW_RANGE[1] - 1) or nil
end
return args
end
-- Normalize input
-- @param table args
-- @return args
function normalizeArgs(args)
-- Perform the various modifications to the input params
args = cleanWhitespace(args)
args = unnamedToNamedArgs(args)
args.series = toFullSeriesName(args.series)
args = convertSubseriesArgs(args)
-- If the format is 'numbers', change it to 'number' (singular)
-- @todo: track and update pages using 'numbers' and drop support for it.
if (args.format == 'numbers') then
args.format = 'number'
end
return args
end
-- Check if the input is okay
-- @param args table - The input from the template parameters
-- @return table - An array of error messages
function validateArgs(args)
args = args or {}
local errors = {}
if (not args['page_name'] and (not args['series'] or not args['number'])) then
table.insert(errors, 'Either a page name or both a series and number must be specified.')
end
if (args['number'] and tonumber(args['number']) == nil) then
table.insert(errors, 'Number (<code>' .. args['number'] .. '</code>) must be numeric.')
end
if (args['format'] and args['format'] ~= 'ref' and args['format'] ~= 'number') then
table.insert(errors, 'Format (<code>' .. args['format'] .. '</code>) was not recognized.')
end
local hasPerArcNumbering = seriesData[args['series']] and seriesData[args['series']].perArcNumbering
if (hasPerArcNumbering and not args['arc']) then
table.insert(errors, 'An arc must be specified if the series is ' .. args['series'] .. '.')
end
return errors
end
-- Check if the series resets numbering each arc
-- @return boolean
function Chapter:hasPerArcNumbering()
return seriesData[self.seriesName] and seriesData[self.seriesName].perArcNumbering
end
-- Render the chapter as its name, linked and quoted
-- @return string
function Chapter:formatAsName()
return '"[[' .. self.pageName .. '|' .. (self.name or self.pageName) .. ']]"'
end
-- Render the chapter as its number, linked
-- @return string
function Chapter:formatAsNumber()
-- Get the number zero-padded
-- Or the page name if there is no number
local numberText = self.number and zeroPad(self.number) or self.pageName
-- If there is a subseries, put an abbreviation and its number in parentheses.
if (self.subseries and self.subseriesNumber) then
local abbreviations = {
['Yu-Gi-Oh! Duelist'] = 'D',
['Yu-Gi-Oh! Millennium World'] = 'MW'
}
numberText = numberText .. ' (<i>' .. abbreviations[self.subseries] .. '</i> ' .. zeroPad(self.subseriesNumber) .. ')'
end
return '[[' .. self.pageName .. '|' .. numberText .. ']]'
end
-- Render the chapter in reference format
-- @return string
function Chapter:formatAsRef()
local args = args or {}
local output = ''
if (self.seriesName) then
output = output .. '<i>' .. self.seriesName .. '</i>'
end
if (self:hasPerArcNumbering() and self.arcName) then
output = output .. ' ' .. self.arcName .. ' arc'
end
if (self.chapterWord) then
output = output .. ' ' .. self.chapterWord
end
if self.number then
output = output .. ' ' .. self.number
end
if self.subseries then
-- Put the subseries name and number in parentheses.
-- Remove the word "Yu-Gi-Oh!" from the subseries name.
output = output .. ' (<i>' .. (self.subseries:gsub('Yu%-Gi%-Oh! ', '')) .. '</i> '
output = output .. self.chapterWord .. ' ' .. self.subseriesNumber .. ')'
end
if self.chapterWord then
output = output .. ':'
end
output = output .. ' ' .. self:formatAsName()
if (self.pageNumber) then
output = output .. '; page ' .. self.pageNumber
end
return mw.text.trim(output)
end
-- Predict what the page name should be
-- (Needed if Semantic MediaWiki is down.)
-- @param args table - the normalized template parameters
-- @return string
function Chapter.predictPageName(args)
-- If the page name is supplied, use that
if (args['page_name']) then
return args['page_name']
end
-- If the series and number are specified, use them to form a page name
if (args['series'] and args['number']) then
local seriesData = seriesData[args['series']] or {}
-- Get a string in the format '{$series} - {$chapterWord} {$number}'
-- Where `$number` is zero-padded to three digits.
return args['series'] .. ' - '
.. (seriesData.chapterWord or 'Chapter') .. ' '
.. zeroPad(args['number'])
end
return nil
end
-- Get the name of the category containing images for this chapter
-- @return string
function Chapter:getImageCategoryName()
local output = ''
local showArcName = self:hasPerArcNumbering() and self.arcName
if (self.seriesName) then output = output .. self.seriesName end
if (showArcName) then output = output .. ' ' .. self.arcName .. ' arc' end
if (self.chapterWord) then output = output .. ' ' .. self.chapterWord end
if (self.number) then output = output .. ' ' .. self.number end
output = output .. ' images'
return output
end
-- Get the link to the image category as it is formatted in a navigation menu
-- @return string
function Chapter:formatImageCategoryNavLink()
local output = ''
-- Add the chapter word and number if applicable to the output.
if (self.chapterWord) then output = output .. ucfirst(self.chapterWord) end
if (self.number) then output = output .. ' ' .. self.number end
-- Add a colon if there is anything in the output so far.
output = output and (output .. ': ')
-- Add the chapter name, linked to the category to the output.
output = output .. '"[[:Category:' .. self:getImageCategoryName() .. '|' .. self.name .. ']]"'
return output
end
-- Get the previous chapter
-- @param prev {string|number|nil}
-- The page name or chapter number of the previous chapter
-- Leave blank to automatically find the previous chapter
-- @return Chapter
function Chapter:getPrev(prev)
-- If the first chapter, there is no previous one
if self.number == 1 then return {} end
-- If a non-numeric `prev` is supplied, find the chapter based on that page name.
if (prev and not tonumber(prev)) then
return Chapter:new({ page = prev })
end
-- If a numeric `prev` is supplied, find the chapter based on that number.
if (prev and tonumber(prev)) then
return Chapter:new({ series = self.seriesName, number = tonumber(prev) })
end
-- Automatically find the previous chapter, by subtracting 1 from the current number
if (self.number) then
return Chapter:new({ series = self.seriesName, number = self.number - 1 })
end
return nil
end
-- Get the next chapter
-- @param next {string|number|nil}
-- The page name or chapter number of the next chapter
-- Leave blank to automatically find the next chapter
-- @return Chapter
function Chapter:getNext(next)
-- If a non-numeric `next` is supplied, find the chapter based on that page name.
if (next and not tonumber(next)) then
return Chapter:new({ page = next })
end
-- If a numeric `next` is supplied, find the chapter based on that number.
if (next and tonumber(next)) then
return Chapter:new({ series = self.seriesName, arc = self.arcName, number = tonumber(next) })
end
-- Find the next chapter, by adding 1 to the current number
if (self.number) then
return Chapter:new({ series = self.seriesName, arc = self.arcName, number = self.number + 1 })
end
return nil
end
-- Function callable by templates for getting a chapter, formatted in various ways
-- @param frame
-- @return string
function Chapter.chapter(frame)
-- Get the template parameters and format them
local args = normalizeArgs(frame:getParent().args)
-- Show error if there is a problem with the input.
local errors = validateArgs(args)
if (#errors > 0) then
return renderErrors('chapter', errors, args)
end
-- Find the chapter data
local c = Chapter:new(args)
-- Show error if the chapter was not found.
if (not c.pageName) then
return renderErrors('chapter', { 'chapter not found' }, args)
end
-- Return the output
if (args['format'] and args['format'] == 'ref') then return c:formatAsRef() end
if (args['format'] and args['format'] == 'number') then return c:formatAsNumber() end
return c:formatAsName()
end
-- Remove HTML tags from a string
-- e.g. '<code>X</code>' -> 'X'
-- @param text string
-- @return string
function stripHtmlTags(text)
return text:gsub('%b<>', '')
end
function zeroPad(number)
return ('%03d'):format(number)
end
function renderErrors(templateName, errors, data)
-- Join the array into a string.
-- Strip out tags which won't work inside the `title` attribute
local errorsString = stripHtmlTags(table.concat(errors, ' '))
local message = mw.html.create('span')
message
:css('color', '#d33')
:node('Error rendering <code>{{' .. templateName .. '}}</code> ')
:tag('abbr')
:attr('title', errorsString)
:node('🛈')
local output = tostring(message)
if (mw.title.getCurrentTitle().nsText ~= 'Template') then
output = output ..'[[Category:Pages with validation errors]]'
end
-- Put errors and other debugging data in the Lua log
-- To access, preview this module on a page with an error
-- then expand "Parser profiling data:" -> "Lua logs"
mw.logObject(errors, 'errors')
mw.logObject(data, 'data')
mw.log('\n\n')
return output
end
-- Function callable by templates for getting a chapter and rendering the
-- wikitext for an images category for the chapter
--
-- @param frame
-- @return string
function Chapter.imageCategory(frame)
-- Get the template parameters and normarize them
local args = normalizeArgs(frame:getParent().args)
-- Look up the current, previous and next chapters
local curr = Chapter:new(args)
if (not curr or not curr.pageName) then
return '<div style="color: #d33;">Could not find chapter</div>' ..
'[[Category:Pages with validation errors]]'
end
local prev = curr:getPrev(args['prev'])
local next = curr:getNext(args['next'])
-- Intro text
local output = 'This category is for images from ' .. curr:formatAsRef() .. '.'
-- Navigation
output = output .. '<div class="toccolours" style="clear: both; display: flex; margin-bottom: .5em;">'
if (prev and prev.pageName) then
local prevText = prev:formatImageCategoryNavLink()
output = output .. '<div style="flex: 1; text-align: left;">←' .. prevText .. '</div>'
end
if (next and next.pageName) then
local nextText = next:formatImageCategoryNavLink()
output = output .. '<div style="flex: 1; text-align: right;">' .. nextText .. '→</div>'
end
output = output .. '</div>'
-- Category
output = output .. '[[Category:' .. curr.seriesName .. ' images by chapter|' .. (curr.number or curr.chapterWord) .. ']]'
return output
end
return Chapter