MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
No edit summary Tag: Manual revert |
No edit summary |
||
| Line 249: | Line 249: | ||
} | } | ||
/* End of includePage */ | /* End of includePage */ | ||
/** | |||
* BattlestarWiki — Main Page JavaScript | |||
* Append to MediaWiki:Common.js | |||
* | |||
* Handles: | |||
* 1. Hero slideshow auto-rotation | |||
* 2. Featured Article of the Day — daily deterministic pick, | |||
* excluding Category:Stub_Pages, with extract + thumbnail | |||
* | |||
* All logic is scoped to .bsw-main-page; safe to load globally. | |||
*/ | |||
( function () { | |||
'use strict'; | |||
var API = 'https://en.battlestarwiki.org/w/api.php'; | |||
/* ── Shared utilities ─────────────────────────────────────────── */ | |||
/** | |||
* Deterministic daily seed — same integer for every visitor | |||
* on a given UTC calendar date. | |||
* @param {string} [dateStr] Optional "YYYY-MM-DD"; defaults to today UTC. | |||
* @returns {number} | |||
*/ | |||
function dailySeed( dateStr ) { | |||
var d; | |||
if ( dateStr ) { | |||
var p = dateStr.split( '-' ); | |||
d = Date.UTC( +p[0], +p[1] - 1, +p[2] ); | |||
} else { | |||
var now = new Date(); | |||
d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() ); | |||
} | |||
return Math.floor( d / 86400000 ); | |||
} | |||
function esc( s ) { | |||
return String( s || '' ) | |||
.replace( /&/g, '&' ) | |||
.replace( /</g, '<' ) | |||
.replace( />/g, '>' ) | |||
.replace( /"/g, '"' ); | |||
} | |||
function apiGet( params ) { | |||
params.format = 'json'; | |||
params.origin = '*'; | |||
var qs = Object.keys( params ) | |||
.map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } ) | |||
.join( '&' ); | |||
return fetch( API + '?' + qs, { headers: { Accept: 'application/json' } } ) | |||
.then( function ( r ) { | |||
if ( !r.ok ) throw new Error( 'API HTTP ' + r.status ); | |||
return r.json(); | |||
} ); | |||
} | |||
/* ════════════════════════════════════════════════════════════════ | |||
1. HERO SLIDESHOW | |||
Each .bsw-slide carries a data-article attribute with the | |||
linked article title. JS batch-fetches the page image for | |||
all slides in one API call and sets it as the slide | |||
background. Falls back gracefully to the dark overlay if | |||
no image is found for a given article. | |||
════════════════════════════════════════════════════════════════ */ | |||
function initHero() { | |||
var slides = document.querySelectorAll( '.bsw-slide' ); | |||
var dots = document.querySelectorAll( '.bsw-hero-dot' ); | |||
if ( !slides.length ) return; | |||
var current = 0; | |||
var total = slides.length; | |||
var timer; | |||
function goTo( n ) { | |||
slides[ current ].classList.remove( 'bsw-active' ); | |||
dots[ current ] && dots[ current ].classList.remove( 'bsw-active' ); | |||
current = ( n + total ) % total; | |||
slides[ current ].classList.add( 'bsw-active' ); | |||
dots[ current ] && dots[ current ].classList.add( 'bsw-active' ); | |||
resetTimer(); | |||
} | |||
function resetTimer() { | |||
clearInterval( timer ); | |||
timer = setInterval( function () { goTo( current + 1 ); }, 6000 ); | |||
} | |||
/* dot clicks */ | |||
dots.forEach( function ( dot, i ) { | |||
dot.addEventListener( 'click', function () { goTo( i ); } ); | |||
} ); | |||
/* prev / next buttons */ | |||
var prevBtn = document.getElementById( 'bsw-hero-prev' ); | |||
var nextBtn = document.getElementById( 'bsw-hero-next' ); | |||
if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } ); | |||
if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } ); | |||
/* pause on hover */ | |||
var hero = document.querySelector( '.bsw-hero' ); | |||
if ( hero ) { | |||
hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } ); | |||
hero.addEventListener( 'mouseleave', resetTimer ); | |||
} | |||
/* ── Fetch article images for all slides ── */ | |||
var titles = []; | |||
slides.forEach( function ( slide ) { | |||
var t = slide.dataset.article; | |||
if ( t ) titles.push( t ); | |||
} ); | |||
if ( titles.length ) { | |||
apiGet( { | |||
action: 'query', | |||
titles: titles.join( '|' ), | |||
prop: 'pageimages', | |||
piprop: 'thumbnail|original', | |||
pithumbsize: '1200' /* large enough for a hero background */ | |||
} ).then( function ( data ) { | |||
var pages = data.query.pages; | |||
/* Build a map of title → image URL. | |||
Prefer the full original; fall back to thumbnail. */ | |||
var imageMap = {}; | |||
Object.values( pages ).forEach( function ( page ) { | |||
if ( !page.title ) return; | |||
var img = ( page.original && page.original.source ) || | |||
( page.thumbnail && page.thumbnail.source ) || | |||
''; | |||
if ( img ) imageMap[ page.title ] = img; | |||
} ); | |||
/* Apply images to slides */ | |||
slides.forEach( function ( slide ) { | |||
var articleTitle = slide.dataset.article; | |||
var imgUrl = articleTitle && imageMap[ articleTitle ]; | |||
if ( !imgUrl ) return; | |||
/* Insert an <img> as the slide background layer, | |||
behind the existing .bsw-slide-overlay and content. | |||
CSS handles object-fit: cover sizing. */ | |||
var bg = slide.querySelector( '.bsw-slide-bg' ); | |||
if ( bg ) { | |||
var img = document.createElement( 'img' ); | |||
img.src = imgUrl; | |||
img.alt = ''; | |||
img.className = 'bsw-slide-bg-img'; | |||
bg.appendChild( img ); | |||
} | |||
} ); | |||
} ).catch( function () { | |||
/* Silently fail — dark gradient fallback is already in CSS */ | |||
} ); | |||
} | |||
resetTimer(); | |||
} | |||
/* ════════════════════════════════════════════════════════════════ | |||
2. FEATURED ARTICLE OF THE DAY | |||
Daily deterministic pick from all mainspace articles, | |||
excluding Category:Stub_Pages. | |||
════════════════════════════════════════════════════════════════ */ | |||
function initFeatured() { | |||
var container = document.getElementById( 'bsw-featured-inner' ); | |||
if ( !container ) return; | |||
var seed = dailySeed(); | |||
/* We fetch batches of 20 random articles, check for stubs, | |||
and pick the first clean one. Two batches covers ~99.99% | |||
of cases given only ~9% of articles are stubs. */ | |||
fetchCandidateBatch( seed ) | |||
.then( function ( pages ) { | |||
var clean = pages.filter( function ( p ) { | |||
/* If categories array is present and contains Stub_Pages, | |||
exclude. Absence of categories key means not a stub. */ | |||
if ( !p.categories ) return true; | |||
return !p.categories.some( function ( c ) { | |||
return c.title === 'Category:Stub_Pages'; | |||
} ); | |||
} ); | |||
if ( !clean.length ) { | |||
/* Extremely unlikely — fall back to second batch */ | |||
return fetchCandidateBatch( seed + 1 ); | |||
} | |||
return clean; | |||
} ) | |||
.then( function ( pages ) { | |||
/* Pick deterministically from the clean pool */ | |||
var pick = pages[ seed % pages.length ]; | |||
renderFeatured( container, pick ); | |||
} ) | |||
.catch( function ( e ) { | |||
container.innerHTML = | |||
'<div class="bsw-error">Could not load featured article. ' + esc( e.message ) + '</div>'; | |||
} ); | |||
} | |||
/** | |||
* Fetch 20 random mainspace articles with extract, thumbnail, | |||
* and stub-category membership check in a single API call. | |||
* Uses rnstart derived from seed for determinism. | |||
*/ | |||
function fetchCandidateBatch( seed ) { | |||
/* Step 1: get 20 random page IDs */ | |||
return apiGet( { | |||
action: 'query', | |||
list: 'random', | |||
rnnamespace: '0', | |||
rnlimit: '20' | |||
} ).then( function ( data ) { | |||
var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' ); | |||
/* Step 2: batch fetch extract + thumbnail + stub check */ | |||
return apiGet( { | |||
action: 'query', | |||
pageids: ids, | |||
prop: 'extracts|pageimages|categories', | |||
exintro: '1', | |||
exchars: '600', | |||
exsectionformat: 'plain', | |||
piprop: 'thumbnail', | |||
pithumbsize: '320', | |||
clcategories: 'Category:Stub_Pages', | |||
cllimit: '1' | |||
} ); | |||
} ).then( function ( data ) { | |||
return Object.values( data.query.pages ); | |||
} ); | |||
} | |||
function renderFeatured( container, page ) { | |||
var title = page.title || ''; | |||
var url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) ); | |||
var extract = page.extract || ''; | |||
var thumb = page.thumbnail ? page.thumbnail.source : ''; | |||
/* Strip any residual HTML tags from extract for safety, | |||
then re-wrap in a paragraph. The API returns clean intro HTML. */ | |||
var thumbHtml; | |||
if ( thumb ) { | |||
thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '">'; | |||
} else { | |||
thumbHtml = '<div class="bsw-fa-thumb-ph">📖</div>'; | |||
} | |||
container.innerHTML = | |||
thumbHtml + | |||
'<div>' + | |||
'<div class="bsw-fa-title">' + | |||
'<a href="' + esc( url ) + '">' + esc( title ) + '</a>' + | |||
'</div>' + | |||
'<div class="bsw-fa-extract">' + extract + '</div>' + | |||
'<div style="margin-top:0.375rem">' + | |||
'<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--bsw-teal-lt)">Read more →</a>' + | |||
'</div>' + | |||
'</div>'; | |||
} | |||
/* ── Entry point ──────────────────────────────────────────────── */ | |||
mw.hook( 'wikipage.content' ).add( function () { | |||
if ( !document.querySelector( '.bsw-main-page' ) ) return; | |||
initHero(); | |||
initFeatured(); | |||
} ); | |||
}() ); | |||
Revision as of 15:49, 11 April 2026
/** Interwiki links to featured articles ***************************************
*
* Description: Highlights interwiki links to featured articles (or
* equivalents) by changing the bullet before the interwiki link
* into a star.
* Maintainers: [[Wikipedia:User:R. Koot|R. Koot]]
*/
function LinkFA()
{
if ( document.getElementById( "p-lang" ) ) {
var InterwikiLinks = document.getElementById( "p-lang" ).getElementsByTagName( "li" );
for ( var i = 0; i < InterwikiLinks.length; i++ ) {
if ( document.getElementById( InterwikiLinks[i].className + "-fa" ) ) {
InterwikiLinks[i].className += " FA"
InterwikiLinks[i].title = "This is a featured article in another language.";
}
}
}
}
addOnloadHook( LinkFA );
/** Collapsible tables *********************************************************
*
* Description: Allows tables to be collapsed, showing only the header. See
* [[Wikipedia:Wikipedia:NavFrame|Wikipedia:NavFrame]].
* Maintainers: [[Wikipedia:User:R. Koot|R. Koot]]
*/
var autoCollapse = 2;
var collapseCaption = "hide";
var expandCaption = "show";
function collapseTable( tableIndex )
{
var Button = document.getElementById( "collapseButton" + tableIndex );
var Table = document.getElementById( "collapsibleTable" + tableIndex );
if ( !Table || !Button ) {
return false;
}
var Rows = Table.getElementsByTagName( "tr" );
if ( Button.firstChild.data == collapseCaption ) {
for ( var i = 1; i < Rows.length; i++ ) {
if(Rows[i].parentNode.parentNode.id == ("collapsibleTable" + tableIndex))
{
Rows[i].style.display = "none";
}
}
Button.firstChild.data = expandCaption;
} else {
for ( var i = 1; i < Rows.length; i++ ) {
if(Rows[i].parentNode.parentNode.id == ("collapsibleTable" + tableIndex))
{
Rows[i].style.display = Rows[0].style.display;
}
}
Button.firstChild.data = collapseCaption;
}
}
function createCollapseButtons()
{
var tableIndex = 0;
var NavigationBoxes = new Object();
var Tables = document.getElementsByTagName( "table" );
for ( var i = 0; i < Tables.length; i++ ) {
if ( hasClass( Tables[i], "collapsible" ) ) {
NavigationBoxes[ tableIndex ] = Tables[i];
Tables[i].setAttribute( "id", "collapsibleTable" + tableIndex );
var Button = document.createElement( "span" );
var ButtonLink = document.createElement( "a" );
var ButtonText = document.createTextNode( collapseCaption );
Button.style.styleFloat = "right";
Button.style.cssFloat = "right";
Button.style.fontWeight = "normal";
Button.style.textAlign = "right";
Button.style.width = "6em";
ButtonLink.setAttribute( "id", "collapseButton" + tableIndex );
ButtonLink.setAttribute( "href", "javascript:collapseTable(" + tableIndex + ");" );
ButtonLink.appendChild( ButtonText );
Button.appendChild( document.createTextNode( "[" ) );
Button.appendChild( ButtonLink );
Button.appendChild( document.createTextNode( "]" ) );
var Header = Tables[i].getElementsByTagName( "tr" )[0].getElementsByTagName( "th" )[0];
/* only add button and increment count if there is a header row to work with */
if (Header) {
Header.insertBefore( Button, Header.childNodes[0] );
tableIndex++;
}
}
}
for ( var i = 0; i < tableIndex; i++ ) {
if ( hasClass( NavigationBoxes[i], "collapsed" ) || ( tableIndex >= autoCollapse && hasClass( NavigationBoxes[i], "autocollapse" ) ) ) {
collapseTable( i );
}
}
}
addOnloadHook( createCollapseButtons );
/** Dynamic Navigation Bars (experimental) *************************************
*
* Description: See [[Wikipedia:NavFrame]].
* Maintainers: UNMAINTAINED
*/
// set up the words in your language
var NavigationBarHide = '[' + collapseCaption + ']';
var NavigationBarShow = '[' + expandCaption + ']';
// shows and hides content and picture (if available) of navigation bars
// Parameters:
// indexNavigationBar: the index of navigation bar to be toggled
function toggleNavigationBar(indexNavigationBar)
{
var NavToggle = document.getElementById("NavToggle" + indexNavigationBar);
var NavFrame = document.getElementById("NavFrame" + indexNavigationBar);
if (!NavFrame || !NavToggle) {
return false;
}
// if shown now
if (NavToggle.firstChild.data == NavigationBarHide) {
for (
var NavChild = NavFrame.firstChild;
NavChild != null;
NavChild = NavChild.nextSibling
) {
if ( hasClass( NavChild, 'NavPic' ) ) {
NavChild.style.display = 'none';
}
if ( hasClass( NavChild, 'NavContent') ) {
NavChild.style.display = 'none';
}
}
NavToggle.firstChild.data = NavigationBarShow;
// if hidden now
} else if (NavToggle.firstChild.data == NavigationBarShow) {
for (
var NavChild = NavFrame.firstChild;
NavChild != null;
NavChild = NavChild.nextSibling
) {
if (hasClass(NavChild, 'NavPic')) {
NavChild.style.display = 'block';
}
if (hasClass(NavChild, 'NavContent')) {
NavChild.style.display = 'block';
}
}
NavToggle.firstChild.data = NavigationBarHide;
}
}
// adds show/hide-button to navigation bars
function createNavigationBarToggleButton()
{
var indexNavigationBar = 0;
// iterate over all < div >-elements
var divs = document.getElementsByTagName("div");
for(
var i=0;
NavFrame = divs[i];
i++
) {
// if found a navigation bar
if (hasClass(NavFrame, "NavFrame")) {
indexNavigationBar++;
var NavToggle = document.createElement("a");
NavToggle.className = 'NavToggle';
NavToggle.setAttribute('id', 'NavToggle' + indexNavigationBar);
NavToggle.setAttribute('href', 'javascript:toggleNavigationBar(' + indexNavigationBar + ');');
var NavToggleText = document.createTextNode(NavigationBarHide);
for (
var NavChild = NavFrame.firstChild;
NavChild != null;
NavChild = NavChild.nextSibling
) {
if ( hasClass( NavChild, 'NavPic' ) || hasClass( NavChild, 'NavContent' ) ) {
if (NavChild.style.display == 'none') {
NavToggleText = document.createTextNode(NavigationBarShow);
break;
}
}
}
NavToggle.appendChild(NavToggleText);
// Find the NavHead and attach the toggle link (Must be this complicated because Moz's firstChild handling is borked)
for(
var j=0;
j < NavFrame.childNodes.length;
j++
) {
if (hasClass(NavFrame.childNodes[j], "NavHead")) {
NavFrame.childNodes[j].appendChild(NavToggle);
}
}
NavFrame.setAttribute('id', 'NavFrame' + indexNavigationBar);
}
}
}
addOnloadHook( createNavigationBarToggleButton );
/* Test if an element has a certain class **************************************
*
* Description: Uses regular expressions and caching for better performance.
* Maintainers: [[Wikipedia:User:Mike Dillon|Mike Dillon]], [[Wikipedia:User:R. Koot|R. Koot]], [[Wikipedia:User:SG|SG]]
*/
var hasClass = (function () {
var reCache = {};
return function (element, className) {
return (reCache[className] ? reCache[className] : (reCache[className] = new RegExp("(?:\\s|^)" + className + "(?:\\s|$)"))).test(element.className);
};
})();
/* Any JavaScript here will be loaded for all users on every page load. */
/** includePage ************
* force the loading of another JavaScript file
*
* Maintainer: [[Commons:User:Dschwen]]
*/
function includePage( name )
{
document.write('<script type="text/javascript" src="' + wgScript + '?title='
+ name
+ '&action=raw&ctype=text/javascript"><\/script>'
);
}
/* End of includePage */
/**
* BattlestarWiki — Main Page JavaScript
* Append to MediaWiki:Common.js
*
* Handles:
* 1. Hero slideshow auto-rotation
* 2. Featured Article of the Day — daily deterministic pick,
* excluding Category:Stub_Pages, with extract + thumbnail
*
* All logic is scoped to .bsw-main-page; safe to load globally.
*/
( function () {
'use strict';
var API = 'https://en.battlestarwiki.org/w/api.php';
/* ── Shared utilities ─────────────────────────────────────────── */
/**
* Deterministic daily seed — same integer for every visitor
* on a given UTC calendar date.
* @param {string} [dateStr] Optional "YYYY-MM-DD"; defaults to today UTC.
* @returns {number}
*/
function dailySeed( dateStr ) {
var d;
if ( dateStr ) {
var p = dateStr.split( '-' );
d = Date.UTC( +p[0], +p[1] - 1, +p[2] );
} else {
var now = new Date();
d = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() );
}
return Math.floor( d / 86400000 );
}
function esc( s ) {
return String( s || '' )
.replace( /&/g, '&' )
.replace( /</g, '<' )
.replace( />/g, '>' )
.replace( /"/g, '"' );
}
function apiGet( params ) {
params.format = 'json';
params.origin = '*';
var qs = Object.keys( params )
.map( function ( k ) { return encodeURIComponent( k ) + '=' + encodeURIComponent( params[k] ); } )
.join( '&' );
return fetch( API + '?' + qs, { headers: { Accept: 'application/json' } } )
.then( function ( r ) {
if ( !r.ok ) throw new Error( 'API HTTP ' + r.status );
return r.json();
} );
}
/* ════════════════════════════════════════════════════════════════
1. HERO SLIDESHOW
Each .bsw-slide carries a data-article attribute with the
linked article title. JS batch-fetches the page image for
all slides in one API call and sets it as the slide
background. Falls back gracefully to the dark overlay if
no image is found for a given article.
════════════════════════════════════════════════════════════════ */
function initHero() {
var slides = document.querySelectorAll( '.bsw-slide' );
var dots = document.querySelectorAll( '.bsw-hero-dot' );
if ( !slides.length ) return;
var current = 0;
var total = slides.length;
var timer;
function goTo( n ) {
slides[ current ].classList.remove( 'bsw-active' );
dots[ current ] && dots[ current ].classList.remove( 'bsw-active' );
current = ( n + total ) % total;
slides[ current ].classList.add( 'bsw-active' );
dots[ current ] && dots[ current ].classList.add( 'bsw-active' );
resetTimer();
}
function resetTimer() {
clearInterval( timer );
timer = setInterval( function () { goTo( current + 1 ); }, 6000 );
}
/* dot clicks */
dots.forEach( function ( dot, i ) {
dot.addEventListener( 'click', function () { goTo( i ); } );
} );
/* prev / next buttons */
var prevBtn = document.getElementById( 'bsw-hero-prev' );
var nextBtn = document.getElementById( 'bsw-hero-next' );
if ( prevBtn ) prevBtn.addEventListener( 'click', function () { goTo( current - 1 ); } );
if ( nextBtn ) nextBtn.addEventListener( 'click', function () { goTo( current + 1 ); } );
/* pause on hover */
var hero = document.querySelector( '.bsw-hero' );
if ( hero ) {
hero.addEventListener( 'mouseenter', function () { clearInterval( timer ); } );
hero.addEventListener( 'mouseleave', resetTimer );
}
/* ── Fetch article images for all slides ── */
var titles = [];
slides.forEach( function ( slide ) {
var t = slide.dataset.article;
if ( t ) titles.push( t );
} );
if ( titles.length ) {
apiGet( {
action: 'query',
titles: titles.join( '|' ),
prop: 'pageimages',
piprop: 'thumbnail|original',
pithumbsize: '1200' /* large enough for a hero background */
} ).then( function ( data ) {
var pages = data.query.pages;
/* Build a map of title → image URL.
Prefer the full original; fall back to thumbnail. */
var imageMap = {};
Object.values( pages ).forEach( function ( page ) {
if ( !page.title ) return;
var img = ( page.original && page.original.source ) ||
( page.thumbnail && page.thumbnail.source ) ||
'';
if ( img ) imageMap[ page.title ] = img;
} );
/* Apply images to slides */
slides.forEach( function ( slide ) {
var articleTitle = slide.dataset.article;
var imgUrl = articleTitle && imageMap[ articleTitle ];
if ( !imgUrl ) return;
/* Insert an <img> as the slide background layer,
behind the existing .bsw-slide-overlay and content.
CSS handles object-fit: cover sizing. */
var bg = slide.querySelector( '.bsw-slide-bg' );
if ( bg ) {
var img = document.createElement( 'img' );
img.src = imgUrl;
img.alt = '';
img.className = 'bsw-slide-bg-img';
bg.appendChild( img );
}
} );
} ).catch( function () {
/* Silently fail — dark gradient fallback is already in CSS */
} );
}
resetTimer();
}
/* ════════════════════════════════════════════════════════════════
2. FEATURED ARTICLE OF THE DAY
Daily deterministic pick from all mainspace articles,
excluding Category:Stub_Pages.
════════════════════════════════════════════════════════════════ */
function initFeatured() {
var container = document.getElementById( 'bsw-featured-inner' );
if ( !container ) return;
var seed = dailySeed();
/* We fetch batches of 20 random articles, check for stubs,
and pick the first clean one. Two batches covers ~99.99%
of cases given only ~9% of articles are stubs. */
fetchCandidateBatch( seed )
.then( function ( pages ) {
var clean = pages.filter( function ( p ) {
/* If categories array is present and contains Stub_Pages,
exclude. Absence of categories key means not a stub. */
if ( !p.categories ) return true;
return !p.categories.some( function ( c ) {
return c.title === 'Category:Stub_Pages';
} );
} );
if ( !clean.length ) {
/* Extremely unlikely — fall back to second batch */
return fetchCandidateBatch( seed + 1 );
}
return clean;
} )
.then( function ( pages ) {
/* Pick deterministically from the clean pool */
var pick = pages[ seed % pages.length ];
renderFeatured( container, pick );
} )
.catch( function ( e ) {
container.innerHTML =
'<div class="bsw-error">Could not load featured article. ' + esc( e.message ) + '</div>';
} );
}
/**
* Fetch 20 random mainspace articles with extract, thumbnail,
* and stub-category membership check in a single API call.
* Uses rnstart derived from seed for determinism.
*/
function fetchCandidateBatch( seed ) {
/* Step 1: get 20 random page IDs */
return apiGet( {
action: 'query',
list: 'random',
rnnamespace: '0',
rnlimit: '20'
} ).then( function ( data ) {
var ids = data.query.random.map( function ( p ) { return p.id; } ).join( '|' );
/* Step 2: batch fetch extract + thumbnail + stub check */
return apiGet( {
action: 'query',
pageids: ids,
prop: 'extracts|pageimages|categories',
exintro: '1',
exchars: '600',
exsectionformat: 'plain',
piprop: 'thumbnail',
pithumbsize: '320',
clcategories: 'Category:Stub_Pages',
cllimit: '1'
} );
} ).then( function ( data ) {
return Object.values( data.query.pages );
} );
}
function renderFeatured( container, page ) {
var title = page.title || '';
var url = 'https://en.battlestarwiki.org/wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
var extract = page.extract || '';
var thumb = page.thumbnail ? page.thumbnail.source : '';
/* Strip any residual HTML tags from extract for safety,
then re-wrap in a paragraph. The API returns clean intro HTML. */
var thumbHtml;
if ( thumb ) {
thumbHtml = '<img class="bsw-fa-thumb" src="' + esc( thumb ) + '" alt="' + esc( title ) + '">';
} else {
thumbHtml = '<div class="bsw-fa-thumb-ph">📖</div>';
}
container.innerHTML =
thumbHtml +
'<div>' +
'<div class="bsw-fa-title">' +
'<a href="' + esc( url ) + '">' + esc( title ) + '</a>' +
'</div>' +
'<div class="bsw-fa-extract">' + extract + '</div>' +
'<div style="margin-top:0.375rem">' +
'<a href="' + esc( url ) + '" style="font-size:0.75rem;color:var(--bsw-teal-lt)">Read more →</a>' +
'</div>' +
'</div>';
}
/* ── Entry point ──────────────────────────────────────────────── */
mw.hook( 'wikipage.content' ).add( function () {
if ( !document.querySelector( '.bsw-main-page' ) ) return;
initHero();
initFeatured();
} );
}() );