Revision as of 06:04, 17 December 2016 view sourceMr. Stradivarius (talk | contribs)Edit filter managers, Administrators59,191 edits only add the tracking category in mainspace← Previous edit | Latest revision as of 01:48, 21 January 2023 view source Alex 21 (talk | contribs)Extended confirmed users, Page movers, File movers, Pending changes reviewers, Template editors139,365 edits Add optional parameter for table's overall width | ||
(28 intermediate revisions by 10 users not shown) | |||
Line 1: | Line 1: | ||
-- This module implements ] | |||
local yesno = require('Module:Yesno') | 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 = 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 | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
Line 9: | Line 115: | ||
local Track = {} | local Track = {} | ||
Track.__index = Track | Track.__index = Track | ||
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 33: | Line 131: | ||
function Track.new(data) | function Track.new(data) | ||
local self = setmetatable({}, Track) | local self = setmetatable({}, Track) | ||
Validation.init(self) | |||
for field in pairs(Track.fields) do | for field in pairs(Track.fields) do | ||
self = data | self = data | ||
end | end | ||
self.number = assert(tonumber(self.number)) | self.number = assert(tonumber(self.number)) | ||
self:validateLength(self.length) | |||
return self | return self | ||
end | end | ||
Line 59: | 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') | |||
:wikitext(' ') | |||
if self.note then | if self.note then | ||
titleCell: |
titleCell:wikitext(string.format(cfg.note, self.note)) | ||
:css('font-size', '85%') | |||
:wikitext(string.format('(%s)', self.note)) | |||
else | |||
titleCell:wikitext(' ') | |||
end | end | ||
return titleCell | return titleCell | ||
Line 105: | 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 | local method = Track.cellMethods | ||
Line 131: | Line 220: | ||
local TrackListing = {} | local TrackListing = {} | ||
TrackListing.__index = TrackListing | TrackListing.__index = TrackListing | ||
addMixin(TrackListing, Validation) | |||
TrackListing.fields = |
TrackListing.fields = cfg.track_listing_field_names | ||
TrackListing.deprecatedFields = cfg.deprecated_track_listing_field_names | |||
all_writing = true, | |||
all_lyrics = true, | |||
all_music = true, | |||
collapsed = true, | |||
headline = 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) | ||
local self = setmetatable({}, TrackListing) | local self = setmetatable({}, TrackListing) | ||
Validation.init(self) | |||
-- Add properties | |||
for field in pairs(TrackListing.fields) do | |||
self = data | |||
end | |||
-- Check for deprecated arguments | -- Check for deprecated arguments | ||
for deprecatedField in pairs(TrackListing.deprecatedFields) do | for deprecatedField in pairs(TrackListing.deprecatedFields) do | ||
if data then | if data then | ||
self:addCategory(cfg.deprecated_parameter_category) | |||
self.hasDeprecatedArgs = true | |||
break | break | ||
end | 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 = data | |||
end | end | ||
-- Evaluate boolean properties | -- Evaluate boolean properties | ||
self.collapsed = yesno(self.collapsed, false) | |||
self.showCategories = yesno(self.category) ~= false | self.showCategories = yesno(self.category) ~= false | ||
self.category = nil | self.category = nil | ||
Line 220: | 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 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 written by %s; all music 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 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 composed by %s. ', | |||
self.all_music | |||
) | |||
else | else | ||
return |
return nil | ||
end | end | ||
end | end | ||
function TrackListing:renderTrackingCategories() | function TrackListing:renderTrackingCategories() | ||
if not self.showCategories or mw.title.getCurrentTitle().namespace ~= 0 then | |||
return '' | |||
end | |||
local ret = '' | local ret = '' | ||
if self.showCategories | |||
local function addCategory(cat) | |||
and self.hasDeprecatedArgs | |||
ret = ret .. string.format(']', cat) | |||
and mw.title.getCurrentTitle().namespace == 0 | |||
then | |||
ret = ret .. ']' | |||
end | 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 | 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 | end | ||
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') | |||
-- 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 | -- Find columns to output | ||
local columns = {'number', 'title'} | local columns = {'number', 'title'} | ||
Line 273: | Line 416: | ||
end | end | ||
columns = 'length' | columns = '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 .. '%' | |||
---- Title column | |||
-- 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') | |||
:addClass(self.collapsed and 'collapsible collapsed' or nil) | |||
:css('display', 'block') | |||
:css('border-spacing', '0px') | |||
:css('border-collapse', 'collapse') | |||
:css('border', self.collapsed and '#aaa 1px solid' or nil) | |||
:css('padding', self.collapsed and '3px' or '4px') | |||
-- Headline | |||
if self.headline then | |||
tableRoot:tag('tr'):tag('th') | |||
:addClass('tlheader mbox-text') | |||
:attr('colspan', nColumns) | |||
:css('text-align', 'left') | |||
:css('background-color', '#fff') | |||
:wikitext(self.headline) | |||
end | |||
-- Header row for collapsed track listings | |||
if self.collapsed then | |||
tableRoot:tag('tr'):tag('th') | |||
:addClass('tlheader mbox-text') | |||
:attr('colspan', nColumns) | |||
:css('text-align', 'left') | |||
:css('background-color', '#fff') | |||
:wikitext('Track listing') | |||
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 356: | Line 441: | ||
if self.optionalColumns then | if self.optionalColumns 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 375: | Line 457: | ||
---- 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 395: | Line 470: | ||
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) | |||
-- Tracking categories | |||
-- Warnings and tracking categories | |||
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 01:48, 21 January 2023
Module documentation[view] [edit] [history] [purge]This Lua module is used on approximately 114,000 pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
This module uses TemplateStyles: |
This module depends on the following other modules: |
This module is used by one or more bots.
If you intend to make significant changes to this module, move it, or nominate it for deletion, please notify the bot operator(s) in advance. The relevant bots are: User:cewbot/log/20201008/configuration. |
This module implements {{track listing}}. Please see the template page for documentation.
The above documentation is transcluded from Module:Track listing/doc. (edit | history)Editors can experiment in this module's sandbox (edit | diff) and testcases (create) pages.
Subpages of this module.
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 = 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 = data 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 if method then row:node(self(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 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 = data 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(trackObj) then optionalColumns = true columnMethods = 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, '<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') -- 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 = {'number', 'title'} if self.optionalColumns.writer then columns = 'writer' else if self.optionalColumns.lyrics then columns = 'lyrics' end if self.optionalColumns.music then columns = 'music' end end if self.optionalColumns.extra then columns = 'extra' end columns = '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 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 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 = tracks or {} tracks = v else data = 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 pCategory: