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
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:BSW/MainPage
-- Module:BSW/MainPage
-- Generates all main page HTML components via mw.html,
-- MW 1.45 compatible. Uses only sanitizer-safe HTML elements.
-- bypassing the wikitext parser entirely.
-- <button> is NOT on the MW 1.45 allowed list — replaced with
-- Usage: {{#invoke:BSW/MainPage|functionName|param=value|...}}
-- <span role="button"> which IS allowed and is wired by JS.
-- External <a class="..."> replaced with wikitext [url text]
-- processed via frame:preprocess().


local p = {}
local p = {}


-- ── Shared helpers ────────────────────────────────────────────────
-- ── Helpers ───────────────────────────────────────────────────────


-- Safe attribute setter skips nil/empty values
local function e( s )
local function attr( el, name, value )
    s = tostring( s or '' )
     if value and value ~= '' then
    s = s:gsub( '&', '&amp;' ):gsub( '<', '&lt;' ):gsub( '>', '&gt;' ):gsub( '"', '&quot;' )
        el:attr( name, value )
    return s
end
 
local function a( name, val )
    if val and val ~= '' then return ' ' .. name .. '="' .. e(val) .. '"' end
    return ''
end
 
-- Wikitext internal link: [[target|label]]
local function wlink( frame, target, label )
    return frame:preprocess( '[[' .. target .. '|' .. (label or target) .. ']]' )
end
 
-- External link via wikitext: [url label] survives sanitizer
local function xlink( frame, url, label )
    return frame:preprocess( '[' .. url .. ' ' .. (label or url) .. ']' )
end
 
-- Span acting as a button — sanitizer-safe, JS wires the behaviour
local function spanBtn( id, cls, onclick, label )
    return '<span' ..
        a('id', id) ..
        a('class', cls) ..
        a('role', 'button') ..
        a('tabindex', '0') ..
        a('onclick', onclick) ..
        '>' .. (label or '') .. '</span>'
end
 
-- Check if transcluded content is just a red link (page doesn't exist).
-- Returns true if content should be hidden.
local function isRedlink( content )
     if not content or mw.text.trim( content ) == '' then return true end
    -- MW renders missing transclusions as red links with class="new"
    -- Check if content is ONLY a red link and nothing else meaningful
    local stripped = mw.text.trim( content )
    -- If it starts with the page title link pattern for a missing page
    -- (the whole content is just one red link), hide it
    if stripped:match( '^<a[^>]+class="[^"]*new[^"]*"' ) and
      not stripped:match( '</a>%s*%S' ) then
        return true
     end
     end
     return el
     return false
end
 
-- Wrap a card so JS can hide it if content is a red link
local function conditionalCard( id, html )
    return '<div class="bsw-conditional-card"' .. a('id', id) .. '>' .. html .. '</div>'
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 html = '<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 art    = 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 art )
         local tlink   = args[ pfx .. 'titlelink' ] or article
         local tlink = mw.text.trim( args[pfx..'titlelink'] or art )
         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' )
            :addClass( 'bsw-slide' )
            :attr( 'data-article', article )
 
        if i == 1 then slide:addClass( 'bsw-active' ) end
 
        -- Background image container (populated by JS)
        slide:tag( 'div' ):addClass( 'bsw-slide-bg' ):done()


         -- Gradient overlay
         local active  = i == 1 and ' bsw-active' or ''
         slide:tag( 'div' ):addClass( 'bsw-slide-overlay' ):done()
         local dot_sty  = bcolor ~= '' and ' style="background:' .. bcolor .. '"' or ''
        local wl      = wlink( frame, tlink, title )


         -- Content
         html = html ..
        local content = slide:tag( 'div' ):addClass( 'bsw-slide-content' )
            '<div class="bsw-slide' .. active .. '"' .. a('data-article', art) .. '>' ..
 
            '<div class="bsw-slide-bg"></div>' ..
        -- Badge
            '<div class="bsw-slide-overlay"></div>' ..
        local badge = content:tag( 'div' ):addClass( 'bsw-slide-badge' )
            '<div class="bsw-slide-content">' ..
        local dot  = badge:tag( 'span' ):addClass( 'bsw-slide-badge-dot' )
            '<div class="bsw-slide-badge">' ..
        if bcolor ~= '' then dot:attr( 'style', 'background:' .. bcolor ) end
            '<span class="bsw-slide-badge-dot"' .. dot_sty .. '></span>' ..
        badge:tag( 'span' ):wikitext( series )
            '<span>' .. e(series) .. '</span>' ..
 
            '</div>' ..
        -- Title as wikilink
             '<div class="bsw-slide-title">' .. wl .. '</div>' ..
        content:tag( 'div' )
            '<div class="bsw-slide-desc">' .. e(desc) .. '</div>' ..
             :addClass( 'bsw-slide-title' )
             '</div>' ..
            :wikitext( '[[' .. tlink .. '|' .. title .. ']]' )
             '</div>'
 
        -- Description
        content:tag( 'div' )
             :addClass( 'bsw-slide-desc' )
             :wikitext( desc )
     end
     end


     -- Dots
     -- Dots: span[role=button] — sanitizer safe
     local dots = root:tag( 'div' ):addClass( 'bsw-hero-dots' )
     html = html .. '<div class="bsw-hero-dots">'
     for i = 1, count do
     for i = 1, count do
         local dot = dots:tag( 'button' )
         local cls = 'bsw-hero-dot' .. (i == 1 and ' bsw-active' or '')
            :addClass( 'bsw-hero-dot' )
        html = html .. spanBtn(nil, cls, 'bswGoSlide(' .. (i-1) .. ')', '')
            :attr( 'aria-label', 'Slide ' .. i )
            :wikitext( '' )
        if i == 1 then dot:addClass( 'bsw-active' ) end
     end
     end
    html = html .. '</div>'


     -- Prev / next buttons
     -- Prev/next: span[role=button]
     local nav = root:tag( 'div' ):addClass( 'bsw-hero-nav' )
     html = html ..
    nav:tag( 'button' )
        '<div class="bsw-hero-nav">' ..
         :attr( 'id', 'bsw-hero-prev' )
         spanBtn('bsw-hero-prev', 'bsw-hero-btn', 'bswPrevSlide()', '&#8249;') ..
        :attr( 'aria-label', 'Previous slide' )
         spanBtn('bsw-hero-next', 'bsw-hero-btn', 'bswNextSlide()', '&#8250;') ..
        :wikitext( '' )
        '</div>' ..
    nav:tag( 'button' )
         '</div>'
         :attr( 'id', 'bsw-hero-next' )
        :attr( 'aria-label', 'Next slide' )
         :wikitext( '' )


     return tostring( root )
     return html
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 html =
    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 '' )
 
        -- Internal links (/Portal:...) work as wikitext links
        -- Use span wrapper styled as link block via CSS
        local stripe_sty = stripe ~= '' and ' style="background:' .. stripe .. '"' or ''
        local inner =
            '<div class="bsw-portal-stripe"' .. stripe_sty .. '></div>' ..
            '<span class="bsw-portal-icon">' .. icon .. '</span>' ..
            '<span class="bsw-portal-name">' .. e(name) .. '</span>' ..
            '<span class="bsw-portal-sub">'  .. e(sub)  .. '</span>'
 
        -- Use MW internal link syntax — href is a /Wiki_Path
        -- Strip leading slash to get page title
        local page = href:gsub('^/', '')
        local link = frame:preprocess( '[[' .. page .. '|<div class="bsw-portal">' .. inner .. '</div>]]' )
        html = html .. link
    end


        local card = grid:tag( 'a' )
    html = html .. '</div></div>'
            :addClass( 'bsw-portal' )
    return html
            :attr( 'href', href )
end


         -- Colour stripe
-- ── card header helper ────────────────────────────────────────────
         local stripeEl = card:tag( 'div' ):addClass( 'bsw-portal-stripe' )
-- Returns card-hd div; link is rendered as wikitext [url text]
        if stripe ~= '' then
local function cardHd( frame, title, linktext, linkurl )
             stripeEl:attr( 'style', 'background:' .. stripe )
    local hd = '<div class="bsw-card-hd">' .. e(title)
    if linkurl and linkurl ~= '' then
         -- Internal links start with /
         local link
        if linkurl:match('^/') or linkurl:match('^#') then
            local page = linkurl:gsub('^/', '')
            if page == '' or page:match('^#') then
                -- anchor-only or hash — can't wikilink, use span
                link = '<span class="bsw-card-hd-link">' .. e(linktext or 'More') .. '</span>'
             else
                link = frame:preprocess( '[[' .. page .. '|' .. (linktext or 'More →') .. ']]' )
            end
        else
            link = xlink( frame, linkurl, linktext or 'More →' )
         end
         end
 
         hd = hd .. link
         card:tag( 'span' ):addClass( 'bsw-portal-icon' ):wikitext( icon )
        card:tag( 'span' ):addClass( 'bsw-portal-name' ):wikitext( name )
        card:tag( 'span' ):addClass( 'bsw-portal-sub'  ):wikitext( sub  )
     end
     end
 
    hd = hd .. '</div>'
     return tostring( wrap )
     return hd
end
end


-- ── card() ────────────────────────────────────────────────────────
-- ── 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 )
function p.card( frame )
     local args    = frame.args
     local args    = frame.args
     local title  = args.title    or ''
     local title  = mw.text.trim( args.title    or '' )
     local ltext  = args.linktext or ''
     local ltext  = mw.text.trim( args.linktext or '' )
     local lurl    = args.linkurl  or ''
     local lurl    = mw.text.trim( args.linkurl  or '' )
    local bodyid  = args.id      or ''
     local content = args.content or ''
     local content = args.content or ''


     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
     -- Hide card entirely if content resolved to only a red link
    if isRedlink( content ) then return '' end


     local hd = wrap:tag( 'div' ):addClass( 'bsw-card-hd' )
     return '<div class="bsw-card">' ..
     hd:wikitext( title )
        cardHd( frame, title, ltext, lurl ) ..
     if lurl ~= '' then
        '<div class="bsw-card-body">' .. content .. '</div>' ..
         hd:tag( 'a' ):attr( 'href', lurl ):wikitext( ltext ~= '' and ltext or 'More →' )
        '</div>'
end
 
-- ── featuredloading() ─────────────────────────────────────────────
function p.featuredloading( frame )
    -- "Read more" link points to '#' — JS will update href
    -- Use a span since we can't href to '#' cleanly in wikitext
    local hd =
        '<div class="bsw-card-hd">Featured article' ..
        '<span class="bsw-card-hd-link" id="bsw-featured-link">Read more →</span>' ..
        '</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\226\128\166' ..
        '</div></div></div></div>'
end
 
-- ── photolab() ────────────────────────────────────────────────────
function p.photolab( frame )
    local args    = frame.args
    local content  = mw.text.trim( args.content or '' )
    local prev_url = mw.text.trim( args.prev_url  or '' )
    local prev_lbl = mw.text.trim( args.prev_label or '‹' )
    local next_url = mw.text.trim( args.next_url  or '' )
    local next_lbl = mw.text.trim( args.next_label or '›' )
 
     -- Caption: use the parent frame's raw (unexpanded) argument to avoid
    -- double-processing strip markers from nowiki/apostrophe markup.
    -- frame.args values are already expanded by MW before Lua sees them,
    -- which corrupts complex markup. Getting via expandTemplate on the
    -- raw caption transclusion gives a clean single-pass expansion.
    local caption_page = mw.text.trim( args.caption_page or '' )
    local caption = ''
     if caption_page ~= '' then
         caption = frame:preprocess( '{{' .. caption_page .. '}}' )
    else
        -- Fallback: use already-expanded value as-is (may have strip markers
        -- in complex captions, but simple text captions will be fine)
        caption = args.caption or ''
     end
     end


     local body = wrap:tag( 'div' ):addClass( 'bsw-card-body' )
     -- Hide if today's PotD subpage doesn't exist yet
     if bodyid ~= '' then body:attr( 'id', bodyid ) end
     if isRedlink( content ) then return '' end
    body:wikitext( content )


     return tostring( wrap )
     -- The Potd: subpage stores just the bare filename without "File:" prefix.
end
    local rendered_content = content
    if content ~= '' and not content:match( '%[%[' ) and not content:match( '<' ) then
        local filename = mw.text.trim( content )
        if filename ~= '' then
            rendered_content = frame:preprocess(
                '[[File:' .. filename .. '|center|frameless|400px]]'
            )
        end
    end


-- ── statbox() ─────────────────────────────────────────────────────
    local hd =
-- Single statistic box.
        '<div class="bsw-card-hd">\226\136\158 Photo Lab \226\128\148 Picture of the Day' ..
-- |n    = number (use MW magic word e.g. {{NUMBEROFARTICLES}})
        frame:preprocess( '[[BW:Potd|View project →]]' ) ..
-- |label = label text
        '</div>'


function p.statbox( frame )
    local prev_link = prev_url ~= '' and frame:preprocess( '[[' .. prev_url:gsub('^/','') .. '|' .. prev_lbl .. ']]' ) or prev_lbl
     local args  = frame.args
     local next_link = next_url ~= '' and frame:preprocess( '[[' .. next_url:gsub('^/','') .. '|' .. next_lbl .. ']]' ) or next_lbl
    local n    = args.n    or ''
    local label = args.label or ''


     local wrap = mw.html.create( 'div' ):addClass( 'bsw-stat' )
     local nav =
    wrap:tag( 'div' ):addClass( 'bsw-stat-n' ):wikitext( n )
        '<div class="bsw-photo-nav">' ..
    wrap:tag( 'div' ):addClass( 'bsw-stat-l' ):wikitext( label )
        prev_link ..
        '<span class="bsw-photo-caption">' .. caption .. '</span>' ..
        next_link ..
        '</div>'


     return tostring( wrap )
     return '<div class="bsw-card">' .. hd .. rendered_content .. nav .. '</div>'
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 grid = '<div class="bsw-stats-grid">'
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
 
    -- 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' )
             grid = grid ..
            box:tag( 'div' ):addClass( 'bsw-stat-n' ):wikitext( n )
                '<div class="bsw-stat">' ..
            box:tag( 'div' ):addClass( 'bsw-stat-l' ):wikitext( label )
                '<div class="bsw-stat-n">' .. n .. '</div>' ..
                '<div class="bsw-stat-l">' .. e(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 page = url:gsub('^/', '')
                :addClass( 'bsw-stat-link' )
            links = links .. frame:preprocess( '[[' .. page .. '|<span class="bsw-stat-link">' .. e(text) .. '</span>]]' )
                :attr( 'href', url )
                :wikitext( text ~= '' and text or url )
         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 html = '<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
        html = html .. spanBtn(nil, cls, "bswSetTab(this,'" .. t[2] .. "')", t[1])
         local btn = wrap:tag( 'button' )
            :addClass( 'bsw-wtab' )
            :attr( 'onclick', "bswSetTab(this,'" .. keys[i] .. "')" )
            :wikitext( label )
        if i == 1 then btn:addClass( 'bsw-active' ) end
     end
     end
 
     return html .. '</div>'
     return tostring( wrap )
end
end


-- ── interwiki() ───────────────────────────────────────────────────
-- ── interwiki() ───────────────────────────────────────────────────
-- Renders the interwiki bar.
-- |1_flag, |1_label, |1_url ... up to 8 entries.
function p.interwiki( frame )
function p.interwiki( frame )
     local args = frame.args
     local args = frame.args
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-interwiki' )
     local html = '<div class="bsw-interwiki">'
 
     for i = 1, 8 do
     for i = 1, 8 do
         local flag  = args[ i .. '_flag' ] or ''
         local flag  = mw.text.trim( args[i..'_flag' ] or '' )
         local label = args[ i .. '_label' ] or ''
         local label = mw.text.trim( args[i..'_label'] or '' )
         local url  = args[ i .. '_url'   ] or ''
         local url  = mw.text.trim( args[i..'_url' ] or '' )
         if url ~= '' then
         if url ~= '' then
             local iw = wrap:tag( 'div' ):addClass( 'bsw-iw' )
             local link
             if flag ~= '' then iw:wikitext( flag .. ' ' ) end
            if url:match('^/') then
             iw:tag( 'a' ):attr( 'href', url ):wikitext( label )
                link = frame:preprocess( '[[' .. url:gsub('^/','') .. '|' .. label .. ']]' )
             else
                link = xlink( frame, url, label )
            end
            local prefix = flag ~= '' and (flag .. ' ') or ''
             html = html .. '<div class="bsw-iw">' .. prefix .. link .. '</div>'
         end
         end
     end
     end
    return html .. '</div>'
end


     return tostring( wrap )
-- ── tagline() ─────────────────────────────────────────────────────
function p.tagline( frame )
     local inner = frame:preprocess(
        "The only original and legitimate '''Battlestar Wiki''' " ..
        "\226\128\148 the free-as-in-beer, non-corporate, open-content encyclopedia " ..
        "on all things ''Battlestar Galactica''.<br>" ..
        "\226\136\158 ''Accept neither subpar substitutes nor subpar clones.'' \226\136\158"
    )
    return '<div class="bsw-tagline">' .. inner .. '</div>'
end
end


-- ── featuredloading() ─────────────────────────────────────────────
-- ── sisterprojects() ──────────────────────────────────────────────
-- Renders the featured article card shell with loading state.
-- Dedicated card for sister wiki links.
-- JS replaces the inner content after fetching.
-- Uses numbered params: |1_label, |1_url, |2_label, |2_url ...
 
-- External URLs rendered via frame:preprocess [url label] syntax.
function p.featuredloading( frame )
function p.sisterprojects( frame )
     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
     local args = frame.args
    local body = '<div style="display:flex;flex-direction:column;gap:0.375rem">'
    for i = 1, 8 do
        local label = mw.text.trim( args[i .. '_label'] or '' )
        local url  = mw.text.trim( args[i .. '_url'  ] or '' )
        if url ~= '' and label ~= '' then
            -- Wrap in span.bsw-sister-link for styling
            -- Use preprocess so [url text] becomes a real hyperlink
            local link = frame:preprocess( '[' .. url .. ' ' .. label .. ']' )
            body = body .. '<div class="bsw-sister-link">' .. link .. '</div>'
        end
    end
    body = body .. '</div>'


     local hd = wrap:tag( 'div' ):addClass( 'bsw-card-hd' )
     return
    hd:wikitext( 'Featured article' )
        '<div class="bsw-card">' ..
    hd:tag( 'a' )
         '<div class="bsw-card-hd">Sister projects</div>' ..
         :attr( 'id', 'bsw-featured-link' )
         '<div class="bsw-card-body">' .. body .. '</div>' ..
         :attr( 'href', '#' )
        '</div>'
        :wikitext( 'Read more →' )
 
    local body = wrap:tag( 'div' ):addClass( 'bsw-card-body' )
    local inner = body:tag( 'div' ):attr( 'id', 'bsw-featured-inner' )
    local loading = inner:tag( 'div' ):addClass( 'bsw-loading' )
    loading:tag( 'div' ):addClass( 'bsw-spinner' ):wikitext( '' )
    loading:wikitext( 'Loading today\'s featured article…' )
 
    return tostring( wrap )
end
end


-- ── photolab() ────────────────────────────────────────────────────
-- ── votd() ────────────────────────────────────────────────────────
-- Renders the Photo Lab card shell.
-- Renders the Video of the Day band.
-- Content (the actual PotD image) is passed as |content=
-- The container div carries data-date and data-override attributes
-- via a nested template call on the main page.
-- so the VotD JS loader can find and populate it.
-- |prev_label, |prev_url, |caption, |next_label, |next_url
-- data-override is populated if a manual subpage exists for today.
--
-- Usage: {{#invoke:BSW/MainPage|votd}}
function p.votd( frame )
    local today =
        frame:callParserFunction( 'CURRENTYEAR'  ) .. '-' ..
        frame:callParserFunction( 'CURRENTMONTH' ) .. '-' ..
        frame:callParserFunction( 'CURRENTDAY'  )


function p.photolab( frame )
     local override_page = 'BattlestarWiki:VotD/' .. today
     local args    = frame.args
     local override_title = mw.title.new( override_page )
     local content  = args.content  or ''
     local override_token = ''
     local prev_url = args.prev_url  or '#'
     if override_title and override_title.exists then
     local prev_lbl = args.prev_label or '‹'
        local content = override_title:getContent()
    local caption  = args.caption  or ''
        if content then
    local next_url = args.next_url  or '#'
            override_token = mw.text.trim( content )
     local next_lbl = args.next_label or '›'
        end
     end


     local wrap = mw.html.create( 'div' ):addClass( 'bsw-card' )
    -- Header bar
     local hd =
        '<div class="bsw-votd-hd">' ..
        '<span class="bsw-votd-hd-inner">' ..
        '<span class="bsw-votd-hd-dot"></span>' ..
        '\226\136\158 Video of the Day' ..
        '</span>' ..
        frame:preprocess( '[https://battlestarpegasus.com battlestarpegasus.com \226\134\151]' ) ..
        '</div>'


     local hd = wrap:tag( 'div' ):addClass( 'bsw-card-hd' )
     -- Inner grid: player (populated by JS) + info panel (populated by JS)
     hd:wikitext( '∞ Photo Lab — Picture of the Day' )
     local inner =
    hd:tag( 'a' ):attr( 'href', '/BW:Potd' ):wikitext( 'View project →' )
        '<div class="bsw-votd-inner">' ..
 
        '<div class="bsw-votd-player">' ..
    -- PotD image transcluded as wikitext
        '<div class="bsw-loading"><div class="bsw-spinner"></div>Loading today\226\128\153s video\226\128\166</div>' ..
    wrap:wikitext( content )
        '</div>' ..
 
        '<div class="bsw-votd-info" id="bsw-votd-info" style="display:none"></div>' ..
    -- Nav row
        '</div>'
    local nav = wrap:tag( 'div' ):addClass( 'bsw-photo-nav' )
    nav:tag( 'a' ):attr( 'href', prev_url ):wikitext( prev_lbl )
    nav:tag( 'span' ):addClass( 'bsw-photo-caption' ):wikitext( caption )
    nav:tag( 'a' ):attr( 'href', next_url ):wikitext( next_lbl )
 
    return tostring( wrap )
end


-- ── tagline() ─────────────────────────────────────────────────────
    return
function p.tagline( frame )
        '<div class="bsw-votd-band" id="bsw-votd-container"' ..
    local wrap = mw.html.create( 'div' ):addClass( 'bsw-tagline' )
         ' data-date="' .. today .. '"' ..
    wrap:wikitext(
         ' data-override="' .. mw.text.trim( override_token ) .. '">' ..
         "The only original and legitimate '''Battlestar Wiki''' " ..
         hd .. inner ..
         "— the free-as-in-beer, non-corporate, open-content encyclopedia " ..
         '</div>'
         "on all things ''Battlestar Galactica''.<br>" ..
         "∞ ''Accept neither subpar substitutes nor subpar clones.'' ∞"
    )
    return tostring( wrap )
end
end


return p
return p

Revision as of 20:43, 11 April 2026

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

-- Module:BSW/MainPage
-- MW 1.45 compatible. Uses only sanitizer-safe HTML elements.
-- <button> is NOT on the MW 1.45 allowed list — replaced with
-- <span role="button"> which IS allowed and is wired by JS.
-- External <a class="..."> replaced with wikitext [url text]
-- processed via frame:preprocess().

local p = {}

-- ── Helpers ───────────────────────────────────────────────────────

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

local function a( name, val )
    if val and val ~= '' then return ' ' .. name .. '="' .. e(val) .. '"' end
    return ''
end

-- Wikitext internal link: [[target|label]]
local function wlink( frame, target, label )
    return frame:preprocess( '[[' .. target .. '|' .. (label or target) .. ']]' )
end

-- External link via wikitext: [url label] — survives sanitizer
local function xlink( frame, url, label )
    return frame:preprocess( '[' .. url .. ' ' .. (label or url) .. ']' )
end

-- Span acting as a button — sanitizer-safe, JS wires the behaviour
local function spanBtn( id, cls, onclick, label )
    return '<span' ..
        a('id', id) ..
        a('class', cls) ..
        a('role', 'button') ..
        a('tabindex', '0') ..
        a('onclick', onclick) ..
        '>' .. (label or '') .. '</span>'
end

-- Check if transcluded content is just a red link (page doesn't exist).
-- Returns true if content should be hidden.
local function isRedlink( content )
    if not content or mw.text.trim( content ) == '' then return true end
    -- MW renders missing transclusions as red links with class="new"
    -- Check if content is ONLY a red link and nothing else meaningful
    local stripped = mw.text.trim( content )
    -- If it starts with the page title link pattern for a missing page
    -- (the whole content is just one red link), hide it
    if stripped:match( '^<a[^>]+class="[^"]*new[^"]*"' ) and
       not stripped:match( '</a>%s*%S' ) then
        return true
    end
    return false
end

-- Wrap a card so JS can hide it if content is a red link
local function conditionalCard( id, html )
    return '<div class="bsw-conditional-card"' .. a('id', id) .. '>' .. html .. '</div>'
end

-- ── hero() ────────────────────────────────────────────────────────
function p.hero( frame )
    local args  = frame.args
    local count = tonumber( args.count ) or 0
    local html  = '<div class="bsw-hero">'

    for i = 1, count do
        local pfx    = 'slide' .. i .. '_'
        local art    = 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 art )
        local tlink  = mw.text.trim( args[pfx..'titlelink']  or art )
        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_sty  = bcolor ~= '' and ' style="background:' .. bcolor .. '"' or ''
        local wl       = wlink( frame, tlink, title )

        html = html ..
            '<div class="bsw-slide' .. active .. '"' .. a('data-article', art) .. '>' ..
            '<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_sty .. '></span>' ..
            '<span>' .. e(series) .. '</span>' ..
            '</div>' ..
            '<div class="bsw-slide-title">' .. wl .. '</div>' ..
            '<div class="bsw-slide-desc">' .. e(desc) .. '</div>' ..
            '</div>' ..
            '</div>'
    end

    -- Dots: span[role=button] — sanitizer safe
    html = html .. '<div class="bsw-hero-dots">'
    for i = 1, count do
        local cls = 'bsw-hero-dot' .. (i == 1 and ' bsw-active' or '')
        html = html .. spanBtn(nil, cls, 'bswGoSlide(' .. (i-1) .. ')', '')
    end
    html = html .. '</div>'

    -- Prev/next: span[role=button]
    html = html ..
        '<div class="bsw-hero-nav">' ..
        spanBtn('bsw-hero-prev', 'bsw-hero-btn', 'bswPrevSlide()', '&#8249;') ..
        spanBtn('bsw-hero-next', 'bsw-hero-btn', 'bswNextSlide()', '&#8250;') ..
        '</div>' ..
        '</div>'

    return html
end

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

    local html =
        '<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 '' )

        -- Internal links (/Portal:...) work as wikitext links
        -- Use span wrapper styled as link block via CSS
        local stripe_sty = stripe ~= '' and ' style="background:' .. stripe .. '"' or ''
        local inner =
            '<div class="bsw-portal-stripe"' .. stripe_sty .. '></div>' ..
            '<span class="bsw-portal-icon">' .. icon .. '</span>' ..
            '<span class="bsw-portal-name">' .. e(name) .. '</span>' ..
            '<span class="bsw-portal-sub">'  .. e(sub)  .. '</span>'

        -- Use MW internal link syntax — href is a /Wiki_Path
        -- Strip leading slash to get page title
        local page = href:gsub('^/', '')
        local link = frame:preprocess( '[[' .. page .. '|<div class="bsw-portal">' .. inner .. '</div>]]' )
        html = html .. link
    end

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

-- ── card header helper ────────────────────────────────────────────
-- Returns card-hd div; link is rendered as wikitext [url text]
local function cardHd( frame, title, linktext, linkurl )
    local hd = '<div class="bsw-card-hd">' .. e(title)
    if linkurl and linkurl ~= '' then
        -- Internal links start with /
        local link
        if linkurl:match('^/') or linkurl:match('^#') then
            local page = linkurl:gsub('^/', '')
            if page == '' or page:match('^#') then
                -- anchor-only or hash — can't wikilink, use span
                link = '<span class="bsw-card-hd-link">' .. e(linktext or 'More') .. '</span>'
            else
                link = frame:preprocess( '[[' .. page .. '|' .. (linktext or 'More →') .. ']]' )
            end
        else
            link = xlink( frame, linkurl, linktext or 'More →' )
        end
        hd = hd .. link
    end
    hd = hd .. '</div>'
    return hd
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 ''

    -- Hide card entirely if content resolved to only a red link
    if isRedlink( content ) then return '' end

    return '<div class="bsw-card">' ..
        cardHd( frame, title, ltext, lurl ) ..
        '<div class="bsw-card-body">' .. content .. '</div>' ..
        '</div>'
end

-- ── featuredloading() ─────────────────────────────────────────────
function p.featuredloading( frame )
    -- "Read more" link points to '#' — JS will update href
    -- Use a span since we can't href to '#' cleanly in wikitext
    local hd =
        '<div class="bsw-card-hd">Featured article' ..
        '<span class="bsw-card-hd-link" id="bsw-featured-link">Read more →</span>' ..
        '</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\226\128\166' ..
        '</div></div></div></div>'
end

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

    -- Caption: use the parent frame's raw (unexpanded) argument to avoid
    -- double-processing strip markers from nowiki/apostrophe markup.
    -- frame.args values are already expanded by MW before Lua sees them,
    -- which corrupts complex markup. Getting via expandTemplate on the
    -- raw caption transclusion gives a clean single-pass expansion.
    local caption_page = mw.text.trim( args.caption_page or '' )
    local caption = ''
    if caption_page ~= '' then
        caption = frame:preprocess( '{{' .. caption_page .. '}}' )
    else
        -- Fallback: use already-expanded value as-is (may have strip markers
        -- in complex captions, but simple text captions will be fine)
        caption = args.caption or ''
    end

    -- Hide if today's PotD subpage doesn't exist yet
    if isRedlink( content ) then return '' end

    -- The Potd: subpage stores just the bare filename without "File:" prefix.
    local rendered_content = content
    if content ~= '' and not content:match( '%[%[' ) and not content:match( '<' ) then
        local filename = mw.text.trim( content )
        if filename ~= '' then
            rendered_content = frame:preprocess(
                '[[File:' .. filename .. '|center|frameless|400px]]'
            )
        end
    end

    local hd =
        '<div class="bsw-card-hd">\226\136\158 Photo Lab \226\128\148 Picture of the Day' ..
        frame:preprocess( '[[BW:Potd|View project →]]' ) ..
        '</div>'

    local prev_link = prev_url ~= '' and frame:preprocess( '[[' .. prev_url:gsub('^/','') .. '|' .. prev_lbl .. ']]' ) or prev_lbl
    local next_link = next_url ~= '' and frame:preprocess( '[[' .. next_url:gsub('^/','') .. '|' .. next_lbl .. ']]' ) or next_lbl

    local nav =
        '<div class="bsw-photo-nav">' ..
        prev_link ..
        '<span class="bsw-photo-caption">' .. caption .. '</span>' ..
        next_link ..
        '</div>'

    return '<div class="bsw-card">' .. hd .. rendered_content .. nav .. '</div>'
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
            grid = grid ..
                '<div class="bsw-stat">' ..
                '<div class="bsw-stat-n">' .. n .. '</div>' ..
                '<div class="bsw-stat-l">' .. e(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 page = url:gsub('^/', '')
            links = links .. frame:preprocess( '[[' .. page .. '|<span class="bsw-stat-link">' .. e(text) .. '</span>]]' )
        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 html = '<div class="bsw-wiki-tabs">'
    for i, t in ipairs(tabs) do
        local cls = 'bsw-wtab' .. (i == 1 and ' bsw-active' or '')
        html = html .. spanBtn(nil, cls, "bswSetTab(this,'" .. t[2] .. "')", t[1])
    end
    return html .. '</div>'
end

-- ── interwiki() ───────────────────────────────────────────────────
function p.interwiki( frame )
    local args = frame.args
    local html = '<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 link
            if url:match('^/') then
                link = frame:preprocess( '[[' .. url:gsub('^/','') .. '|' .. label .. ']]' )
            else
                link = xlink( frame, url, label )
            end
            local prefix = flag ~= '' and (flag .. ' ') or ''
            html = html .. '<div class="bsw-iw">' .. prefix .. link .. '</div>'
        end
    end
    return html .. '</div>'
end

-- ── tagline() ─────────────────────────────────────────────────────
function p.tagline( frame )
    local inner = frame:preprocess(
        "The only original and legitimate '''Battlestar Wiki''' " ..
        "\226\128\148 the free-as-in-beer, non-corporate, open-content encyclopedia " ..
        "on all things ''Battlestar Galactica''.<br>" ..
        "\226\136\158 ''Accept neither subpar substitutes nor subpar clones.'' \226\136\158"
    )
    return '<div class="bsw-tagline">' .. inner .. '</div>'
end

-- ── sisterprojects() ──────────────────────────────────────────────
-- Dedicated card for sister wiki links.
-- Uses numbered params: |1_label, |1_url, |2_label, |2_url ...
-- External URLs rendered via frame:preprocess [url label] syntax.
function p.sisterprojects( frame )
    local args = frame.args
    local body = '<div style="display:flex;flex-direction:column;gap:0.375rem">'
    for i = 1, 8 do
        local label = mw.text.trim( args[i .. '_label'] or '' )
        local url   = mw.text.trim( args[i .. '_url'  ] or '' )
        if url ~= '' and label ~= '' then
            -- Wrap in span.bsw-sister-link for styling
            -- Use preprocess so [url text] becomes a real hyperlink
            local link = frame:preprocess( '[' .. url .. ' ' .. label .. ']' )
            body = body .. '<div class="bsw-sister-link">' .. link .. '</div>'
        end
    end
    body = body .. '</div>'

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

-- ── votd() ────────────────────────────────────────────────────────
-- Renders the Video of the Day band.
-- The container div carries data-date and data-override attributes
-- so the VotD JS loader can find and populate it.
-- data-override is populated if a manual subpage exists for today.
--
-- Usage: {{#invoke:BSW/MainPage|votd}}
function p.votd( frame )
    local today =
        frame:callParserFunction( 'CURRENTYEAR'  ) .. '-' ..
        frame:callParserFunction( 'CURRENTMONTH' ) .. '-' ..
        frame:callParserFunction( 'CURRENTDAY'   )

    local override_page = 'BattlestarWiki:VotD/' .. today
    local override_title = mw.title.new( override_page )
    local override_token = ''
    if override_title and override_title.exists then
        local content = override_title:getContent()
        if content then
            override_token = mw.text.trim( content )
        end
    end

    -- Header bar
    local hd =
        '<div class="bsw-votd-hd">' ..
        '<span class="bsw-votd-hd-inner">' ..
        '<span class="bsw-votd-hd-dot"></span>' ..
        '\226\136\158 Video of the Day' ..
        '</span>' ..
        frame:preprocess( '[https://battlestarpegasus.com battlestarpegasus.com \226\134\151]' ) ..
        '</div>'

    -- Inner grid: player (populated by JS) + info panel (populated by JS)
    local inner =
        '<div class="bsw-votd-inner">' ..
        '<div class="bsw-votd-player">' ..
        '<div class="bsw-loading"><div class="bsw-spinner"></div>Loading today\226\128\153s video\226\128\166</div>' ..
        '</div>' ..
        '<div class="bsw-votd-info" id="bsw-votd-info" style="display:none"></div>' ..
        '</div>'

    return
        '<div class="bsw-votd-band" id="bsw-votd-container"' ..
        ' data-date="' .. today .. '"' ..
        ' data-override="' .. mw.text.trim( override_token ) .. '">' ..
        hd .. inner ..
        '</div>'
end

return p