Modul:Sporliste
Hopp til navigering
Hopp til søk
Dokumentasjon for denne modulen kan opprettes på Modul:Sporliste/dok
-- 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