User:Rua/Gadget-AcceleratedFormCreation.js

Note: You may have to bypass your browser’s cache to see the changes. In addition, after saving a sitewide CSS file such as MediaWiki:Common.css, it will take 5-10 minutes before the changes take effect, even if you clear your cache.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.


// <nowiki>
/* globals mw, $ */
// jshint maxerr:500

/*
 * The starting point of the whole script.
 * 
 * This adds a hook to the page load event so that the script runs
 * adds the generated text to the edit window once the page is done loading.
 */

"use strict";

mw.loader.using(["mediawiki.util"]).done(function() {
	// Don't do anything unless the current page is in the main namespace.
	if (mw.config.get("wgAction") === "view" && (mw.config.get("wgNamespaceNumber") === 0 || mw.config.get("wgPageName") == "Wiktionary:Sandbox")) {
		// Stores all accelerated data, by language, by target pagename.
		// Sub-arrays are in HTML order.
		var accelParamsByPagename = {};
		
		var getTargetPagename = function(link) {
			var targetPagename = mw.util.getParamValue("title", link.href);
			
			if (targetPagename === null) {
				var match = link.href.match(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/);
				
				if (match) {
					targetPagename = decodeURIComponent(match[2]);
				}
			}
			
			return targetPagename;
		};
		
		var getLang = function (element) {
			if (element.accelLang !== undefined) {
				return element.accelLang;
			}
			var hasLang = $(element).closest("[lang]")[0];
			var lang = hasLang ? hasLang.getAttribute("lang") : null;
			element.accelLang = lang;
			return lang;
		};
		
		var getPartOfSpeech = function(link) {
			// Acceleration can be added to inflection tables too.
			// This tells the search script to skip headers with these names.
			var skipheaders = [
				"alternative forms",
				"antonyms",
				"conjugation",
				"declension",
				"derived terms", 
				"inflection",
				"mutation",
				"related terms",
				"synonyms",
				"translations",
				"usage notes"
			];
			var node = link;
			
			while (node) {
				if (node.nodeType == 1 && node.nodeName.match(/^[hH][3456]$/)) {
					var header = $(node).find(".mw-headline").text().replace(/^[1-9.]* /, "").toLowerCase();
					
					if (skipheaders.indexOf(header) == -1) {
						return header;
					}
				}
				
				node = node.previousSibling || node.parentNode;
			}
			
			throw new Error("This entry seems to be formatted incorrectly. Does it have a part-of-speech header?");
		};

		var getHeadword = function(link) {
			var node = link;
			
			while (node) {
				var headword_node = $(node).find("strong.headword").first();
				var headword_translit_node = headword_node.nextAll(".headword-tr.manual-tr").first();

				if (headword_node.length == 1) {
					return {
						headword: headword_node.text(),
						headword_translit: headword_translit_node.text(),
					};
				}
				
				node = node.previousSibling || node.parentNode;
			}
			
			throw new Error("This entry seems to be formatted incorrectly. Does it have a headword line?");
		};
		
		var createAccelParam = function(link) {
			var classList = $(link).closest(".form-of")[0].classList;
			var classNames = [];
			
			for (var i = 0; i < classList.length; ++i) {
				// Filter out anything that doesn't belong
				if ((/^(gender|origin|origin_transliteration|pos|target|transliteration)-(.+)|(.+)-form-of$/.test(classList[i]))) {
					classNames.push(classList[i]);
				}
			}
			
			var accelParam = classNames.join(" ");
			
			var targetPagename = getTargetPagename(link);
			var targetHead = (link.innerText || link.textContent).replace(" ", "_");
			
			if (targetPagename != targetHead) {
				accelParam = "target-" + targetHead + " " + accelParam;
			}
			
			var lemmaPagename = encodeURIComponent(mw.config.get("wgTitle"));
			var lemmaHead = getHeadword(link);

			if (lemmaHead.headword_translit) {
				accelParam = "origin_transliteration-" + lemmaHead.headword_translit + " " + accelParam;
			}
			
			if (lemmaPagename != lemmaHead.headword) {
				accelParam = "origin-" + lemmaHead.headword + " " + accelParam;
			}

			return "pos-" + getPartOfSpeech(link).replace(" ", "_") + " " + accelParam;
		};
		
		var storeAccelParam = function(link) {
			// Extract the targeted pagename from the URL,
			// and language code from the nearest element with a lang attribute
			var lang = getLang(link);
			var targetPagename = getTargetPagename(link);
			
			// Add page name to the list
			if (accelParamsByPagename[lang] === undefined) {
				accelParamsByPagename[lang] = {};
			}
			
			if (accelParamsByPagename[lang][targetPagename] === undefined) {
				accelParamsByPagename[lang][targetPagename] = [];
			}
			
			var accelParam = createAccelParam(link);
			
			if (accelParamsByPagename[lang][targetPagename].indexOf(accelParam) === -1) {
				accelParamsByPagename[lang][targetPagename].push(accelParam);
			}
		};
		
		var processLink = function(link) {
			// Extract the targeted pagename from the URL,
			// and language code from the nearest element with a lang attribute
			var lang = getLang(link);
			var targetPagename = getTargetPagename(link);
			
			// Fetch the acceleration parameters from the store
			var accelParam = accelParamsByPagename[lang][targetPagename].slice(0);
			
			for (var i = 0; i < accelParam.length; ++i) {
				accelParam[i] = "accel" + (i + 1).toString() + "=" + encodeURIComponent(accelParam[i]);
			}
			
			accelParam = accelParam.join("&");
			
			// Convert an orange link into an edit link
			if ($(link).hasClass("partlynew")) {
				link.href = link.href.replace(/^(.*)\/wiki\/([^#]+)(?:#.+)?$/, "$1/w/index.php?title=$2&action=edit");
			}
			
			// Now build a new "green link" URL to replace the original red link with
			link.href +=
				"&editintro=User:Conrad.Irwin/creation.js/intro" +
				"&accel_lang=" + encodeURIComponent(lang) +
				"&accel_lemma=" + encodeURIComponent(mw.config.get("wgTitle")) +
				"&" + accelParam;
			link.style.color = "#22CC00";
			link.processedLink = true;
		};
		
		// Mutation observer to respond when OrangeLinks modifies links
		var mutobs = new MutationObserver(function(mutations, observer) {
			mutations.forEach(function(mutation) {
				if (mutation.attributeName != "class") {
					return;
				}
				
				var link = mutation.target;
				
				// Don't process a link we've already been to
				if (link.processedLink) {
					return;
				}
				
				if (!$(link).hasClass("partlynew")) {
					return;
				}
				
				// Process
				processLink(link);
			});
		});
		
		// First generate and store all the parameters
		var oldtable = null;  // Were we previously inside a table?
		var columns = [];
		
		$(".form-of a").each(function() {
			// Are we currently inside a table?
			var $this = $(this);
			var table = $this.closest("table");
			
			if (table.length > 0) {
				table = table[0];
			} else {
				table = null;
			}
			
			// Was a column number specified on the current table cell?
			// jQuery.fn.data automatically converts an integer-like string
			// to a number.
			var col = $this.closest("td[data-accel-col]").first().data("accel-col");
			
			if (typeof col !== "number") {
				col = null;
			}
			
			// If we were in a table, and we changed to another table or are no longer in one,
			// or if there is no column number attribute, flush the column lists.
			if (oldtable && (oldtable !== table || col === null)) {
				for (var i = 0; i < columns.length; ++i) {
					for (var j = 0; j < columns[i].length; ++j) {
						storeAccelParam(columns[i][j]);
					}
				}
				
				columns = [];
			}
			
			oldtable = table;
			
			// The nostore parameter causes the link to not be stored,
			// but it is processed later. The effect is that this link has no
			// effect on the ordering of forms.
			if ($(this).closest(".form-of").first().hasClass("form-of-nostore")) {
				return;
			}
			
			// If there is a column number attribute, defer storing the link,
			// put it in the columns array instead.
			if (col !== null) {
				--col;  // Column attributes are 1-based, JS arrays are 0-based
				
				// Expand the columns list to fit the number of columns
				while (columns.length <= col) {
					columns.push([]);
				}
				
				// Save the link in the columns list
				columns[col].push(this);
			} else {
				// Store the link directly
				storeAccelParam(this);
			}
		});
		
		// Flush column lists
		for (var i = 0; i < columns.length; ++i) {
			for (var j = 0; j < columns[i].length; ++j) {
				storeAccelParam(columns[i][j]);
			}
		}
		
		// Then add them onto the links, or add a mutation observer
		$(".form-of a").each(function() {
			var $this = $(this);
			if ($this.hasClass("new") || $this.hasClass("partlynew")) {
				processLink(this);
			} else {
				// FIXME: There's a small window for a race condition here.
				// If the "partlynew" class is added by OrangeLinks after the above if-statement is evaluated,
				// but before the observer is added, then the link won't be processed.
				mutobs.observe(this, {attributes : true});
			}
		});
	} else if (mw.config.get("wgAction") === "edit") {
		// Get the parameters from the URL
		var getAccelParams = function() {
			var accelParams = [];
			var i = 1;
			
			while (true) {
				var acceldata = mw.util.getParamValue("accel" + i.toString());
				
				if (!acceldata) {
					break;
				}
				
				// Default values
				var params = {
					pos: null,
					form: null,
					gender: null,
					transliteration: null,
					origin: mw.util.getParamValue("accel_lemma"),
					origin_transliteration: null,
					target: mw.config.get("wgTitle"),
				};
				
				// Go over each part and add it
				var parts = acceldata.split(" ");
				
				for (var j = 0; j < parts.length; ++j) {
					var part = parts[j];
					
					if (part.match(/^(gender|origin|origin_transliteration|pos|target|transliteration)-(.+)$/)) {
						params[RegExp.$1] = RegExp.$2.replace("_", " ");
					} else if (part.match(/^(.+)-form-of$/)) {
						params.form = RegExp.$1.replace("_", " ");
					}
				}
				
				accelParams.push(params);
				++i;
			}
			
			return accelParams;
		};
		
		// Generates entries from the information
		var printArgs = function(accelParams) {
			var args = [
				"lang=" + mw.util.getParamValue("accel_lang"),
				"origin_pagename=" + mw.util.getParamValue("accel_lemma"),
				"target_pagename=" + mw.config.get("wgTitle"),
				"num=" + accelParams.length,
			];
			
			for (var i = 0; i < accelParams.length; ++i) {
				for (var key in accelParams[i]) {
					if (accelParams[i][key] !== null) {
						args.push(key + (i + 1) + "=" + accelParams[i][key].replace(/\|/g, "&#124;"));
					}
				}
			}
			
			return args.join("|");
		};
		
		var showModuleError = function(errorText) {
			// Attempt to link to the line of the module in which the error occurred.
			errorText = errorText.replace(
				/(Module:[^#<>\[\]|{}_]+)(?: at line |:)(\d+)/,
				function (wholeMatch, moduleName, lineNumber) {
					var link = document.createElement('a');
					link.href = mw.util.getUrl(moduleName, {action: "edit"}) + "#mw-ce-l" + lineNumber;
					link.innerHTML = moduleName + " at line " + lineNumber;
					return "Lua error in " + link.outerHTML;
				});
			
			var errorBox =
				"<div id=\"accel-error\">" +
				"<p><big>An error occurred while generating the entry:</big></p>" +
				"<p>" + errorText + "</p>" +
				"</div>";
			
			wikipreview.insertAdjacentHTML("beforebegin", errorBox);
		};
		
		var receiveModuleResponse = function(response) {
			var newtext, result;
			
			try {
				result = JSON.parse(response.expandtemplates.wikitext);
			} catch (err) { // JSON parse error should not happen.
				mw.notify(err.msg);
				return;
			}
			
			if (result.error) { // module error
				showModuleError(result.error);
			} else { // successfully generated entries
				newtext = result.entries;
			}
			
			for (i = 0; i < result.messages.length; ++i) {
				mw.notify(result.messages[i]);
			}
			
			if (!newtext) {
				return;
			}
			
			var newValue;
			
			// Does the page already exist?
			if (textbox.value) {
				var langsection_regex = /^==([^=\n]+)==$/mg;
				var match = langsection_regex.exec(newtext);
				
				if (!match) {
					showModuleError("No language section was found in the returned text.");
				}
				
				var langname = match[1];
				
				// Reset position at which regex starts its search.
				// Otherwise, regex starts matching after the index where it
				// found the language header in newtext.
				langsection_regex.lastIndex = 0;
				
				// Go over language sections to find where to insert our new one
				while ((match = langsection_regex.exec(textbox.value)) !== null) {
					if (match[1] == langname) {
						// There already exists a section for our language, abort.
						return;
					} else if (match[1] == "Translingual" || match[1] == "English" || (langname != "English" && match[1] < langname)) {
						// Skip past English and Translingual, or if the language sorts higher
						continue;
					} else {
						// We found the first match that sorts lower than our language, great.
						break;
					}
				}
				
				var scrollIndex;
				newValue = textbox.value;
				
				if (match === null) {
					// We found no language that our section should go before, so insert it at the end.
					newValue = newValue.trimEnd() + "\n\n----\n\n";
					scrollIndex = newValue.length;
					newValue = newValue + newtext;
				} else {
					// We found a language to insert before, so do that.
					newValue = newValue.substring(0, match.index) + newtext + "\n\n----\n\n" + newValue.substring(match.index);
					scrollIndex = match.index;
				}
				
				// Scroll the textbox to the newly added section. First scroll all the way down,
				// then set the cursor to the start of the new section, which scrolls back up
				// to the new section's language header.
				textbox.scrollTop = textbox.scrollHeight;
				textbox.selectionStart = scrollIndex;
				textbox.selectionEnd = scrollIndex;
				
				summary.value = "Adding forms of [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
			} else {
				newValue = newtext;
				summary.value = "Creating forms of [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
			}
			
			// Set textbox text. Setting textbox.value is unreliable.
			$(textbox).val(newValue);
		};
		
		var wikipreview = document.getElementById("wikiPreview");
		var textbox = document.getElementById("wpTextbox1");
		var summary = document.getElementById("wpSummary");
		var lang = mw.util.getParamValue("accel_lang");
		var lemma = mw.util.getParamValue("accel_lemma");
		
		if (!(wikipreview && textbox && summary && lang && lemma)) {
			return;
		}
		
		// Gather all the information that was given in the URL
		var accelParams = getAccelParams();
		
		if (!accelParams) {
			return;
		}
		
		var module = "accel", funcName = "generate_JSON";
		mw.loader.using("mediawiki.api", function() {
			new mw.Api().get({
				"action": "expandtemplates",
				"format": "json",
				"text": "{{#invoke:" + module + "|" + funcName + "|" + printArgs(accelParams) + "}}",
				"prop": "wikitext"
			}).done(receiveModuleResponse);
			
		});
	}
});

// </nowiki>