Módulo:Lista de faixas
local yesno = require('Module:Yesno') local checkType = require('libraryUtil').checkType local cfg = mw.loadData('Módulo:Lista de faixas/configuração')
local fieldMapping = { -- Título titulo = 'title', ['título'] = 'title', -- Nota nota = 'note', -- Créditos por faixa escritor = 'writer', letra = 'lyrics', musica = 'music', ['música'] = 'music', diretor = 'director', extra = 'extra', -- Duração duracao = 'length', ['duração'] = 'length', duracao_total = 'total_length', ['duração_total'] = 'total_length', -- Créditos de coluna (booleanos de nível de listagem) credito_letra = 'lyrics_credit', ['crédito_letra'] = 'lyrics_credit', credito_musica = 'music_credit', ['crédito_música'] = 'music_credit', credito_escrita = 'writing_credit', ['crédito_escrita'] = 'writing_credit', credito_direcao = 'direction_credit', ['crédito_direção'] = 'direction_credit', -- Coluna extra e linha vazia coluna_extra = 'extra_column', linha_vazia = 'blank_row', -- Larguras largura = 'width', largura_titulo = 'title_width', ['largura_título'] = 'title_width', largura_escrita = 'writing_width', largura_letra = 'lyrics_width', largura_musica = 'music_width', ['largura_música'] = 'music_width', largura_extra = 'extra_width', -- Cabeçalho e estado colapsável topo = 'headline', fechado = 'collapsed', -- Créditos globais ['toda_composição'] = 'all_writing', toda_composicao = 'all_writing', toda_letra = 'all_lyrics', ['toda_música'] = 'all_music', toda_musica = 'all_music', }
-- Funções auxiliares
local function addMixin(classe, mixin) for k, v in pairs(mixin) do if k ~= 'init' then classe[k] = v end end end
-- Mixin de validação
local Validacao = {}
function Validacao.init(self) self.avisos = {} self.categorias = {} end
function Validacao:adicionarAviso(msg, categoria) table.insert(self.avisos, msg) if categoria then table.insert(self.categorias, categoria) end end
function Validacao:adicionarCategoria(categoria) table.insert(self.categorias, categoria) end
function Validacao:obterAvisos() return self.avisos end
function Validacao:obterCategorias() return self.categorias end
function Validacao:validarDuracao(duracao) checkType('validarDuracao', 1, duracao, 'string', true) if duracao == nil then return nil end
local horas, minutos, segundos
-- Tenta "1:23:45" horas, minutos, segundos = duracao:match('^(%d+):(%d%d):(%d%d)$') if horas and horas:sub(1, 1) == '0' then self:adicionarAviso( string.format(cfg.leading_0_in_hours, mw.text.nowiki(duracao)), cfg.input_error_category ) return nil end
if not segundos then -- Tenta "1:23" minutos, segundos = duracao:match('^(%d?%d):(%d%d)$') if minutos and minutos:find('^0%d$') then self:adicionarAviso( string.format(cfg.leading_0_in_minutes, mw.text.nowiki(duracao)), cfg.input_error_category ) return nil end end
if not segundos then self:adicionarAviso( string.format(cfg.not_a_time, mw.text.nowiki(duracao)), cfg.input_error_category ) return nil end
if horas and tonumber(minutos) >= 60 then self:adicionarAviso( string.format(cfg.more_than_60_minutes, mw.text.nowiki(duracao)), cfg.input_error_category ) return nil end
if tonumber(segundos) >= 60 then self:adicionarAviso( string.format(cfg.more_than_60_seconds, mw.text.nowiki(duracao)), cfg.input_error_category ) end
return nil end
-- Classe Faixa
local Faixa = {} Faixa.__index = Faixa addMixin(Faixa, Validacao)
Faixa.campos = { number = true, title = true, note = true, writer = true, lyrics = true, music = true, director = true, extra = true, length = true, }
Faixa.metodosCelula = { number = 'makeCelulaNumero', title = 'makeCelulaTitulo', writer = 'makeCelulaEscritor', lyrics = 'makeCelulaLetra', music = 'makeCelulaMusica', director = 'makeCelulaDiretor', extra = 'makeCelulaExtra', length = 'makeCelulaDuracao', }
function Faixa.new(dados) local self = setmetatable({}, Faixa) Validacao.init(self) for campo in pairs(Faixa.campos) do self[campo] = dados[campo] end self.number = assert(tonumber(self.number)) self:validarDuracao(self.length) return self end
function Faixa:getCreditoLetra() return self.lyrics end
function Faixa:getCreditoMusica() return self.music end
function Faixa:getCreditoEscritor() return self.writer end
function Faixa:getCreditoDiretor() return self.director end
function Faixa:getCampoExtra() return self.extra end
function Faixa.makeCelulaSimples(wikitexto) return mw.html.create('td') :wikitext(wikitexto or cfg.blank_cell) end
function Faixa:makeCelulaNumero() 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 Faixa:makeCelulaTitulo() local celula = mw.html.create('td') celula:wikitext( self.title and string.format(cfg.track_title, self.title) or cfg.untitled ) if self.note then celula:wikitext(string.format(cfg.note, self.note)) end return celula end
function Faixa:makeCelulaEscritor() return Faixa.makeCelulaSimples(self.writer) end
function Faixa:makeCelulaLetra() return Faixa.makeCelulaSimples(self.lyrics) end
function Faixa:makeCelulaMusica() return Faixa.makeCelulaSimples(self.music) end
function Faixa:makeCelulaDiretor() return Faixa.makeCelulaSimples(self.director) end
function Faixa:makeCelulaExtra() return Faixa.makeCelulaSimples(self.extra) end
function Faixa:makeCelulaDuracao() return mw.html.create('td') :addClass('tracklist-length') :wikitext(self.length or cfg.blank_cell) end
function Faixa:exportarLinha(colunas, ehPar) local linha = mw.html.create('tr') if ehPar then linha:addClass('tracklist-even') end for _, coluna in ipairs(colunas) do local metodo = Faixa.metodosCelula[coluna] if metodo then linha:node(self[metodo](self)) end end return linha end
-- Classe ListaDeFaixas
local ListaDeFaixas = {} ListaDeFaixas.__index = ListaDeFaixas addMixin(ListaDeFaixas, Validacao)
ListaDeFaixas.campos = { headline = true, collapsed = true, width = true, title_width = true, writing_width = true, lyrics_width = true, music_width = true, extra_width = true, extra_column = true, blank_row = true, total_length = true, all_writing = true, all_lyrics = true, all_music = true, lyrics_credit = true, music_credit = true, writing_credit = true, direction_credit = true, category = true, }
ListaDeFaixas.camposObsoletos = cfg.deprecated_track_listing_field_names or {}
function ListaDeFaixas.new(dados) local self = setmetatable({}, ListaDeFaixas) Validacao.init(self)
for campoObsoleto in pairs(ListaDeFaixas.camposObsoletos) do if dados[campoObsoleto] then self:adicionarCategoria(cfg.deprecated_parameter_category) break end end
if dados.total_length then self:validarDuracao(dados.total_length) end
for campo in pairs(ListaDeFaixas.campos) do self[campo] = dados[campo] end
self.exibirCategorias = yesno(self.category) ~= false self.category = nil
self.collapsed = ( self.collapsed == 'sim' or self.collapsed == 'yes' or yesno(self.collapsed) == true )
self.forcaEscritor = yesno(self.writing_credit) == true self.forcaLetra = yesno(self.lyrics_credit) == true self.forcaMusica = yesno(self.music_credit) == true self.forcaDiretor = yesno(self.direction_credit) == true
self.faixas = {} for _, dadosFaixa in ipairs(dados.faixas or {}) do table.insert(self.faixas, Faixa.new(dadosFaixa)) end
do local opt = {}
if self.forcaEscritor then opt.writer = true else if self.forcaLetra then opt.lyrics = true end if self.forcaMusica then opt.music = true end end if self.forcaDiretor then opt.director = true end
local verificar = { writer = not opt.writer, lyrics = not opt.lyrics, music = not opt.music, director = not opt.director, extra = true, }
for _, faixa in ipairs(self.faixas) do if verificar.writer and faixa:getCreditoEscritor() then opt.writer = true verificar.writer = false opt.lyrics = nil opt.music = nil verificar.lyrics = false verificar.music = false end if verificar.lyrics and faixa:getCreditoLetra() then opt.lyrics = true; verificar.lyrics = false end if verificar.music and faixa:getCreditoMusica() then opt.music = true; verificar.music = false end if verificar.director and faixa:getCreditoDiretor() then opt.director = true; verificar.director = false end if verificar.extra and faixa:getCampoExtra() then opt.extra = true; verificar.extra = false end
local continua = false for _, v in pairs(verificar) do if v then continua = true; break end end if not continua then break end end
self.colunasOpcionais = opt end
return self end
function ListaDeFaixas:fazerIntro() 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 ListaDeFaixas:renderizarCategorias() if not self.exibirCategorias or mw.title.getCurrentTitle().namespace ~= 0 then return end
local ret = {} local function addCat(cat) table.insert(ret, string.format(, cat)) end
for _, cat in ipairs(self:obterCategorias()) do addCat(cat) end for _, faixa in ipairs(self.faixas) do for _, cat in ipairs(faixa:obterCategorias()) do addCat(cat) end end
return table.concat(ret) end
function ListaDeFaixas:renderizarAvisos() if not cfg.show_warnings then return end
local ret = {} local function addAviso(msg) table.insert(ret, string.format(cfg.track_listing_error, msg)) end
for _, aviso in ipairs(self:obterAvisos()) do addAviso(aviso) end for _, faixa in ipairs(self.faixas) do for _, aviso in ipairs(faixa:obterAvisos()) do addAviso(aviso) end end
return table.concat(ret, '
')
end
function ListaDeFaixas:__tostring() local raiz = mw.html.create('div') :addClass('track-listing') if self.collapsed then raiz:css('border', 'var(--border-color-base, #aaa) 1px solid') raiz:css('padding', '3px') else raiz:css('padding', '4px') end
local intro = self:fazerIntro() if intro then raiz:tag('p'):wikitext(intro):done() end
local tabela = mw.html.create('table') tabela:addClass('tracklist') if self.collapsed then tabela:addClass('collapsible'):addClass('collapsed') tabela:css('width', self.width or '100%') else if self.width then tabela:css('width', self.width) end end
local colunas = {'number', 'title'}
if self.colunasOpcionais.writer then colunas[#colunas + 1] = 'writer' else if self.colunasOpcionais.lyrics then colunas[#colunas + 1] = 'lyrics' end if self.colunasOpcionais.music then colunas[#colunas + 1] = 'music' end end if self.colunasOpcionais.director then colunas[#colunas + 1] = 'director' end if self.colunasOpcionais.extra then colunas[#colunas + 1] = 'extra' end colunas[#colunas + 1] = 'length'
local nColunas = #colunas local nColunasOpcionais = nColunas - 3 local larguraTitulo
if nColunas >= 5 then larguraTitulo = 40 elseif nColunas >= 4 then larguraTitulo = 60 else larguraTitulo = 100 end
local larguraOpcional = nColunasOpcionais > 0 and (math.floor((100 - larguraTitulo) / nColunasOpcionais) .. '%') or '0%' larguraTitulo = larguraTitulo .. '%'
-- Linha de título/colapso (equivalente ao original)
-- Exibida quando há |topo= ou |fechado=sim; o mw-collapsible ancora o toggle
-- no desta linha, deixando o cabeçalho de colunas intacto.
if self.headline or self.collapsed then local linhaTopo = tabela:tag('tr') linhaTopo:tag('th') :addClass('tlheader mbox-text') :attr('colspan', nColunas) :attr('scope', 'col') :css('text-align', 'left') :css('background-color', 'var(--background-color-base, #fff)') :css ('color', 'var(--color-base, black)') :wikitext(self.headline or cfg.track_listing) linhaTopo:tag('td') :addClass('mbox-empty-cell') end
-- Linha de cabeçalho das colunas local linhaCabecalho = tabela:tag('tr')
linhaCabecalho:tag('th') :addClass('tracklist-number-header') :attr('scope', 'col') :tag('abbr') :attr('title', cfg.number) :wikitext(cfg.number_abbr)
linhaCabecalho:tag('th') :attr('scope', 'col') :css('width', self.title_width or larguraTitulo) :css('text-align', 'left') :wikitext(cfg.title)
local function adicionarCabecalhoOpcional(campo, textoCabecalho, largura) if self.colunasOpcionais[campo] then linhaCabecalho:tag('th') :attr('scope', 'col') :css('width', largura or larguraOpcional) :css('text-align', 'left') :wikitext(textoCabecalho) end end
adicionarCabecalhoOpcional('writer', cfg.writer, self.writing_width) adicionarCabecalhoOpcional('lyrics', cfg.lyrics, self.lyrics_width) adicionarCabecalhoOpcional('music', cfg.music, self.music_width) adicionarCabecalhoOpcional('director', cfg.director or 'Diretor(es)', nil) adicionarCabecalhoOpcional('extra', self.extra_column or cfg.extra, self.extra_width)
linhaCabecalho:tag('th') :addClass('tracklist-length-header') :attr('scope', 'col') :wikitext(cfg.length)
-- Célula vazia no cabeçalho de colunas para alinhar com o mbox-empty-cell do topo if self.headline or self.collapsed then linhaCabecalho:tag('td') :addClass('mbox-empty-cell') end
local separadorInserido = false for i, faixa in ipairs(self.faixas) do if not faixa.number then self:adicionarAviso( string.format(cfg.no_param_number), cfg.input_error_category ) end local blankRowNum = tonumber(self.blank_row)
if self.blank_row and not blankRowNum then self:adicionarAviso( string.format(cfg.no_param_number), cfg.input_error_category ) end
if blankRowNum and not separadorInserido and faixa.number >= blankRowNum then local linhaSep = tabela:tag('tr'):addClass('tracklist-separator') linhaSep:tag('td') :attr('colspan', nColunas) :css('height', '4px') :css('padding', '0') separadorInserido = true end
tabela:node(faixa:exportarLinha(colunas, i % 2 == 0)) end
if self.total_length then tabela:tag('tr') :addClass('tracklist-total-length') :tag('th') :attr('colspan', nColunas - 1) :attr('scope', 'row') :tag('span') :wikitext(cfg.total_length) :done() :done() :tag('td') :wikitext(self.total_length) end
raiz:node(tabela) raiz:wikitext(self:renderizarAvisos()) raiz:wikitext(self:renderizarCategorias())
return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = 'Módulo:Lista de faixas/styles.css' } } .. tostring(raiz) end
-- Exportações
local p = {}
function p._main(args) local dados = {} local faixas = {}
for k, v in pairs(args) do if type(k) == 'string' then
local prefixo, num = k:match('^(%D.-)(%d+)$')
local prefixoTraduzido = prefixo and (fieldMapping[prefixo] or prefixo)
if prefixoTraduzido and Faixa.campos[prefixoTraduzido] and (num == '0' or num:sub(1, 1) ~= '0') then num = tonumber(num) faixas[num] = faixas[num] or {} faixas[num][prefixoTraduzido] = v else dados[fieldMapping[k] or k] = v end end end
dados.faixas = (function(t) local ret = {} for num, dadosFaixa in pairs(t) do dadosFaixa.number = num table.insert(ret, dadosFaixa) end table.sort(ret, function(a, b) return a.number < b.number end) return ret end)(faixas)
return tostring(ListaDeFaixas.new(dados)) end
function p.main(frame) local args = require('Module:Arguments').getArgs(frame, { parentOnly = true, }) return p._main(args) end
return p