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: RefGen (v2.0)
 * Interactive citation generator. Paste a URL, the gadget fetches
 * metadata via a public citation metadata API, auto-detects the
 * source type (web/book/journal/news), and builds a proper
 * <ref>{{cite ...}}</ref> with editable fields. Page number and Quote
 * have dedicated manual inputs. All user-facing text is in English.
 *
 * v2.0: also available inside the editors —
 *  - source editor (WikiEditor): "Citation generator" toolbar button
 *  - VisualEditor (+ 2017 wikitext editor): Insert-menu tool
 *    (via the hidden ext.gadget.RefGenVe plugin)
 * ===================================================================== */
( function () {
	'use strict';

	var TOOL_PAGE = 'Bharatpedia:Citation_generator',
		CITOID = atob( 'aHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL2FwaS9yZXN0X3YxL2RhdGEvY2l0YXRpb24vbWVkaWF3aWtpLw==' );

	var TYPES = {
		webpage: { template: 'cite web', label: 'Web' },
		blogPost: { template: 'cite web', label: 'Web' },
		book: { template: 'cite book', label: 'Book' },
		bookSection: { template: 'cite book', label: 'Book' },
		journalArticle: { template: 'cite journal', label: 'Journal' },
		magazineArticle: { template: 'cite magazine', label: 'Magazine' },
		newspaperArticle: { template: 'cite news', label: 'News' },
		encyclopediaArticle: { template: 'cite encyclopedia', label: 'Encyclopedia' },
		thesis: { template: 'cite thesis', label: 'Thesis' },
		report: { template: 'cite report', label: 'Report' }
	};

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

	function formatAuthors( creators ) {
		if ( !creators || !creators.length ) return '';
		return creators.filter( function ( c ) {
			return !c.creatorType || c.creatorType === 'author';
		} ).map( function ( c ) {
			if ( c.firstName && c.lastName ) return c.firstName + ' ' + c.lastName;
			return c.lastName || c.firstName || c.name || '';
		} ).filter( Boolean ).join( '; ' );
	}

	// Only reformat full ISO dates (YYYY-MM-DD) and year-month (YYYY-MM);
	// years / other partial dates pass through untouched
	function fmtPubDate( d ) {
		if ( !d ) return '';
		d = d.trim();
		if ( /^\d{4}-\d{2}-\d{2}$/.test( d ) ) {
			return fmtAccessDate( d );
		}
		var ym = d.match( /^(\d{4})-(\d{2})$/ );
		if ( ym ) {
			var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
			var mi = parseInt( ym[ 2 ], 10 ) - 1;
			if ( mi >= 0 && mi < 12 ) {
				return months[ mi ] + ' ' + ym[ 1 ];
			}
		}
		return d;
	}
	function fmtAccessDate( iso ) {
		if ( !iso ) return '';
		var d = new Date( iso.length === 10 ? iso + 'T00:00:00Z' : iso );
		if ( isNaN( d.getTime() ) ) return '';
		var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
		return d.getUTCDate() + ' ' + months[ d.getUTCMonth() ] + ' ' + d.getUTCFullYear();
	}

	// |website= must never be a raw URL (cite flags "External link in |website=").
	// Turn a URL into its bare domain, e.g. https://www.example.com/x -> example.com.
	function cleanWebsite( v ) {
		v = String( v == null ? '' : v ).trim();
		var m = v.match( /^https?:\/\/([^\/\s?#]+)/i );
		if ( m ) {
			return m[ 1 ].toLowerCase().replace( /^www\./, '' );
		}
		return v;
	}

	// Map Citoid Zotero item to cite template params
	function mapToParams( item ) {
		var type = TYPES[ item.itemType ] || TYPES.webpage;
		var p = { _template: type.template, _label: type.label };
		p.url = item.url || '';
		p.title = item.title || '';
		p.author = formatAuthors( item.creators );
		p.date = fmtPubDate( item.date );
		p.accessdate = fmtAccessDate( item.accessDate );
		if ( type.template === 'cite web' ) {
			p.website = item.websiteTitle || item.publicationTitle || '';
		}
		if ( type.template === 'cite book' ) {
			p.publisher = item.publisher || '';
			// ISBN may come back as an array — take the first, buildRef needs a string
			var isbn = item.ISBN;
			p.isbn = Array.isArray( isbn ) ? ( isbn[ 0 ] || '' ) : ( isbn || '' );
		}
		if ( type.template === 'cite journal' || type.template === 'cite magazine' ) {
			p.journal = item.publicationTitle || '';
			p.volume = item.volume || '';
			p.issue = item.issue || '';
			p.pages = item.pages || '';
			p.doi = item.DOI || '';
		}
		if ( type.template === 'cite news' ) {
			p.newspaper = item.publicationTitle || '';
			// Only keep website too when it differs from the newspaper name
			if ( item.websiteTitle && item.websiteTitle !== item.publicationTitle ) {
				p.website = item.websiteTitle;
			}
			// Never emit both: |website= conflicts with |newspaper= in cite news
			if ( p.newspaper && p.website && p.website.toLowerCase() === p.newspaper.toLowerCase() ) {
				p.website = '';
			}
		}
		return p;
	}

	function fieldDefs( template ) {
		var base = [
			{ key: 'title', label: 'Title' },
			{ key: 'author', label: 'Author(s)' },
			{ key: 'date', label: 'Date' },
			{ key: 'url', label: 'URL' }
		];
		if ( template === 'cite web' ) {
			base.splice( 3, 0, { key: 'website', label: 'Website' } );
			base.push( { key: 'accessdate', label: 'Access date' } );
		} else if ( template === 'cite book' ) {
			base.push( { key: 'publisher', label: 'Publisher' } );
			base.push( { key: 'isbn', label: 'ISBN' } );
		} else if ( template === 'cite journal' || template === 'cite magazine' ) {
			base.splice( 3, 0, { key: 'journal', label: 'Journal' } );
			base.push( { key: 'volume', label: 'Volume' } );
			base.push( { key: 'issue', label: 'Issue' } );
			base.push( { key: 'pages', label: 'Pages' } );
			base.push( { key: 'doi', label: 'DOI' } );
		} else if ( template === 'cite news' ) {
			base.splice( 3, 0, { key: 'newspaper', label: 'Newspaper' } );
			base.push( { key: 'accessdate', label: 'Access date' } );
		}
		return base;
	}

	function buildRef( params, page, quote ) {
		var parts = [ '{{' + params._template ];
		// Guard: |website= is invalid alongside |newspaper= (cite news) — keep newspaper.
		var dropWebsite = params._template === 'cite news' &&
			( params.newspaper || '' ).trim() && ( params.website || '' ).trim();
		var order = [ 'url', 'title', 'author', 'date', 'website', 'newspaper', 'journal', 'publisher', 'volume', 'issue', 'pages', 'isbn', 'doi', 'accessdate' ];
		order.forEach( function ( k ) {
			if ( k === 'website' && dropWebsite ) return;
			var v = String( params[ k ] == null ? '' : params[ k ] ).trim();
			if ( !v ) return;
			if ( k === 'website' ) {
				v = cleanWebsite( v );
				if ( !v ) return;
			}
			if ( k === 'title' && /^https?:\/\//i.test( v ) ) {
				return; // never emit a raw URL as |title=
			}
			v = v.replace( /\|/g, '{{!}}' ); // unescaped pipes break template params
			var ck = k === 'accessdate' ? 'access-date' : k;
			parts.push( ' |' + ck + '=' + v );
		} );
		if ( page.trim() ) parts.push( ' |page=' + page.trim().replace( /\|/g, '{{!}}' ) );
		if ( quote.trim() ) parts.push( ' |quote=' + quote.trim().replace( /\|/g, '{{!}}' ) );
		parts.push( '}}' );
		return '<ref>' + parts.join( '' ) + '</ref>';
	}

	function buildTool( autoUrl ) {
		var $root = $( '<div>' ).addClass( 'bp-refgen' ).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( 'Citation generator' ).css( { margin: '0 0 4px 0', 'font-size': '22px' } ) );
		$root.append( $( '<p>' ).text( 'Paste a URL to auto-generate a citation. Add page number and quote manually if needed.' )
			.css( { margin: '0 0 12px 0', 'font-size': '13px', color: '#54595d' } ) );

		// URL input row
		var $urlRow = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '12px' } );
		var $urlInput = $( '<input>' ).attr( { type: 'url', placeholder: 'Paste a URL (e.g. https://www.bbc.com/news/...)' } ).css( {
			flex: '1', padding: '10px 12px', 'font-size': '15px',
			border: '2px solid #36c', 'border-radius': '6px', 'box-sizing': 'border-box'
		} );
		var $fetchBtn = $( '<button>' ).text( 'Fetch' ).css( {
			padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
			background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px', 'font-weight': 'bold'
		} );
		$urlRow.append( $urlInput, $fetchBtn );
		$root.append( $urlRow );

		var $msg = $( '<div>' ).css( { 'font-size': '13px', color: '#d33', 'margin-bottom': '8px' } ).hide();
		$root.append( $msg );

		// Type selector
		var $typeRow = $( '<div>' ).css( { 'margin-bottom': '12px', 'font-size': '14px' } ).hide();
		$typeRow.append( $( '<label>' ).text( 'Source type: ' ).css( { 'font-weight': 'bold', 'margin-right': '8px' } ) );
		var $typeSel = $( '<select>' ).css( { padding: '6px 10px', 'font-size': '14px', 'border-radius': '4px', border: '1px solid #a2a9b1' } );
		[ 'cite web', 'cite book', 'cite journal', 'cite news', 'cite magazine' ].forEach( function ( t ) {
			$typeSel.append( $( '<option>' ).attr( 'value', t ).text( '{{' + t + '}}' ) );
		} );
		$typeRow.append( $typeSel );
		$root.append( $typeRow );

		// Fields container
		var $fields = $( '<div>' ).css( { 'margin-bottom': '12px' } ).hide();
		$root.append( $fields );

		// Manual Page and Quote (always visible, prominent)
		var $manual = $( '<div>' ).css( { 'margin-bottom': '12px', padding: '12px', background: '#fff', border: '1px solid #c8ccd1', 'border-radius': '6px' } ).hide();
		$manual.append( $( '<div>' ).text( 'Add manually (optional)' ).css( { 'font-weight': 'bold', 'margin-bottom': '8px', 'font-size': '14px' } ) );
		var $pageRow = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '8px', 'align-items': 'center' } );
		$pageRow.append( $( '<label>' ).text( 'Page(s):' ).css( { width: '80px', 'font-size': '14px' } ) );
		var $pageInput = $( '<input>' ).attr( { type: 'text', placeholder: 'e.g. 42 or 42-45' } ).css( { flex: '1', padding: '8px 10px', 'font-size': '14px', border: '1px solid #a2a9b1', 'border-radius': '4px' } );
		$pageRow.append( $pageInput );
		var $quoteRow = $( '<div>' ).css( { display: 'flex', gap: '8px', 'align-items': 'flex-start' } );
		$quoteRow.append( $( '<label>' ).text( 'Quote:' ).css( { width: '80px', 'font-size': '14px', 'padding-top': '8px' } ) );
		var $quoteInput = $( '<textarea>' ).attr( { rows: 2, placeholder: 'Exact quote from the source (optional)' } ).css( { flex: '1', padding: '8px 10px', 'font-size': '14px', border: '1px solid #a2a9b1', 'border-radius': '4px', resize: 'vertical' } );
		$quoteRow.append( $quoteInput );
		$manual.append( $pageRow, $quoteRow );
		$root.append( $manual );

		// Output
		var $outWrap = $( '<div>' ).hide();
		$outWrap.append( $( '<div>' ).text( 'Generated reference:' ).css( { 'font-weight': 'bold', 'margin-bottom': '6px', 'font-size': '14px' } ) );
		var $output = $( '<textarea>' ).attr( { rows: 4, readonly: true } ).css( {
			width: '100%', padding: '10px', 'font-size': '13px', 'font-family': 'monospace',
			border: '1px solid #a2a9b1', 'border-radius': '4px', 'box-sizing': 'border-box', resize: 'vertical', background: '#fff'
		} );
		var $copyBtn = $( '<button>' ).text( 'Copy reference' ).css( {
			'margin-top': '8px', padding: '8px 20px', 'font-size': '14px', cursor: 'pointer',
			background: '#14866d', color: '#fff', border: 'none', 'border-radius': '6px', 'font-weight': 'bold'
		} );
		$outWrap.append( $output, $copyBtn );
		$root.append( $outWrap );

		var params = {};
		var fieldInputs = {};

		function refreshOutput() {
			var page = $pageInput.val() || '';
			var quote = $quoteInput.val() || '';
			// read current field values
			Object.keys( fieldInputs ).forEach( function ( k ) {
				params[ k ] = fieldInputs[ k ].val();
			} );
			params._template = $typeSel.val();
			$output.val( buildRef( params, page, quote ) );
		}

		function renderFields() {
			$fields.empty();
			fieldInputs = {};
			fieldDefs( params._template ).forEach( function ( fd ) {
				var $row = $( '<div>' ).css( { display: 'flex', gap: '8px', 'margin-bottom': '6px', 'align-items': 'center' } );
				$row.append( $( '<label>' ).text( fd.label + ':' ).css( { width: '110px', 'font-size': '14px' } ) );
				var $inp = $( '<input>' ).attr( 'type', 'text' ).val( params[ fd.key ] || '' ).css( {
					flex: '1', padding: '8px 10px', 'font-size': '14px', border: '1px solid #a2a9b1', 'border-radius': '4px'
				} );
				$inp.on( 'input', refreshOutput );
				fieldInputs[ fd.key ] = $inp;
				$row.append( $inp );
				$fields.append( $row );
			} );
			$fields.show();
		}

		$typeSel.on( 'change', function () {
			params._template = $typeSel.val();
			renderFields();
			refreshOutput();
		} );
		$pageInput.on( 'input', refreshOutput );
		$quoteInput.on( 'input', refreshOutput );

		$copyBtn.on( 'click', function () {
			$output[ 0 ].select();
			try {
				var ok = document.execCommand( 'copy' );
				$copyBtn.text( ok ? 'Copied!' : 'Copy failed' );
			} catch ( e ) {
				$copyBtn.text( 'Copy failed' );
			}
			setTimeout( function () { $copyBtn.text( 'Copy reference' ); }, 2000 );
		} );

		function fetchUrl() {
			var url = $.trim( $urlInput.val() );
			if ( !url ) return;
			if ( !/^https?:\/\//i.test( url ) ) url = 'https://' + url;
			$msg.hide();
			$fetchBtn.prop( 'disabled', true ).text( 'Loading…' );
			fetch( CITOID + encodeURIComponent( url ), {
				headers: { 'Api-User-Agent': 'Bharatpedia-RefGen/1.0' }
			} ).then( function ( r ) {
				if ( !r.ok ) throw new Error( 'Could not fetch data for this URL.' );
				return r.json();
			} ).then( function ( data ) {
				if ( !data || !data.length || data.error ) {
					throw new Error( data && data.error ? data.error : 'No citation data found for this URL.' );
				}
				params = mapToParams( data[ 0 ] );
				$typeSel.val( params._template );
				$typeRow.show();
				renderFields();
				$manual.show();
				$outWrap.show();
				refreshOutput();
			} ).catch( function ( err ) {
				$msg.text( ( err && err.message ) || 'Something went wrong.' ).show();
			} ).finally( function () {
				$fetchBtn.prop( 'disabled', false ).text( 'Fetch' );
			} );
		}

		$fetchBtn.on( 'click', fetchUrl );
		$urlInput.on( 'keydown', function ( e ) { if ( e.key === 'Enter' ) fetchUrl(); } );

		if ( autoUrl ) {
			$urlInput.val( autoUrl );
			fetchUrl();
		}

		return $root;
	}

	function init() {
		try {
			var pageName = mw.config.get( 'wgPageName' );
			if ( pageName === TOOL_PAGE || pageName.indexOf( TOOL_PAGE + '/' ) === 0 ) {
				var $static = $( '#bp-rg-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', '#', 'Citation generator', 't-refgen', 'Generate a citation from a URL' );
			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( 'RefGen init failed: ' + ( e && e.message ) );
			}
		}
	}

	function boot() {
		try {
			if ( document.readyState === 'loading' ) {
				document.addEventListener( 'DOMContentLoaded', init );
			} else {
				init();
			}
		} catch ( e ) {
			if ( window.console && console.error ) {
				console.error( 'RefGen bootstrap failed: ' + ( e && e.message ) );
			}
		}
	}

	// Wait for jQuery and mediawiki.util before running.
	// Polls every 100ms (up to 10s) because ResourceLoader may execute
	// this gadget before its dependencies in batched mode.
	function waitForDeps( callback, attempts ) {
		attempts = attempts || 0;
		try {
			if ( typeof $ !== 'undefined' && typeof mw !== 'undefined' && mw.util ) {
				callback();
				return;
			}
		} catch ( e ) {}
		if ( attempts < 100 ) {
			setTimeout( function () { waitForDeps( callback, attempts + 1 ); }, 100 );
		} else {
			// Give up waiting; try anyway (will fail gracefully via try/catch)
			try { callback(); } catch ( e ) {}
		}
	}
	waitForDeps( boot );

	/* =====================================================================
	 * Shared library for the editor integrations.
	 * The VisualEditor plugin (ext.gadget.RefGenVe) uses mw.libs.refGen.
	 * ===================================================================== */
	mw.libs.refGen = mw.libs.refGen || {};
	mw.libs.refGen.TYPES = TYPES;
	mw.libs.refGen.mapToParams = mapToParams;
	mw.libs.refGen.buildRef = buildRef;
	mw.libs.refGen.fetchCitation = function ( url ) {
		url = $.trim( url || '' );
		var d = $.Deferred();
		if ( !url ) {
			d.reject( 'Please enter a URL.' );
			return d.promise();
		}
		if ( !/^https?:\/\//i.test( url ) ) {
			url = 'https://' + url;
		}
		$.ajax( {
			url: CITOID + encodeURIComponent( url ),
			headers: { 'Api-User-Agent': 'Bharatpedia-RefGen/1.0' },
			dataType: 'json'
		} ).done( function ( data ) {
			if ( !data || !data.length || data.error ) {
				d.reject( ( data && data.error ) || 'No citation data found for this URL.' );
			} else {
				d.resolve( data[ 0 ] );
			}
		} ).fail( function ( jqXHR ) {
			var status = jqXHR && jqXHR.status;
			if ( status === 404 ) {
				d.reject( 'No citation data (page not found - the link may be dead).' );
			} else {
				d.reject( 'Could not fetch data for this URL.' );
			}
		} );
		return d.promise();
	};

	/* =====================================================================
	 * Source editor (WikiEditor) integration: toolbar button + dialog.
	 * ===================================================================== */
	function refGenInsertAtCursor( $textarea, text ) {
		try {
			$textarea.textSelection( 'encapsulateSelection', { pre: '', peri: text, post: '' } );
			return;
		} catch ( e ) {}
		// Fallback: manual insertion
		var el = $textarea[ 0 ];
		if ( el && typeof el.selectionStart === 'number' ) {
			var s = el.selectionStart, e2 = el.selectionEnd, v = el.value;
			el.value = v.slice( 0, s ) + text + v.slice( e2 );
			try {
				el.selectionStart = el.selectionEnd = s + text.length;
			} catch ( e3 ) {}
			el.focus();
		}
	}

	function openRefGenEditorDialog( $textarea ) {
		var $tool = buildTool();
		var $overlay = $( '<div>' ).css( {
			position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
			background: 'rgba(0,0,0,0.55)', 'z-index': 2000,
			display: 'flex', 'align-items': 'center', 'justify-content': 'center',
			padding: '16px', 'box-sizing': 'border-box'
		} );
		var $box = $( '<div>' ).css( {
			background: '#fff', 'border-radius': '10px', padding: '18px',
			width: '640px', 'max-width': '100%', 'max-height': '92vh',
			overflow: 'auto', 'box-sizing': 'border-box', position: 'relative'
		} );
		var $close = $( '<button>' ).attr( { type: 'button', 'aria-label': 'Close' } ).text( '×' ).css( {
			position: 'absolute', top: '8px', right: '12px', border: 'none',
			background: 'transparent', 'font-size': '24px', cursor: 'pointer', color: '#54595d'
		} );
		var $insertBtn = $( '<button>' ).attr( 'type', 'button' ).text( 'Insert citation' ).css( {
			'margin-top': '12px', padding: '10px 24px', 'font-size': '15px', cursor: 'pointer',
			background: '#36c', color: '#fff', border: 'none', 'border-radius': '6px', 'font-weight': 'bold'
		} );
		$insertBtn.on( 'click', function () {
			var ref = $tool.find( 'textarea[readonly]' ).val();
			if ( !ref ) {
				return;
			}
			refGenInsertAtCursor( $textarea, ref );
			$overlay.remove();
		} );
		$close.on( 'click', function () { $overlay.remove(); } );
		$overlay.on( 'click', function ( e ) {
			if ( e.target === $overlay[ 0 ] ) {
				$overlay.remove();
			}
		} );
		$box.append( $close, $tool, $insertBtn );
		$overlay.append( $box );
		$( document.body ).append( $overlay );
	}

	function initWikiEditorButton() {
		if ( mw.config.get( 'wgAction' ) !== 'edit' ) {
			return;
		}
		mw.hook( 'wikiEditor.toolbarReady' ).add( function ( $textarea ) {
			$textarea.wikiEditor( 'addToToolbar', {
				section: 'main',
				group: 'insert',
				tools: {
					refgen: {
						label: 'Ref-Gen',
						type: 'button',
						oouiIcon: 'code',
						filters: [ 'body.ns-subject' ],
						action: {
							type: 'callback',
							execute: function () {
								openRefGenEditorDialog( $textarea );
							}
						}
					}
				}
			} );
		} );
	}
	initWikiEditorButton();
}() );