Jump to content

Module:Track listing

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Mr. Stradivarius (talk | contribs) at 03:50, 26 October 2015 (more work on TrackListing:__tostring). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-- This module implements [[Template:Track listing]]

local yesno = require('Module:Yesno')

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

local Track = {}
Track.__index = Track

Track.fields = {
	number = true,
	title = true,
	note = true,
	length = true,
	lyrics = true,
	music = true,
	writer = true,
	extra = true,
}

Track.cellMethods = {
	number = 'makeNumberCell',
	title = 'makeTitleCell',
	writer = 'makeWriterCell',
	lyrics = 'makeLyricsCell',
	music = 'makeMusicCell',
	extra = 'makeExtraCell',
	length = 'makeLengthCell',
}

function Track.new(data)
	local self = setmetatable({}, Track)
	for field in pairs(Track.fields) do
		self[field] = data[field]
	end
	self.number = assert(tonumber(self.number))
	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')
		:css('vertical-align', 'top')
		:wikitext(wikitext or ' ')
end

function Track:makeNumberCell()
	return mw.html.create('td')
		:css('padding-right', '10px')
		:css('text-align', 'right')
		:css('vertical-align', 'top')
		:wikitext(self.number .. '.')
end

function Track:makeTitleCell()
	local titleCell = mw.html.create('td')
	titleCell
		:css('text-align', 'left')
		:css('vertical-align', 'top')
		:wikitext(self.title and string.format('"%s"', self.title) or 'Untitled')
		:wikitext(' ')
	if self.note then
		titleCell:tag('span')
			:css('font-size', '85%')
			:wikitext(string.format('(%s)', self.note))
	else
		titleCell:wikitext(' ')
	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')
		:css('padding-right', '10px')
		:css('text-align', 'right')
		:css('vertical-align', 'top')
		:wikitext(self.length or ' ')
end

function Track:exportRow(options)
	options = options or {}
	local columns = options.columns or {}
	local row = mw.html.create('tr')
	row:css('background-color', options.color or '#fff')
	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

TrackListing.fields = {
	all_writing = true,
	all_lyrics = true,
	all_music = true,
	collapsed = true,
	headline = true,
	extra_column = true,
	total_length = true,
}

TrackListing.deprecatedFields = {
	writing_credits = true,
	lyrics_credits = true,
	music_credits = true,
}

function TrackListing.new(data)
	local self = setmetatable({}, TrackListing)

	-- Add properties
	for field in pairs(TrackListing.fields) do
		self[field] = data[field]
	end
	
	-- Evaluate boolean properties
	self.collapsed = yesno(self.collapsed, false)

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

	-- Count the number of optional columns.
	self.nOptionalColumns = 0
	for k in pairs(self.optionalColumns) do
		self.nOptionalColumns = self.nOptionalColumns + 1
	end

	return self
end

function TrackListing:makeIntro()
	local ret = ''
	if self.all_writing then
		ret = ret .. string.format(
			'All songs written and composed by %s. ',
			self.all_writing
		)
	else
		if self.all_lyrics then
			ret = ret .. 'All lyrics written by ' .. self.all_lyrics
			if self.all_music then
				ret = ret .. ','
			else
				ret = ret .. '.'
			end
			ret = ret .. ' '
		end
		if self.all_music then
			if self.all_lyrics then
				ret = ret .. 'All'
			else
				ret = ret .. 'all'
			end
			ret = ret .. ' music composed by ' .. self.all_music .. '.'
		end
	end
	return ret
end

function TrackListing:__tostring()
	local root = mw.html.create()

	-- 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 number of columns
	local nColumns = #columns

	-- Intro
	root:node(self:makeIntro())

	-- Start 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')
	local titleColumnWidth, optionalColumnWidth
	if nColumns >= 6 then
		titleColumnWidth = '40%'
		optionalColumnWidth = '20%'
	elseif nColumns >= 5 then
		titleColumnWidth = '40%'
		optionalColumnWidth = '30%'
	elseif nColumns >= 4 then
		titleColumnWidth = '60%'
		optionalColumnWidth = '40%'
	else
		-- If there are three
		titleColumnWidth = '100%'
	end

	--- 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')
		:addClass('tlheader')
		:attr('scope', 'col')
		:css('width', titleColumnWidth)
		:css('text-align', 'left')
		:css('background-color', '#eee')
		:wikitext('Title')	
	
	-- Writer
	if self.optionalColumns.writer then
		headerRow
			:tag('th')
				:addClass('tlheader')
				:attr('scope', 'col')
				:css('width', optionalColumnWidth)
				:css('text-align', 'left')
				:css('background-color', '#eee')
				:wikitext('Writer(s)')
	end
	
	for i, track in ipairs(self.tracks) do
		root:node(track:exportRow({columns = columns}))
	end
	
	return 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