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.
More actions
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..." |
No edit summary |
||
| (5 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module:BSW/MainPage | -- 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 = {} | local p = {} | ||
-- ── | -- ── Helpers ─────────────────────────────────────────────────────── | ||
-- | local function e( s ) | ||
local function | s = tostring( s or '' ) | ||
if | s = s:gsub( '&', '&' ):gsub( '<', '<' ):gsub( '>', '>' ):gsub( '"', '"' ) | ||
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 | 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() ──────────────────────────────────────────────────────── | ||
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 | local html = '<div class="bsw-hero">' | ||
for i = 1, count do | for i = 1, count do | ||
local pfx | local pfx = 'slide' .. i .. '_' | ||
local | local art = mw.text.trim( args[pfx..'article'] or '' ) | ||
local series | local series = mw.text.trim( args[pfx..'series'] or '' ) | ||
local title | local title = mw.text.trim( args[pfx..'title'] or art ) | ||
local tlink | local tlink = mw.text.trim( args[pfx..'titlelink'] or art ) | ||
local desc | local desc = mw.text.trim( args[pfx..'desc'] or '' ) | ||
local bcolor | 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 | end | ||
-- Dots | -- Dots: span[role=button] — sanitizer safe | ||
html = html .. '<div class="bsw-hero-dots">' | |||
for i = 1, count do | for i = 1, count do | ||
local | local cls = 'bsw-hero-dot' .. (i == 1 and ' bsw-active' or '') | ||
html = html .. spanBtn(nil, cls, 'bswGoSlide(' .. (i-1) .. ')', '') | |||
end | end | ||
html = html .. '</div>' | |||
-- Prev / next | -- Prev/next: span[role=button] | ||
html = html .. | |||
'<div class="bsw-hero-nav">' .. | |||
spanBtn('bsw-hero-prev', 'bsw-hero-btn', 'bswPrevSlide()', '‹') .. | |||
spanBtn('bsw-hero-next', 'bsw-hero-btn', 'bswNextSlide()', '›') .. | |||
'</div>' .. | |||
'</div>' | |||
return | return html | ||
end | end | ||
-- ── portals() ───────────────────────────────────────────────────── | -- ── portals() ───────────────────────────────────────────────────── | ||
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 | 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 | for i = 1, count do | ||
local pfx = 'portal' .. i .. '_' | local pfx = 'portal' .. i .. '_' | ||
local href = args[ pfx .. 'href' | 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' | local icon = mw.text.trim( args[pfx..'icon'] or '' ) | ||
local name = args[ pfx .. 'name' | local name = mw.text.trim( args[pfx..'name'] or '' ) | ||
local sub = args[ pfx .. 'sub' | 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 ──────────────────────────────────────────── | ||
local | -- 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 | end | ||
hd = hd .. link | |||
end | end | ||
hd = hd .. '</div>' | |||
return | return hd | ||
end | end | ||
-- ── card() ──────────────────────────────────────────────────────── | -- ── card() ──────────────────────────────────────────────────────── | ||
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 content = args.content or '' | |||
local content = args.content | |||
-- 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 ) .. | |||
if | '<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 | end | ||
-- Hide if today's PotD subpage doesn't exist yet | |||
if | if isRedlink( content ) then return '' end | ||
-- 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 | |||
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 | local next_link = next_url ~= '' and frame:preprocess( '[[' .. next_url:gsub('^/','') .. '|' .. next_lbl .. ']]' ) or next_lbl | ||
local | local nav = | ||
'<div class="bsw-photo-nav">' .. | |||
prev_link .. | |||
'<span class="bsw-photo-caption">' .. caption .. '</span>' .. | |||
next_link .. | |||
'</div>' | |||
return | return '<div class="bsw-card">' .. hd .. rendered_content .. nav .. '</div>' | ||
end | end | ||
-- ── statsblock() ────────────────────────────────────────────────── | -- ── statsblock() ────────────────────────────────────────────────── | ||
function p.statsblock( frame ) | function p.statsblock( frame ) | ||
local args = frame.args | local args = frame.args | ||
local grid = '<div class="bsw-stats-grid">' | |||
local | |||
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 | ||
grid = grid .. | |||
'<div class="bsw-stat">' .. | |||
'<div class="bsw-stat-n">' .. n .. '</div>' .. | |||
'<div class="bsw-stat-l">' .. e(label) .. '</div>' .. | |||
'</div>' | |||
end | end | ||
end | end | ||
grid = grid .. '</div>' | |||
local links = '<div class="bsw-stat-links">' | |||
local links = | |||
for i = 1, 4 do | for i = 1, 4 do | ||
local url = args[ 'link' .. i .. '_url' | 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 | ||
local page = url:gsub('^/', '') | |||
links = links .. frame:preprocess( '[[' .. page .. '|<span class="bsw-stat-link">' .. e(text) .. '</span>]]' ) | |||
end | end | ||
end | end | ||
links = links .. '</div>' | |||
return | 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() ────────────────────────────────────────────────── | ||
function p.recenttabs( frame ) | function p.recenttabs( frame ) | ||
local tabs | 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 | local cls = 'bsw-wtab' .. (i == 1 and ' bsw-active' or '') | ||
for i, | html = html .. spanBtn(nil, cls, "bswSetTab(this,'" .. t[2] .. "')", t[1]) | ||
local | |||
end | end | ||
return html .. '</div>' | |||
return | |||
end | end | ||
-- ── interwiki() ─────────────────────────────────────────────────── | -- ── interwiki() ─────────────────────────────────────────────────── | ||
function p.interwiki( frame ) | function p.interwiki( frame ) | ||
local args = frame.args | local args = frame.args | ||
local | local html = '<div class="bsw-interwiki">' | ||
for i = 1, 8 do | for i = 1, 8 do | ||
local flag = args[ i .. '_flag' | 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' | local url = mw.text.trim( args[i..'_url' ] or '' ) | ||
if url ~= '' then | if url ~= '' then | ||
local | local link | ||
if url:match('^/') then | |||
iw | 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 | |||
-- ── 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 | ||
-- ── | -- ── 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. | function p.sisterprojects( frame ) | ||
local | 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 | end | ||
-- ── | -- ── votd() ──────────────────────────────────────────────────────── | ||
-- Renders the | -- 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 | local override_title = mw.title.new( override_page ) | ||
local | local override_token = '' | ||
local | if override_title and override_title.exists then | ||
local content = override_title:getContent() | |||
if content then | |||
override_token = mw.text.trim( content ) | |||
end | |||
end | |||
local | -- 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 | end | ||
return p | return p | ||
Latest 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( '&', '&' ):gsub( '<', '<' ):gsub( '>', '>' ):gsub( '"', '"' )
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()', '‹') ..
spanBtn('bsw-hero-next', 'bsw-hero-btn', 'bswNextSlide()', '›') ..
'</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