Module:Track listing explained

local yesno = require('Module:Yesno')local checkType = require('libraryUtil').checkTypelocal 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 endend

---------------------------------------------------------------------------------- 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.warningsend

function Validation:getCategories return self.categoriesend

-- 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 nilend

---------------------------------------------------------------------------------- Track class--------------------------------------------------------------------------------

local Track = Track.__index = TrackaddMixin(Track, Validation)

Track.fields = cfg.track_field_names

Track.cellMethods =

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 selfend

function Track:getLyricsCredit return self.lyricsend

function Track:getMusicCredit return self.musicend

function Track:getWriterCredit return self.writerend

function Track:getExtraField return self.extraend

-- Note: called with single dot syntaxfunction 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 titleCellend

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 rowend

---------------------------------------------------------------------------------- TrackListing class--------------------------------------------------------------------------------

local TrackListing = TrackListing.__index = TrackListingaddMixin(TrackListing, Validation)TrackListing.fields = cfg.track_listing_field_namesTrackListing.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 = 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 selfend

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 endend

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 retend

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') :addClass('track-listing') local intro = self:makeIntro if intro then root:tag('p') :wikitext(intro) :done end -- Start of track listing table local tableRoot = mw.html.create('table') tableRoot :addClass('tracklist') -- Overall table width if self.width then tableRoot :css('width', self.width) end -- Header row if self.headline then tableRoot:tag('caption') :wikitext(self.headline or cfg.track_listing) end

-- Headers local headerRow = tableRoot:tag('tr')

---- Track number headerRow :tag('th') :addClass('tracklist-number-header') :attr('scope', 'col') :tag('abbr') :attr('title', cfg.number) :wikitext(cfg.number_abbr)

-- Find columns to output local columns = if self.optionalColumns.writer then columns[#columns + 1] = 'writer' else if self.optionalColumns.lyrics then columns[#columns + 1] = 'lyrics' end if self.optionalColumns.music then columns[#columns + 1] = 'music' end end if self.optionalColumns.extra then columns[#columns + 1] = 'extra' end columns[#columns + 1] = 'length' -- Find column width local nColumns = #columns local nOptionalColumns = nColumns - 3 local titleColumnWidth = 100 if nColumns >= 5 then titleColumnWidth = 40 elseif nColumns >= 4 then titleColumnWidth = 60 end local optionalColumnWidth = ((100 - titleColumnWidth) / nOptionalColumns) .. '%' titleColumnWidth = titleColumnWidth .. '%' ---- Title column headerRow:tag('th') :attr('scope', 'col') :css('width', self.title_width or titleColumnWidth) :wikitext(cfg.title)

---- Optional headers: writer, lyrics, music, and extra local function addOptionalHeader(field, headerText, width) if self.optionalColumns[field] then headerRow:tag('th') :attr('scope', 'col') :css('width', width or optionalColumnWidth) :wikitext(headerText) end end addOptionalHeader('writer', cfg.writer, self.writing_width) addOptionalHeader('lyrics', cfg.lyrics, self.lyrics_width) addOptionalHeader('music', cfg.music, self.music_width) addOptionalHeader('extra', self.extra_column or cfg.extra, self.extra_width )

---- Track length headerRow:tag('th') :addClass('tracklist-length-header') :attr('scope', 'col') :wikitext(cfg.length)

-- Tracks for i, track in ipairs(self.tracks) do tableRoot:node(track:exportRow(columns)) end

-- Total length if self.total_length then tableRoot :tag('tr') :addClass('tracklist-total-length') :tag('th') :attr('colspan', nColumns - 1) :attr('scope', 'row') :tag('span') :wikitext(cfg.total_length) :done :done :tag('td') :wikitext(self.total_length) end root:node(tableRoot) -- Warnings and tracking categories root:wikitext(self:renderWarnings) root:wikitext(self:renderTrackingCategories) return mw.getCurrentFrame:extensionTag .. tostring(root)end

---------------------------------------------------------------------------------- Exports--------------------------------------------------------------------------------

local p =

function p._main(args) -- Process numerical args so that we can iterate through them. local data, tracks =, for k, v in pairs(args) do if type(k)

'string' then local prefix, num = k:match('^(%D.-)(%d+)$') if prefix and Track.fields[prefix] and (num

'0' or num:sub(1, 1) ~= '0') then -- Allow numbers like 0, 1, 2 ..., but not 00, 01, 02..., -- 000, 001, 002... etc. num = tonumber(num) tracks[num] = tracks[num] or tracks[num][prefix] = v else data[k] = v end end end data.tracks = (function (t) -- Compress sparse array local ret = for num, trackData in pairs(t) do trackData.number = num table.insert(ret, trackData) end table.sort(ret, function (t1, t2) return t1.number < t2.number end) return ret end)(tracks)

return tostring(TrackListing.new(data))end

function p.main(frame) local args = require('Module:Arguments').getArgs(frame,) return p._main(args)end

return p