User:Teapeat/segregate-refs.js

//

/* 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 Copyright (c) 2011, Teapeat This software is licensed under these licenses: 1. Creative Commons Attribution-Share Alike 3.0 Unported License (see  for the text) 2. GNU Free Documentation License, any published version. (see  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, SegregateRefsJsEmptyRefsWarningGiven, SegregateRefsJsAllowConversion, SegregateRefsJsCompleteSearch, wikEdUseWikEd, WikEdUpdateTextarea, WikEdUpdateFrame*/

// 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", emptyRefsWarning: "IMPORTANT: This page includes one or more named " + "footnotes of which the first occurrence(s) have no contents, which is " + "not best practice. This script does not harm such footnotes, however " + "you should be aware that this script ONLY CHECKS THE FIRST REF TAG " + "it finds for the actual citation or note text, and you must work within " + "this limitation. Any change to the empty ref tag will replace the " + "footnote entirely and leave the old note text hidden.\n\n" + "BEFORE concluding from the footnotes list that a ref is in fact empty, " + "please manually check all identically-named ref tags for the contents. " + "\n\nDo you acknowledge this limitation of the script? DO NOT CLICK OK " + "UNTIL YOU HAVE READ THE ABOVE INFORMATION.", 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", refsCommentIncomplete: "\n\n", refsCommentComplete: "\n\n", convertSummary: "Converted footnotes to LDR format (using " +   "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: " };

// Begin encapsulation (prevent interference with other scripts) (function{

// Semi-global variables (private to this script) var editForm, refsDiv, refsH2, mainTextbox, refsTextbox, randPrefix, messages, refsButton, convertButton, complete, unloadHandlerRegistered = false;

// Define a (very important) function that is better than hasOwnProperty function has(obj, key) { return Object.hasOwnProperty.call(obj, key); }

// Extend the string object with new methods // Begin with the prefix "Ps" to avoid name clashes

// Add support for setting a slice of a string. // (Only works with positive indices.) String.prototype.PsSetSlice = function(replacement, indexFrom, indexTo) { if(typeof indexTo == "undefined") { return this.slice(0, indexFrom) + replacement; }   return this.slice(0, indexFrom) + replacement + this.slice(indexTo); };

// Add support for unquoting from HTML-quoted form. String.prototype.PsHTMLUnquote = function { // Let's use the browser's functionality for the hard work, // since MediaWiki/PHP supports many different HTML entities. // (Note: innerHTML is not W3C-standard) var d = window.document.createElement("div"); d.innerHTML = " "; return d.firstChild.value; };

// Add support for quoting using HTML quotes. Chooses single quotes versus // double quotes depending on which is shorter. String.prototype.PsHTMLQuote = function { // Escape ampersands var s = this.replace(/\&/g, "&amp;"); // Try both kinds of quotes var sQ = "'" + s.replace(/'/g, "&#39;") + "'", dQ = '"' + s.replace(/"/g, "&quot;") + '"';   // Choose the shorter, preferring double quotes if equal in length    return (sQ.length < dQ.length ? sQ : dQ); };

// OBJECTS

// RefScanner: Use for identifying ref tags in text. (No nested refs please) function RefScanner(argWikiText) { this.wikiText = argWikiText; // The tags listed below other than "ref" are there for an obvious reason. // NB: "references" is here to prevent out-of-line refs from being returned. this.refScanRegex = /(?:|<(nowiki|source|references|ref)(?:\s|(?:[^"']|"[^"]*"|'[^']*')*?)(?:\/>|(?:>[\s\S]*?<\/\1(?:|\s[^>]*)>)))/gi; } RefScanner.prototype = { // Returns the next ref found in the text getRef: function getRef { var results; do { results = this.refScanRegex.exec(this.wikiText); if(!results) { return null; }           if(typeof results[1] == "undefined") { results = [0,0]; }       } while(results[1].toString.toLowerCase != "ref"); return results[0]; } };

// RefParser: Use for extracting attributes from ref tags function RefParser(argWikiText) { this.wikiText = argWikiText; // The below regex is mostly a copy of the refScanRegex above, 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 = /^|(?:>[\s\S]*?<\/ref(?:|\s[^>]*)>))$/i; this.parsedRef = refParseRegex.exec(this.wikiText); if(!this.parsedRef) { throw new Error("invalid ref"); } } RefParser.prototype = { getAttributes: function getAttributes { // 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 = {}, results; while((results = attParseRegex.exec(this.parsedRef[1]))) { attributes[results[1].toLowerCase] = results[2].PsHTMLUnquote; }       return attributes; } };

// FUNCTIONS

// segregateRefs: Use for segregating refs from content. // If completeSearch == true, the function looks in all occurrences for ref // contents. If completeSearch == false, the function only checks the first. // group is the reference group to limit the operation to. The empty string // refers to all ungrouped refs. function segregateRefs(argWikiText, completeSearch, group) { // Create a random prefix for autogenerated ref names. // in theory this has a 1/1296 probability of collision - extremely low var 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 var refPreferred = "<ref "; // Variables for the main code var scanner = new RefScanner(argWikiText), unnamedRefs = 0, refNames = {}, ref, refStored, parser, attributes, refName, refLong, refShort, refCodes = [], refEmpty, emptyRefsWarningGiven = false, refGroup; // Disable the empty refs warning (see below) if the user has disabled it   if(typeof SegregateRefsJsEmptyRefsWarningGiven != "undefined" &&    SegregateRefsJsEmptyRefsWarningGiven) { emptyRefsWarningGiven = true; }   while((ref = scanner.getRef)) { parser = new RefParser(ref); attributes = parser.getAttributes; // First make sure that the ref is in the right group. refGroup = has(attributes, "group") ? attributes.group : ""; if(group != refGroup) { continue; }       // Does the ref have a name? // (Note: No matter how incorrect it seems, the empty string is       // an acceptable ref name to the MediaWiki parser, as verified by        // informal testing.) if(!has(attributes, "name")) { // Bad: it doesn't have one - create a name for it           refStored = false; refEmpty = false; refName = false; // randPrefix + (++unnamedRefs).toString(10);

refLong = ref;

// Change the corresponding ref code //refLong = "") || (parser.parsedRef[2].slice(0, 3) == "></"); // Since this script, when not set in complete search mode, checks // only the first occurrence of a ref for contents, inform the user // of this limitation if it may pose a problem. //           if(!completeSearch && !emptyRefsWarningGiven && !refStored) { //               if(refEmpty) { //                   if(!window.confirm(messages.emptyRefsWarning)) { //                       return false; //                   } //                    emptyRefsWarningGiven = true; //               } //            }        }        // Is the ref's name unique? if ((!refStored) && (!refName == false) && (!refEmpty)) { // Unique: add it to the list of refs 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.length) { refShort = refPreferred + "name=" + refName.PsHTMLQuote + "/>"; } else { refShort = refPreferred + "name=" + refName.PsHTMLQuote + " " + "group=" + refGroup.PsHTMLQuote + "/>"; }       // is this an unnamed reference? } else if (refName == false) { // yes, igmore it               refShort = refLong; // Otherwise, is the current longcode not empty? // (only when in complete search mode, and when another non-empty,       // same-named ref has not been encountered) } else if (completeSearch && !refEmpty && refNames[refName].empty) { // Not empty: fill in the long code and make a short code refCodes[refNames[refName].code] = refLong; refNames[refName].empty = false; if(!refGroup.length) { refShort = refPreferred + "name=" + refName.PsHTMLQuote + "/>"; } else { refShort = refPreferred + "name=" + refName.PsHTMLQuote + " " + "group=" + refGroup.PsHTMLQuote + "/>"; }       } else { // Always leave the ref as is if we're not truncating due to being named- teapeat refShort = refLong; }       // Replace the long code with the short code scanner.wikiText = scanner.wikiText.PsSetSlice(refShort,           scanner.refScanRegex.lastIndex - ref.length,            scanner.refScanRegex.lastIndex); // Update lastIndex accordingly scanner.refScanRegex.lastIndex += refShort.length - ref.length; }   return { wikiText: scanner.wikiText, refCodes: refCodes, randPrefix: randPrefix }; }

// integrateRefs: Use for inserting ref contents back into text function integrateRefs(argWikiText, argRefText, randPrefix) { // A function to remove an autogenerated ref name (if possible) function cleanRefLong(dirtyRef) { var cleanRegex = /^<(ref) name=(?:"[^"]*"|'[^']*'|[^\s"']*)/i; return dirtyRef.replace(cleanRegex, "<$1"); }   // Variables for the main code var scanner, ref, parser, attributes, refCodes = {}, usageFreq = {}, preferredRef = {}, refLong; // First, we build an associative array of all the ref codes // that we might need to put back into the text. // NOTE: JavaScript does not actually offer real associative array // functionality, so we emulate it using objects and the has function // we defined earlier that is used to verify a property's existence. scanner = new RefScanner(argRefText); while((ref = scanner.getRef)) { parser = new RefParser(ref); attributes = parser.getAttributes; if(has(attributes, "name")) { // Only use the first ref with each name if(!refCodes.hasOwnProperty(attributes.name)) { refCodes[attributes.name] = ref; }       }    }    // Next, we build an associative array that holds the usage frequency // of every ref name used in text scanner = new RefScanner(argWikiText); while((ref = scanner.getRef)) { parser = new RefParser(ref); attributes = parser.getAttributes; if(has(attributes, "name")) { if(!has(usageFreq, attributes.name)) { // We found a new name usageFreq[attributes.name] = 1; } else { // We already found this name usageFreq[attributes.name]++; }       }    }    // 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(has(attributes, "name")) { // Is this name on the replacement list? if(has(refCodes, attributes.name)) { // 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 scanner.wikiText = scanner.wikiText.PsSetSlice(refLong,                   scanner.refScanRegex.lastIndex - ref.length,                    scanner.refScanRegex.lastIndex); // Update lastIndex accordingly scanner.refScanRegex.lastIndex += refLong.length - ref.length; // Delete the name from the replacement list delete refCodes[attributes.name]; }       }    }    // Return both the combined output and the ref codes that were not used. return {wikiText: scanner.wikiText, unusedRefs: refCodes}; }

// Clears the undo history of a textarea by removing it // from the DOM and then inserting it again. 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, complete); // Find all unused ref names for(refName in result.unusedRefs) { if(has(result.unusedRefs, refName)) { unusedRefNamesQuoted.push(refName.PsHTMLQuote); }   }    // 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); }   // Allow for disabling usage of the complete search mode // when segregating refs for editing. if(typeof SegregateRefsJsCompleteSearch != "undefined" &&   !SegregateRefsJsCompleteSearch) { complete = false; } else { complete = true; }   // 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, complete, ""); 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 = window.document.createElement("h2"); refsH2.appendChild(window.document.createTextNode(messages.refsHeader)); // Inline refs textbox refsTextbox = window.document.createElement("textarea"); refsTextbox.id = "PsRefsTextbox"; if(!complete) { refsTextbox.value = messages.refsCommentIncomplete + segFormat.refCodes.join("\n\n"); } else { refsTextbox.value = segFormat.refCodes.join("\n\n"); }   refsTextbox.rows = Math.floor(mainTextbox.rows / 2); refsTextbox.cols = mainTextbox.cols; // 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 = group.PsHTMLUnquote; // 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, true, group); 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 = window.document.createElement("h2"); refsH2.appendChild(window.document.createTextNode(messages.convertHeader)); // Inline refs textbox if(!refsTextbox) { // Does not exist; creating refsTextbox = window.document.createElement("textarea"); refsTextbox.id = "PsRefsTextbox"; refsTextbox.value = messages.refsCommentComplete + segFormat.refCodes.join("\n"); refsTextbox.rows = Math.floor(mainTextbox.rows / 2); refsTextbox.cols = mainTextbox.cols; // 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. window.document.getElementById("wpSummary").value = messages.convertSummary; // Show the further instructions. window.alert(messages.convertFurther); // Don't submit form return false; }

function getEditboxContents { // ajaxPreview compatibility if(unloadHandlerRegistered) { // wikEd compatibility (frame -> textarea) if(typeof wikEdUseWikEd != "undefined" && wikEdUseWikEd) { WikEdUpdateTextarea; }       return integrateRefs(mainTextbox.value, refsTextbox.value,            randPrefix, complete).wikiText; } else { return mainTextbox.value; } }

// Leave a global for ajaxPreview to use. window.getEditboxContents = getEditboxContents;

function loadHandler { // This function is called on page load try { // Don't continue if the browser is Internet Explorer if(window.navigator.appName == "Microsoft Internet Explorer") { return; }       // 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(!window.document.getElementById("editform") ||       window.document.getElementById("editform").wpSection.value.length) { return; }       // Get the edit form editForm = window.document.getElementById("editform"); // Get the edit box mainTextbox = window.document.getElementById("wpTextbox1"); // Make the "segregate" button refsButton = window.document.createElement("input"); refsButton.type = "button"; refsButton.value = messages.buttonText; refsButton.setAttribute("style", messages.buttonStyle); refsButton.onclick = refsButtonHandler; // Make the "convert" button convertButton = window.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 = window.document.createElement("div"); refsDiv.appendChild(refsButton); refsDiv.appendChild(convertButton); editForm.insertBefore(refsDiv,           window.document.getElementById("editpage-copywarn")); } catch(e) { } }

// Register load handler $(loadHandler);

});

//