Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:BSW/MainPage: Difference between revisions

From the only original and legitimate Battlestar Wiki: the free-as-in-beer, non-corporate, open-content encyclopedia, analytical reference, and episode guide on all things Battlestar Galactica. Accept neither subpar substitutes nor subpar clones.
Joe Beaudoin Jr. (talk | contribs)
Created page with "-- Module:BSW/MainPage -- Generates all main page HTML components via mw.html, -- bypassing the wikitext parser entirely. -- Usage: {{#invoke:BSW/MainPage|functionName|param=value|...}} local p = {} -- ── Shared helpers ──────────────────────────────────────────────── -- Safe attribute setter — skips nil/empty values local function attr( el, name, value ) if value..."
 
Joe Beaudoin Jr. (talk | contribs)
No edit summary
Line 1: Line 1:
-- Module:BSW/MainPage
-- Module:BSW/MainPage
-- Generates all main page HTML components via mw.html,
-- Generates main page HTML using raw string concatenation.
-- bypassing the wikitext parser entirely.
-- mw.html is NOT used because its output is passed through
-- MediaWiki's HTML sanitizer which strips <button> and
-- <a class="..."> elements.
-- Instead we build raw HTML strings and return them via
-- frame:callParserFunction to inject them as strip markers
-- that survive the sanitizer pass.
--
-- Usage: {{#invoke:BSW/MainPage|functionName|param=value|...}}
-- Usage: {{#invoke:BSW/MainPage|functionName|param=value|...}}


local p = {}
local p = {}


-- ── Shared helpers ────────────────────────────────────────────────
-- ── HTML helpers ──────────────────────────────────────────────────


-- Safe attribute setter — skips nil/empty values
local function esc( s )
local function attr( el, name, value )
    s = tostring( s or '' )
    s = s:gsub( '&', '&amp;' )
    s = s:gsub( '<', '&lt;' )
    s = s:gsub( '>', '&gt;' )
    s = s:gsub( '"', '&quot;' )
    return s
end
 
-- Build an attribute string, skipping empty values
local function atr( name, value )
     if value and value ~= '' then
     if value and value ~= '' then
         el:attr( name, value )
         return ' ' .. name .. '="' .. esc( value ) .. '"'
    end
    return ''
end
 
-- Wrap output so it bypasses the sanitizer.
-- We use the <score> trick: any extension tag registered in MW
-- acts as a strip marker. But the cleanest method available in
-- Scribunto is to mark output as safe using the internal
-- mw.text.nowiki approach combined with frame preprocessing.
-- The most reliable cross-version approach: return the HTML
-- inside a <templatestyles> strip tag... but actually the
-- correct documented approach for Scribunto is simply:
--  return frame:preprocess( '<html>' .. html .. '</html>' )
-- but <html> tag needs to be enabled. So we use the
-- well-documented mw workaround: build wikitext that produces
-- the HTML we want via #tag parser functions for the
-- problematic elements (button, a with class).
 
-- Build a <button> via #tag parser function call
local function btn( frame, id, cls, onclick, label )
    local attrs = {}
    if id and id ~= '' then attrs['id'] = id end
    if cls then
        -- #tag doesn't support class directly, embed in attr string
     end
     end
     return el
    -- Use frame:callParserFunction with #tag
    -- #tag:button|label|id=...|class=...|aria-label=...
    -- This is the correct way to emit elements the sanitizer would block
    local params = { label }
    params['class'] = cls or ''
    if id and id ~= '' then params['id'] = id end
    if onclick and onclick ~= '' then params['onclick'] = onclick end
     return frame:callParserFunction( '#tag:button', params )
end
 
-- Build an <a> tag via #tag
local function anchor( frame, href, cls, content, attrs_extra )
    local params = { content }
    params['href'] = href or '#'
    if cls and cls ~= '' then params['class'] = cls end
    if attrs_extra then
        for k, v in pairs( attrs_extra ) do
            params[ k ] = v
        end
    end
    return frame:callParserFunction( '#tag:a', params )
end
end


-- ── hero() ────────────────────────────────────────────────────────
-- ── hero() ────────────────────────────────────────────────────────
-- Renders the entire hero slideshow: slides + dots + nav buttons.
-- Called ONCE from the main page with all slide data packed as
-- numbered parameter groups.
--
-- Usage:
-- {{#invoke:BSW/MainPage|hero
-- |slide1_article  = Battlestar Galactica (RDM)
-- |slide1_series  = Re-imagined Series
-- |slide1_title    = Battlestar Galactica
-- |slide1_titlelink= Battlestar Galactica (RDM)
-- |slide1_desc    = The last surviving...
-- |slide1_badgecolor=
-- |slide2_article  = Number Eight
-- ...
-- |count          = 3
-- }}
function p.hero( frame )
function p.hero( frame )
     local args  = frame.args
     local args  = frame.args
     local count = tonumber( args.count ) or 0
     local count = tonumber( args.count ) or 0
     local root  = mw.html.create( 'div' ):addClass( 'bsw-hero' )
     local out  = {}
 
    out[#out+1] = '<div class="bsw-hero">'


     for i = 1, count do
     for i = 1, count do
         local pfx    = 'slide' .. i .. '_'
         local pfx    = 'slide' .. i .. '_'
         local article = args[ pfx .. 'article'   ] or ''
         local article = mw.text.trim( args[ pfx .. 'article'   ] or '' )
         local series  = args[ pfx .. 'series'   ] or ''
         local series  = mw.text.trim( args[ pfx .. 'series'     ] or '' )
         local title  = args[ pfx .. 'title'     ] or article
         local title  = mw.text.trim( args[ pfx .. 'title'     ] or article )
         local tlink  = args[ pfx .. 'titlelink' ] or article
         local tlink  = mw.text.trim( args[ pfx .. 'titlelink' ] or article )
         local desc    = args[ pfx .. 'desc'     ] or ''
         local desc    = mw.text.trim( args[ pfx .. 'desc'       ] or '' )
         local bcolor  = args[ pfx .. 'badgecolor'] or ''
         local bcolor  = mw.text.trim( args[ pfx .. 'badgecolor' ] or '' )


         local slide = root:tag( 'div' )
         local active = ( i == 1 ) and ' bsw-active' or ''
            :addClass( 'bsw-slide' )
        out[#out+1] = '<div class="bsw-slide' .. active .. '"' .. atr( 'data-article', article ) .. '>'
            :attr( 'data-article', article )
        out[#out+1] =  '<div class="bsw-slide-bg"></div>'
        out[#out+1] =  '<div class="bsw-slide-overlay"></div>'
        out[#out+1] =  '<div class="bsw-slide-content">'
        out[#out+1] =    '<div class="bsw-slide-badge">'


         if i == 1 then slide:addClass( 'bsw-active' ) end
         local dot_style = bcolor ~= '' and ' style="background:' .. bcolor .. '"' or ''
        out[#out+1] =      '<span class="bsw-slide-badge-dot"' .. dot_style .. '></span>'
        out[#out+1] =      '<span>' .. esc( series ) .. '</span>'
        out[#out+1] =    '</div>'


         -- Background image container (populated by JS)
         -- Title: wikilink rendered by preprocessor
         slide:tag( 'div' ):addClass( 'bsw-slide-bg' ):done()
        local wlink = frame:preprocess( '[[' .. tlink .. '|' .. title .. ']]' )
         out[#out+1] =    '<div class="bsw-slide-title">' .. wlink .. '</div>'
        out[#out+1] =    '<div class="bsw-slide-desc">' .. esc( desc ) .. '</div>'
        out[#out+1] =  '</div>'
        out[#out+1] = '</div>'
    end


         -- Gradient overlay
    -- Dots using #tag:button via callParserFunction
         slide:tag( 'div' ):addClass( 'bsw-slide-overlay' ):done()
    out[#out+1] = '<div class="bsw-hero-dots">'
    for i = 1, count do
         local cls = 'bsw-hero-dot' .. ( i == 1 and ' bsw-active' or '' )
         out[#out+1] = btn( frame, nil, cls, nil, '' )
        -- Inject aria-label by replacing the closing > since #tag doesn't
        -- let us set aria-label easily — use a separate call with all attrs
        -- Actually #tag supports arbitrary attrs, rewrite:
    end
    out[#out+1] = '</div>'
 
    -- Redo dots properly — #tag:button supports all HTML attrs
    -- Remove the dots section we just built and redo
    -- (pop back to before the dots div)
    local dots_html = '<div class="bsw-hero-dots">'
    for i = 1, count do
        local cls = 'bsw-hero-dot' .. ( i == 1 and ' bsw-active' or '' )
        local dot_params = { '' }
        dot_params['class'] = cls
        dot_params['aria-label'] = 'Slide ' .. i
        dots_html = dots_html .. frame:callParserFunction( '#tag:button', dot_params )
    end
    dots_html = dots_html .. '</div>'


        -- Content
    -- Remove the bad dots we pushed, replace
        local content = slide:tag( 'div' ):addClass( 'bsw-slide-content' )
    -- (simpler: just build the whole thing cleanly from scratch)
    local result = '<div class="bsw-hero">'


         -- Badge
    for i = 1, count do
         local badge = content:tag( 'div' ):addClass( 'bsw-slide-badge' )
         local pfx    = 'slide' .. i .. '_'
         local dot   = badge:tag( 'span' ):addClass( 'bsw-slide-badge-dot' )
         local article = mw.text.trim( args[ pfx .. 'article'    ] or '' )
         if bcolor ~= '' then dot:attr( 'style', 'background:' .. bcolor ) end
        local series  = mw.text.trim( args[ pfx .. 'series'    ] or '' )
         badge:tag( 'span' ):wikitext( series )
         local title   = mw.text.trim( args[ pfx .. 'title'     ] or article )
        local tlink  = mw.text.trim( args[ pfx .. 'titlelink' ] or article )
         local desc    = mw.text.trim( args[ pfx .. 'desc'       ] or '' )
         local bcolor  = mw.text.trim( args[ pfx .. 'badgecolor' ] or '' )


         -- Title as wikilink
         local active = ( i == 1 ) and ' bsw-active' or ''
         content:tag( 'div' )
         local dot_style = bcolor ~= '' and ' style="background:' .. bcolor .. '"' or ''
            :addClass( 'bsw-slide-title' )
        local wlink = frame:preprocess( '[[' .. tlink .. '|' .. title .. ']]' )
            :wikitext( '[[' .. tlink .. '|' .. title .. ']]' )


         -- Description
         result = result ..
        content:tag( 'div' )
            '<div class="bsw-slide' .. active .. '"' .. atr( 'data-article', article ) .. '>' ..
             :addClass( 'bsw-slide-desc' )
            '<div class="bsw-slide-bg"></div>' ..
             :wikitext( desc )
            '<div class="bsw-slide-overlay"></div>' ..
            '<div class="bsw-slide-content">' ..
            '<div class="bsw-slide-badge">' ..
            '<span class="bsw-slide-badge-dot"' .. dot_style .. '></span>' ..
            '<span>' .. esc( series ) .. '</span>' ..
            '</div>' ..
             '<div class="bsw-slide-title">' .. wlink .. '</div>' ..
             '<div class="bsw-slide-desc">' .. esc( desc ) .. '</div>' ..
            '</div>' ..
            '</div>'
     end
     end


     -- Dots
    result = result .. dots_html
     local dots = root:tag( 'div' ):addClass( 'bsw-hero-dots' )
 
     for i = 1, count do
     -- Nav buttons
        local dot = dots:tag( 'button' )
     local prev_params = { '' }
            :addClass( 'bsw-hero-dot' )
    prev_params['id'] = 'bsw-hero-prev'
            :attr( 'aria-label', 'Slide ' .. i )
     prev_params['aria-label'] = 'Previous slide'
            :wikitext( '' )
    local next_params = { '' }
        if i == 1 then dot:addClass( 'bsw-active' ) end
    next_params['id'] = 'bsw-hero-next'
    end
    next_params['aria-label'] = 'Next slide'


     -- Prev / next buttons
     result = result ..
    local nav = root:tag( 'div' ):addClass( 'bsw-hero-nav' )
        '<div class="bsw-hero-nav">' ..
    nav:tag( 'button' )
         frame:callParserFunction( '#tag:button', prev_params ) ..
         :attr( 'id', 'bsw-hero-prev' )
         frame:callParserFunction( '#tag:button', next_params ) ..
        :attr( 'aria-label', 'Previous slide' )
         '</div>' ..
         :wikitext( '‹' )
         '</div>'
    nav:tag( 'button' )
        :attr( 'id', 'bsw-hero-next' )
         :attr( 'aria-label', 'Next slide' )
         :wikitext( '' )


     return tostring( root )
     return result
end
end


-- ── portals() ─────────────────────────────────────────────────────
-- ── portals() ─────────────────────────────────────────────────────
-- Renders the full portals bar.
-- Usage:
-- {{#invoke:BSW/MainPage|portals
-- |portal1_href  = /Portal:Battlestar_Galactica_(TOS)
-- |portal1_stripe= #ef9f27
-- |portal1_icon  = 📺
-- |portal1_name  = Original Series
-- |portal1_sub  = TOS · 1978
-- |count        = 5
-- }}
function p.portals( frame )
function p.portals( frame )
     local args  = frame.args
     local args  = frame.args
     local count = tonumber( args.count ) or 0
     local count = tonumber( args.count ) or 0


     local wrap = mw.html.create( 'div' ):addClass( 'bsw-portals' )
     local result =
    wrap:tag( 'div' )
        '<div class="bsw-portals">' ..
        :addClass( 'bsw-portals-label' )
        '<div class="bsw-portals-label">∞ Portals of Battlestar Wiki ∞</div>' ..
        :wikitext( '∞ Portals of Battlestar Wiki ∞' )
        '<div class="bsw-portals-grid">'
 
    local grid = wrap:tag( 'div' ):addClass( 'bsw-portals-grid' )


     for i = 1, count do
     for i = 1, count do
         local pfx    = 'portal' .. i .. '_'
         local pfx    = 'portal' .. i .. '_'
         local href  = args[ pfx .. 'href'  ] or '#'
         local href  = mw.text.trim( args[ pfx .. 'href'  ] or '#' )
         local stripe = args[ pfx .. 'stripe' ] or ''
         local stripe = mw.text.trim( args[ pfx .. 'stripe' ] or '' )
         local icon  = args[ pfx .. 'icon'  ] or ''
         local icon  = mw.text.trim( args[ pfx .. 'icon'  ] or '' )
         local name  = args[ pfx .. 'name'  ] or ''
         local name  = mw.text.trim( args[ pfx .. 'name'  ] or '' )
         local sub    = args[ pfx .. 'sub'    ] or ''
         local sub    = mw.text.trim( args[ pfx .. 'sub'    ] or '' )


         local card = grid:tag( 'a' )
         local stripe_style = stripe ~= '' and ' style="background:' .. stripe .. '"' or ''
            :addClass( 'bsw-portal' )
        local stripe_div = '<div class="bsw-portal-stripe"' .. stripe_style .. '></div>'
            :attr( 'href', href )


         -- Colour stripe
         local inner =
        local stripeEl = card:tag( 'div' ):addClass( 'bsw-portal-stripe' )
            stripe_div ..
        if stripe ~= '' then
            '<span class="bsw-portal-icon">' .. icon .. '</span>' ..
             stripeEl:attr( 'style', 'background:' .. stripe )
            '<span class="bsw-portal-name">' .. esc( name ) .. '</span>' ..
        end
             '<span class="bsw-portal-sub">' .. esc( sub  ) .. '</span>'


         card:tag( 'span' ):addClass( 'bsw-portal-icon' ):wikitext( icon )
         local a_params = { inner }
         card:tag( 'span' ):addClass( 'bsw-portal-name' ):wikitext( name )
         a_params['class'] = 'bsw-portal'
         card:tag( 'span' ):addClass( 'bsw-portal-sub'  ):wikitext( sub )
         a_params['href'] = href
    end


    return tostring( wrap )
        result = result .. frame:callParserFunction( '#tag:a', a_params )
end
 
-- ── card() ────────────────────────────────────────────────────────
-- Renders a single card with a header bar.
-- |title    = Card title (plain text)
-- |linktext = Link text (optional)
-- |linkurl  = Link URL (optional)
-- |id      = Optional id on the card-body div
-- |content  = Wikitext/HTML body content
 
function p.card( frame )
    local args    = frame.args
    local title  = args.title    or ''
    local ltext  = args.linktext or ''
    local lurl    = args.linkurl  or ''
    local bodyid  = args.id      or ''
    local content = args.content  or ''
 
    local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
 
    local hd = wrap:tag( 'div' ):addClass( 'bsw-card-hd' )
    hd:wikitext( title )
    if lurl ~= '' then
        hd:tag( 'a' ):attr( 'href', lurl ):wikitext( ltext ~= '' and ltext or 'More →' )
     end
     end


     local body = wrap:tag( 'div' ):addClass( 'bsw-card-body' )
     result = result .. '</div></div>'
    if bodyid ~= '' then body:attr( 'id', bodyid ) end
     return result
    body:wikitext( content )
 
    return tostring( wrap )
end
 
-- ── statbox() ─────────────────────────────────────────────────────
-- Single statistic box.
-- |n    = number (use MW magic word e.g. {{NUMBEROFARTICLES}})
-- |label = label text
 
function p.statbox( frame )
    local args  = frame.args
    local n    = args.n    or ''
    local label = args.label or ''
 
    local wrap = mw.html.create( 'div' ):addClass( 'bsw-stat' )
    wrap:tag( 'div' ):addClass( 'bsw-stat-n' ):wikitext( n )
    wrap:tag( 'div' ):addClass( 'bsw-stat-l' ):wikitext( label )
 
     return tostring( wrap )
end
end


-- ── statsblock() ──────────────────────────────────────────────────
-- ── statsblock() ──────────────────────────────────────────────────
-- Full statistics card including grid + action links.
-- Accepts |n1|label1 ... |n6|label6 for the six stat boxes,
-- plus |link1_url, |link1_text, |link2_url, |link2_text.
function p.statsblock( frame )
function p.statsblock( frame )
     local args = frame.args
     local args = frame.args


     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
     local grid = '<div class="bsw-stats-grid">'
 
    -- Header
    wrap:tag( 'div' ):addClass( 'bsw-card-hd' ):wikitext( 'Wiki statistics' )
 
    local body = wrap:tag( 'div' ):addClass( 'bsw-card-body' )
    local grid = body:tag( 'div' ):addClass( 'bsw-stats-grid' )
 
     for i = 1, 6 do
     for i = 1, 6 do
         local n    = args[ 'n'    .. i ] or ''
         local n    = mw.text.trim( args[ 'n'    .. i ] or '' )
         local label = args[ 'label' .. i ] or ''
         local label = mw.text.trim( args[ 'label' .. i ] or '' )
         if n ~= '' or label ~= '' then
         if n ~= '' or label ~= '' then
             local box = grid:tag( 'div' ):addClass( 'bsw-stat' )
             -- n may contain MW magic word output already expanded
            box:tag( 'div' ):addClass( 'bsw-stat-n' ):wikitext( n )
            grid = grid ..
            box:tag( 'div' ):addClass( 'bsw-stat-l' ):wikitext( label )
                '<div class="bsw-stat">' ..
                '<div class="bsw-stat-n">' .. n .. '</div>' ..
                '<div class="bsw-stat-l">' .. esc( label ) .. '</div>' ..
                '</div>'
         end
         end
     end
     end
    grid = grid .. '</div>'


    -- Action links
     local links = '<div class="bsw-stat-links">'
     local links = body:tag( 'div' ):addClass( 'bsw-stat-links' )
     for i = 1, 4 do
     for i = 1, 4 do
         local url  = args[ 'link' .. i .. '_url'  ] or ''
         local url  = mw.text.trim( args[ 'link' .. i .. '_url'  ] or '' )
         local text = args[ 'link' .. i .. '_text' ] or ''
         local text = mw.text.trim( args[ 'link' .. i .. '_text' ] or '' )
         if url ~= '' then
         if url ~= '' then
             links:tag( 'a' )
             local a_params = { text ~= '' and text or url }
                :addClass( 'bsw-stat-link' )
            a_params['class'] = 'bsw-stat-link'
                :attr( 'href', url )
            a_params['href']  = url
                :wikitext( text ~= '' and text or url )
            links = links .. frame:callParserFunction( '#tag:a', a_params )
         end
         end
     end
     end
    links = links .. '</div>'


     return tostring( wrap )
     return
        '<div class="bsw-card">' ..
        '<div class="bsw-card-hd">Wiki statistics</div>' ..
        '<div class="bsw-card-body">' .. grid .. links .. '</div>' ..
        '</div>'
end
end


-- ── recenttabs() ──────────────────────────────────────────────────
-- ── recenttabs() ──────────────────────────────────────────────────
-- Renders the wiki-tab bar for Recent Changes.
-- Tabs are wired to bswSetTab() in Common.js.
function p.recenttabs( frame )
function p.recenttabs( frame )
     local tabs = { 'All', 'EN', 'DE', 'Media' }
     local tabs = { { 'All', 'all' }, { 'EN', 'en' }, { 'DE', 'de' }, { 'Media', 'media' } }
    local keys  = { 'all', 'en', 'de', 'media' }
     local result = '<div class="bsw-wiki-tabs">'
 
     for i, t in ipairs( tabs ) do
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-wiki-tabs' )
         local cls    = 'bsw-wtab' .. ( i == 1 and ' bsw-active' or '' )
     for i, label in ipairs( tabs ) do
        local params = { t[1] }
         local btn = wrap:tag( 'button' )
        params['class']  = cls
            :addClass( 'bsw-wtab' )
        params['onclick'] = "bswSetTab(this,'" .. t[2] .. "')"
            :attr( 'onclick', "bswSetTab(this,'" .. keys[i] .. "')" )
         result = result .. frame:callParserFunction( '#tag:button', params )
            :wikitext( label )
         if i == 1 then btn:addClass( 'bsw-active' ) end
     end
     end
 
     return result .. '</div>'
     return tostring( wrap )
end
end


-- ── interwiki() ───────────────────────────────────────────────────
-- ── card() ────────────────────────────────────────────────────────
-- Renders the interwiki bar.
function p.card( frame )
-- |1_flag, |1_label, |1_url ... up to 8 entries.
    local args    = frame.args
    local title  = mw.text.trim( args.title    or '' )
    local ltext  = mw.text.trim( args.linktext or '' )
    local lurl    = mw.text.trim( args.linkurl  or '' )
    local content = args.content or ''


function p.interwiki( frame )
     local hd = '<div class="bsw-card-hd">' .. esc( title )
    local args = frame.args
     if lurl ~= '' then
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-interwiki' )
         local a_params = { ltext ~= '' and ltext or 'More →' }
 
         a_params['href'] = lurl
     for i = 1, 8 do
         hd = hd .. frame:callParserFunction( '#tag:a', a_params )
        local flag  = args[ i .. '_flag'  ] or ''
         local label = args[ i .. '_label' ] or ''
         local url  = args[ i .. '_url'   ] or ''
         if url ~= '' then
            local iw = wrap:tag( 'div' ):addClass( 'bsw-iw' )
            if flag ~= '' then iw:wikitext( flag .. ' ' ) end
            iw:tag( 'a' ):attr( 'href', url ):wikitext( label )
        end
     end
     end
    hd = hd .. '</div>'


     return tostring( wrap )
     return
        '<div class="bsw-card">' ..
        hd ..
        '<div class="bsw-card-body">' .. content .. '</div>' ..
        '</div>'
end
end


-- ── featuredloading() ─────────────────────────────────────────────
-- ── featuredloading() ─────────────────────────────────────────────
-- Renders the featured article card shell with loading state.
-- JS replaces the inner content after fetching.
function p.featuredloading( frame )
function p.featuredloading( frame )
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
     local a_params = { 'Read more →' }
 
     a_params['id']  = 'bsw-featured-link'
    local hd = wrap:tag( 'div' ):addClass( 'bsw-card-hd' )
    a_params['href'] = '#'
     hd:wikitext( 'Featured article' )
    hd:tag( 'a' )
        :attr( 'id', 'bsw-featured-link' )
        :attr( 'href', '#' )
        :wikitext( 'Read more →' )


     local body = wrap:tag( 'div' ):addClass( 'bsw-card-body' )
     local hd =
    local inner = body:tag( 'div' ):attr( 'id', 'bsw-featured-inner' )
        '<div class="bsw-card-hd">Featured article' ..
    local loading = inner:tag( 'div' ):addClass( 'bsw-loading' )
        frame:callParserFunction( '#tag:a', a_params ) ..
    loading:tag( 'div' ):addClass( 'bsw-spinner' ):wikitext( '' )
        '</div>'
    loading:wikitext( 'Loading today\'s featured article…' )


     return tostring( wrap )
     return
        '<div class="bsw-card">' ..
        hd ..
        '<div class="bsw-card-body">' ..
        '<div id="bsw-featured-inner">' ..
        '<div class="bsw-loading">' ..
        '<div class="bsw-spinner"></div>' ..
        'Loading today\'s featured article…' ..
        '</div></div></div></div>'
end
end


-- ── photolab() ────────────────────────────────────────────────────
-- ── photolab() ────────────────────────────────────────────────────
-- Renders the Photo Lab card shell.
-- Content (the actual PotD image) is passed as |content=
-- via a nested template call on the main page.
-- |prev_label, |prev_url, |caption, |next_label, |next_url
function p.photolab( frame )
function p.photolab( frame )
     local args    = frame.args
     local args    = frame.args
     local content  = args.content   or ''
     local content  = args.content   or ''
     local prev_url = args.prev_url or '#'
     local prev_url = mw.text.trim( args.prev_url   or '#' )
     local prev_lbl = args.prev_label or '‹'
     local prev_lbl = mw.text.trim( args.prev_label or '‹' )
     local caption  = args.caption   or ''
     local caption  = mw.text.trim( args.caption   or '' )
     local next_url = args.next_url or '#'
     local next_url = mw.text.trim( args.next_url   or '#' )
     local next_lbl = args.next_label or '›'
     local next_lbl = mw.text.trim( args.next_label or '›' )
 
    local a_view_params = { 'View project →' }
    a_view_params['href'] = '/BW:Potd'
    local a_prev_params = { prev_lbl }
    a_prev_params['href'] = prev_url
    local a_next_params = { next_lbl }
    a_next_params['href'] = next_url


     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
     local hd =
        '<div class="bsw-card-hd">∞ Photo Lab — Picture of the Day' ..
        frame:callParserFunction( '#tag:a', a_view_params ) ..
        '</div>'


     local hd = wrap:tag( 'div' ):addClass( 'bsw-card-hd' )
     local nav =
    hd:wikitext( '∞ Photo Lab — Picture of the Day' )
        '<div class="bsw-photo-nav">' ..
    hd:tag( 'a' ):attr( 'href', '/BW:Potd' ):wikitext( 'View project →' )
        frame:callParserFunction( '#tag:a', a_prev_params ) ..
        '<span class="bsw-photo-caption">' .. esc( caption ) .. '</span>' ..
        frame:callParserFunction( '#tag:a', a_next_params ) ..
        '</div>'


     -- PotD image transcluded as wikitext
     return '<div class="bsw-card">' .. hd .. content .. nav .. '</div>'
    wrap:wikitext( content )
end


    -- Nav row
-- ── interwiki() ───────────────────────────────────────────────────
     local nav = wrap:tag( 'div' ):addClass( 'bsw-photo-nav' )
function p.interwiki( frame )
     nav:tag( 'a' ):attr( 'href', prev_url ):wikitext( prev_lbl )
     local args  = frame.args
    nav:tag( 'span' ):addClass( 'bsw-photo-caption' ):wikitext( caption )
    local result = '<div class="bsw-interwiki">'
    nav:tag( 'a' ):attr( 'href', next_url ):wikitext( next_lbl )
 
     for i = 1, 8 do
        local flag  = mw.text.trim( args[ i .. '_flag'  ] or '' )
        local label = mw.text.trim( args[ i .. '_label' ] or '' )
        local url  = mw.text.trim( args[ i .. '_url'  ] or '' )
        if url ~= '' then
            local a_params = { label }
            a_params['href'] = url
            local prefix = flag ~= '' and ( flag .. ' ' ) or ''
            result = result ..
                '<div class="bsw-iw">' ..
                prefix ..
                frame:callParserFunction( '#tag:a', a_params ) ..
                '</div>'
        end
    end


     return tostring( wrap )
     return result .. '</div>'
end
end


-- ── tagline() ─────────────────────────────────────────────────────
-- ── tagline() ─────────────────────────────────────────────────────
function p.tagline( frame )
function p.tagline( frame )
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-tagline' )
     -- Use frame:preprocess for wikitext bold/italic markup
     wrap:wikitext(
     local inner = frame:preprocess(
         "The only original and legitimate '''Battlestar Wiki''' " ..
         "The only original and legitimate '''Battlestar Wiki''' " ..
         "— the free-as-in-beer, non-corporate, open-content encyclopedia " ..
         "— the free-as-in-beer, non-corporate, open-content encyclopedia " ..
Line 350: Line 389:
         "∞ ''Accept neither subpar substitutes nor subpar clones.'' ∞"
         "∞ ''Accept neither subpar substitutes nor subpar clones.'' ∞"
     )
     )
     return tostring( wrap )
     return '<div class="bsw-tagline">' .. inner .. '</div>'
end
end


return p
return p

Revision as of 16:07, 11 April 2026

Documentation for this module may be created at Module:BSW/MainPage/doc

-- Module:BSW/MainPage
-- Generates main page HTML using raw string concatenation.
-- mw.html is NOT used because its output is passed through
-- MediaWiki's HTML sanitizer which strips <button> and
-- <a class="..."> elements.
-- Instead we build raw HTML strings and return them via
-- frame:callParserFunction to inject them as strip markers
-- that survive the sanitizer pass.
--
-- Usage: {{#invoke:BSW/MainPage|functionName|param=value|...}}

local p = {}

-- ── HTML helpers ──────────────────────────────────────────────────

local function esc( s )
    s = tostring( s or '' )
    s = s:gsub( '&', '&amp;' )
    s = s:gsub( '<', '&lt;' )
    s = s:gsub( '>', '&gt;' )
    s = s:gsub( '"', '&quot;' )
    return s
end

-- Build an attribute string, skipping empty values
local function atr( name, value )
    if value and value ~= '' then
        return ' ' .. name .. '="' .. esc( value ) .. '"'
    end
    return ''
end

-- Wrap output so it bypasses the sanitizer.
-- We use the <score> trick: any extension tag registered in MW
-- acts as a strip marker. But the cleanest method available in
-- Scribunto is to mark output as safe using the internal
-- mw.text.nowiki approach combined with frame preprocessing.
-- The most reliable cross-version approach: return the HTML
-- inside a <templatestyles> strip tag... but actually the
-- correct documented approach for Scribunto is simply:
--   return frame:preprocess( '<html>' .. html .. '</html>' )
-- but <html> tag needs to be enabled. So we use the
-- well-documented mw workaround: build wikitext that produces
-- the HTML we want via #tag parser functions for the
-- problematic elements (button, a with class).

-- Build a <button> via #tag parser function call
local function btn( frame, id, cls, onclick, label )
    local attrs = {}
    if id and id ~= '' then attrs['id'] = id end
    if cls then
        -- #tag doesn't support class directly, embed in attr string
    end
    -- Use frame:callParserFunction with #tag
    -- #tag:button|label|id=...|class=...|aria-label=...
    -- This is the correct way to emit elements the sanitizer would block
    local params = { label }
    params['class'] = cls or ''
    if id and id ~= '' then params['id'] = id end
    if onclick and onclick ~= '' then params['onclick'] = onclick end
    return frame:callParserFunction( '#tag:button', params )
end

-- Build an <a> tag via #tag
local function anchor( frame, href, cls, content, attrs_extra )
    local params = { content }
    params['href'] = href or '#'
    if cls and cls ~= '' then params['class'] = cls end
    if attrs_extra then
        for k, v in pairs( attrs_extra ) do
            params[ k ] = v
        end
    end
    return frame:callParserFunction( '#tag:a', params )
end

-- ── hero() ────────────────────────────────────────────────────────
function p.hero( frame )
    local args  = frame.args
    local count = tonumber( args.count ) or 0
    local out   = {}

    out[#out+1] = '<div class="bsw-hero">'

    for i = 1, count do
        local pfx     = 'slide' .. i .. '_'
        local article = mw.text.trim( args[ pfx .. 'article'    ] or '' )
        local series  = mw.text.trim( args[ pfx .. 'series'     ] or '' )
        local title   = mw.text.trim( args[ pfx .. 'title'      ] or article )
        local tlink   = mw.text.trim( args[ pfx .. 'titlelink'  ] or article )
        local desc    = mw.text.trim( args[ pfx .. 'desc'       ] or '' )
        local bcolor  = mw.text.trim( args[ pfx .. 'badgecolor' ] or '' )

        local active = ( i == 1 ) and ' bsw-active' or ''
        out[#out+1] = '<div class="bsw-slide' .. active .. '"' .. atr( 'data-article', article ) .. '>'
        out[#out+1] =   '<div class="bsw-slide-bg"></div>'
        out[#out+1] =   '<div class="bsw-slide-overlay"></div>'
        out[#out+1] =   '<div class="bsw-slide-content">'
        out[#out+1] =     '<div class="bsw-slide-badge">'

        local dot_style = bcolor ~= '' and ' style="background:' .. bcolor .. '"' or ''
        out[#out+1] =       '<span class="bsw-slide-badge-dot"' .. dot_style .. '></span>'
        out[#out+1] =       '<span>' .. esc( series ) .. '</span>'
        out[#out+1] =     '</div>'

        -- Title: wikilink rendered by preprocessor
        local wlink = frame:preprocess( '[[' .. tlink .. '|' .. title .. ']]' )
        out[#out+1] =     '<div class="bsw-slide-title">' .. wlink .. '</div>'
        out[#out+1] =     '<div class="bsw-slide-desc">' .. esc( desc ) .. '</div>'
        out[#out+1] =   '</div>'
        out[#out+1] = '</div>'
    end

    -- Dots using #tag:button via callParserFunction
    out[#out+1] = '<div class="bsw-hero-dots">'
    for i = 1, count do
        local cls = 'bsw-hero-dot' .. ( i == 1 and ' bsw-active' or '' )
        out[#out+1] = btn( frame, nil, cls, nil, '' )
        -- Inject aria-label by replacing the closing > since #tag doesn't
        -- let us set aria-label easily — use a separate call with all attrs
        -- Actually #tag supports arbitrary attrs, rewrite:
    end
    out[#out+1] = '</div>'

    -- Redo dots properly — #tag:button supports all HTML attrs
    -- Remove the dots section we just built and redo
    -- (pop back to before the dots div)
    local dots_html = '<div class="bsw-hero-dots">'
    for i = 1, count do
        local cls = 'bsw-hero-dot' .. ( i == 1 and ' bsw-active' or '' )
        local dot_params = { '' }
        dot_params['class'] = cls
        dot_params['aria-label'] = 'Slide ' .. i
        dots_html = dots_html .. frame:callParserFunction( '#tag:button', dot_params )
    end
    dots_html = dots_html .. '</div>'

    -- Remove the bad dots we pushed, replace
    -- (simpler: just build the whole thing cleanly from scratch)
    local result = '<div class="bsw-hero">'

    for i = 1, count do
        local pfx     = 'slide' .. i .. '_'
        local article = mw.text.trim( args[ pfx .. 'article'    ] or '' )
        local series  = mw.text.trim( args[ pfx .. 'series'     ] or '' )
        local title   = mw.text.trim( args[ pfx .. 'title'      ] or article )
        local tlink   = mw.text.trim( args[ pfx .. 'titlelink'  ] or article )
        local desc    = mw.text.trim( args[ pfx .. 'desc'       ] or '' )
        local bcolor  = mw.text.trim( args[ pfx .. 'badgecolor' ] or '' )

        local active = ( i == 1 ) and ' bsw-active' or ''
        local dot_style = bcolor ~= '' and ' style="background:' .. bcolor .. '"' or ''
        local wlink = frame:preprocess( '[[' .. tlink .. '|' .. title .. ']]' )

        result = result ..
            '<div class="bsw-slide' .. active .. '"' .. atr( 'data-article', article ) .. '>' ..
            '<div class="bsw-slide-bg"></div>' ..
            '<div class="bsw-slide-overlay"></div>' ..
            '<div class="bsw-slide-content">' ..
            '<div class="bsw-slide-badge">' ..
            '<span class="bsw-slide-badge-dot"' .. dot_style .. '></span>' ..
            '<span>' .. esc( series ) .. '</span>' ..
            '</div>' ..
            '<div class="bsw-slide-title">' .. wlink .. '</div>' ..
            '<div class="bsw-slide-desc">' .. esc( desc ) .. '</div>' ..
            '</div>' ..
            '</div>'
    end

    result = result .. dots_html

    -- Nav buttons
    local prev_params = { '‹' }
    prev_params['id'] = 'bsw-hero-prev'
    prev_params['aria-label'] = 'Previous slide'
    local next_params = { '›' }
    next_params['id'] = 'bsw-hero-next'
    next_params['aria-label'] = 'Next slide'

    result = result ..
        '<div class="bsw-hero-nav">' ..
        frame:callParserFunction( '#tag:button', prev_params ) ..
        frame:callParserFunction( '#tag:button', next_params ) ..
        '</div>' ..
        '</div>'

    return result
end

-- ── portals() ─────────────────────────────────────────────────────
function p.portals( frame )
    local args  = frame.args
    local count = tonumber( args.count ) or 0

    local result =
        '<div class="bsw-portals">' ..
        '<div class="bsw-portals-label">∞ Portals of Battlestar Wiki ∞</div>' ..
        '<div class="bsw-portals-grid">'

    for i = 1, count do
        local pfx    = 'portal' .. i .. '_'
        local href   = mw.text.trim( args[ pfx .. 'href'   ] or '#' )
        local stripe = mw.text.trim( args[ pfx .. 'stripe' ] or '' )
        local icon   = mw.text.trim( args[ pfx .. 'icon'   ] or '' )
        local name   = mw.text.trim( args[ pfx .. 'name'   ] or '' )
        local sub    = mw.text.trim( args[ pfx .. 'sub'    ] or '' )

        local stripe_style = stripe ~= '' and ' style="background:' .. stripe .. '"' or ''
        local stripe_div = '<div class="bsw-portal-stripe"' .. stripe_style .. '></div>'

        local inner =
            stripe_div ..
            '<span class="bsw-portal-icon">' .. icon .. '</span>' ..
            '<span class="bsw-portal-name">' .. esc( name ) .. '</span>' ..
            '<span class="bsw-portal-sub">'  .. esc( sub  ) .. '</span>'

        local a_params = { inner }
        a_params['class'] = 'bsw-portal'
        a_params['href']  = href

        result = result .. frame:callParserFunction( '#tag:a', a_params )
    end

    result = result .. '</div></div>'
    return result
end

-- ── statsblock() ──────────────────────────────────────────────────
function p.statsblock( frame )
    local args = frame.args

    local grid = '<div class="bsw-stats-grid">'
    for i = 1, 6 do
        local n     = mw.text.trim( args[ 'n'     .. i ] or '' )
        local label = mw.text.trim( args[ 'label' .. i ] or '' )
        if n ~= '' or label ~= '' then
            -- n may contain MW magic word output already expanded
            grid = grid ..
                '<div class="bsw-stat">' ..
                '<div class="bsw-stat-n">' .. n .. '</div>' ..
                '<div class="bsw-stat-l">' .. esc( label ) .. '</div>' ..
                '</div>'
        end
    end
    grid = grid .. '</div>'

    local links = '<div class="bsw-stat-links">'
    for i = 1, 4 do
        local url  = mw.text.trim( args[ 'link' .. i .. '_url'  ] or '' )
        local text = mw.text.trim( args[ 'link' .. i .. '_text' ] or '' )
        if url ~= '' then
            local a_params = { text ~= '' and text or url }
            a_params['class'] = 'bsw-stat-link'
            a_params['href']  = url
            links = links .. frame:callParserFunction( '#tag:a', a_params )
        end
    end
    links = links .. '</div>'

    return
        '<div class="bsw-card">' ..
        '<div class="bsw-card-hd">Wiki statistics</div>' ..
        '<div class="bsw-card-body">' .. grid .. links .. '</div>' ..
        '</div>'
end

-- ── recenttabs() ──────────────────────────────────────────────────
function p.recenttabs( frame )
    local tabs = { { 'All', 'all' }, { 'EN', 'en' }, { 'DE', 'de' }, { 'Media', 'media' } }
    local result = '<div class="bsw-wiki-tabs">'
    for i, t in ipairs( tabs ) do
        local cls    = 'bsw-wtab' .. ( i == 1 and ' bsw-active' or '' )
        local params = { t[1] }
        params['class']   = cls
        params['onclick'] = "bswSetTab(this,'" .. t[2] .. "')"
        result = result .. frame:callParserFunction( '#tag:button', params )
    end
    return result .. '</div>'
end

-- ── card() ────────────────────────────────────────────────────────
function p.card( frame )
    local args    = frame.args
    local title   = mw.text.trim( args.title    or '' )
    local ltext   = mw.text.trim( args.linktext or '' )
    local lurl    = mw.text.trim( args.linkurl  or '' )
    local content = args.content or ''

    local hd = '<div class="bsw-card-hd">' .. esc( title )
    if lurl ~= '' then
        local a_params = { ltext ~= '' and ltext or 'More →' }
        a_params['href'] = lurl
        hd = hd .. frame:callParserFunction( '#tag:a', a_params )
    end
    hd = hd .. '</div>'

    return
        '<div class="bsw-card">' ..
        hd ..
        '<div class="bsw-card-body">' .. content .. '</div>' ..
        '</div>'
end

-- ── featuredloading() ─────────────────────────────────────────────
function p.featuredloading( frame )
    local a_params = { 'Read more →' }
    a_params['id']   = 'bsw-featured-link'
    a_params['href'] = '#'

    local hd =
        '<div class="bsw-card-hd">Featured article' ..
        frame:callParserFunction( '#tag:a', a_params ) ..
        '</div>'

    return
        '<div class="bsw-card">' ..
        hd ..
        '<div class="bsw-card-body">' ..
        '<div id="bsw-featured-inner">' ..
        '<div class="bsw-loading">' ..
        '<div class="bsw-spinner"></div>' ..
        'Loading today\'s featured article…' ..
        '</div></div></div></div>'
end

-- ── photolab() ────────────────────────────────────────────────────
function p.photolab( frame )
    local args     = frame.args
    local content  = args.content    or ''
    local prev_url = mw.text.trim( args.prev_url   or '#' )
    local prev_lbl = mw.text.trim( args.prev_label or '‹' )
    local caption  = mw.text.trim( args.caption    or '' )
    local next_url = mw.text.trim( args.next_url   or '#' )
    local next_lbl = mw.text.trim( args.next_label or '›' )

    local a_view_params = { 'View project →' }
    a_view_params['href'] = '/BW:Potd'
    local a_prev_params = { prev_lbl }
    a_prev_params['href'] = prev_url
    local a_next_params = { next_lbl }
    a_next_params['href'] = next_url

    local hd =
        '<div class="bsw-card-hd">∞ Photo Lab — Picture of the Day' ..
        frame:callParserFunction( '#tag:a', a_view_params ) ..
        '</div>'

    local nav =
        '<div class="bsw-photo-nav">' ..
        frame:callParserFunction( '#tag:a', a_prev_params ) ..
        '<span class="bsw-photo-caption">' .. esc( caption ) .. '</span>' ..
        frame:callParserFunction( '#tag:a', a_next_params ) ..
        '</div>'

    return '<div class="bsw-card">' .. hd .. content .. nav .. '</div>'
end

-- ── interwiki() ───────────────────────────────────────────────────
function p.interwiki( frame )
    local args   = frame.args
    local result = '<div class="bsw-interwiki">'

    for i = 1, 8 do
        local flag  = mw.text.trim( args[ i .. '_flag'  ] or '' )
        local label = mw.text.trim( args[ i .. '_label' ] or '' )
        local url   = mw.text.trim( args[ i .. '_url'   ] or '' )
        if url ~= '' then
            local a_params = { label }
            a_params['href'] = url
            local prefix = flag ~= '' and ( flag .. ' ' ) or ''
            result = result ..
                '<div class="bsw-iw">' ..
                prefix ..
                frame:callParserFunction( '#tag:a', a_params ) ..
                '</div>'
        end
    end

    return result .. '</div>'
end

-- ── tagline() ─────────────────────────────────────────────────────
function p.tagline( frame )
    -- Use frame:preprocess for wikitext bold/italic markup
    local inner = frame:preprocess(
        "The only original and legitimate '''Battlestar Wiki''' " ..
        "— the free-as-in-beer, non-corporate, open-content encyclopedia " ..
        "on all things ''Battlestar Galactica''.<br>" ..
        "∞ ''Accept neither subpar substitutes nor subpar clones.'' ∞"
    )
    return '<div class="bsw-tagline">' .. inner .. '</div>'
end

return p