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: RefGenBarefix (v1.6)
 * v1.4: also detect malformed refs like <ref>(URL text)</ref>.
 * v1.5: also detect bare URLs tagged with {{Bare URL inline}} / {{Bare URL PDF}}.
 * v1.6: show the reason next to each skipped URL (e.g. no citation data, dead link).
  * v1.3: search suggestions (autocomplete) on the article title input.
 * Bare reference fixer tool page (Bharatpedia:Bare_reference_fixer).
 *
 * Enter an article title: the tool scans the wikitext for bare-URL
 * <ref>s, builds formatted citations with the Citation generator
 * engine (mw.libs.refGen from ext.gadget.RefGen), shows a review list,
 * and publishes the fix as the logged-in user when they click
 * "Publish changes". Tracking params (fbclid, utm_*) are stripped.
 * Duplicate URLs reuse a named reference.
 *
 * All user-facing text is in English.
 * ===================================================================== */
( function () {
	'use strict';

	var TOOL_PAGE = 'Bharatpedia:Bare_reference_fixer';

	var TRACKING = [ 'fbclid', 'gclid', 'igshid', 'mc_cid', 'mc_eid',
		'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content' ];

	function cleanUrl( url ) {
		try {
			var u = new URL( url );
			TRACKING.forEach( function ( p ) { u.searchParams.delete( p ); } );
			return u.toString();
		} catch ( e ) {
			return url;
		}
	}

	var BARE_RE = /<ref(\s+name\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s/>]+))?)?\s*>\s*(https?:\/\/[^\s<{]+?)\s*({{\s*bare url(?: pdf| inline)?\s*(?:\|[^}]*)?}})?\s*<\/ref>/gi;

	var BADREF_RE = /<ref(\s+name\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s/>]+))?)?\s*>\s*[(\[]\s*(https?:\/\/[^\s<)\]]+)\s+([^<)\]]+?)\s*[)\]]\s*<\/ref>/gi;

	function findBareRefs( text ) {
		var out = [], m;
		BARE_RE.lastIndex = 0;
		while ( ( m = BARE_RE.exec( text ) ) !== null ) {
			var url = m[ 5 ].replace( /[.,;:!?)]+$/, '' );
			out.push( {
				start: m.index, end: m.index + m[ 0 ].length,
				raw: m[ 0 ], name: m[ 2 ] || m[ 3 ] || m[ 4 ] || null,
				url: url, kind: 'bare'
			} );
		}
		return out;
	}

	function findBadRefs( text ) {
		var out = [], m;
		BADREF_RE.lastIndex = 0;
		while ( ( m = BADREF_RE.exec( text ) ) !== null ) {
			var url = m[ 5 ].replace( /[.,;:!?)]+$/, '' );
			out.push( {
				start: m.index, end: m.index + m[ 0 ].length,
				raw: m[ 0 ], name: m[ 2 ] || m[ 3 ] || m[ 4 ] || null,
				url: url, kind: 'malformed'
			} );
		}
		return out;
	}

	function esc( s ) {
		return String( s == null ? '' : s ).replace( /[&<>"']/g, function ( c ) {
			return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ c ];
		} );
	}

	function buildTool() {
		var RefGen = mw.libs.refGen;
		var api = new mw.Api();

		var $root = $( '<div>' ).addClass( 'bp-brf' ).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( 'Bare reference fixer' )
			.css( { margin: '0 0 4px 0', 'font-size': '22px' } ) );
		$root.append( $( '<p>' )
			.text( 'Enter an article title. The tool finds bare-URL and malformed references, converts them to formatted citations, and lets you review before publishing.' )
			.css( { margin: '0 0 12px 0', 'font-size': '13px', color: '#54595d' } ) );

		var $row = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '12px' } );
		var $inputWrap = $( '<div>' ).css( { flex: '1', position: 'relative', 'box-sizing': 'border-box' } );
		var $titleInput = $( '<input>' ).attr( { type: 'text', placeholder: 'Article title (e.g. Abhijit Das)', autocomplete: 'off' } ).css( {
			width: '100%', padding: '10px 12px', 'font-size': '15px',
			border: '2px solid #36c', 'border-radius': '6px', 'box-sizing': 'border-box'
		} );
		var $suggest = $( '<div>' ).css( {
			display: 'none', position: 'absolute', top: '100%', left: 0, right: 0, 'z-index': 50,
			background: '#fff', border: '1px solid #a2a9b1', 'border-top': 'none',
			'border-radius': '0 0 6px 6px', 'box-shadow': '0 4px 12px rgba(0,0,0,0.15)',
			'max-height': '220px', overflow: 'auto', 'font-size': '14px'
		} );
		$inputWrap.append( $titleInput, $suggest );
		var $scanBtn = $( '<button>' ).text( 'Scan article' ).css( {
			padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
			background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px', 'font-weight': 'bold'
		} );
		$row.append( $inputWrap, $scanBtn );
		$root.append( $row );
		// Search suggestions (autocomplete) for the article title input
		var suggestTimer = null;
		function hideSuggest() { $suggest.hide().empty(); }
		$titleInput.on( 'input', function () {
			var q = $.trim( $titleInput.val() );
			if ( suggestTimer ) { clearTimeout( suggestTimer ); }
			if ( q.length < 2 ) { hideSuggest(); return; }
			suggestTimer = setTimeout( function () {
				api.get( { action: 'opensearch', search: q, limit: 8, namespace: 0 } ).done( function ( d ) {
					if ( $.trim( $titleInput.val() ) !== q ) { return; }
					var titles = ( d && d[ 1 ] ) || [];
					if ( !titles.length ) { hideSuggest(); return; }
					$suggest.empty();
					titles.forEach( function ( t ) {
						var $opt = $( '<div>' ).text( t ).css( {
							padding: '8px 12px', cursor: 'pointer', 'word-break': 'break-word'
						} );
						$opt.on( 'mouseenter', function () { $opt.css( 'background', '#eaecf0' ); } );
						$opt.on( 'mouseleave', function () { $opt.css( 'background', '' ); } );
						$opt.on( 'mousedown', function ( e ) {
							e.preventDefault();
							$titleInput.val( t );
							hideSuggest();
						} );
						$suggest.append( $opt );
					} );
					$suggest.show();
				} ).fail( function () { hideSuggest(); } );
			}, 300 );
		} );
		$titleInput.on( 'keydown', function ( e ) { if ( e.key === 'Escape' ) { hideSuggest(); } } );
		$titleInput.on( 'blur', function () { setTimeout( hideSuggest, 150 ); } );

		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 scan() {
			var title = $.trim( $titleInput.val() );
			$msg.hide(); $results.hide().empty();
			if ( !title ) {
				showMsg( 'Please enter an article title.' );
				return;
			}
			$scanBtn.prop( 'disabled', true ).text( 'Scanning…' );
			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;
				var bare = findBareRefs( state.wikitext );
				var bad = findBadRefs( state.wikitext );
				var all = bare.concat( bad ); 
				all.sort( function ( a, b ) { return a.start - b.start; } );
				if ( !all.length ) {
					showStatus( 'No bare-URL or malformed references found in ' + state.title + '.' );
					return;
				}
				showStatus( 'Found ' + all.length + ' reference(s) (' + bare.length + ' bare, ' + bad.length + ' malformed). Fetching citation data…' );
				return processItems( all );
			} ).catch( function ( err ) {
				showMsg( ( err && err.message ) || 'Something went wrong.' );
				$status.hide();
			} ).always( function () {
				$scanBtn.prop( 'disabled', false ).text( 'Scan article' );
			} );
		}

		function processItems( bare ) {
			state.items = [];
			var queue = bare.slice();
			var done = 0;

			function next() {
				if ( !queue.length ) {
					renderResults();
					return;
				}
				var b = queue.shift();
				done++;
				showStatus( 'Fetching citation data… (' + done + '/' + bare.length + ')' );
				var url = cleanUrl( b.url );
				RefGen.fetchCitation( url ).done( function ( item ) {
					var params = RefGen.mapToParams( item );
					params.url = url;
					var inner = RefGen.buildRef( params, '', '' );
					inner = inner.replace( /^<ref>/, '' ).replace( /<\/ref>$/, '' );
					state.items.push( {
						bare: b, url: url, ok: true,
						template: params._template,
						title: params.title || url,
						inner: inner
					} );
				} ).fail( function ( reason ) {
					state.items.push( { bare: b, url: url, ok: false, reason: reason || 'No citation data.' } );
				} ).always( function () {
					setTimeout( next, 300 );
				} );
			}
			next();
		}

		function renderResults() {
			$status.hide();
			var okItems = state.items.filter( function ( it ) { return it.ok; } );
			var skipItems = state.items.filter( function ( it ) { return !it.ok; } );

			$results.append(
				$( '<p>' ).html(
					'Found <b>' + state.items.length + '</b> bare reference(s) in <b>' +
				esc( state.title ) + '</b>: <b>' + okItems.length +
					'</b> can be fixed, <b>' + skipItems.length + '</b> skipped (reason below).'
				).css( { 'font-size': '14px' } )
			);

			if ( !okItems.length ) {
				$results.append( $( '<p>' ).text( 'Nothing to fix.' ).css( { color: '#54595d' } ) );
			} else {
				var $list = $( '<div>' ).css( {
					'max-height': '320px', overflow: 'auto', border: '1px solid #c8ccd1',
					'border-radius': '6px', background: '#fff', padding: '8px', 'margin-bottom': '12px'
				} );
				okItems.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', 'word-break': 'break-all' } );
					$txt.append( $( '<div>' ).css( { color: '#72777d' } ).text( it.url ) );
					$txt.append( $( '<div>' ).html( '→ {{' + esc( it.template ) + '}} — <b>' + esc( it.title ) + '</b>' ) );
					$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 changes' ).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' } ) );
			}

			if ( skipItems.length ) {
				var $skip = $( '<details>' ).css( { 'margin-top': '12px', 'font-size': '13px' } );
				$skip.append( $( '<summary>' ).text( 'Skipped URLs (' + skipItems.length + ')' )
					.css( { cursor: 'pointer', 'font-weight': 'bold' } ) );
				var $ul = $( '<ul>' ).css( { color: '#72777d', 'word-break': 'break-all' } );
				skipItems.forEach( function ( it ) {
					var $li = $( '<li>' );
					$li.append( $( '<div>' ).text( it.url ) );
					if ( it.reason ) {
						$li.append( $( '<div>' ).css( { color: '#d33', 'font-size': '12px' } ).text( it.reason ) );
					}
					$ul.append( $li );
					} );
				$skip.append( $ul );
				$results.append( $skip );
			}

			$results.show();
		}

		function publish() {
			if ( mw.config.get( 'wgUserName' ) === null ) {
				showMsg( 'Please log in to publish changes.' );
				return;
			}
			var okItems = state.items.filter( function ( it ) { return it.ok && it.checked.is( ':checked' ); } );
			if ( !okItems.length ) {
				showMsg( 'No references selected.' );
			return;
			}

			var urlToName = {};
			var usedNames = {};
			state.items.forEach( function ( it ) {
				if ( it.bare.name ) {
					usedNames[ it.bare.name ] = true;
				}
			} );
			var n = 0;
			function freshName() {
				n++;
				while ( usedNames[ 'barefix' + n ] ) {
					n++;
				}
				usedNames[ 'barefix' + n ] = true;
				return 'barefix' + n;
			}

			var reps = [];
			state.items.forEach( function ( it ) {
				if ( !( it.ok && it.checked.is( ':checked' ) ) ) {
					return;
				}
				var newRef;
				if ( it.bare.name ) {
					newRef = '<ref name="' + it.bare.name + '">' + it.inner + '</ref>';
				} else if ( urlToName[ it.url ] ) {
					newRef = '<ref name="' + urlToName[ it.url ] + '" />';
				} else {
					var name = freshName();
					urlToName[ it.url ] = name;
					newRef = '<ref name="' + name + '">' + it.inner + '</ref>';
				}
				reps.push( { start: it.bare.start, end: it.bare.end, text: newRef } );
			} );
			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: 'Fix bare URLs: convert to formatted citations',
				assert: 'user'
			} ).then( function ( d ) {
				var rev = d.edit && d.edit.newrevid;
				$results.empty().append(
					$( '<p>' ).html(
						'<b>Published!</b> ' + reps.length + ' reference(s) fixed in <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;
			}
			if ( !mw.libs.refGen ) {
				return;
			}
			var $static = $( '#bp-brf-static' );
			var $tool = buildTool();
			if ( $static.length ) {
				$static.replaceWith( $tool );
			} else {
				var $content = $( '#mw-content-text' );
				if ( $content.length ) {
					$content.prepend( $tool );
				}
			}
			var $link = mw.util.addPortletLink( 'p-tb', mw.util.getUrl( TOOL_PAGE ),
				'Fix bare references', 't-barefix', 'Convert bare URLs to formatted citations' );
			if ( $link ) {
				$( $link ).on( 'click', function ( e ) {
					e.preventDefault();
				window.open( mw.util.getUrl( TOOL_PAGE ), '_blank' );
				} );
			}
		} catch ( e ) {
			if ( window.console && console.error ) {
				console.error( 'RefGenBarefix init failed: ' + ( e && e.message ) );
			}
		}
	}

	function waitForDeps( callback, attempts ) {
		attempts = attempts || 0;
		try {
			if ( typeof $ !== 'undefined' && typeof mw !== 'undefined' &&
					mw.util && mw.libs.refGen ) {
				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 ) {}
	} );
}() );