MediaWiki:Gadget-AcceleratedFormCreation.js

(Redirected from Wiktionary:ACCEL)

Note – after saving, you may have to bypass your browser’s cache to see the changes.

  • 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.
To edit the rules for generating new form-of entries, go to Module:accel.

This gadget allows you to automatically generate entries for inflected forms of words.

Enabling accelerated links

To enable the gadget, enable it in the Gadgets tab of your preferences: item "Add accelerated creation links for common inflections of some words.". If you do not have an account, you can enable it in your per-browser preferences instead.

Creating new entries

Many existing pages and templates already support the acceleration gadget. If a link allows you to generate a new entry automatically, it will appear bright green. To start creating an entry, simply click the link, which will take you to edit it. The edit window will automatically have the new entry added to it. You can then save the page like you would any other page. The order in which the definitions appear is determined by the order in which they appear in the lemma entry, but can be modified; see below under #Inflection tables organised by column.

It is important that you check the new entry to make sure that everything is correct! Although editors should take great care to make sure that the gadget does not generate bad entries, it is your own responsibility to make sure you don't inadvertently add incorrect or badly formatted content to Wiktionary. If the gadget is generating wrong entries, you should report this to the Grease pit, or fix it yourself if you are able.

Making a template use the script

Adjusting a template or module to use the script is relatively easy. Whenever your template/module shows a link to an inflected form, you need to provide it with some extra information. This extra information consists of "tags" that tell the script how to process your link, so that the right entry is generated.

Module:accel will generate a default entry based on the tags you provide (the specific details are explained in its documentation). If this is not sufficient, i.e. if the entries need to be formatted in a language-specific way, you can create your own submodule of Module:accel and write your own rules. If you don't know how, ask in WT:GP for help.

Acceleration tags

The following is a list of tags that you can add to links to enable accelerated forms to be created. Each tag is associated with either (a) a template parameter to provide it when used with link templates such as {{l}} or {{l-self}}; (b) a key in the table passed as the accel key in the data parameter to the full_link() function in Module:links, when called from a module.

form= (in modules) or |accel-form= (in templates)

Which particular form the link is for. It should use the inflection codes used in {{inflection of}}, with pipe symbols (vertical bars) separating the inflection codes. When called from a module, use the pipe symbols directly, e.g. form="nom|m|p" for nominative masculine plural. When used in template code, you will need to encode the pipe symbols using {{!}}, e.g. |accel-form=nom{{!}}m{{!}}p. You can also specify arbitrary text such as comparative-of, but this must be interpreted by a language-specific module (such as Module:accel/ru for Russian) and converted into the appropriate definition. This is the only tag that is mandatory.

gender= (in modules) or |accel-gender= (in templates)

The gender code that should be added to the headword line, using the same format that {{head}} uses, e.g. m-p for masculine plural. This should only be needed in rare occasions. If the definition already mentions the gender, like "feminine form of" for example, then this is redundant and you should not add it.

translit= (in modules) or |accel-translit= (in templates)

The transliteration that should appear on the headword line. The value of this parameter goes into param |tr= of the call to {{head}} in the accelerated entry. You only need to specify this for languages that use a non-Latin script, and only when the auto-generated transliteration is insufficient, incorrect or nonexistent.

lemma= (in modules) or |accel-lemma= (in templates)

The lemma to be included in the call to {{inflection of}} on the definition line. The value of this parameter goes into param |2= of the call to {{inflection of}}. It defaults to the name of the current page, and only needs to be specified when the lemma contains additional diacritics that are stripped in order to generate the pagename (as in Latin, Russian, Ancient Greek, Old English, etc.).

lemma_translit= (in modules) or |accel-lemma-translit= (in templates)

The manual transliteration of the lemma to be included in the call to {{inflection of}} on the definition line. The value of this parameter goes into param |tr= of the call to {{inflection of}}. You only need to specify this for languages that use a non-Latin script, and only when the auto-generated transliteration is insufficient, incorrect or nonexistent.

nostore=true (in modules) or |accel-nostore=true (in templates)

Normally, the forms on a page are processed in reading order (left to right, top to bottom), and the entries/definitions will be added to the page in that order. This tag is used when the same form appears multiple times in a page/inflection table, and causes a particular link to be ignored for the ordering. This is useful when an inflection table shows some forms in its collapsed state, like for example {{se-infl-adj-even}} or {{nl-conj-wk}}. By placing this tag on the links that appear in the collapsed state, their forms will not be shown before the forms in the rest of the table.

Adding the tags to a template

There are various ways to add accelerated creation to links, both in templates/wikicode and in modules/Lua. Acceleration tags should not be added directly to entries, there should always be a template that wraps around it.

Links that are created with the standard linking templates {{l}}, {{l-self}} and such have parameters for each of the tag types listed above, with the name prefixed with accel-. Thus, for the form tag, you use the accel-form= parameter, e.g.

{{l-self|en|foos|accel-form=p}}

If you are working in a module using the full_link function of Module:links, the accel key can be provided as one of the values in the table that's passed as the first parameter. The value should be a table containing the acceleration tags as keys, e.g.

full_link({lang = lang, term = "foos", accel = {form = "p"}})

When working on a headword template, you'll typically be using {{head}}, and providing it with one or more inflected forms to show on the headword line. To provide acceleration tags for the inflected forms, the tag name is prefixed with fNaccel- N stands for the number of the form, e.g.

{{head|en|noun|plural|foos|f1accel-form=p|diminutive|bar|f2accel-form=diminutive}}

In a module using the full_headword function of Module:headword, you provide the accel key in the list of inflections of a particular label. Its value is a table of tags, like as with Module:links, e.g.

full_headword({lang = lang, inflections = { {label = "plural", accel = {form = "p"}, "foos"}, {label = "diminutive", accel = {form = "diminutive"}, "bar"} }

When you're done, add {{isAccelerated}} to the documentation page of the template. This shows a notice that accelerated links are enabled for this template. It also adds the template to Category:Templates with acceleration.

Inflection tables organised by column

The order in which definitions appear in combined entries is determined by the order in which the links appear in the page/HTML code. This order is always in reading order: left to right, top to bottom. Many inflection tables are organised by column, and often you want the forms in one column to take priority over forms in another column. For example, it's generally desirable to have the definitions of singular forms appear before the definitions of plural forms. For this, there is the data-accel-col= attribute.

The data-accel-col= attribute is placed on the table cell which contains the accelerated inflection links, and is given a column number as a value. For example:

|-
! Locative
| data-accel-col="1" | {{l-self|se|soddjilis|accel-form=loc{{!}}s}}
| data-accel-col="2" | {{l-self|se|soddjiliin|accel-form=loc{{!}}p}}
|-
! Comitative
| data-accel-col="1" | {{l-self|se|soddjiliin|accel-form=com{{!}}s}}
| data-accel-col="2" | {{l-self|se|soddjiliiguin|accel-form=com{{!}}p}}

Columns with the same number are processed as a single unit, so all forms with column number 1 are processed before all forms with column number 2. Consequently, definitions for forms in column 1 will always appear in an entry before definitions for forms in column 2. If the inflection table contains another set of columns below the first set, the count continues, so that the columns are numbered 1, 2, 3, 4 and so on.

Column numbers are applied on a per-table basis. This means that if the same column number appears in multiple tables in the same language section, they will not interfere with each other; each new table "resets" the columns. Any table cell (but not a header cell with !) that does not have the data-accel-col= attribute will also cause a column reset. This means that you can have two different columns numbered 1, as long as there is a table cell without a column number in between them. Once a cell lacking a column number is encountered in the reading order, processing of the forms will continue in reading order as usual.

Rules for generating entries

The new entries themselves are not generated by this script, but by Module:accel and its language-specific submodules. If you want to add or edit rules for your language, look on the module's page for information and instructions.


// <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() {
	var pageName = mw.config.get("wgPageName");
	
	// Don't do anything unless the current page is in the main namespace.
	// Set window.accelEverywhere = true to test this gadget elsewhere.
	if (window.accelEverywhere || (mw.config.get("wgAction") === "view" && (mw.config.get("wgNamespaceNumber") === 0 || pageName == "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"
			];
			
			for (var node = link; node !== null; node = node.previousSibling || node.parentNode) {
				if (node.nodeType == 1 && node.nodeName.match(/^H[3-6]$/)) {
					var header = $(node).find(".mw-headline").text().replace(/^[1-9.]* /, "").toLowerCase();
					
					if (skipheaders.indexOf(header) == -1) {
						return header;
					}
				}
			}
			
			throw new Error("This entry seems to be formatted incorrectly. Does it have a language and part-of-speech header?");
		};
		
		var createAccelParam = function(link) {
			var classNames = Array.prototype.filter.call($(link).closest(".form-of")[0].classList,
				function (className) {
					return (/^(gender|origin|origin_transliteration|pos|target|transliteration)-.+|.+-form-of$/.test(className));
				});
			
			var accelParam = classNames.join(" ");
			
			var targetPagename = getTargetPagename(link);
			var targetHead = (link.innerText || link.textContent).replace(/ /g, "_");
			
			if (targetPagename != targetHead) {
				accelParam = "target-" + targetHead + " " + accelParam;
			}
			
			return "pos-" + getPartOfSpeech(link).replace(/ /g, "_") + " " + 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)
				.map(function (accel, i) {
					return "accel" + (i + 1).toString() + "=" + encodeURIComponent(accel);
				})
				.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=MediaWiki:Gadget-AcceleratedFormCreation.js/intro" +
				"&accel_lang=" + encodeURIComponent(lang) +
				"&accel_lemma=" + encodeURIComponent(pageName.replace(/_/g, " ")) +
				"&" + accelParam;
			link.classList.add("accelerated");
			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: pageName,
				};
				
				// 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(/_/g, " ").replace(/\uFFF0/g, "_");
					} else if (part.match(/^(.+)-form-of$/)) {
						params.form = RegExp.$1.replace(/_/g, " ").replace(/\uFFF0/g, "_");
					}
				}
				
				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=" + pageName,
				"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;

			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];
			
			// Does the page already exist?
			if (textbox.value) {
				// 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";
					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" + 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 " + langname + " [[" + lemma + "]] ([[WT:ACCEL|Accelerated]])";
			} else {
				newValue = newtext;
				summary.value = "Creating forms of " + langname + " [[" + 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>