Misplaced Pages

Module:Track listing: Difference between revisions

Article snapshot taken from Wikipedia with creative commons attribution-sharealike license. Give it a read and then ask your questions in the chat. We can research this topic together.
Browse history interactively← Previous editContent deleted Content added
Revision as of 06:35, 14 December 2016 view sourceMr. Stradivarius (talk | contribs)Edit filter managers, Administrators59,191 edits use "deprecated parameters" instead of "deprecated arguments" in the tracking category - while it's technically wrong, it's what everyone else does← 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,304 edits Add optional parameter for table's overall width 
(30 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('td') return mw.html.create('th')
:attr('id', string.format(cfg.track_id, self.number))
:css('padding-right', '10px')
:css('text-align', 'right') :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:tag('span') 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')
:css('padding-right', '10px') :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(options) function Track:exportRow(columns)
options = options or {} 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 string.format( 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 and self.hasDeprecatedArgs then
local function addCategory(cat)
ret = ret .. ']'
ret = ret .. string.format(']', cat)
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 270: Line 416:
end end
columns = 'length' columns = 'length'

-- Find colspan and column width -- 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')
: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 351: 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', 'Writer(s)', self.writing_width) addOptionalHeader('writer', cfg.writer, self.writing_width)
addOptionalHeader('lyrics', 'Lyrics', self.lyrics_width) addOptionalHeader('lyrics', cfg.lyrics, self.lyrics_width)
addOptionalHeader('music', 'Music', self.music_width) addOptionalHeader('music', cfg.music, self.music_width)
addOptionalHeader( addOptionalHeader(
'extra', 'extra',
self.extra_column or '{{{extra_column}}}', self.extra_column or cfg.extra,
self.extra_width self.extra_width
) )
Line 370: Line 457:
---- Track length ---- Track length
headerRow:tag('th') headerRow:tag('th')
:addClass('tlheader') :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 390: Line 470:
tableRoot tableRoot
:tag('tr') :tag('tr')
:tag('td') :addClass('tracklist-total-length')
:tag('th')
:attr('colspan', nColumns - 1) :attr('colspan', nColumns - 1)
:css('padding', 0) :attr('scope', 'row')
:tag('span') :tag('span')
:css('width', '7.5em') :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')
:css('padding', '0 10px 0 0') :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 tostring(root) 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]
WarningThis 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.
CSSThis 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 p
Category: