Modul:Sporliste
-- This module implements [[Mal:Sporliste]]
local yesno = require('Modul:Yesno')
local checkType = require('libraryUtil').checkType
local SHOW_WARNINGS = false
local INPUT_ERROR_CATEGORY = 'Sporliste med input feil'
--------------------------------------------------------------------------------
-- 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:validateLengde(lengde)
checkType('validateLength', 1, lengde, 'string', true)
if lengde == 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 = lengde: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(
"Ugyldig tid '%s' (tid-er i format 't:mm:ss' kan ikke starte med et null)",
mw.text.nowiki(lengde)
), 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 = lengde: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(
"Ugyldig tid '%s' (tid-er i format 'mm:ss' kan ikke starte med et null)",
mw.text.nowiki(lengde)
), 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(
"Ugyldig tid '%s' (tid-er må være i et av følgende format 'm:ss', 'mm:ss' eller 't:mm:ss')",
mw.text.nowiki(lengde)
), 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(
"Ugyldig sporlengde '%s' (hvis timer er spesifisert, må antall minutter være mindre enn 60)",
mw.text.nowiki(lengde)
), INPUT_ERROR_CATEGORY)
return nil
end
-- Check that the seconds are less than 60
if tonumber(seconds) >= 60 then
self:addWarning(string.format(
"Ugyldig sporlengde '%s' (antall sekunder må være mindre enn 60)",
mw.text.nowiki(lengde)
), INPUT_ERROR_CATEGORY)
end
return nil
end
--------------------------------------------------------------------------------
-- Track class
--------------------------------------------------------------------------------
local Track = {}
Track.__index = Track
addMixin(Track, Validation)
Track.fields = {
nummer = true,
tittel = true,
notat = true,
lengde = true,
sangtekst = true,
musikk = true,
tekst = true,
ekstra = true,
}
Track.cellMethods = {
nummer = 'makeNummerCell',
tittel = 'makeTittelCell',
tekst = 'makeTekstCell',
sangtekst = 'makeSangtekstCell',
musikk = 'makeMusikkCell',
ekstra = 'makeEkstraCell',
lengde = 'makeLengdeCell',
}
function Track.new(data)
local self = setmetatable({}, Track)
Validation.init(self)
for field in pairs(Track.fields) do
self[field] = data[field]
end
self.nummer = assert(tonumber(self.nummer))
self:validateLengde(self.lengde)
return self
end
function Track:getSangtekstCredit()
return self.sangtekst
end
function Track:getMusikkCredit()
return self.musikk
end
function Track:getTekstCredit()
return self.tekst
end
function Track:getEkstraField()
return self.ekstra
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:makeNummerCell()
return mw.html.create('td')
:css('padding-right', '10px')
:css('text-align', 'right')
:css('vertical-align', 'top')
:wikitext(self.nummer .. '.')
end
function Track:makeTittelCell()
local tittelCell = mw.html.create('td')
tittelCell
:css('vertical-align', 'top')
:wikitext(self.tittel and string.format('«%s»', self.tittel) or 'Uten navn')
if self.notat then
tittelCell
:wikitext(' ')
:tag('span')
:css('font-size', '85%')
:wikitext(string.format('(%s)', self.notat))
end
return tittelCell
end
function Track:makeTekstCell()
return Track.makeSimpleCell(self.tekst)
end
function Track:makeSangtekstCell()
return Track.makeSimpleCell(self.sangtekst)
end
function Track:makeMusikkCell()
return Track.makeSimpleCell(self.musikk)
end
function Track:makeEkstraCell()
return Track.makeSimpleCell(self.ekstra)
end
function Track:makeLengdeCell()
return mw.html.create('td')
:css('padding-right', '10px')
:css('text-align', 'right')
:css('vertical-align', 'top')
:wikitext(self.lengde 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
addMixin(TrackListing, Validation)
TrackListing.fields = {
alt_tekst = true,
alt_sangtekst = true,
alt_musikk = true,
kollapset = true,
overskrift = true,
ekstra_kolonne = true,
total_lengde = true,
tittel_bredde = true,
skriving_bredde = true,
sangtekst_bredde = true,
musikk_bredde = true,
ekstra_bredde = true,
category = true,
}
TrackListing.deprecatedFields = {
skriving_kreditt = true,
sangtekst_kreditt = true,
musikk_kreditt = true,
}
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('Sporliste med utdaterte parametere')
break
end
end
-- Validate total length
if data.total_lengde then
self:validateLengde(data.total_lengde)
end
-- Add properties
for field in pairs(TrackListing.fields) do
self[field] = data[field]
end
-- Evaluate boolean properties
self.kollapset = yesno(self.kollapset, false)
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 = {
sangtekst = 'getSangtekstCredit',
musikk = 'getMusikkCredit',
tekst = 'getTekstCredit',
ekstra = 'getEkstraField',
}
local doneTekstCheck = 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 doneTekstCheck and optionalColumns.tekst then
doneTekstCheck = true
optionalColumns.sangtekst = nil
optionalColumns.musikk = nil
columnMethods.sangtekst = nil
columnMethods.musikk = nil
end
if not next(columnMethods) then
break
end
end
self.optionalColumns = optionalColumns
end
return self
end
function TrackListing:makeIntro()
if self.alt_tekst then
return string.format(
'Alle sporene er skrevet av %s.',
self.alt_tekst
)
elseif self.alt_sangtekst and self.alt_musikk then
return string.format(
'Alle tekstene er skrevet av %s; all musikk er komponert av %s.',
self.alt_sangtekst,
self.alt_musikk
)
elseif self.alt_sangtekst then
return string.format(
'Alle tekstene er skrevet av %s.',
self.alt_sangtekst
)
elseif self.alt_musikk then
return string.format(
'All musikk er komponert av %s.',
self.alt_musikk
)
else
return ''
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 SHOW_WARNINGS then
return ''
end
local ret = {}
local function addWarning(msg)
table.insert(ret, string.format(
'<strong class="error">Sporliste feil: %s</strong>',
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()
-- Find columns to output
local columns = {'nummer', 'tittel'}
if self.optionalColumns.tekst then
columns[#columns + 1] = 'tekst'
else
if self.optionalColumns.sangtekst then
columns[#columns + 1] = 'sangtekst'
end
if self.optionalColumns.musikk then
columns[#columns + 1] = 'musikk'
end
end
if self.optionalColumns.ekstra then
columns[#columns + 1] = 'ekstra'
end
columns[#columns + 1] = 'lengde'
-- Find colspan and column width
local nColumns = #columns
local nOptionalColumns = nColumns - 3
local tittelColumnWidth
if nColumns >= 5 then
tittelColumnWidth = 40
elseif nColumns >= 4 then
tittelColumnWidth = 60
else
tittelColumnWidth = 100
end
local optionalColumnWidth = (100 - tittelColumnWidth) / nOptionalColumns
tittelColumnWidth = tittelColumnWidth .. '%'
optionalColumnWidth = optionalColumnWidth .. '%'
-- 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.kollapset and 'collapsible kollapset' or nil)
:css('display', 'block')
:css('border-spacing', '0px')
:css('border-collapse', 'collapse')
:css('border', self.kollapset and '#aaa 1px solid' or nil)
:css('padding', self.kollapset and '3px' or '4px')
-- Header row
if self.overskrift or self.kollapset then
tableRoot:tag('tr'):tag('th')
:addClass('tlheader mbox-text')
:attr('colspan', nColumns)
:css('text-align', 'left')
:css('background-color', '#fff')
:wikitext(self.overskrift or 'Sporliste')
end
-- Headers
local headerRow = tableRoot:tag('tr')
---- Track nummer
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', 'Nummer')
:wikitext('Nr.')
---- Tittel
headerRow:tag('th')
:addClass('tlheader')
:attr('scope', 'col')
:css('width', self.tittel_bredde or tittelColumnWidth)
:css('text-align', 'left')
:css('background-color', '#eee')
:wikitext('Tittel')
---- Optional headers: writer, lyrics, music, and extra
local function addOptionalHeader(field, headerText, width)
if self.optionalColumns[field] then
headerRow:tag('th')
:addClass('tlheader')
:attr('scope', 'col')
:css('width', width or optionalColumnWidth)
:css('text-align', 'left')
:css('background-color', '#eee')
:wikitext(headerText)
end
end
addOptionalHeader('tekst', 'Låtskriver(e)', self.skriving_bredde)
addOptionalHeader('sangtekst', 'Sangtekst', self.sangtekst_bredde)
addOptionalHeader('musikk', 'Musikk', self.musikk_bredde)
addOptionalHeader(
'ekstra',
self.ekstra_kolonne or '{{{ekstra_kolonne}}}',
self.ekstra_bredde
)
---- Track length
headerRow:tag('th')
:addClass('tlheader')
:attr('scope', 'col')
:css('width', '4em')
:css('padding-right', '10px')
:css('text-align', 'right')
:css('background-color', '#eee')
:wikitext('Lengde')
-- Tracks
for i, track in ipairs(self.tracks) do
tableRoot:node(track:exportRow({
columns = columns,
color = i % 2 == 0 and '#f7f7f7' or '#fff'
}))
end
-- Total length
if self.total_lengde then
tableRoot
:tag('tr')
:tag('td')
:attr('colspan', nColumns - 1)
:css('padding', 0)
:tag('span')
:css('width', '7.5em')
:css('float', 'right')
:css('padding-left', '10px')
:css('background-color', '#eee')
:css('margin-right', '2px')
:wikitext("'''Total lengde:'''")
:done()
:done()
:tag('td')
:css('padding', '0 10px 0 0')
:css('text-align', 'right')
:css('background-color', '#eee')
:wikitext(string.format("'''%s'''", self.total_lengde))
end
-- Warnings and tracking categories
root:wikitext(self:renderWarnings())
root:wikitext(self:renderTrackingCategories())
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.nummer = num
table.insert(ret, trackData)
end
table.sort(ret, function (t1, t2)
return t1.nummer < t2.nummer
end)
return ret
end)(tracks)
return tostring(TrackListing.new(data))
end
function p.main(frame)
local args = require('Modul:Arguments').getArgs(frame, {
wrappers = 'Mal:Sporliste'
})
return p._main(args)
end
return p