local yesno = require('Module:Yesno')
local checkType = require('libraryUtil').checkType
local cfg = mw.loadData('Module:Track listing/configuration')  
---
-- Helper functions
---  
-- Add a mixin to a class.
local function addMixin(class, mixin)
for k, v in pairs(mixin) do
if k ~= 'init' then
class[k] = v
end
end
end  
---
-- Validation mixin
---  
local Validation = {}  
function Validation.init(self)
self.warnings = {}
self.categories = {}
end  
function Validation:addWarning(msg, category)
table.insert(self.warnings, msg)
table.insert(self.categories, category)
end  
function Validation:addCategory(category)
table.insert(self.categories, category)
end  
function Validation:getWarnings
return self.warnings
end  
function Validation:getCategories
return self.categories
end  
-- Validate a track length. If a track length is invalid, a warning is added.
-- A type error is raised if the length is not of type string or nil.
function Validation:validateLength(length)
checkType('validateLength', 1, length, 'string', true)
if length == nil then
-- Do nothing if no length specified
return nil
end  
local hours, minutes, seconds  
-- Try to match times like "1:23:45".
hours, minutes, seconds = length:match('^(%d+):(%d%d):(%d%d)$')
if hours and hours:sub(1, 1) == '0' then
-- Disallow times like "0:12:34"
self:addWarning(
string.format(cfg.leading_0_in_hours, mw.text.nowiki(length)),
cfg.input_error_category
return nil
end  
if not seconds then
-- The previous attempt didn't match. Try to match times like "1:23".
minutes, seconds = length:match('^(%d?%d):(%d%d)$')
if minutes and minutes:find('^0%d$') then
-- Special case to disallow lengths like "01:23". This check has to
-- be here so that lengths like "1:01:23" are still allowed.
self:addWarning(
string.format(cfg.leading_0_in_minutes, mw.text.nowiki(length)),
cfg.input_error_category
return nil
end
end  
-- Add a warning and return if we did not find a match.
if not seconds then
self:addWarning(
string.format(cfg.not_a_time, mw.text.nowiki(length)),
cfg.input_error_category
return nil
end  
-- Check that the minutes are less than 60 if we have an hours field.
if hours and tonumber(minutes) >= 60 then
self:addWarning(
string.format(cfg.more_than_60_minutes, mw.text.nowiki(length)),
cfg.input_error_category
return nil
end  
-- Check that the seconds are less than 60
if tonumber(seconds) >= 60 then
self:addWarning(
string.format(cfg.more_than_60_seconds, mw.text.nowiki(length)),
cfg.input_error_category
end  
return nil
end  
---
-- Track class
---  
local Track = {}
Track.__index = Track
addMixin(Track, Validation)  
Track.fields = cfg.track_field_names  
Track.cellMethods = {
number = 'makeNumberCell',
title = 'makeTitleCell',
writer = 'makeWriterCell',
lyrics = 'makeLyricsCell',
music = 'makeMusicCell',
extra = 'makeExtraCell',
length = 'makeLengthCell',
}  
function Track.new(data)
local self = setmetatable({}, Track)
Validation.init(self)
for field in pairs(Track.fields) do
self[field] = data[field]
end
self.number = assert(tonumber(self.number))
self:validateLength(self.length)
return self
end  
function Track:getLyricsCredit
return self.lyrics
end  
function Track:getMusicCredit
return self.music
end  
function Track:getWriterCredit
return self.writer
end  
function Track:getExtraField
return self.extra
end  
-- Note: called with single dot syntax
function Track.makeSimpleCell(wikitext)
return mw.html.create('td')
:wikitext(wikitext or cfg.blank_cell)
end  
function Track:makeNumberCell
return mw.html.create('th')
:attr('id', string.format(cfg.track_id, self.number))
:attr('scope', 'row')
:wikitext(string.format(cfg.number_terminated, self.number))
end  
function Track:makeTitleCell
local titleCell = mw.html.create('td')
titleCell:wikitext(
self.title and string.format(cfg.track_title, self.title) or cfg.untitled
if self.note then
titleCell:wikitext(string.format(cfg.note, self.note))
end
return titleCell
end  
function Track:makeWriterCell
return Track.makeSimpleCell(self.writer)
end  
function Track:makeLyricsCell
return Track.makeSimpleCell(self.lyrics)
end  
function Track:makeMusicCell
return Track.makeSimpleCell(self.music)
end  
function Track:makeExtraCell
return Track.makeSimpleCell(self.extra)
end  
function Track:makeLengthCell
return mw.html.create('td')
:addClass('tracklist-length')
:wikitext(self.length or cfg.blank_cell)
end  
function Track:exportRow(columns)
local columns = columns or {}
local row = mw.html.create('tr')
for i, column in ipairs(columns) do
local method = Track.cellMethods[column]
if method then
row:node(self[method](self))
end
end
return row
end  
---
-- TrackListing class
---  
local TrackListing = {}
TrackListing.__index = TrackListing
addMixin(TrackListing, Validation)
TrackListing.fields = cfg.track_listing_field_names
TrackListing.deprecatedFields = cfg.deprecated_track_listing_field_names  
function TrackListing.new(data)
local self = setmetatable({}, TrackListing)
Validation.init(self)  
-- Check for deprecated arguments
for deprecatedField in pairs(TrackListing.deprecatedFields) do
if data[deprecatedField] then
self:addCategory(cfg.deprecated_parameter_category)
break
end
end  
-- Validate total length
if data.total_length then
self:validateLength(data.total_length)
end  
-- Add properties
for field in pairs(TrackListing.fields) do
self[field] = data[field]
end  
-- Evaluate boolean properties
self.showCategories = yesno(self.category) ~= false
self.category = nil  
-- Make track objects
self.tracks = {}
for i, trackData in ipairs(data.tracks or {}) do
table.insert(self.tracks, Track.new(trackData))
end  
-- Find which of the optional columns we have.
-- We could just check every column for every track object, but that would
-- be no fun^H^H^H^H^H^H inefficient, so we use four different strategies
-- to try and check only as many columns and track objects as necessary.
do
local optionalColumns = {}
local columnMethods = {
lyrics = 'getLyricsCredit',
music = 'getMusicCredit',
writer = 'getWriterCredit',
extra = 'getExtraField',
}
local doneWriterCheck = false
for i, trackObj in ipairs(self.tracks) do
for column, method in pairs(columnMethods) do
if trackObj[method](trackObj) then
optionalColumns[column] = true
columnMethods[column] = nil
end
end
if not doneWriterCheck and optionalColumns.writer then
doneWriterCheck = true
optionalColumns.lyrics = nil
optionalColumns.music = nil
columnMethods.lyrics = nil
columnMethods.music = nil
end
if not next(columnMethods) then
break
end
end
self.optionalColumns = optionalColumns
end  
return self
end  
function TrackListing:makeIntro
if self.all_writing then
return string.format(cfg.tracks_written, self.all_writing)
elseif self.all_lyrics and self.all_music then
return mw.message.newRawMessage(
cfg.lyrics_written_music_composed,
self.all_lyrics,
self.all_music
):plain
elseif self.all_lyrics then
return string.format(cfg.lyrics_written, self.all_lyrics)
elseif self.all_music then
return string.format(cfg.music_composed, self.all_music)
else
return nil
end
end  
function TrackListing:renderTrackingCategories
if not self.showCategories or mw.title.getCurrentTitle.namespace ~= 0 then
return ''
end  
local ret = ''  
local function addCategory(cat)
ret = ret .. string.format('', cat)
end  
for i, category in ipairs(self:getCategories) do
addCategory(category)
end  
for i, track in ipairs(self.tracks) do
for j, category in ipairs(track:getCategories) do
addCategory(category)
end
end  
return ret
end  
function TrackListing:renderWarnings
if not cfg.show_warnings then
return ''
end  
local ret = {}  
local function addWarning(msg)
table.insert(ret, string.format(cfg.track_listing_error, msg))
end  
for i, warning in ipairs(self:getWarnings) do
addWarning(warning)
end  
for i, track in ipairs(self.tracks) do
for j, warning in ipairs(track:getWarnings) do
addWarning(warning)
end
end  
return table.concat(ret, '
')
end  
function TrackListing:__tostring
-- Root of the output
local root = mw.html.create('div')