User:PleaseStand/segregate-refs-dev.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*  segregate-refs.js: A user script to simplify editing of articles
	using inline ref tags with the Cite.php extension to MediaWiki.
	
	Copyright (c) 2010, PleaseStand
	
	This software is licensed under these licenses:
	1.  Creative Commons Attribution-Share Alike 3.0 Unported License
		(see <http://creativecommons.org/licenses/by-sa/3.0/> for the text)
	2.  GNU Free Documentation License, any published version.
		(see <http://www.gnu.org/copyleft/fdl.html> for the text)
	3.  Permission to use, copy, modify, and/or distribute this software for any
		purpose with or without fee is hereby granted, provided that the above
		copyright notice and this permission notice appear in all copies.
	
		THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
		WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
		MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
		ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
		WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
		ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
		OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
	
	You may select the license(s) of your choice if you wish to copy, modify, or
	distribute this software. If you modify the software and do not wish to
	license your changes under one or more of the licenses, please remove
	the license(s) from the list above.
*/

/*global window, addOnloadHook, SegregateRefsJsL10n, SegregateRefsJsAllowConversion,
wikEdUseWikEd, WikEdUpdateTextarea, WikEdUpdateFrame*/

// <nowiki>

// Translate the right-hand side of these if necessary.
// Put translations in a separate file, changing the first line to:
// var SegregateRefsJsL10n = {
var SegregateRefsJsMsgs = {
	version: 1.11,
	buttonText: "Segregate refs for editing",
	buttonStyle: "background: #dfd;",
	buttonConvertText: "Migrate article to LDR",
	buttonConvertStyle: "background: #fdd;",
	autoWord: "Auto",
	convertRefsWarning: "WARNING: You need consensus to migrate an article to list-defined references format (LDR) BEFORE you do so.\n\nClick Cancel now if consensus has not been established in favor of this migration. If there is consensus to make the conversion, click OK to do so.",
	groupPrompt: "Please enter the name of a group (as it appears in the wikitext, including any quotes). Leave this blank if unsure.",
	refsHeader: "Inline footnotes",
	convertHeader: "Generated refs list",
	refsCommentComplete: "<!-- Converted to LDR format\n     using [[User:PleaseStand/References segregator]] -->\n\n",
	convertSummary: "Converted footnotes to LDR format (using [[User:PleaseStand/References segregator|segregate-refs]])",
	convertFurther: "This script has done most of the work. However, you still need to do the following:\n\n* Insert the refs list in the new textbox into the proper place in the wikitext.\n* If converting a special group, optionally remove the group attributes.\n* Replace all autogenerated names with human-generated names.\n\nYou can do the above with the Find/Replace command in many text editors. (Always use the quoted form of the attributes.) Then, paste the text back into the edit form and save the page.",
	integrateWarning: "The refs listed below are missing from the text. If you continue, they will be permanently deleted. Are you sure?\n\nUnused refs: "
};

( function ( $ ) {

var editForm, refsDiv, refsH2, mainTextbox, refsTextbox, randPrefix, messages,
	refsButton, convertButton, unloadHandlerRegistered = false;

/**
 * Unquote a wikitext tag attribute.
 *
 * @param string quotedValue
 * @return string
 */
function htmlUnquote( quotedValue ) {
	var d = document.createElement( 'div' );
	d.innerHTML = '<input value=' + quotedValue + '></input>';
	return d.firstChild.value;
}

/**
 * Quote a wikitext tag attribute, choosing single quotes versus
 * double quotes depending on which is shorter.
 *
 * @param string value
 * @return string
 */
function htmlQuote( value ) {
	var sQ, dQ;

	value = value.replace( /\&/g, '&amp;' );
	sQ = "'" + value.replace( /'/g, '&#39;' ) + "'",
	dQ = '"' + value.replace( /"/g, '&quot;' ) + '"';

	return sQ.length < dQ.length ? sQ : dQ;
}

// Looks for ref tags in the text, skipping problematic extension tags.
// For example, "references" may contain out-of-line refs, which should be skipped.
function RefScanner( argWikiText ) {
	this.wikiText = argWikiText;
	this.refScanRegex = /(?:<!--[\s\S]*?-->|<(nowiki|source|references|ref)(?:|\s(?:[^"']|"[^"]*"|'[^']*')*?)(?:\/>|(?:>[\s\S]*?<\/\1(?:|\s[^>]*)>)))/gi;
}

// Get the next ref from the text.
RefScanner.prototype.getRef = function () {
	var results;
	do {
		results = this.refScanRegex.exec( this.wikiText );
		if ( !results ) {
			return null;
		}
		if ( results[1] === undefined ) {
			results = [0, 0];
		}
	} while ( results[1].toString().toLowerCase() !== 'ref' );
	return results[0];
};

// Extracts attributes from ref tags.
function RefParser( argWikiText ) {
	// This is mostly a copy of refScanRegex, except that the whole string must be a ref,
	// and no more, and two parts are extracted: $1=attributes, $2=remaining portion of ref
	var refParseRegex = /^<ref(|\s(?:[^"']|"[^"]*"|'[^']*')*?)(\/>|(?:>[\s\S]*?<\/ref(?:|\s[^>]*)>))$/i;

	this.wikiText = argWikiText;
	this.parsedRef = refParseRegex.exec( this.wikiText );
	if ( !this.parsedRef ) {
		throw new Error( 'invalid ref' );
	}
}

// Get all attributes of the tag.
RefParser.prototype.getAttributes = function () {
	// In this regex, we need to extract a single name-value pair at a time.
	var attParseRegex = /\s([^\s=>]+)\s*=\s*("[^"]*"|'[^']*'|[^\s"']*)/g;
	if ( !this.parsedRef ) {
		return null;
	}
	var attributes = Object.create( null ), results;
	while ( ( results = attParseRegex.exec( this.parsedRef[1] ) ) ) {
		attributes[results[1].toLowerCase()] = htmlUnquote( results[2] );
	}
	return attributes;
};

/**
 * Segregate refs from content.
 *
 * @param string argWikiText The original wikitext
 * @param string group The name of the ref group to process (default group is '')
 * @param bool caseCues Mark the original ref code locations using capitalization?
 * @return Object
 */
function segregateRefs( argWikiText, group, caseCues ) {
	var prefixChars, randNo, randPrefix, refPreferred, scanner, ref, parser, attributes,
		refGroup, refName, refStored, refEmpty, refLong, unnamedRefs = 0,
		refNames = Object.create( null ), refCodes = [], refShort, outWikiText = '', offset = 0;

	// Create a random prefix for autogenerated ref names
	prefixChars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
	randNo = Math.floor( Math.random() * ( prefixChars.length * prefixChars.length ) );
	randPrefix = messages.autoWord
		+ prefixChars.charAt( Math.floor( randNo / prefixChars.length ) )
		+ prefixChars.charAt(randNo % prefixChars.length) +
		'-';

	// Create the beginning of the code for a preferred ref location
	refPreferred = caseCues ? '<REF ' : '<ref ';
	
	scanner = new RefScanner( argWikiText );
	
	while ( ( ref = scanner.getRef() ) ) {
		parser = new RefParser( ref );
		attributes = parser.getAttributes();

		refGroup = attributes.group || '';
		if ( group != refGroup ) {
			// The ref is in a different group
			continue;
		}

		if ( attributes.name !== undefined ) {
			// The ref already has a name (possibly the empty string)
			refName = attributes.name;
			refStored = refName in refNames;
			refEmpty = parser.parsedRef[2].slice( -2 ) == '/>' ||
				parser.parsedRef[2].slice( 0, 3 ) == '></';
			refLong = ref;

		} else {
			// We have to autogenerate one
			refName = randPrefix + ( ++unnamedRefs ).toString(10);
			refStored = false;
			refEmpty = false;
			refLong = '<ref name=' + htmlQuote( refName ) +
				parser.parsedRef[1] + parser.parsedRef[2];
		}

		if ( !refStored ) {
			// Found the first ref of this name
			refNames[refName] = {
				code: refCodes.length,
				empty: refEmpty
			};
			refCodes[refNames[refName].code] = refLong;

			// Make a short code for the ref
			if ( refEmpty ) {
				refShort = refLong;
			} else if ( refGroup === '' ) {
				refShort = refPreferred + 'name=' + htmlQuote( refName ) + '/>';
			} else {
				refShort = refPreferred + 'name=' + htmlQuote( refName ) +
					' group=' + htmlQuote( refGroup ) + '/>';
			}

		} else if ( !refEmpty && refNames[refName].empty ) {
			// Already found an empty ref under this name, yet this one is non-empty
			// Fill in the long code for the existing entry
			refCodes[refNames[refName].code] = refLong;
			refNames[refName].empty = false;

			// Make a short code for the ref
			refShort = refPreferred + 'name=' + htmlQuote( refName );
			if ( refGroup !== '' ) {
				refShort += ' group=' + htmlQuote( refGroup ) + '/>';
			}
			refShort += '/>';

		} else {
			// Leave the ref as-is
			refShort = caseCues ? refLong.replace( /^<REF/, "<ref" ) : refLong;
		}

		// Replace the long code with the short code
		outWikiText += argWikiText.slice( offset, scanner.refScanRegex.lastIndex - ref.length );
		outWikiText += refShort;
		offset = scanner.refScanRegex.lastIndex;
	}

	outWikiText += argWikiText.slice( offset );

	return {
		wikiText: outWikiText,
		refCodes: refCodes,
		randPrefix: randPrefix
	};
}

/**
 * Insert ref contents back into the text.
 *
 * @param string argWikiText The wikitext without ref contents
 * @param string argRefText The ref contents
 * @param string randPrefix The randPrefix value returned by segregateRefs()
 * @param string caseCues The caseCues argument passed to segregateRefs()
 */
function integrateRefs(argWikiText, argRefText, randPrefix, caseCues) {

	// Remove an autogenerated ref name if possible.
	function cleanRefLong(dirtyRef) {
		var cleanRegex = /^<(ref) name=(?:"[^"]*"|'[^']*'|[^\s"']*)/i;
		return dirtyRef.replace(cleanRegex, '<$1');
	}

	var scanner, ref, parser, attributes, refCodes = Object.create( null ), usageFreq = Object.create( null ),
		preferredRef = Object.create( null ), refLong, outWikiText = '', offset = 0;

	// First, we build an associative array of all the ref codes
	// that we might need to put back into the text.
	scanner = new RefScanner( argRefText );
	while ( ( ref = scanner.getRef() ) ) {
		parser = new RefParser( ref );
		attributes = parser.getAttributes();
		if ( attributes.name !== undefined ) {
			// Only use the first ref having each name
			if ( !( attributes.name in refCodes ) ) {
				refCodes[attributes.name] = ref;   
			}
		}
	}

	// Next, we build an associative array that holds the usage frequency
	// of every ref name used in text, and whether there is a preferred ref,
	// if caseCues are enabled.
	scanner = new RefScanner( argWikiText );
	while ( ( ref = scanner.getRef() ) ) {
		parser = new RefParser( ref );
		attributes = parser.getAttributes();
		if ( attributes.name !== undefined ) {
			if ( !( attributes.name in usageFreq ) ) {
				usageFreq[attributes.name] = 1;
			} else {
				usageFreq[attributes.name]++;
			}
			if ( caseCues && ref.slice( 0, 4 ) == '<REF' ) {
				preferredRef[attributes.name] = true;
			}
		}
	}

	// Finally, we go through the text again and this time we insert the
	// ref codes where we need to, but only in the first place
	// a ref name appears (or the first preferred location).
	scanner = new RefScanner( argWikiText );
	while ( ( ref = scanner.getRef() ) ) {
		parser = new RefParser( ref );
		attributes = parser.getAttributes();
		if ( 'name' in attributes ) {
			// Is this name on the replacement list?
			if ( attributes.name in refCodes ) {

				// If we are using caseCues, and another location is
				// preferred, skip to the next ref.
				if ( caseCues && attributes.name in preferredRef &&
					ref.slice( 0, 4 ) != '<REF'
				) {
					continue;
				}
				
				// Is this name an autogenerated name?
				if ( attributes.name.slice( 0, randPrefix.length ) == randPrefix ) {
					// Yes: is the name used multiple times?
					if ( usageFreq[attributes.name] > 1 ) {
						// Multiple: the replacement code should be the same
						// as that stored in the ref textbox.
						refLong = refCodes[attributes.name];
					} else {
						// Single: replacement code must not include the name,
						// at least not if the citation was untouched.
						// (We don't want to add unnecessary autonames)
						refLong = cleanRefLong( refCodes[attributes.name] );
					}
				} else {
					// No: the replacement code should be the same
					// as that stored in the ref textbox.
					// (We want to preserve all human-generated names)
					refLong = refCodes[attributes.name];
				}
				
				// Replace the short code with the long code
				outWikiText += argWikiText.slice( offset, scanner.refScanRegex.lastIndex - ref.length );
				outWikiText += refLong;
				offset = scanner.refScanRegex.lastIndex;

				// Delete the name from the replacement list
				delete refCodes[attributes.name];
			}
		}
	}

	outWikiText += argWikiText.slice( offset );

	return {
		wikiText: outWikiText,
		unusedRefs: refCodes
	};
}

/**
 * Clear the undo history of a textarea by removing it from the document
 * and then inserting it again.
 *
 * @param HTMLTextareaElement ta The textarea element
 */
function clearUndoHistory(ta) {
	var pn = ta.parentNode, ns = ta.nextSibling;
	pn.insertBefore(pn.removeChild(ta), ns);
}

function unloadHandler(evt) {
	// Local variables
	var result, refName, unusedRefNamesQuoted = [];
	
	// wikEd compatibility (frame -> textarea)
	if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) {
		WikEdUpdateTextarea();
	}
	
	// Do the actual integration work
	result = integrateRefs(mainTextbox.value, refsTextbox.value, randPrefix, true);

	// Find all unused ref names
	for(refName in result.unusedRefs) {
		unusedRefNamesQuoted.push(htmlQuote(refName));
	}
	// If any refs are unused, warn and allow the user to cancel;
	// we do not do this on unload because it is not really possible.
	if(evt.type == "submit" && unusedRefNamesQuoted.length) {
		if(!window.confirm(messages.integrateWarning +
		unusedRefNamesQuoted.join(", "))) {
			// Don't submit form
			evt.preventDefault();
			return false;
		}
	}
	
	// Otherwise, update the textbox.
	mainTextbox.value = result.wikiText;
	
	// wikEd compatibility (textarea -> frame)
	if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) {
		WikEdUpdateFrame();
	}
	
	// Deactivate this event handler
	window.removeEventListener("submit", unloadHandler, false);
	window.removeEventListener("unload", unloadHandler, false);
	unloadHandlerRegistered = false;
	
	// We can delete the header and refs textbox now
	refsDiv.removeChild(refsH2);
	refsDiv.removeChild(refsTextbox);
	
	return true;
}

function refsButtonHandler() { // Called when script activated by button click
	
	// Both buttons should disappear
	if(convertButton.parentNode){
		convertButton.parentNode.removeChild(convertButton);   
	}
	if(refsButton.parentNode) {
		refsButton.parentNode.removeChild(refsButton);
	}

	// wikEd compatibility (frame -> textarea)
	if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) {
		WikEdUpdateTextarea();
	}
	
	// Do the actual segregation work and save the random prefix
	var segFormat = segregateRefs(mainTextbox.value, "", true);
	if(!segFormat) {
		return false;
	}
	randPrefix = segFormat.randPrefix;
	
	// Update the textbox
	mainTextbox.value = segFormat.wikiText;
	clearUndoHistory(mainTextbox);
	
	// wikEd compatibility (textarea -> frame)
	if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) {
		WikEdUpdateFrame();
	}
	
	// Inline refs header
	refsH2 = document.createElement("h2");
	refsH2.appendChild(document.createTextNode(messages.refsHeader));
	
	refsH2.style.borderColor = "silver";
	refsH2.style.borderStyle = "solid";
	refsH2.style.borderWidth = "1px 0";
	refsH2.style.fontSize = "100%";
	refsH2.style.fontWeight = "100%";
	refsH2.style.lineHeight = "normal";
	refsH2.style.margin = "0";
	refsH2.style.padding = "0.5em 0 0";
	refsH2.style.textAlign = "center";
	
	// Inline refs textbox
	refsTextbox = document.createElement("textarea");
	refsTextbox.id = "PsRefsTextbox";
	refsTextbox.value = segFormat.refCodes.join("\n\n");
	refsTextbox.rows = 12;
	
	refsTextbox.style.border = "none";
	refsTextbox.style.lineHeight = "1.5em";
	refsTextbox.style.margin = "0";
	refsTextbox.style.resize = "vertical";
	refsTextbox.style.width = "100%";

	// Add to document
	refsDiv.appendChild(refsH2);
	refsDiv.appendChild(refsTextbox);
	
	// Set up the submit handler (to integrate refs when done editing)
	window.addEventListener("submit", unloadHandler, false);
	window.addEventListener("unload", unloadHandler, false);
	unloadHandlerRegistered = true;
	
	// Don't submit form
	return false;
}

function convertButtonHandler() { // Called when script activated by button click
	// Display warning
	if(!window.confirm(messages.convertRefsWarning)) {
		return false;
	}
	
	// Which group?
	var group = window.prompt(messages.groupPrompt, "");
	if(group === null) {
		return false;
	}
	group = htmlUnquote(group);
	
	// The first button should disappear
	if(refsButton.parentNode) {
		refsButton.parentNode.removeChild(refsButton);
	}
	
	// Do the actual segregation work and save the random prefix
	var segFormat = segregateRefs(mainTextbox.value, group, false);
	if(!segFormat) {
		return false;
	}
	randPrefix = segFormat.randPrefix;
	
	// wikEd compatibility (frame -> textarea)
	if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) {
		WikEdUpdateTextarea();
	}
	
	// Update the textbox
	mainTextbox.value = segFormat.wikiText;
	clearUndoHistory(mainTextbox);
	
	// wikEd compatibility (textarea -> frame)
	if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) {
		WikEdUpdateFrame();
	}
	
	// Inline refs header
	refsH2 = document.createElement("h2");
	refsH2.appendChild(document.createTextNode(messages.convertHeader));
	
	refsH2.style.borderColor = "silver";
	refsH2.style.borderStyle = "solid";
	refsH2.style.borderWidth = "1px 0";
	refsH2.style.fontSize = "100%";
	refsH2.style.fontWeight = "100%";
	refsH2.style.lineHeight = "normal";
	refsH2.style.margin = "0";
	refsH2.style.padding = "0.5em 0 0";
	refsH2.style.textAlign = "center";
	
	// Inline refs textbox
	if(!refsTextbox) {
		// Does not exist; creating
		refsTextbox = document.createElement("textarea");
		refsTextbox.id = "PsRefsTextbox";
		refsTextbox.value = messages.refsCommentComplete +
			segFormat.refCodes.join("\n");
		refsTextbox.rows = 12;

		refsTextbox.style.border = "none";
		refsTextbox.style.lineHeight = "1.5em";
		refsTextbox.style.margin = "0";
		refsTextbox.style.resize = "vertical";
		refsTextbox.style.width = "100%";
		
		// Add to document
		refsDiv.appendChild(refsH2);
		refsDiv.appendChild(refsTextbox);
	} else {
		// Already exists
		refsTextbox.value = messages.refsCommentComplete +
			segFormat.refCodes.join("\n");
	}
	
	// Set a default edit summary.
	document.getElementById("wpSummary").value = messages.convertSummary;
	
	// Show the further instructions.
	window.alert(messages.convertFurther);
	
	// Don't submit form
	return false;
}


$( function () {

	try {

		// Handle message translations
		messages = (typeof SegregateRefsJsL10n == "object" &&
					typeof SegregateRefsJsL10n.version != "undefined" &&
					SegregateRefsJsL10n.version == 1.11 ? SegregateRefsJsL10n :
					SegregateRefsJsMsgs);
		
		// Only activate on edit pages (that are not section edit pages)
		if(!document.getElementById("editform") ||
		document.getElementById("editform").wpSection.value.length) {
			return;
		}
		
		// Get the edit form
		editForm = document.getElementById("editform");
		// Get the edit box
		mainTextbox = document.getElementById("wpTextbox1");
		
		// Make the "segregate" button
		refsButton = document.createElement("input");
		refsButton.type = "button";
		refsButton.value = messages.buttonText;
		refsButton.setAttribute("style", messages.buttonStyle);
		refsButton.onclick = refsButtonHandler;
		
		// Make the "convert" button
		convertButton = document.createElement("input");
		convertButton.type = "button";
		convertButton.value = messages.buttonConvertText;
		convertButton.setAttribute("style", messages.buttonConvertStyle);
		convertButton.onclick = convertButtonHandler;
		
		if(typeof SegregateRefsJsAllowConversion == "undefined" ||
		!SegregateRefsJsAllowConversion) {
			convertButton.setAttribute("style", "display: none;");
		}
		
		// Add the refs div
		refsDiv = document.createElement("div");
		refsDiv.appendChild(refsButton);
		refsDiv.appendChild(convertButton);
		// Find position within the edit form to insert it at
		var refsDivPos = mainTextbox, refsDivPosParent = refsDivPos.parentNode;
		while (refsDivPosParent !== editForm) {
			refsDivPos = refsDivPosParent;
			refsDivPosParent = refsDivPos.parentNode;
			if (!refsDivPosParent) {
				refsDivPos = mainTextbox;
				refsDivPosParent = refsDivPos.parentNode;
				break;
			}
		}
		refsDivPos = refsDivPos.nextSibling;
		if (refsDivPos && refsDivPos.classList.contains("wikiEditor-ui-clear")) {
			refsDivPos = refsDivPos.nextSibling;
		}
		refsDivPosParent.insertBefore(refsDiv, refsDivPos);
	} catch(e) {
	}

} );

})( jQuery );

// </nowiki>