Permanently protected module
From Wikipedia, the free encyclopedia


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

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

		selffield = datafield

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

		if method then

			row:node(selfmethod](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 datadeprecatedField 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

		selffield = datafield

	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 trackObjmethod](trackObj) then

					optionalColumnscolumn = true

					columnMethodscolumn = 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.optionalColumnsfield 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.fieldsprefix 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)

				tracksnum = tracksnum or {}

				tracksnum][prefix = v

			else

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