Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* =====================================================================
* Bharatpedia Gadget: AutoInterlink (v1.0)
* Auto interlinker tool page (Bharatpedia:Auto interlinker).
*
* Enter an article title: the tool scans the wikitext for unlinked
* terms that exist as Bharatpedia articles, shows a review list with
* checkboxes, and publishes the chosen links as the logged-in user
* when they click "Publish changes". "Discard" clears the results.
*
* All user-facing text is in English.
* ===================================================================== */
( function () {
'use strict';
var TOOL_PAGE = 'Bharatpedia:Auto_interlinker';
var MAX_CHECK = 100;
var MAX_LINKS = 50;
var STOP = ( 'a,an,the,and,or,but,nor,of,in,on,at,to,for,with,by,from,as,is,was,were,are,be,been,being,' +
'has,have,had,do,does,did,will,would,can,could,shall,should,may,might,must,he,she,it,they,them,we,us,you,i,' +
'me,my,mine,his,her,hers,its,their,theirs,our,ours,your,yours,this,that,these,those,there,here,where,when,' +
'what,which,who,whom,whose,how,why,not,no,yes,so,such,than,then,too,very,just,also,well,however,although,' +
'though,because,since,until,while,during,between,among,through,over,under,above,below,into,out,off,up,down,' +
'about,against,after,before,per,via,etc,vs,mr,mrs,ms,dr,st,jr,sr,' +
'january,february,march,april,june,july,august,september,october,november,december,' +
'monday,tuesday,wednesday,thursday,friday,saturday,sunday,today,tomorrow,yesterday' ).split( ',' );
function esc( s ) {
return String( s == null ? '' : s ).replace( /[&<>"']/g, function ( c ) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ c ];
} );
}
function mwNorm( s ) {
s = String( s || '' ).replace( /_/g, ' ' ).replace( /\s+/g, ' ' ).trim();
return s ? s.charAt( 0 ).toUpperCase() + s.slice( 1 ) : s;
}
// Regions of wikitext that must never get links inserted inside them.
function getSafeRanges( text ) {
// Returns [start,end) ranges of text SAFE for linking (excludes [[links]], <ref>, {{templates}})
if ( !text ) return [];
var n = text.length, i = 0;
var unsafe = []; // [start,end) ranges to EXCLUDE
var guard = 0;
while ( i < n && guard < 100000 ) {
guard++;
// ==heading== (exclude entire line if it's a heading)
if ( text.substr( i, 2 ) === '==' ) {
var nl = text.indexOf( '\n', i );
unsafe.push( [ i, nl === -1 ? n : nl ] );
i = nl === -1 ? n : nl;
continue;
}
// <!-- comment -->
if ( text.substr( i, 4 ) === '<!--' ) {
var e = text.indexOf( '-->', i + 4 );
unsafe.push( [ i, e === -1 ? n : e + 3 ] );
i = e === -1 ? n : e + 3;
continue;
}
// [[link]]
if ( text.substr( i, 2 ) === '[[' ) {
var j = text.indexOf( ']]', i + 2 );
unsafe.push( [ i, j === -1 ? n : j + 2 ] );
i = j === -1 ? n : j + 2;
continue;
}
// {{template}} with nesting
if ( text.substr( i, 2 ) === '{{' ) {
var depth = 1, k = i + 2;
var g2 = 0;
while ( k < n && depth > 0 && g2 < 50000 ) {
g2++;
if ( text.substr( k, 2 ) === '{{' ) { depth++; k += 2; }
else if ( text.substr( k, 2 ) === '}}' ) { depth--; k += 2; }
else k++;
}
unsafe.push( [ i, k ] );
i = k;
continue;
}
// <ref>...</ref> or <ref .../>
if ( text.substr( i, 4 ).toLowerCase() === '<ref' ) {
var gt = text.indexOf( '>', i + 4 );
if ( gt === -1 ) { unsafe.push( [ i, n ] ); i = n; continue; }
var tag = text.slice( i, gt + 1 );
if ( tag.slice( -2 ) === '/>' ) { unsafe.push( [ i, gt + 1 ] ); i = gt + 1; continue; }
var ce = text.toLowerCase().indexOf( '</ref>', gt + 1 );
unsafe.push( [ i, ce === -1 ? n : ce + 6 ] );
i = ce === -1 ? n : ce + 6;
continue;
}
i++;
}
// Build safe ranges = complement of unsafe
unsafe.sort( function ( a, b ) { return a[0] - b[0]; } );
var safe = [], pos = 0, u;
for ( u = 0; u < unsafe.length; u++ ) {
if ( unsafe[u][0] > pos ) safe.push( [ pos, unsafe[u][0] ] );
if ( unsafe[u][1] > pos ) pos = unsafe[u][1];
}
if ( pos < n ) safe.push( [ pos, n ] );
return safe;
}
function collectExistingLinks( text ) {
// Simplified safe version
var set = {};
if ( !text ) return set;
try {
var re = /\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g, m;
var count = 0;
while ( ( m = re.exec( text ) ) !== null && count < 1000 ) {
count++;
set[ m[ 1 ].trim().toLowerCase().replace( /_/g, ' ' ) ] = true;
if ( m[ 2 ] !== undefined ) set[ m[ 2 ].trim().toLowerCase().replace( /_/g, ' ' ) ] = true;
}
} catch ( e ) {}
return set;
}
function stripPossessive( w ) {
return w.replace( /'s$/i, '' ).replace( /'$/, '' );
}
function extractCandidates( text, safeRanges ) {
// Simplified robust version - extracts capitalized phrases with REAL positions
var cands = [], seen = {};
var r, rs, seg, base;
var re = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3})\b/g;
var m, count = 0;
for ( r = 0; r < safeRanges.length && count < 500; r++ ) {
rs = safeRanges[ r ];
seg = text.slice( rs[ 0 ], rs[ 1 ] );
base = rs[ 0 ];
re.lastIndex = 0;
while ( ( m = re.exec( seg ) ) !== null && count < 500 ) {
count++;
var phrase = m[1];
var absStart = base + m.index;
var absEnd = absStart + phrase.length;
var words = phrase.split(/\s+/);
if ( words.length === 1 ) {
var lw = words[0].toLowerCase();
if ( STOP.indexOf( lw ) !== -1 ) continue;
}
var key = phrase.toLowerCase();
if ( seen[ key ] ) continue;
seen[ key ] = true;
var pluralS = '';
var term = phrase;
var display = phrase;
if ( /s$/i.test( term ) && term.length > 3 ) {
var sing = term.replace( /s$/i, '' );
if ( sing.length > 2 ) {
pluralS = 's';
term = sing;
display = sing; // Display the singular, s goes outside
key = term.toLowerCase();
}
if ( seen[ key ] ) continue;
seen[ key ] = true;
}
cands.push( { term: term, pluralS: pluralS, display: display, start: absStart, end: absEnd, words: words.length } );
}
}
return cands;
}
function greedyFilter( cands ) {
// Simplified: just return first 100 to avoid O(n^2) hang
// TODO: Implement proper overlap filtering later
return cands.slice( 0, 100 );
}
function makeLink( display, finalTitle, pluralS ) {
pluralS = pluralS || '';
if ( display.toLowerCase() === finalTitle.toLowerCase() ) return '[[' + display + ']]' + pluralS;
return '[[' + finalTitle + '|' + display + ']]' + pluralS;
}
function contextSnippet( text, c ) {
var s = Math.max( 0, c.start - 60 ), e = Math.min( text.length, c.end + 60 );
var before = text.slice( s, c.start ), mid = text.slice( c.start, c.end ), after = text.slice( c.end, e );
before = before.replace( /\s+/g, ' ' );
after = after.replace( /\s+/g, ' ' );
return ( s > 0 ? '…' : '' ) + before + '[[' + mid + ']]' + after + ( e < text.length ? '…' : '' );
}
function buildTool() {
var api = new mw.Api();
var $root = $( '<div>' ).addClass( 'bp-ail' ).css( {
border: '2px solid #36c', 'border-radius': '10px', padding: '18px',
margin: '0 0 18px 0', background: '#f8f9fa', 'box-sizing': 'border-box'
} );
$root.append( $( '<h2>' ).text( 'Auto interlinker' )
.css( { margin: '0 0 4px 0', 'font-size': '22px' } ) );
$root.append( $( '<p>' )
.text( 'Enter an article title. The tool finds terms that exist as Bharatpedia articles, lets you review each suggestion, and publishes the chosen links.' )
.css( { margin: '0 0 12px 0', 'font-size': '13px', color: '#54595d' } ) );
var $row = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '12px' } );
var $titleInput = $( '<input>' ).attr( { type: 'text', placeholder: 'Article title (e.g. Hacktivist Vanguard)' } ).css( {
flex: '1', padding: '10px 12px', 'font-size': '15px',
border: '2px solid #36c', 'border-radius': '6px', 'box-sizing': 'border-box'
} );
var $scanBtn = $( '<button>' ).text( 'Scan' ).css( {
padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px', 'font-weight': 'bold'
} );
$row.append( $titleInput, $scanBtn );
$root.append( $row );
// Autocomplete dropdown for article titles
var $suggest = $( '<div>' ).css( {
position: 'absolute', background: '#fff', border: '1px solid #a2a9b1',
'border-radius': '4px', 'box-shadow': '0 2px 8px rgba(0,0,0,0.15)',
'z-index': '1000', 'max-height': '200px', overflow: 'auto', display: 'none',
'font-size': '14px', 'min-width': '300px'
} );
$( 'body' ).append( $suggest );
var suggestTimer = null;
$titleInput.on( 'input', function () {
var q = $titleInput.val().trim();
if ( suggestTimer ) clearTimeout( suggestTimer );
if ( q.length < 2 ) { $suggest.hide(); return; }
suggestTimer = setTimeout( function () {
api.get( { action: 'opensearch', search: q, limit: 8, namespace: 0 } ).then( function ( d ) {
var titles = d[1] || [];
$suggest.empty();
if ( !titles.length ) { $suggest.hide(); return; }
titles.forEach( function ( t ) {
var $it = $( '<div>' ).text( t ).css( { padding: '8px 12px', cursor: 'pointer' } );
$it.on( 'mouseenter', function () { $it.css( 'background', '#eaf3ff' ); } );
$it.on( 'mouseleave', function () { $it.css( 'background', '#fff' ); } );
$it.on( 'click', function () { $titleInput.val( t ); $suggest.hide(); $titleInput.focus(); } );
$suggest.append( $it );
} );
var off = $titleInput.offset();
$suggest.css( { top: off.top + $titleInput.outerHeight() + 4, left: off.left, width: $titleInput.outerWidth() } ).show();
} ).catch( function () { $suggest.hide(); } );
}, 300 );
} );
$( document ).on( 'click', function ( e ) {
if ( !$( e.target ).closest( $suggest ).length && !$( e.target ).closest( $titleInput ).length ) $suggest.hide();
} );
var $msg = $( '<div>' ).css( { 'font-size': '13px', color: '#d33', 'margin-bottom': '8px' } ).hide();
$root.append( $msg );
var $status = $( '<div>' ).css( { 'font-size': '13px', color: '#54595d', 'margin-bottom': '8px' } ).hide();
$root.append( $status );
var $results = $( '<div>' ).hide();
$root.append( $results );
var state = {
title: '', wikitext: '', timestamp: '', items: []
};
function showMsg( t ) { $msg.text( t ).show(); }
function showStatus( t ) { $status.text( t ).show(); }
function setScanBtn( busy ) {
$scanBtn.prop( 'disabled', busy ).text( busy ? 'Scanning…' : 'Scan' );
}
function resolveTitles( lookups, onProgress ) {
var map = {};
var batches = [];
for ( var i = 0; i < lookups.length; i += 100 ) {
batches.push( lookups.slice( i, i + 100 ) );
}
var chain = $.Deferred().resolve().promise();
batches.forEach( function ( batch, bi ) {
chain = chain.then( function () {
if ( onProgress ) onProgress( Math.min( ( bi + 1 ) * 100, lookups.length ), lookups.length );
return api.get( {
action: 'query', titles: batch.join( '|' ), prop: 'info', redirects: 1
} ).then( function ( d ) {
var byTitle = {}, p, id;
var pages = ( d.query && d.query.pages ) || {};
for ( id in pages ) {
p = pages[ id ];
byTitle[ mwNorm( p.title ) ] = p;
}
var redirs = {};
( ( d.query && d.query.redirects ) || [] ).forEach( function ( r ) {
redirs[ mwNorm( r.from ) ] = mwNorm( r.to );
} );
var norms = {};
( ( d.query && d.query.normalized ) || [] ).forEach( function ( x ) {
norms[ mwNorm( x.from ) ] = mwNorm( x.to );
} );
batch.forEach( function ( t ) {
var nt = mwNorm( t );
if ( norms[ nt ] ) nt = norms[ nt ];
var seenR = {}, guard = 0;
while ( redirs[ nt ] && !seenR[ nt ] && guard < 10 ) { seenR[ nt ] = true; nt = redirs[ nt ]; guard++; }
var pg = byTitle[ nt ];
if ( pg && pg.missing === undefined ) {
map[ mwNorm( t ) ] = { exists: true, final: pg.title };
} else {
map[ mwNorm( t ) ] = { exists: false, final: null };
}
} );
}, function () {
batch.forEach( function ( t ) {
map[ mwNorm( t ) ] = { exists: false, final: null };
} );
} );
} ).then( function () {
var d = $.Deferred();
setTimeout( d.resolve, 200 );
return d.promise();
} );
} );
return chain.then( function () { return map; } );
}
function scan() {
var title = $.trim( $titleInput.val() );
$msg.hide(); $results.hide().empty();
if ( !title ) {
showMsg( 'Please enter an article title.' );
return;
}
setScanBtn( true );
showStatus( 'Fetching article…' );
api.get( {
action: 'query', prop: 'revisions', titles: title,
rvprop: 'content|timestamp', rvslots: 'main', formatversion: 2
} ).then( function ( d ) {
var pages = d.query.pages;
var page = pages[ 0 ];
if ( page.missing ) {
throw new Error( 'Article not found: ' + title );
}
state.title = page.title;
state.wikitext = page.revisions[ 0 ].slots.main.content;
state.timestamp = page.revisions[ 0 ].timestamp;
state.normTitle = mwNorm( page.title ).toLowerCase().replace( / /g, '_' );
showStatus( 'Extracting candidate terms…' );
var safeRanges = getSafeRanges( state.wikitext );
var existing = collectExistingLinks( state.wikitext );
var cands = greedyFilter( extractCandidates( state.wikitext, safeRanges ) );
cands = cands.filter( function ( c ) {
return !existing[ c.term.toLowerCase() ] && !existing[ c.display.toLowerCase() ];
} );
if ( !cands.length ) {
showStatus( 'No linkable terms found in ' + state.title + '.' );
return;
}
var toCheck = cands.slice( 0, MAX_CHECK );
showStatus( 'Checking ' + toCheck.length + ' term(s)…' );
return resolveTitles(
toCheck.map( function ( c ) { return c.term; } ),
function ( done, total ) { showStatus( 'Checking terms… (' + done + '/' + total + ')' ); }
).then( function ( map ) {
var items = [];
toCheck.forEach( function ( c ) {
var info = map[ mwNorm( c.term ) ];
if ( !info || !info.exists ) return;
if ( mwNorm( info.final ).toLowerCase().replace( / /g, '_' ) === state.normTitle ) return;
items.push( {
cand: c, finalTitle: info.final,
link: makeLink( c.display, info.final, c.pluralS ),
checked: null
} );
if ( items.length >= MAX_LINKS ) return;
} );
state.items = items;
if ( !items.length ) {
showStatus( 'No new links to suggest for ' + state.title + '.' );
return;
}
renderResults();
} );
} ).catch( function ( err ) {
showMsg( ( err && err.message ) || 'Something went wrong.' );
$status.hide();
} ).always( function () {
setScanBtn( false );
} );
}
function renderResults() {
$status.hide();
var items = state.items;
var $head = $( '<p>' ).html(
'Found <b>' + items.length + '</b> suggested link(s) in <b>' +
esc( state.title ) + '</b>. Review and publish, or discard.'
).css( { 'font-size': '14px' } );
$results.append( $head );
var $togRow = $( '<div>' ).css( { 'margin-bottom': '8px', 'font-size': '13px' } );
var $selAll = $( '<button>' ).text( 'Select all' ).css( btnSmallCss() );
var $selNone = $( '<button>' ).text( 'Select none' ).css( btnSmallCss() );
$selAll.on( 'click', function () { toggleAll( true ); } );
$selNone.on( 'click', function () { toggleAll( false ); } );
$togRow.append( $selAll, ' ', $selNone );
$results.append( $togRow );
var $list = $( '<div>' ).css( {
'max-height': '340px', overflow: 'auto', border: '1px solid #c8ccd1',
'border-radius': '6px', background: '#fff', padding: '8px', 'margin-bottom': '12px'
} );
items.forEach( function ( it ) {
var $item = $( '<div>' ).css( {
display: 'flex', gap: '8px', 'align-items': 'flex-start',
padding: '8px', 'border-bottom': '1px solid #eaecf0', 'font-size': '13px'
} );
var $cb = $( '<input>' ).attr( { type: 'checkbox', checked: true } )
.css( { 'margin-top': '3px' } );
it.checked = $cb;
var $txt = $( '<div>' ).css( { flex: '1' } );
var $termLine = $( '<div>' );
$termLine.append( $( '<b>' ).text( it.cand.display ) );
$termLine.append( document.createTextNode( ' → ' ) );
$termLine.append( $( '<a>' )
.attr( { href: mw.util.getUrl( it.finalTitle ), target: '_blank', rel: 'noopener' } )
.text( it.finalTitle ) );
$txt.append( $termLine );
var ctx = contextSnippet( state.wikitext, it.cand );
var $ctx = $( '<div>' ).css( { color: '#72777d', 'font-size': '12px', 'margin-top': '2px' } );
$ctx.append( document.createTextNode( ctx.replace( /\[\[/g, '' ).replace( /\]\]/g, '' ) ) );
$txt.append( $ctx );
$item.append( $cb, $txt );
$list.append( $item );
} );
$results.append( $list );
var $btnRow = $( '<div>' ).css( { display: 'flex', gap: '8px', 'align-items': 'center' } );
var $pubBtn = $( '<button>' ).text( 'Publish' ).css( {
padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
background: '#14866d', color: '#fff', border: 'none',
'border-radius': '6px', 'font-weight': 'bold'
} );
$pubBtn.on( 'click', publish );
var $discardBtn = $( '<button>' ).text( 'Discard' ).css( {
padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
background: '#72777d', color: '#fff', border: 'none',
'border-radius': '6px', 'font-weight': 'bold'
} );
$discardBtn.on( 'click', function () {
state.items = [];
$results.hide().empty();
showStatus( 'Discarded. Enter an article title to scan again.' );
} );
$btnRow.append( $pubBtn, $discardBtn );
$results.append( $btnRow );
$results.append( $( '<p>' ).text( 'The edit will be saved under your own account. Please review the list above first.' )
.css( { 'font-size': '12px', color: '#54595d', 'margin-top': '8px' } ) );
$results.show();
}
function btnSmallCss() {
return {
padding: '4px 12px', 'font-size': '12px', cursor: 'pointer',
background: '#eaecf0', color: '#202122', border: '1px solid #c8ccd1',
'border-radius': '4px'
};
}
function toggleAll( v ) {
state.items.forEach( function ( it ) {
if ( it.checked ) it.checked.prop( 'checked', v );
} );
}
function publish() {
if ( mw.config.get( 'wgUserName' ) === null ) {
showMsg( 'Please log in to publish changes.' );
return;
}
var chosen = state.items.filter( function ( it ) { return it.checked && it.checked.is( ':checked' ); } );
if ( !chosen.length ) {
showMsg( 'No links selected.' );
return;
}
// Build replacements from end to start so offsets stay valid
var reps = chosen.map( function ( it ) {
return { start: it.cand.start, end: it.cand.end, text: it.link };
} );
reps.sort( function ( a, b ) { return b.start - a.start; } );
var text = state.wikitext;
reps.forEach( function ( r ) {
text = text.slice( 0, r.start ) + r.text + text.slice( r.end );
} );
showStatus( 'Publishing…' );
api.postWithToken( 'csrf', {
action: 'edit',
title: state.title,
text: text,
basetimestamp: state.timestamp,
summary: 'Add internal wikilinks via Auto interlinker tool',
assert: 'user'
} ).then( function ( d ) {
var rev = d.edit && d.edit.newrevid;
$results.empty().append(
$( '<p>' ).html(
'<b>Published!</b> ' + reps.length + ' link(s) added to <b>' +
esc( state.title ) + '</b>.' +
( rev ? ' <a href="' + mw.util.getUrl( 'Special:Diff/' + rev ) + '">View changes</a>' : '' )
).css( { 'font-size': '14px', color: '#14866d' } )
);
$status.hide();
} ).fail( function ( code, err ) {
$status.hide();
showMsg( 'Publish failed: ' + ( err && err.message ? err.message : code ) );
} );
}
$scanBtn.on( 'click', scan );
$titleInput.on( 'keydown', function ( e ) { if ( e.key === 'Enter' ) scan(); } );
return $root;
}
function init() {
try {
var pageName = mw.config.get( 'wgPageName' );
if ( pageName !== TOOL_PAGE && pageName.indexOf( TOOL_PAGE + '/' ) !== 0 ) {
return;
}
var $mount = $( '#autointerlink-root' );
if ( !$mount.length ) {
return;
}
var $tool = buildTool();
$mount.replaceWith( $tool );
} catch ( e ) {
if ( window.console && console.error ) {
console.error( 'AutoInterlink init failed: ' + ( e && e.message ) );
}
}
}
function waitForDeps( callback, attempts ) {
attempts = attempts || 0;
try {
if ( typeof $ !== 'undefined' && typeof mw !== 'undefined' &&
mw.Api && mw.util && document.getElementById( 'autointerlink-root' ) ) {
callback();
return;
}
} catch ( e ) {}
if ( attempts < 100 ) {
setTimeout( function () { waitForDeps( callback, attempts + 1 ); }, 100 );
} else {
try { callback(); } catch ( e ) {}
}
}
waitForDeps( function () {
try {
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
} catch ( e ) {}
} );
}() );
// cache-bust 1505