No edit summary |
m (1 revision imported) |
||
(10 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
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 | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
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(string.format( | self:addWarning( | ||
string.format(cfg.leading_0_in_hours, mw.text.nowiki(length)), | |||
cfg.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(string.format( | self:addWarning( | ||
string.format(cfg.leading_0_in_minutes, mw.text.nowiki(length)), | |||
cfg.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(string.format( | self:addWarning( | ||
string.format(cfg.not_a_time, mw.text.nowiki(length)), | |||
cfg.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(string.format( | self:addWarning( | ||
string.format(cfg.more_than_60_minutes, mw.text.nowiki(length)), | |||
cfg.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(string.format( | self:addWarning( | ||
string.format(cfg.more_than_60_seconds, mw.text.nowiki(length)), | |||
cfg.input_error_category | |||
) | |||
end | end | ||
Line 122: | Line 117: | ||
addMixin(Track, Validation) | addMixin(Track, Validation) | ||
Track.fields = | Track.fields = cfg.track_field_names | ||
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) | |||
:wikitext(wikitext or | |||
end | end | ||
function Track:makeNumberCell() | function Track:makeNumberCell() | ||
return mw.html.create('th') | return mw.html.create('th') | ||
:attr('id', | :attr('id', string.format(cfg.track_id, self.number)) | ||
:attr('scope', 'row') | :attr('scope', 'row') | ||
: | :wikitext(string.format(cfg.number_terminated, 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 | |||
) | |||
if self.note then | if self.note then | ||
titleCell | titleCell:wikitext(string.format(cfg.note, self.note)) | ||
end | end | ||
return titleCell | return titleCell | ||
Line 220: | 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) | |||
:wikitext(self.length or | |||
end | end | ||
function Track:exportRow( | function Track:exportRow(columns) | ||
local columns = columns or {} | |||
local columns = | |||
local row = mw.html.create('tr') | local row = mw.html.create('tr') | ||
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 247: | Line 221: | ||
TrackListing.__index = TrackListing | TrackListing.__index = TrackListing | ||
addMixin(TrackListing, Validation) | addMixin(TrackListing, Validation) | ||
TrackListing.fields = cfg.track_listing_field_names | |||
TrackListing.fields = | TrackListing.deprecatedFields = cfg.deprecated_track_listing_field_names | ||
TrackListing.deprecatedFields = | |||
function TrackListing.new(data) | function TrackListing.new(data) | ||
Line 276: | 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 340: | 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) | ||
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, | |||
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) | ||
elseif self.all_music then | elseif self.all_music then | ||
return string.format( | return string.format(cfg.music_composed, self.all_music) | ||
else | else | ||
return | return nil | ||
end | end | ||
end | end | ||
Line 390: | Line 336: | ||
function TrackListing:renderWarnings() | function TrackListing:renderWarnings() | ||
if not | if not cfg.show_warnings then | ||
return '' | return '' | ||
end | end | ||
Line 397: | 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)) | ||
end | end | ||
Line 417: | 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') | |||
-- 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 433: | Line 416: | ||
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 | ||
end | end | ||
local optionalColumnWidth = (100 - titleColumnWidth) / nOptionalColumns | |||
local optionalColumnWidth = ((100 - titleColumnWidth) / nOptionalColumns) .. '%' | |||
titleColumnWidth = titleColumnWidth .. '%' | titleColumnWidth = titleColumnWidth .. '%' | ||
---- Title column | |||
---- Title | |||
headerRow:tag('th') | headerRow:tag('th') | ||
:attr('scope', 'col') | :attr('scope', 'col') | ||
:css('width', self.title_width or titleColumnWidth) | :css('width', self.title_width or titleColumnWidth) | ||
:wikitext(cfg.title) | |||
:wikitext( | |||
---- Optional headers: writer, lyrics, music, and extra | ---- Optional headers: writer, lyrics, music, and extra | ||
Line 508: | Line 441: | ||
if self.optionalColumns[field] then | if self.optionalColumns[field] then | ||
headerRow:tag('th') | headerRow:tag('th') | ||
:attr('scope', 'col') | :attr('scope', 'col') | ||
:css('width', width or optionalColumnWidth) | :css('width', width or optionalColumnWidth) | ||
: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 527: | 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) | |||
:wikitext( | |||
-- 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)) | ||
end | end | ||
Line 547: | Line 470: | ||
tableRoot | tableRoot | ||
:tag('tr') | :tag('tr') | ||
:addClass('tracklist-total-length') | |||
:tag('th') | :tag('th') | ||
:attr('colspan', nColumns - 1) | :attr('colspan', nColumns - 1) | ||
:attr('scope', 'row') | :attr('scope', 'row') | ||
:tag('span') | :tag('span') | ||
: | :wikitext(cfg.total_length) | ||
:done() | :done() | ||
:done() | :done() | ||
:tag('td') | :tag('td') | ||
:wikitext(self.total_length) | |||
:wikitext( | |||
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 tostring(root) | return mw.getCurrentFrame():extensionTag{ | ||
name = 'templatestyles', args = { src = 'Module:Track listing/styles.css' } | |||
} .. tostring(root) | |||
end | end | ||
Latest revision as of 19:59, 9 June 2023
This Lua module is used on approximately 106,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 implements {{track listing}}. Please see the template page for documentation.
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') -- 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[#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