Module:Track listing: Difference between revisions
Content deleted Content added
Johnrdorazio (talk | contribs) m 1 revision imported |
Johnrdorazio (talk | contribs) m 1 revision imported |
||
(One intermediate revision by one other user not shown) | |||
Line 1: | Line 1: | ||
-- This module implements [[Template:Track listing]] |
|||
local yesno = require('Module:Yesno') |
local yesno = require('Module:Yesno') |
||
local checkType = require('libraryUtil').checkType |
local checkType = require('libraryUtil').checkType |
||
local cfg = mw.loadData('Module:Track listing/configuration') |
|||
local SHOW_WARNINGS = false |
|||
local INPUT_ERROR_CATEGORY = 'Track listings with input errors' |
|||
local COLLAPSED_PARAMETER_CATEGORY = 'Track listings that use the collapsed parameter ' |
|||
-------------------------------------------------------------------------------- |
-------------------------------------------------------------------------------- |
||
Line 64: | Line 59: | ||
if hours and hours:sub(1, 1) == '0' then |
if hours and hours:sub(1, 1) == '0' then |
||
-- Disallow times like "0:12:34" |
-- Disallow times like "0:12:34" |
||
self:addWarning |
self:addWarning( |
||
string.format(cfg.leading_0_in_hours, mw.text.nowiki(length)), |
|||
"Invalid time '%s' (times in format 'h:mm:ss' cannot start with zero)", |
|||
cfg.input_error_category |
|||
mw.text.nowiki(length) |
|||
) |
|||
), INPUT_ERROR_CATEGORY) |
|||
return nil |
return nil |
||
end |
end |
||
Line 77: | Line 72: | ||
-- Special case to disallow lengths like "01:23". This check has to |
-- Special case to disallow lengths like "01:23". This check has to |
||
-- be here so that lengths like "1:01:23" are still allowed. |
-- be here so that lengths like "1:01:23" are still allowed. |
||
self:addWarning |
self:addWarning( |
||
string.format(cfg.leading_0_in_minutes, mw.text.nowiki(length)), |
|||
"Invalid time '%s' (times in format 'mm:ss' cannot start with zero)", |
|||
cfg.input_error_category |
|||
mw.text.nowiki(length) |
|||
) |
|||
), INPUT_ERROR_CATEGORY) |
|||
return nil |
return nil |
||
end |
end |
||
Line 87: | Line 82: | ||
-- Add a warning and return if we did not find a match. |
-- Add a warning and return if we did not find a match. |
||
if not seconds then |
if not seconds then |
||
self:addWarning |
self:addWarning( |
||
string.format(cfg.not_a_time, mw.text.nowiki(length)), |
|||
"Invalid time '%s' (times must be in a format of 'm:ss', 'mm:ss' or 'h:mm:ss')", |
|||
cfg.input_error_category |
|||
mw.text.nowiki(length) |
|||
) |
|||
), INPUT_ERROR_CATEGORY) |
|||
return nil |
return nil |
||
end |
end |
||
Line 96: | Line 91: | ||
-- Check that the minutes are less than 60 if we have an hours field. |
-- Check that the minutes are less than 60 if we have an hours field. |
||
if hours and tonumber(minutes) >= 60 then |
if hours and tonumber(minutes) >= 60 then |
||
self:addWarning |
self:addWarning( |
||
string.format(cfg.more_than_60_minutes, mw.text.nowiki(length)), |
|||
"Invalid track length '%s' (if hours are specified, the number of minutes must be less than 60)", |
|||
cfg.input_error_category |
|||
mw.text.nowiki(length) |
|||
) |
|||
), INPUT_ERROR_CATEGORY) |
|||
return nil |
return nil |
||
end |
end |
||
Line 105: | Line 100: | ||
-- Check that the seconds are less than 60 |
-- Check that the seconds are less than 60 |
||
if tonumber(seconds) >= 60 then |
if tonumber(seconds) >= 60 then |
||
self:addWarning |
self:addWarning( |
||
string.format(cfg.more_than_60_seconds, mw.text.nowiki(length)), |
|||
"Invalid track length '%s' (number of seconds must be less than 60)", |
|||
cfg.input_error_category |
|||
mw.text.nowiki(length) |
|||
) |
|||
), INPUT_ERROR_CATEGORY) |
|||
end |
end |
||
Line 122: | Line 117: | ||
addMixin(Track, Validation) |
addMixin(Track, Validation) |
||
Track.fields = |
Track.fields = cfg.track_field_names |
||
number = true, |
|||
title = true, |
|||
note = true, |
|||
length = true, |
|||
lyrics = true, |
|||
music = true, |
|||
writer = true, |
|||
extra = true, |
|||
} |
|||
Track.cellMethods = { |
Track.cellMethods = { |
||
Line 173: | Line 159: | ||
function Track.makeSimpleCell(wikitext) |
function Track.makeSimpleCell(wikitext) |
||
return mw.html.create('td') |
return mw.html.create('td') |
||
:wikitext(wikitext or cfg.blank_cell) |
|||
:css('vertical-align', 'top') |
|||
:wikitext(wikitext or ' ') |
|||
end |
end |
||
function Track:makeNumberCell() |
function Track:makeNumberCell() |
||
return mw.html.create(' |
return mw.html.create('th') |
||
:attr('id', string.format(cfg.track_id, self.number)) |
|||
:css('padding-right', '10px') |
|||
: |
:attr('scope', 'row') |
||
:wikitext(string.format(cfg.number_terminated, self.number)) |
|||
:css('vertical-align', 'top') |
|||
:wikitext(self.number .. '.') |
|||
end |
end |
||
function Track:makeTitleCell() |
function Track:makeTitleCell() |
||
local titleCell = mw.html.create('td') |
local titleCell = mw.html.create('td') |
||
titleCell |
titleCell:wikitext( |
||
self.title and string.format(cfg.track_title, self.title) or cfg.untitled |
|||
:css('vertical-align', 'top') |
|||
) |
|||
:wikitext(self.title and string.format('"%s"', self.title) or 'Untitled') |
|||
if self.note then |
if self.note then |
||
titleCell:wikitext(string.format(cfg.note, self.note)) |
|||
titleCell |
|||
:wikitext(' ') |
|||
:tag('span') |
|||
:wikitext(string.format('(%s)', self.note)) |
|||
end |
end |
||
return titleCell |
return titleCell |
||
Line 217: | Line 198: | ||
function Track:makeLengthCell() |
function Track:makeLengthCell() |
||
return mw.html.create('td') |
return mw.html.create('td') |
||
: |
:addClass('tracklist-length') |
||
:wikitext(self.length or cfg.blank_cell) |
|||
:css('text-align', 'right') |
|||
:css('vertical-align', 'top') |
|||
:wikitext(self.length or ' ') |
|||
end |
end |
||
function Track:exportRow( |
function Track:exportRow(columns) |
||
local columns = columns or {} |
|||
local columns = options.columns or {} |
|||
local row = mw.html.create('tr') |
local row = mw.html.create('tr') |
||
row:css('background-color', options.color or '#fff') |
|||
for i, column in ipairs(columns) do |
for i, column in ipairs(columns) do |
||
local method = Track.cellMethods[column] |
local method = Track.cellMethods[column] |
||
Line 244: | Line 221: | ||
TrackListing.__index = TrackListing |
TrackListing.__index = TrackListing |
||
addMixin(TrackListing, Validation) |
addMixin(TrackListing, Validation) |
||
TrackListing.fields = cfg.track_listing_field_names |
|||
TrackListing. |
TrackListing.deprecatedFields = cfg.deprecated_track_listing_field_names |
||
headline = true, |
|||
all_writing = true, |
|||
all_lyrics = true, |
|||
all_music = true, |
|||
extra_column = true, |
|||
total_length = true, |
|||
title_width = true, |
|||
writing_width = true, |
|||
lyrics_width = true, |
|||
music_width = true, |
|||
extra_width = true, |
|||
category = true, |
|||
} |
|||
TrackListing.deprecatedFields = { |
|||
writing_credits = true, |
|||
lyrics_credits = true, |
|||
music_credits = true, |
|||
} |
|||
function TrackListing.new(data) |
function TrackListing.new(data) |
||
Line 273: | Line 231: | ||
for deprecatedField in pairs(TrackListing.deprecatedFields) do |
for deprecatedField in pairs(TrackListing.deprecatedFields) do |
||
if data[deprecatedField] then |
if data[deprecatedField] then |
||
self:addCategory( |
self:addCategory(cfg.deprecated_parameter_category) |
||
break |
break |
||
end |
end |
||
Line 337: | Line 295: | ||
function TrackListing:makeIntro() |
function TrackListing:makeIntro() |
||
if self.all_writing then |
if self.all_writing then |
||
return string.format( |
return string.format(cfg.tracks_written, self.all_writing) |
||
'All tracks are written by %s.', |
|||
self.all_writing |
|||
) |
|||
elseif self.all_lyrics and self.all_music then |
elseif self.all_lyrics and self.all_music then |
||
return |
return mw.message.newRawMessage( |
||
cfg.lyrics_written_music_composed, |
|||
'All lyrics are written by %s; all music is composed by %s.', |
|||
self.all_lyrics, |
self.all_lyrics, |
||
self.all_music |
self.all_music |
||
) |
):plain() |
||
elseif self.all_lyrics then |
elseif self.all_lyrics then |
||
return string.format( |
return string.format(cfg.lyrics_written, self.all_lyrics) |
||
'All lyrics are written by %s.', |
|||
self.all_lyrics |
|||
) |
|||
elseif self.all_music then |
elseif self.all_music then |
||
return string.format( |
return string.format(cfg.music_composed, self.all_music) |
||
'All music is composed by %s.', |
|||
self.all_music |
|||
) |
|||
else |
else |
||
return |
return nil |
||
end |
end |
||
end |
end |
||
Line 387: | Line 336: | ||
function TrackListing:renderWarnings() |
function TrackListing:renderWarnings() |
||
if not |
if not cfg.show_warnings then |
||
return '' |
return '' |
||
end |
end |
||
Line 394: | Line 343: | ||
local function addWarning(msg) |
local function addWarning(msg) |
||
table.insert(ret, string.format( |
table.insert(ret, string.format(cfg.track_listing_error, msg)) |
||
'<strong class="error">Track listing error: %s</strong>', |
|||
msg |
|||
)) |
|||
end |
end |
||
Line 414: | Line 360: | ||
function TrackListing:__tostring() |
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') |
|||
-- 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 |
-- Find columns to output |
||
local columns = {'number', 'title'} |
local columns = {'number', 'title'} |
||
Line 430: | Line 410: | ||
end |
end |
||
columns[#columns + 1] = 'length' |
columns[#columns + 1] = 'length' |
||
-- Find |
-- Find column width |
||
local nColumns = #columns |
local nColumns = #columns |
||
local nOptionalColumns = nColumns - 3 |
local nOptionalColumns = nColumns - 3 |
||
local titleColumnWidth |
|||
local titleColumnWidth = 100 |
|||
if nColumns >= 5 then |
if nColumns >= 5 then |
||
titleColumnWidth = 40 |
titleColumnWidth = 40 |
||
elseif nColumns >= 4 then |
elseif nColumns >= 4 then |
||
titleColumnWidth = 60 |
titleColumnWidth = 60 |
||
else |
|||
titleColumnWidth = 100 |
|||
end |
end |
||
local optionalColumnWidth = (100 - titleColumnWidth) / nOptionalColumns |
|||
local optionalColumnWidth = ((100 - titleColumnWidth) / nOptionalColumns) .. '%' |
|||
titleColumnWidth = titleColumnWidth .. '%' |
titleColumnWidth = titleColumnWidth .. '%' |
||
optionalColumnWidth = optionalColumnWidth .. '%' |
|||
-- Root of the output |
|||
local root = mw.html.create() |
|||
-- Intro |
|||
root:node(self:makeIntro()) |
|||
-- Start of track listing table |
|||
local tableRoot = root:tag('table') |
|||
tableRoot |
|||
:addClass('tracklist') |
|||
:css('display', 'block') |
|||
:css('border-spacing', '0px') |
|||
-- Header row |
|||
if self.headline then |
|||
tableRoot:tag('caption') |
|||
:addClass('tlheader mbox-text') |
|||
:attr('colspan', nColumns) |
|||
:css('text-align', 'left') |
|||
:css('background-color', '#fff') |
|||
:css('font-weight', '700') |
|||
:wikitext(self.headline or 'Track listing') |
|||
end |
|||
---- Title column |
|||
-- Deprecated collapsed parameter |
|||
if self.collapsed then |
|||
self:addWarning("Deprecated collapsed parameter in use", COLLAPSED_PARAMETER_CATEGORY); |
|||
end |
|||
-- Headers |
|||
local headerRow = tableRoot:tag('tr') |
|||
---- Track number |
|||
headerRow |
|||
:tag('th') |
|||
:addClass('tlheader') |
|||
:attr('scope', 'col') |
|||
:css('width', '2em') |
|||
:css('padding-left', '10px') |
|||
:css('padding-right', '10px') |
|||
:css('text-align', 'right') |
|||
:css('background-color', '#eee') |
|||
:tag('abbr') |
|||
:attr('title', 'Number') |
|||
:wikitext('No.') |
|||
---- Title |
|||
headerRow:tag('th') |
headerRow:tag('th') |
||
:addClass('tlheader') |
|||
:attr('scope', 'col') |
:attr('scope', 'col') |
||
:css('width', self.title_width or titleColumnWidth) |
:css('width', self.title_width or titleColumnWidth) |
||
:wikitext(cfg.title) |
|||
:css('text-align', 'left') |
|||
:css('background-color', '#eee') |
|||
:wikitext('Title') |
|||
---- Optional headers: writer, lyrics, music, and extra |
---- Optional headers: writer, lyrics, music, and extra |
||
Line 505: | Line 435: | ||
if self.optionalColumns[field] then |
if self.optionalColumns[field] then |
||
headerRow:tag('th') |
headerRow:tag('th') |
||
:addClass('tlheader') |
|||
:attr('scope', 'col') |
:attr('scope', 'col') |
||
:css('width', width or optionalColumnWidth) |
:css('width', width or optionalColumnWidth) |
||
:css('text-align', 'left') |
|||
:css('background-color', '#eee') |
|||
:wikitext(headerText) |
:wikitext(headerText) |
||
end |
end |
||
end |
end |
||
addOptionalHeader('writer', |
addOptionalHeader('writer', cfg.writer, self.writing_width) |
||
addOptionalHeader('lyrics', |
addOptionalHeader('lyrics', cfg.lyrics, self.lyrics_width) |
||
addOptionalHeader('music', |
addOptionalHeader('music', cfg.music, self.music_width) |
||
addOptionalHeader( |
addOptionalHeader( |
||
'extra', |
'extra', |
||
self.extra_column or |
self.extra_column or cfg.extra, |
||
self.extra_width |
self.extra_width |
||
) |
) |
||
Line 524: | Line 451: | ||
---- Track length |
---- Track length |
||
headerRow:tag('th') |
headerRow:tag('th') |
||
:addClass(' |
:addClass('tracklist-length-header') |
||
:attr('scope', 'col') |
:attr('scope', 'col') |
||
:wikitext(cfg.length) |
|||
:css('width', '4em') |
|||
:css('padding-right', '10px') |
|||
:css('text-align', 'right') |
|||
:css('background-color', '#eee') |
|||
:wikitext('Length') |
|||
-- Tracks |
-- Tracks |
||
for i, track in ipairs(self.tracks) do |
for i, track in ipairs(self.tracks) do |
||
tableRoot:node(track:exportRow( |
tableRoot:node(track:exportRow(columns)) |
||
columns = columns, |
|||
color = i % 2 == 0 and '#f7f7f7' or '#fff' |
|||
})) |
|||
end |
end |
||
Line 544: | Line 464: | ||
tableRoot |
tableRoot |
||
:tag('tr') |
:tag('tr') |
||
: |
:addClass('tracklist-total-length') |
||
:tag('th') |
|||
:attr('colspan', nColumns - 1) |
:attr('colspan', nColumns - 1) |
||
: |
:attr('scope', 'row') |
||
:tag('span') |
:tag('span') |
||
: |
:wikitext(cfg.total_length) |
||
:css('float', 'right') |
|||
:css('padding-left', '10px') |
|||
:css('background-color', '#eee') |
|||
:css('margin-right', '2px') |
|||
:wikitext("'''Total length:'''") |
|||
:done() |
:done() |
||
:done() |
:done() |
||
:tag('td') |
:tag('td') |
||
: |
:wikitext(self.total_length) |
||
:css('text-align', 'right') |
|||
:css('background-color', '#eee') |
|||
:wikitext(string.format("'''%s'''", self.total_length)) |
|||
end |
end |
||
root:node(tableRoot) |
|||
-- Warnings and tracking categories |
-- Warnings and tracking categories |
||
root:wikitext(self:renderWarnings()) |
root:wikitext(self:renderWarnings()) |
||
root:wikitext(self:renderTrackingCategories()) |
root:wikitext(self:renderTrackingCategories()) |
||
return |
return mw.getCurrentFrame():extensionTag{ |
||
name = 'templatestyles', args = { src = 'Module:Track listing/styles.css' } |
|||
} .. tostring(root) |
|||
end |
end |
||
Latest revision as of 15:19, October 25, 2022
Documentation for this module may be created at Module:Track listing/doc
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('[[Category:%s]]', 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, '<br>')
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')
-- 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 = {'number', 'title'}
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{
name = 'templatestyles', args = { src = 'Module:Track listing/styles.css' }
} .. 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, {
wrappers = 'Template:Track listing'
})
return p._main(args)
end
return p