MediaWiki:Gadget-Hotkeys.js

//

(function {	var CONTEXT_LOOKBACK = 50; 	var openCurly = '{{'; // prevents MediaWiki from doing its unwanted black magic where JS strings surrounded by –  are counted as transclusions	registerHotkey("X", function(callbacks) { // Replace terms with simple links callbacks.replaceSelection(function(selection) {			return selection.replace(/{{Term\|Series\|([^\|]+)\|link}}/g, function(match, term) { return ""+term+""; });		});		// Insert List template callbacks.insertText(openCurly+"List|", "}}"); });	registerHotkey("T", function(callbacks) { const termContext = getTermContext(callbacks.getContext); callbacks.expandSelection; // if cursor is surrounded by, expand the selection to include that const containsLink = callbacks.replaceSelection(replaceLink); // strip that were contained in or around the original selection const afterText = containsLink ? "|link}}" : "}}"; callbacks.insertText(openCurly+'Term|'+termContext+'|', afterText); });	registerHotkey("P", function(callbacks) { var termContext = getTermContext(callbacks.getContext); callbacks.expandSelection; const containsLink = callbacks.replaceSelection(replaceLink); const afterText = containsLink ? "|link}}" : "}}"; callbacks.insertText(openCurly+'Plural|'+termContext+'|', afterText); });	registerHotkey("I", function(callbacks) { var termContext = getTermContext(callbacks.getContext); var currentPage = mw.config.get('wgTitle'); callbacks.insertText(openCurly+'Term|'+termContext+'|'+currentPage+'}}'); });	// Different implementations are needed to maniplate the edit box text when WikiEditor syntax highlighting (CodeMirror) is enabled vs. disabled	function registerHotkey(key, callbackFn) {		// Plain text area		$(document).ready(function { $("#wpTextbox1").keydown(function(e) {				if (e.ctrlKey && e.altKey && e.code == "Key"+key) {					callbackFn(textArea);				}			}); });		// CodeMirror		mw.hook('ext.CodeMirror.switch').add(function(cmEnabled, cm) { cm = cm && cm[0] && cm[0].CodeMirror; if (cm) { var keyMap = {}; keyMap["Ctrl-"+"Alt-"+key] = function { callbackFn(codeMirror(cm)); };				cm.addKeyMap(keyMap); }		});	}	function codeMirror(cm) {		return {			getContext: function(lookback) {				var cursor = cm.getCursor;				var lookBack = {					line: cursor.line - lookback, // only look back a certain number of lines, for performance					char: 0				};				var text = cm.getRange(lookBack, cursor);				return text;			},			insertText: function(textBeforeCursor, textAfterCursor) {				var cursorPos = cm.getCursor;				textBeforeCursor = textBeforeCursor || ;				textAfterCursor = textAfterCursor || ;				var selection = cm.getSelection;				var replacement = textBeforeCursor + selection + textAfterCursor;				cm.replaceSelection(replacement);				if (selection != '') {					cm.setSelection( {							line: cursorPos.line, ch: cursorPos.ch + textBeforeCursor.length - selection.length },						{ 							line: cursorPos.line, ch: cursorPos.ch + textBeforeCursor.length }					);				} else {					cursorPos.ch = cursorPos.ch + textBeforeCursor.length;					cm.setCursor(cursorPos);				}			},			expandSelection: function {				const cursor = cm.getCursor;				const selection = cm.getSelection;				const beforeSelection = {					line: cursor.line,					ch: 0,				};				const afterSelection = {					line: cursor.line,					ch: cm.getLine(cursor.line).length				};				const textBeforeSelection = cm.getRange( beforeSelection, {						line: cursor.line, ch: cursor.ch - selection.length, }				);				const textAfterSelection = cm.getRange(cursor, afterSelection);				const offsets = expandSelection(textBeforeSelection, textAfterSelection);				cm.setSelection( {						line: cursor.line, ch: cursor.ch - selection.length - offsets.start, },					{						line: cursor.line, ch: cursor.ch + offsets.end, }				);			},			replaceSelection: function(replaceFn) {				const selection = cm.getSelection;				const replacement = replaceFn(selection);				cm.replaceSelection(replacement, "start");

const cursor = cm.getCursor; cm.setSelection(					{						line: cursor.line,						ch: cursor.ch,					},					{						line: cursor.line,						ch: cursor.ch+replacement.length,					}				); return selection != replacement; },		};	}	function textArea { return { getContext: function { var editBox = $("#wpTextbox1")[0]; var text = editBox.value.substring(0, editBox.selectionStart); return text; },			insertText: function(textBeforeCursor, textAfterCursor) { textBeforeCursor = textBeforeCursor || ''; textAfterCursor = textAfterCursor || ''; var editBox = $("#wpTextbox1")[0]; var start = editBox.selectionStart; var end = editBox.selectionEnd; editBox.value = editBox.value.substring(0, start) + textBeforeCursor + editBox.value.substring(start, end) + textAfterCursor + editBox.value.substring(end, editBox.value.length); editBox.selectionStart = start + textBeforeCursor.length; editBox.selectionEnd = end + textBeforeCursor.length; },			expandSelection: function { const editBox = $("#wpTextbox1")[0]; const start = editBox.selectionStart; const end = editBox.selectionEnd; const textBeforeSelection = editBox.value.substring(0, start); const textAfterSelection = editBox.value.substring(end, editBox.value.length);

const offsets = expandSelection(textBeforeSelection, textAfterSelection); editBox.setSelectionRange(start - offsets.start, end + offsets.end); },			replaceSelection: function(replaceFn) { const editBox = $("#wpTextbox1")[0]; const start = editBox.selectionStart; const end = editBox.selectionEnd; const selection = editBox.value.substring(start, end);

const replacement = replaceFn(selection); editBox.value = editBox.value.substring(0, start) + replacement + editBox.value.substring(end, editBox.value.length); editBox.selectionStart = start; editBox.selectionEnd = start + replacement.length;

return selection != replacement; }		};	}

function replaceLink(selection) { const match = selection.match(/\[\[([^\]\|]+)/); if (!match) { return selection; }		const linkedArticle = match[1]; return linkedArticle; }

function expandSelection(textBeforeSelection, textAfterSelection) { const charsBefore = textBeforeSelection.split("").reverse; var hasLink = false; var i = 0; while (i < charsBefore.length) { if (charsBefore[i] == "[" && charsBefore[i+1] == "[") { hasLink = true; break; }			if (charsBefore[i] == "]") { break; }			i = i + 1; }		i = i + 2 if (!hasLink) { return { start: 0, end: 0 }; }

const charsAfter = textAfterSelection.split(""); var j = 0; while (j < charsAfter.length) { if (charsAfter[j] == "]" && charsAfter[j+1] == "]") { break; }			j = j + 1; }		j = j + 2 while (charsAfter[j].match(/\w/) != null) { // to account for links like Hearts j = j + 1 }		return { start: i, end: j }; }

function getTermContext(getContext) { var wikitext = getContext(CONTEXT_LOOKBACK); var game = getCurrentGameContext(wikitext); if (game === null) { // if context not loaded yet, don't infer any game return ''; }		else if (game === '') { //if context loaded but no current context exists, use "Series" return 'Series'; }		else { return getLatestVersion(game); }	}	/** CONTEXT **/ var gameContextRegex = null; var gameToLatestVersion = {}; (function loadGames {		new mw.Api.get({ action: 'cargoquery', format: 'json', limit: '200', tables: 'Games', fields: 'code,supersededBy', order_by: 'canonOrder' }).then(function(result) { gameContextRegex = ''; var gameTokens = result.cargoquery.map(function(queryResult) {				var game = queryResult.title;				gameToLatestVersion[game.code] = game.supersededBy || game.code;				var gameCapture = '(' + escapeRegExp(game.code) + ')';				// Each sub-expression represents a game-based template which can be used to infer the current game context.				return [					openCurly + gameCapture + '}}',					openCurly + gameCapture + '\\|-}}',					openCurly + 'Term\\|' + gameCapture + '\\|',					openCurly + 'Plural\\|' + gameCapture + '\\|',				].join('|');			}); gameTokens.push([				openCurly + 'Term\\|(Series)\\|',				openCurly + 'Plural\\|(Series)\\|',			].join('|'));

gameContextRegex = new RegExp(gameTokens.join('|'), 'g'); });	});	function escapeRegExp(string) { return string.replace(/[{}.*+\-?^$|[\]\\]/g, '\\$&'); }	/**	 * Attemps to infer the current game that should be inserted into text by looking at the templates used before it (namely Template:Term, Template:Plural, and game links) * @returns a game code (e.g. OoT, MM, BotW), or an empty string if none found. Returns nil if games haven't been loaded yet. **/ 	function getCurrentGameContext(wikitext) { if (!gameContextRegex) { // list of games hasn't loaded yet return null; }		var gameMatch = getLastMatchBeforeCursor(gameContextRegex, wikitext); if (!gameMatch) { return ''; }		var headingMatch = getLastMatchBeforeCursor(/[^=]={2,3}[^=]+={2,3}/g, wikitext); // match L2 or L3 heading if (headingMatch && headingMatch.index > gameMatch.index) { //Effectively: L2 and L3 headings clear the game context return ''; }		var captures = gameMatch.filter(Boolean); // remove empty capture groups return captures[1]; }	function getLastMatchBeforeCursor(regex, wikitext) { var it = wikitext.matchAll(regex); var lastMatch; var result = it.next; while(!result.done) { lastMatch = result.value; result = it.next; }		return lastMatch; }	function getLatestVersion(game) { return gameToLatestVersion && gameToLatestVersion[game] || game; } });