Vés al contingut

Mòdul:Series overview

De Viki.cat



-- This module implements {{Series overview}}.

require('strict')
local yesno = require('Module:Yesno')
local HTMLcolor = mw.loadData('Module:Color contrast/colors')
local tableEmptyCellModule = require('Module:Table empty cell')

--------------------------------------------------------------------------------
-- SeriesOverview class
-- The main class.
--------------------------------------------------------------------------------

local SeriesOverview = {}

function SeriesOverview.cellspan(SeasonEntries, SeasonEntries_ordered, key, cell, multipart, setspan)
    if setspan ~= nil then return setspan end
    
    local spanlength = 1
    
 local firstEntry = SeasonEntries[SeasonEntries_ordered[cell]]
    if (key == 'network' or key == 'catnetwork') and firstEntry[key .. 'A'] and not firstEntry[key .. 'B'] then
        spanlength = 2
    end
    
    for i = cell+1, #SeasonEntries_ordered do
        local entry = SeasonEntries[SeasonEntries_ordered[i]]
        -- Split season, then regular season
        if entry.startA or entry.releasedA then
            if not entry[key..'A'] then spanlength = spanlength + 1
            else break end
            if not entry[key..'B'] then spanlength = spanlength + 1
            else break end
        else
            if not entry[key] and (key == 'network' or key == 'catnetwork' or ((string.sub(key,0,7) == 'postaux' or string.sub(key,0,3) == 'aux') and (not entry.special or entry.episodes)) or (string.sub(key,0,4) == 'info') and multipart) then
                spanlength = spanlength + 1
            else break end
        end
    end
    return spanlength
end

-- Sorting function
function SeriesOverview.series_sort(op1, op2)
    local n1,s1 = string.match(op1,"(%d+)(%a*)")
    local n2,s2 = string.match(op2,"(%d+)(%a*)")
    local n1N,n2N = tonumber(n1),tonumber(n2)

    if n1N == n2N then
        return s1 < s2
    else
        return n1N < n2N
    end
end

-- Function to add either text or {{N/a}} to cell
function SeriesOverview.season_cell(text)
    local cell
    
    if string.find(text or '', 'table-na', 0, true) ~= nil then
        cell = SeriesOverview.series_attributes(text)
    else
        cell = mw.html.create('td'):wikitext(text)
    end
    
    return cell
end

-- Allow usages of {{N/A}} cells
function SeriesOverview.series_attributes(infoParam)
    local infoCell = mw.html.create('td')

    -- Separa atributs del contingut pel primer ' | ' (o '|' amb espais variables)
    local pipePos = string.find(infoParam, '%s*|%s*')
    local attrStr, content
    if pipePos then
        attrStr  = string.sub(infoParam, 1, pipePos - 1)
        content  = string.match(infoParam, '%s*|%s*(.*)')
    else
        attrStr  = infoParam
        content  = ''
    end

    -- Parseja parells atribut="valor" de la cadena d'atributs
    local attrMatch = '([%a][%a%d-]*)="([^"]*)"'
    for a, b in string.gmatch(attrStr, attrMatch) do
        infoCell:attr(a, b)
    end

    infoCell:wikitext(content)
    return infoCell
end

function SeriesOverview.endtable()
    return "</table></div>"
end

local function firstIndexWithKey(SeasonEntries, SeasonEntries_ordered, keyPrefix)
    for i = 1, #SeasonEntries_ordered do
        local entry = SeasonEntries[SeasonEntries_ordered[i]]
        for j0 = string.byte('A')-1, string.byte('Z') do
            local j = j0 == string.byte('A')-1 and '' or string.char(j0)
            if entry[keyPrefix .. j] then return i end
        end
    end
    return nil
end

function SeriesOverview.new(frame, args)
    args = args or {}
    
    local initialArticle = args['1'] or ''
    local categories = ''
    local title = mw.title.getCurrentTitle()

    -- Create series overview table
    local root = mw.html.create((args.multiseries or not args.series) and 'table' or '')
    local cellPadding = '0 8px'
    local basePadding = '0.2em 0.4em'

    root
        :addClass('wikitable')
        :addClass('plainrowheaders')
        :css('text-align', 'center')
        :css('height', '1px')
        :css('display', 'table')
		:css('white-space', 'nowrap')
    
    -- Sortable
    if args.sortable then
        root:addClass('sortable');
    end
    
    -- "Series overview" ID
    if args.id then
        root:attr('id', 'Series overview')
    end
    
    -- Width
    if args.width then
        root:css('width', args.width)
    end

    -- Caption
    if args.caption then
        root:tag('caption'):wikitext(frame:expandTemplate{title='Screen reader-only',args={args.caption}})
    end

    -- Extract seasons info and place into a 3D array
    local SeasonEntries = {}
    for k,v in pairs(args) do
        local str, num, str2 = string.match(k, '([^%d]*)(%d*)(%a*)')
        if tonumber(k) ~= 1 and num ~= '' then 
            -- Special
            local special = false
            if string.sub(str2,1,1) == 'S' then
                special = true
                num = num .. str2
                str2 = ''
            end
            -- Add to entries, create if necessary
            if not SeasonEntries[num] then
                SeasonEntries[num] = {}
            end
            SeasonEntries[num][str .. str2] = v
            if special then
                SeasonEntries[num]['special'] = 'y'
            end
        end
    end

    -- Order table by season number
    local SeasonEntries_ordered = {}
    for k in pairs(SeasonEntries) do
        table.insert(SeasonEntries_ordered, k)
    end
    table.sort(SeasonEntries_ordered,SeriesOverview.series_sort)

    if #SeasonEntries_ordered == 0 and not args.multiseries then return '' end

    local firstRow = args.multiseries and {} or SeasonEntries[SeasonEntries_ordered[1]]
	local firstNetworkIndex = firstIndexWithKey(SeasonEntries, SeasonEntries_ordered, 'network')
	local firstCatNetworkIndex = firstIndexWithKey(SeasonEntries, SeasonEntries_ordered, 'catnetwork')


    -- Colspan calculation for information cells (0 = no info set)
    local numAuxCells = 0
    local numInfoCells = 0
    for i = string.byte('A'), string.byte('Z') do
        local param = 'info' .. string.char(i)
        if args[param] then numInfoCells = numInfoCells + 1 end
    end
    
    -- Use of colors and network
    local noColors = true
    local setNetwork = false
    local allReleased = true
    local noEndDates = true
    local hasCatDates = false
    local allCatReleased = true
    local hasCatNetwork = false
    
    if (args.multiseries and args.network) then setNetwork = true end
    if (args.multiseries and not args.allreleased) then allReleased = false end
    if args.multiseries and type(args.multiseries) == 'string' and string.find(args.multiseries, '<!--hasCatDates-->', 1, true) then
        hasCatDates = true
    end
    if args.multiseries and type(args.multiseries) == 'string' and string.find(args.multiseries, '<!--notAllCatReleased-->', 1, true) then
        allCatReleased = false
    end
    if args.multiseries and type(args.multiseries) == 'string' and string.find(args.multiseries, '<!--hasCatNetwork-->', 1, true) then
        hasCatNetwork = true
    end
    
    for i = 1, #SeasonEntries_ordered do
        local _, entry = SeasonEntries_ordered[i], SeasonEntries[SeasonEntries_ordered[i]]
        for j0 = string.byte('A')-1, string.byte('Z') do
            local j = string.char(j0)
            if j0 == string.byte('A')-1 then j = '' end
            if entry['color' .. j] then noColors = false end
            if entry['network' .. j] then setNetwork = true end
            if entry['start' .. j] then allReleased = false end
            if entry['end' .. j] then noEndDates = false end
            if entry['catstart'..j] or entry['catstart'] or entry['catreleased'..j] or entry['catreleased'] or entry['catend'..j] or entry['catend'] then hasCatDates = true end
            if entry['catstart'..j] or entry['catstart'] or entry['catend'..j] or entry['catend'] then allCatReleased = false end
            if entry['catnetwork' .. j] then hasCatNetwork = true end
        end
    end

    -- A child series (args.series) must always render in MODE 2 (two cat columns)
    -- when it has any cat dates, so it stays consistent with the parent table
    -- which may have other children with catstart (notAllCatReleased).
    if args.series and hasCatDates then
        allCatReleased = false
    end
    
    if title.namespace == 0 and not args.multiseries and not allReleased and noEndDates then
        categories = categories .. '[[Category:Articles using Template:Series overview with deprecated start-parameter format]]'
    end
    
    -- Top info cell
    -- @ = string.char(64), A = string.char(65)
    local topInfoCell = numInfoCells > 0 and string.char(numInfoCells + (string.byte('A') - 1)) or '@'
    
    -- Networks are included if the very first entry sets the first network
    local networkTransclude = args.network_transclude
    if (networkTransclude == 'onlyinclude' and title.fullText == initialArticle) or (networkTransclude == 'noinclude' and title.fullText ~= initialArticle) then
        setNetwork = false
    end

    -- Headers
    do
        if args.multiseries or not args.series then
            local headerRow = root:tag('tr')
            headerRow
                :css('text-align', 'center')
            
            -- Base series/season content on the format of the first date; Series = D M Y, Season = M D, Y
            local matchDMY = false
            local thisStart = firstRow.start or firstRow.startA or firstRow.released or firstRow.releasedA
            if thisStart then
                if string.match(thisStart:gsub("&nbsp;"," "), '(%d+)%s(%a+)%s(%d+)') then
                    matchDMY = true
                end
            end
            
            -- Multiple series header
            if args.multiseries then
                headerRow:tag('th')
                    :attr('scope', 'col')
                    :css('padding', cellPadding)
                    :attr('rowspan', not allReleased and 2 or nil)
                    :wikitext('Sèrie')
            end
            
            -- Season header
            headerRow:tag('th')
                :attr('scope', 'col')
                :attr('rowspan', not allReleased and 2 or nil)
                :css('min-width', '50px')
                :css('padding', cellPadding)
                :wikitext(args.seriesT or args.seasonT or (matchDMY and 'Sèrie') or 'Temporada')
            
            for _a = 1, 3 do
                if _a == 1 or _a == 3 then
                    -- Aux headers
                    local auxtype = (_a == 3 and 'post' or '') .. 'aux'
                    for i = string.byte('A'), string.byte('Z') do
                        local param = auxtype .. string.char(i)
                        if args[param] then
                            numAuxCells = numAuxCells + 1
                            headerRow:tag('th')
                                :attr('scope', 'col')
                                :css('padding', cellPadding)
                                :attr('rowspan', not allReleased and 2 or nil)
                                :wikitext(args[param])
                        end
                    end
                end
                
                if _a == 2 then
                    -- Episodes header
                    headerRow:tag('th')
                        :attr('scope', 'col')
                        :attr('rowspan', not allReleased and 2 or nil)
                        :attr('colspan', 2)
                        :css('padding', cellPadding)
                        :wikitext(args.episodesT or 'Episodis')
                end
            end

            -- Originally aired header
            local OriginallyColspan = (not allReleased and setNetwork) and 3 or 2
            local countryBlurb = ''
            if args.country then
                countryBlurb = ' (' .. args.country .. ')'
            end
            headerRow:tag('th')
                :attr('scope', (setNetwork and allReleased) and 'col' or 'colgroup')
                :attr('colspan', OriginallyColspan)
                :wikitext('Data d&#39;emissió original' .. countryBlurb)
            
            -- Catalan aired header
            if hasCatDates then
                local catColspanVal = allCatReleased and 1 or OriginallyColspan
                headerRow:tag('th')
                    :attr('scope', allCatReleased and 'col' or 'colgroup')
                    :attr('colspan', catColspanVal)
                    :attr('rowspan', (not allReleased and allCatReleased) and 2 or 1)
                    :wikitext('Data d&#39;emissió a Catalunya')
            end
            
            -- Network subheader for released series
            if setNetwork and allReleased then
                headerRow:tag('th')
                    :attr('scope', 'col')
                    :attr('rowspan', not allReleased and 2 or nil)
                    :css('padding', cellPadding)
                    :wikitext('Canal')
            end
            
            -- Information headers
            if topInfoCell ~= '@' then
                for i = string.byte('A'), string.byte(topInfoCell) do
                    local param = 'info' .. string.char(i)
                    local infoTransclude = args[param .. '_transclude']
                    if (infoTransclude == 'onlyinclude' and title.fullText == initialArticle) or (infoTransclude == 'noinclude' and title.fullText ~= initialArticle) then else
                        headerRow:tag('th')
                            :attr('scope', 'col')
                            :attr('rowspan', not allReleased and 2 or nil)
                            :css('padding', cellPadding)
                            :wikitext(args[param])
                    end
                end
            end
            
            -- Subheader row
            local subheaderRow = mw.html.create('tr')

            if not allReleased then
                -- First aired subheader
                subheaderRow:tag('th')
                    :attr('scope', 'col')
                    :wikitext('Primera emissió')

                -- Last aired subheader
                subheaderRow:tag('th')
                    :attr('scope', 'col')
                    :wikitext('Última emissió')
                
                -- Network subheader for aired series
                if setNetwork then
                    subheaderRow:tag('th')
                        :attr('scope', 'col')
                        :css('padding', cellPadding)
                        :wikitext('Canal')
                end
                
                -- Catalan date subheaders
                if hasCatDates and not allCatReleased then
                    subheaderRow:tag('th')
                        :attr('scope', 'col')
                        :wikitext('Primera emissió')
                    subheaderRow:tag('th')
                        :attr('scope', 'col')
                        :wikitext('Última emissió')
                    if hasCatNetwork then
                        subheaderRow:tag('th')
                            :attr('scope', 'col')
                            :wikitext('Canal')
                    end
                end
            end
        
            -- Check for scenarios with an empty subheaderRow
            if not allReleased then
    		root:node(subheaderRow)
			end
        end
    end

    -- Season rows
    do
        if args.multiseries then
            -- Multi series individual entries
            if args.multiseries ~= "y" then
                root:node(args.multiseries)
            end
        else
            -- One row entries, only categorized in the mainspace
            if title.namespace == 0 and #SeasonEntries == 1 then
                categories = categories .. '[[Category:Articles using Template:Series overview with only one row]]'
            end
        
            -- Determine number of rows in the whole overview
            local SeasonEntriesRows = 0
            for X = 1, #SeasonEntries_ordered do
                local _, entry = SeasonEntries_ordered[X], SeasonEntries[SeasonEntries_ordered[X]]
                local splits = 0
                for i = string.byte('A'), string.byte('Z') do
                    local paramS = 'start' .. string.char(i)
                    local paramR = 'released' .. string.char(i)
                    if entry[paramS] or entry[paramR] then splits = splits + 1 end
                end
                if splits == 0 then splits = 1 end
                SeasonEntriesRows = SeasonEntriesRows + splits
            end
            
            for X = 1, #SeasonEntries_ordered do
                local season, entry = SeasonEntries_ordered[X], SeasonEntries[SeasonEntries_ordered[X]]
                
                -- Determine number of splits in a season
                local splits = 0
                for i = string.byte('A'), string.byte('Z') do
                    local paramS = 'start' .. string.char(i)
                    local paramR = 'released' .. string.char(i)
                    if entry[paramS] or entry[paramR] then splits = splits + 1 end
                end
                local splitSeason = (splits > 1)
                
                -- Season rows for each season
                for k0 = string.byte('A')-1, string.byte('Z') do
                    local k = string.char(k0)
                    if k0 == string.byte('A')-1 then k = '' end
                    
                    -- Part header
                    if entry.part and k == '' then
                        root:node(entry.part)
                    end
                    
                    -- New season row
                    local seasonRow = (entry['start' .. k] or entry['released' .. k]) and root:tag('tr') or mw.html.create('tr')
                    seasonRow:css('height', '100%')
                    
                    local borderBottom = '2px solid #8D939A'
                    
                    -- Series name for group overviews
                    if X == 1 and (k == '' or k == 'A') and args.series then
                        seasonRow:tag('th')
                            :attr('scope', 'row')
                            :attr('rowspan', SeasonEntriesRows)
                            :wikitext(args.series)
                            :css('border-bottom', borderBottom)
                    end
                    if X == #SeasonEntries_ordered and args.series then
                        seasonRow:css('border-bottom', borderBottom)
                    end
                    
                    -- Season number link, included only in the first row
                    local cellColor
                    if not noColors then
                        if entry['color' .. k] ~= nil and HTMLcolor[entry['color' .. k]] == nil then 
                            entry['color' .. k] = '#'..(mw.ustring.match(entry['color' .. k], '^[%s#]*([a-fA-F0-9]*)[%s]*$') or '')
                        end
                        if splitSeason then
                            if entry.color then
                                cellColor = entry.color
                            else
                                cellColor = "linear-gradient(to bottom"
                                for i = 0, splits-1 do
                                    local _color = 'color' .. string.upper(string.char(i+97))
                                    cellColor = cellColor .. ", " .. (entry[_color] or 'rgba(0,0,0,0)') .. " " .. (100/splits *i) .. "%"
                                                          .. ", " .. (entry[_color] or 'rgba(0,0,0,0)') .. " " .. (100/splits *(i+1)) .. "%"
                                end
                                cellColor = cellColor .. ")"
                            end
                        else
                            cellColor = entry['color' .. k]
                        end
                    end
                    
                    if k == '' or k == 'A' then
                        local colorWidth = '14px'
                        
                        -- Overall table cell
                        local cellRow = mw.html.create(args.series and 'td' or 'th')
                            :attr('scope', splitSeason and 'rowgroup' or 'row')
                            :attr('rowspan', splitSeason and splits or nil)
                            :attr('colspan', entry.special and not entry.episodes and 3+numAuxCells or 1)
                            :css('height', 'inherit')
                            :css('padding', '0')
                            
                        -- Overall inner span
                        local spanRow = mw.html.create('span')
                        spanRow
                            :css('width', '100%')
                            :css('text-align', 'center')
                            :css('float', 'left')
                            :css('width', '100%')
                            :css('height', '100%')
                        
                        -- Coloured nested span
                        local spanRow2 = mw.html.create('span')
                        spanRow2
                            :css('width', colorWidth)
                            :css('background', cellColor)
                            :css('color', '#202122')
                            :css('height', '100%')
                            :css('float', 'left')
                            :css('box-shadow', 'inset -1px 0 #A2A9B1')
                        
                        -- Link nested span
                        local spanRow3 = mw.html.create('span')
                        spanRow3
                            :css('height', '100%')
                            :css('width', not noColors and 'calc(100% - ' .. colorWidth .. ' - 8px)' or '100%')
                            :css('display', 'flex')
                            :css('vertical-align', 'middle')
                            :css('align-items', 'center')
                            :css('justify-content', 'center')
                            :css('padding', not noColors and '0 4px' or '')
                            
                        local spanRow4 = mw.html.create('span')
                        spanRow4
                            :addClass('nowrap')
                        
                        -- Coloured span first into the overall span
                        if not noColors then
                            spanRow:node(spanRow2)
                        end
                        -- Link into the blank span
                        spanRow4:wikitext((entry.link and '[[' .. entry.link .. '|' .. (entry.linkT or season) .. ']]' or (entry.linkT or season)) .. (entry.linkR or ''))
                        -- Blank span into the Link nested span
                        spanRow3:node(spanRow4)
                        -- Link span second into the overall span
                        spanRow:node(spanRow3)
                        -- Overall span into the actual cell
                        cellRow:node(spanRow)
                        -- The actual cell into the season row
                        seasonRow:node(cellRow)
                    end
                    
                    for _a = 1, 3 do
                        if _a == 1 or _a == 3 then
                            -- Aux headers
                            local auxtype = (_a == 3 and 'post' or '') .. 'aux'
                            -- Aux cells
                            for i = string.byte('A'), string.byte('Z') do
                                local param = auxtype .. string.char(i)
                                if entry[param .. k] then
                                    local thisCell = SeriesOverview.season_cell(entry[param .. k])
                                        :attr('scope', 'col')
                                        :attr('rowspan', SeriesOverview.cellspan(SeasonEntries, SeasonEntries_ordered, param, X, (args.series and true or false), entry[param .. k .. 'span'] or nil))
                                        :css('padding', cellPadding)
                                    seasonRow:node(thisCell)
                                end
                            end
                        end
                        
                        if _a == 2 then
                            -- Episodes counts
                            if ((splitSeason and k == 'A' and entry.episodes ~= 'hide') or not splitSeason) then
                                if entry.episodes then
                                    local thisCell = SeriesOverview.season_cell(entry.episodes)
                                        :attr('colspan', (splitSeason and entry.episodesA ~= 'hide') and 1 or 2)
                                        :attr('rowspan', splitSeason and splits or nil)
                                    seasonRow:node(thisCell)
                                elseif not entry.special then
                                    local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                                    infoCell
                                        :attr('colspan', (splitSeason and entry.episodesA ~= 'hide') and 1 or 2)
                                        :attr('rowspan', splitSeason and splits or nil)
                                    seasonRow:node(infoCell)
                                end
                            end
                            if splitSeason and entry.episodesA ~= 'hide' then
                                if entry['episodes' .. k] then
                                    local thisCell = SeriesOverview.season_cell(entry['episodes' .. k])
                                        :attr('colspan', (entry.episodes ~= 'hide') and 1 or 2)
                                    seasonRow:node(thisCell)
                                else
                                    local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                                        :attr('colspan', (entry.episodes ~= 'hide') and 1 or 2)
                                    seasonRow:node(infoCell)
                                end
                            end
                        end
                    end
                    
                    -- Start date
                    if entry['start' .. k] or entry['released' .. k] then
                        if title.namespace == 0 and entry.special and entry['start' .. k] and not entry['end' .. k] then
                            categories = categories .. '[[Category:Articles using Template:Series overview with deprecated start-parameter format]]'
                        end
                        local thisCell = SeriesOverview.season_cell((not allReleased or entry['end' .. k]) and entry['start' .. k] or entry['released' .. k])
                        thisCell:attr('colspan', (entry['released' .. k] or allReleased) and 2 or 1)
                            :css('padding',basePadding)
                        seasonRow:node(thisCell)
                    else
                        local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                        infoCell:css('padding',basePadding)
                        seasonRow:node(infoCell)
                    end
                    
                    -- End date
                    local canIncludeEnd = yesno(not allReleased and not entry['released' .. k] and ((entry.special and entry['end' .. k]) or not entry.special))
					if canIncludeEnd then
                        if entry['end' .. k] then
                            local thisCell = SeriesOverview.season_cell(entry['end' .. k])
                                :css('padding',cellPadding)
                            seasonRow:node(thisCell)
                        else
                            local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                            infoCell:css('padding',cellPadding)
                            seasonRow:node(infoCell)
                        end
                    end

                    ---------------------------------------------------------
                    -- NETWORK (VERSIÓ DEFINITIVA)
                    ---------------------------------------------------------

                    if setNetwork then
                        -- Detectar si la temporada té ALGUNA fila amb network
                        local seasonHasNetwork = false
                        for _, key in ipairs({'', 'A', 'B', 'C', 'D'}) do
                            if entry['network' .. key] then
                                seasonHasNetwork = true
                                break
                            end
                        end

                        if entry['network' .. k] then
                            -- Cel·la real de network
                            local thisCell = SeriesOverview.season_cell(entry['network' .. k])
                                :attr('rowspan', SeriesOverview.cellspan(
                                    SeasonEntries, SeasonEntries_ordered,
                                    'network', X,
                                    (args.series and true or false),
                                    entry['network' .. k .. 'span'] or nil
                                ))
                            seasonRow:node(thisCell)

                        elseif k == '' or k == 'A' then
                            -- Comprovar si una fila anterior ja cobreix aquesta via rowspan
                            local coveredByRowspan = false

                            for prevX = 1, X-1 do
                                local prevEntry = SeasonEntries[SeasonEntries_ordered[prevX]]
                                if prevEntry['network' .. k] then
                                    local span = SeriesOverview.cellspan(
                                        SeasonEntries, SeasonEntries_ordered,
                                        'network', prevX,
                                        (args.series and true or false),
                                        prevEntry['network' .. k .. 'span'] or nil
                                    )
                                    if prevX + span > X then
                                        coveredByRowspan = true
                                        break
                                    end
                                end
                            end

                            -- Casos on cal crear cel·la buida:
                            -- 1) La pròpia temporada té algun network (altres subfiles)
                            -- 2) És una temporada abans o igual a la primera amb network (cas 0S)
                            local mustCreateEmpty =
                                seasonHasNetwork or
                                (firstNetworkIndex and X <= firstNetworkIndex)

                            if not coveredByRowspan and mustCreateEmpty then
                                local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                                seasonRow:node(infoCell)
                            end
                        end
                    end

                    ---------------------------------------------------------
                    -- CATALAN AIRED DATES (VERSIÓ DEFINITIVA)
                    ---------------------------------------------------------

                    if hasCatDates then
                        local catStart = entry['catstart' .. k] or entry['catstart']
                        local catEnd   = entry['catend'   .. k] or entry['catend']
                        local catRel   = entry['catreleased' .. k] or entry['catreleased']

                        -- Hi ha alguna data catalana en aquesta fila?
                        local rowHasCatDates2 = catStart or catEnd or catRel

                        ---------------------------------------------------------
                        -- LÒGICA DE COL·LAPSE (colspan=2)
                        ---------------------------------------------------------
                        local catColspan2 = false

                        -- Cas general: col·lapsar si només hi ha catreleased
                        if not allCatReleased
                            and catRel
                            and not catStart
                            and not catEnd
                        then
                            catColspan2 = true
                        end

                        -- Cas: cap data catalana en aquesta fila → una sola cel·la "No emès"
                        if not allCatReleased and not rowHasCatDates2 then
                            catColspan2 = true
                        end

                        ---------------------------------------------------------
                        -- MODE 1: UNA SOLA COLUMNA (allCatReleased = true)
                        ---------------------------------------------------------
                        if allCatReleased then
                            if catRel or catStart then
                                local catCell = SeriesOverview.season_cell(catRel or catStart)
                                    :css('padding', basePadding)
                                seasonRow:node(catCell)
                            else
                                local noEmesCell = mw.html.create('td')
                                    :css('padding', basePadding)
                                    :wikitext('No emès')
                                seasonRow:node(noEmesCell)
                            end

                        ---------------------------------------------------------
                        -- MODE 2: DUES SUBCOLUMNES (Primera emissió / Última emissió)
                        ---------------------------------------------------------
                        else
                            if rowHasCatDates2 then
                                -- Primera subcolumna
                                local catStartCell = SeriesOverview.season_cell(catStart or catRel)
                                    :attr('colspan', catColspan2 and 2 or 1)
                                    :css('padding', basePadding)
                                seasonRow:node(catStartCell)

                                -- Segona subcolumna només si no hi ha col·lapse
                                if not catColspan2 then
                                    if catEnd then
                                        local catEndCell = SeriesOverview.season_cell(catEnd)
                                            :css('padding', basePadding)
                                        seasonRow:node(catEndCell)
                                    else
                                        local noEmesCell = mw.html.create('td')
                                            :css('padding', basePadding)
                                            :wikitext('No emès')
                                        seasonRow:node(noEmesCell)
                                    end
                                end
                            else
                                -- Cap data catalana: una sola cel·la "No emès" amb colspan=3
                                local noEmesCell = mw.html.create('td')
                                    :attr('colspan', 2)
                                    :css('padding', basePadding)
                                    :wikitext('No emès')
                                seasonRow:node(noEmesCell)
                            end
                        end
                    end

                    ---------------------------------------------------------
                    -- CATNETWORK (VERSIÓ DEFINITIVA)
                    ---------------------------------------------------------

                    if hasCatDates and hasCatNetwork then
                        -- Detectar si la temporada té ALGUNA fila amb catnetwork
                        local seasonHasCatNetwork = false
                        for _, key in ipairs({'', 'A', 'B', 'C', 'D'}) do
                            if entry['catnetwork' .. key] then
                                seasonHasCatNetwork = true
                                break
                            end
                        end

                        if entry['catnetwork' .. k] then
                            -- Cel·la real de catnetwork
                            local thisCell = SeriesOverview.season_cell(entry['catnetwork' .. k])
                                :attr('rowspan', SeriesOverview.cellspan(
                                    SeasonEntries, SeasonEntries_ordered,
                                    'catnetwork', X,
                                    (args.series and true or false),
                                    entry['catnetwork' .. k .. 'span'] or nil
                                ))
                            seasonRow:node(thisCell)

                        elseif k == '' or k == 'A' then
                            -- Comprovar si una fila anterior ja cobreix aquesta via rowspan
                            local coveredByRowspan = false

                            for prevX = 1, X-1 do
                                local prevEntry = SeasonEntries[SeasonEntries_ordered[prevX]]
                                if prevEntry['catnetwork' .. k] then
                                    local span = SeriesOverview.cellspan(
                                        SeasonEntries, SeasonEntries_ordered,
                                        'catnetwork', prevX,
                                        (args.series and true or false),
                                        prevEntry['catnetwork' .. k .. 'span'] or nil
                                    )
                                    if prevX + span > X then
                                        coveredByRowspan = true
                                        break
                                    end
                                end
                            end

                            -- Casos on cal crear cel·la buida:
                            -- 1) La pròpia temporada té algun catnetwork (altres subfiles)
                            -- 2) És una temporada abans o igual a la primera amb catnetwork (cas 0S)
                            local mustCreateEmpty =
                                seasonHasCatNetwork or
                                (firstCatNetworkIndex and X <= firstCatNetworkIndex)

                            if not coveredByRowspan and mustCreateEmpty then
                                local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                                seasonRow:node(infoCell)
                            end
                        end
                    end


                    -- Information
                    for i = string.byte('A'), string.byte(topInfoCell) do
                        local param0 = 'info' .. string.char(i)
                        local param = 'info' .. string.char(i) .. k
                        
                        local infoTransclude = args[param .. '_transclude']
                        if (infoTransclude == 'onlyinclude' and title.fullText == initialArticle) or (infoTransclude == 'noinclude' and title.fullText ~= initialArticle) then else
                            local infoParam = entry[param]
                            
                            if infoParam and splitSeason and k == '' and not entry[param .. 'A'] then
                                entry[param .. 'A'] = entry[param]
                                entry[param .. 'spanning'] = 'y'
                            end
                            
                            local rowspan = (entry[param0 .. 'spanning'] and splits) or
                                            (args.series and SeriesOverview.cellspan(SeasonEntries, SeasonEntries_ordered, param0, X, (args.series and true or false), entry[param0 .. 'span'] or nil))
                                            or nil
                            
                            if k == 'A' or (k ~= 'A' and not entry[param0 .. 'spanning']) then
                                -- Cells with {{N/A|...}} already expanded
                                if infoParam then
                                    if string.sub(infoParam,1,5) == 'style' then
                                        local infoCell = SeriesOverview.series_attributes(infoParam)
                                        infoCell:attr('rowspan', rowspan)
                                        seasonRow:node(infoCell)
                                    else
                                        -- Unstyled content info cell
                                        local thisCell = SeriesOverview.season_cell(infoParam)
                                            :attr('rowspan', rowspan)
                                        seasonRow:node(thisCell)
                                    end
                                else
                                    if not args.series then
                                        local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                                        infoCell:attr('rowspan', rowspan)
                                        seasonRow:node(infoCell)
                                    end
                                end
                            elseif not entry[param0 .. 'spanning'] then
                                if not args.series then
                                    local infoCell = SeriesOverview.series_attributes( tableEmptyCellModule._main({}) )
                                    infoCell:attr('rowspan', rowspan)
                                    seasonRow:node(infoCell)
                                end
                            end
                        end
                    end
                
                end -- End k0 string.byte
            end -- End 'for' SeasonEntries_ordered
        end -- End 'if' multiseries
    end -- End 'do' season rows
    
    local rootdiv
    if args.multiseries or not args.series then
        rootdiv = mw.html.create('div')
        rootdiv
            :css('display', 'block')
            :css('overflow-x', 'auto')
        rootdiv:node(root)
        rootdiv = tostring(rootdiv)
    else
        rootdiv = tostring(root)
    end
    
    if args.dontclose then 
        rootdiv = mw.ustring.gsub(rootdiv, "</div>", "")
        rootdiv = mw.ustring.gsub(rootdiv, "</table>", "")
    end

    -- If this is a child series with Catalan dates, inject markers for the parent
    if args.series and hasCatDates then
        rootdiv = rootdiv .. '<!--hasCatDates-->'
        if not allCatReleased then
            rootdiv = rootdiv .. '<!--notAllCatReleased-->'
        end
        if hasCatNetwork then
            rootdiv = rootdiv .. '<!--hasCatNetwork-->'
        end
    end

    return rootdiv .. categories
end

--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------

local p = {}

function p.main(frame)
    local args = require('Module:Arguments').getArgs(frame, {
        wrappers = 'Template:Series overview'
    })
    return SeriesOverview.new(frame, args)
end

function p._end()
    return SeriesOverview.endtable()
end

return p