Zelda Wiki

Want to contribute to this wiki?
Sign up for an account, and get started!

Come join the Zelda Wiki community Discord server!

READ MORE

Zelda Wiki
Register
Advertisement

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * WikitextAutocompleter
 * Provides Autocomplete (a dropdown list) for wiki editing such as for 
 * completing templates
 * Uses https://yuku-t.com/textcomplete/
 *
 * Notes:
 * - Won't work with CodeEditor since it doesn't use a textarea but that's fine
 *   since CodeEditor won't be used for wikitext
 * Bugs:
 * - Overlapping strategies, e.g. "{{Template1|param1|para{{Templ"; it seems 
 *   both run, but the earlier strategy in the array registered appears?
 */

if (mw.config.get("wgAction") == "edit") {
	
	var api = new mw.Api();
	var killSwitchCount = 10; // a limit for recursive mw.api queries
	
	/** Global Variables */
	strategies = []; // Array of strategies
	templates = {}; // Contains all the parameter arrays/definitions
	autoComplete = {}; // Contains supporting variables, e.g. initialismsArray, createTemplateStrategy()
	
	/** Supporting definitions */
	
	/* Array of Standard Initialisms */
	autoComplete.initialismsArray = [];
	var readInitialisms = function(callback) {
		$.get( "https://zelda.gamepedia.com/Template:Zelda?action=raw", function( data ) {
			var initialisms = [];
			data.split("<noinclude>")[1]
				.match(/\|\|[a-zA-Z0-9-+& ]*\n/g)
				.forEach(function(value, index){
				var name = value.slice(2).trim();
				initialisms.push(name);
			});
			autoComplete.initialismsArray = initialisms; // Leave autoComplete.initialismsArray empty until fully filled
			if (typeof callback === "function") callback();
		});
	};
	/* Call it, optionally with a callback if needed */
	readInitialisms(); // No specific need for the callback yet?
	
	/** Template parameter definitions */
	
	/* Template:Color */
	templates.Color = {};
	$.get( "https://zelda.gamepedia.com/Template:Color?action=raw", function( data ) {
		var colorsArray = [];
		data.split("</includeonly>")[0]
			.split("#switch:{{{1\|}}}")[1]
			.split("\|#default")[0]
			.match(/\|[a-zA-Z0-9 ]*/g)
			.forEach(function(value, index){
				var name = value.slice(1).trim();
				colorsArray.push(name);
			});
		templates.Color = {
			"1": colorsArray
		};
	});
	
	/* Template:KSTest */
	templates.KSTest = {
		"1": ["testparam1value1","testparam1value2","testparam1value3"],
		"2": ["testparam2value1","testparam2value2","testparam2value3"],
		"3": ["testparam3value1","testparam3value2","testparam3value3"]
	};
	
	/* Template:Icon */
	templates.Icon = {};
	$.get( "https://zelda.gamepedia.com/Template:Icon?action=raw", function( data ) {
		var iconsArray = [];
		data.split("</includeonly>")[0]
			.split("\|#default")[0]
			.match(/\n\|[a-zA-Z0-9-+ ]*/g)
			.forEach(function(value, index){
				var name = value.slice(2).trim();
				iconsArray.push(name);
			});
		templates.Icon = {
			"1": iconsArray
		};
	});
	
	/* Template:Exp Game */
	templates["Exp Game"] = {};
	templates["Exp Game"].getParams = function(paramNum) {
		return autoComplete.initialismsArray; // Assumes an integer paramNum > 0
	};
	
	/** Strategy to autocomplete template names */
	/* Get the template name list */
	autoComplete.templatesArray = [];
	var templateListQuery = {
		"action": "query",
		"format": "json",
		"prop": "",
		"list": "allpages",
		"apnamespace": "10", // Template
		"aplimit": "max"
	};
	killSwitchCount = 10;
	var templateListQueryAjax = function() {
		api.get( templateListQuery ).done( function ( data ) {
			killSwitchCount--;
			if (killSwitchCount < 0) return;
			//console.log( data.query.allpages );
			data.query.allpages.forEach(function(current) {
				autoComplete.templatesArray.push(current.title.slice("Template:".length));
			});
			if ("continue" in data) {
				templateListQuery["continue"]   = data["continue"]["continue"];
				templateListQuery.apcontinue = data["continue"].apcontinue;
				templateListQueryAjax();
			}
		} );
	};
	templateListQueryAjax();
	
	/* Create the template name strategy */
	autoComplete.templateListStrategy = 
	{
		id: "TemplateList",
		match: /(\{\{)([^#<>{}_\[\]\|]*)$/, // invalid article title characters https://www.mediawiki.org/wiki/Manual:Page_title#Invalid_page_titles
		search: function (term, callback) {
			console.log("term: " + term);
			var nameArray;
			if (term === "")
				nameArray = autoComplete.templatesArray;
			else
				nameArray = autoComplete.templatesArray.filter(function(currentValue) { return currentValue.startsWith(term); });
			callback(nameArray); // List of possible completions ("names")
		},
		template: function (name) {
			var displayName = name;
			return displayName; // What to display in the list
		},
		replace: function (name) {
			var replacementString = "$1" + name;
			return replacementString; // What to replace the matched typed text with
		}
	};
	
	/** Strategy to autocomplete boilerplate */
	autoComplete.boilerplateMap = {};
	autoComplete.boilerplateNameArray = [];
	/* Get the template name list */
	var boilerplateListQuery = {
		"action": "query",
		"format": "json",
		"prop": "",
		"list": "allpages",
		"apnamespace": "450", //Boilerplate namespace
		"aplimit": "max"
	};
	killSwitchCount = 10;
	var boilerplateListQueryAjax = function() {
		api.get( boilerplateListQuery ).done( function ( data ) {
			killSwitchCount--;
			if (killSwitchCount < 0) return;
			//console.log( data.query.allpages );
			data.query.allpages.forEach(function(current) {
				autoComplete.boilerplateNameArray.push(current.title.slice("Boilerplate:".length));
			});
			if ("continue" in data) {
				boilerplateListQuery["continue"]   = data["continue"]["continue"];
				boilerplateListQuery.apcontinue = data["continue"].apcontinue;
				boilerplateListQueryAjax();
			} else {
				// Callback
				//console.log(autoComplete.boilerplateNameArray);
				autoComplete.boilerplateNameArray.forEach(function(boilerplateName, index){
					$.get( "https://zelda.gamepedia.com/Boilerplate:" + boilerplateName + "?action=raw", function( data ) {
						//console.log(data);
						autoComplete.boilerplateMap[boilerplateName] = 
							data.split("<boilerplate>\n")[1]
								.split("\n</boilerplate>")[0];
					});
				});
			}
		} );
	};
	boilerplateListQueryAjax();
	/* Create the boilerplate strategy */
	autoComplete.boilerplateStrategy = 
	{
		id: "boilerplate",
		match: /(\\boilerplate\:)([^#<>{}_\[\]\|]*)$/, // invalid article title characters https://www.mediawiki.org/wiki/Manual:Page_title#Invalid_page_titles
		search: function (term, callback) {
			console.log("term: " + term);
			var nameArray = Object.keys(autoComplete.boilerplateMap).filter(function(currentValue) { return currentValue.startsWith(term); });
			callback(nameArray); // List of possible completions ("names")
		},
		template: function (name) {
			var displayName = name;
			return displayName; // What to display in the list
		},
		replace: function (name) {
			var replacementString = autoComplete.boilerplateMap[name];
			return replacementString; // What to replace the matched typed text with
		}
	};
	
	/** Registering strategies */
	
	autoComplete.currentHeader = ""; // An attempt to generate a dropdown menu header to describe the parameter in some way, to accompany choices, or when there are no preset choices.
	
	autoComplete.getTextcompleteScript = function() {
		console.log( "Loading textcomplete..." );
		$.getScript( "https://unpkg.com/textcomplete/dist/textcomplete.min.js", function( data, textStatus, jqxhr ) {
			//console.log( [ data, textStatus, jqxhr.status ] ); // Data returned, Success, 200
			console.log( "Loaded textcomplete. (Warning: May not be executed yet)" );
			// Textarea object: https://github.com/yuku-t/textcomplete/issues/114#issuecomment-318352383
			Textarea = Textcomplete.editors.Textarea; // Global Variable
			autoComplete.registerStrategies();
		});
	};
	
	/* Note: The param arrays need not exist before strategy is registered */
	autoComplete.registerStrategies = function() {
		var editor = new Textarea(document.getElementById("wpTextbox1"))
		  , options = {
				dropdown: {
					maxCount: 5000,
					header: function() { return autoComplete.currentHeader; },
					style: { "margin-top": (-parseFloat($("body").css("margin-top")))+"px" }
				}
			}
		  , textcomplete = new Textcomplete(editor, options);
		
		/** Register strategies */ // The earier strategy in the list will appear if both match (both still run I think)
		strategies.push(autoComplete.boilerplateStrategy);
		strategies.push(autoComplete.templateListStrategy);
		strategies.push(autoComplete.createTemplateStrategy());
		textcomplete.register(strategies);
	};
	
	autoComplete.createTemplateStrategy = function() {
		console.log("/* createTemplateStrategy - start */");
		
		// Get text preceding text cursor, 
		// like https://github.com/yuku-t/textcomplete/blob/6f07195c47ace5e787cf7b46604b37a8bd5c6d30/src/textarea.js#L82
		var getBeforeCursor = function(/**textarea*/) { 
			textarea = document.getElementById("wpTextbox1");
			return textarea.selectionStart !== textarea.selectionEnd ? null : textarea.value.substring(0, textarea.selectionEnd);
		};
		
		var getLastTemplateOpenBracketPos = function(text) { // Assumes no "{{{" and "}}}" (perhaps ok even with these, anyway?), nor <nowiki> tags that would render "{{" or "}}" escaped/inactive,
			var count = 0
			  , index = text.length - 2
			  , foundBracketPair
			  , chars;
			while (index >= 0) {
				chars = [text.charAt(index), text.charAt(index+1)];
				///console.log(chars);
				foundBracketPair = false;
				if (chars[0] === '}' && chars[1] === '}') {
					count++;
					foundBracketPair = true;
				} else if (chars[0] === '{' && chars[1] === '{') {
					count--;
					foundBracketPair = true;
				}
				if (count < 0) return index;
				if (foundBracketPair) index -= 2; else index -= 1;
			}
			return -1;
		};
		
		var templateStrategy = 
		{
			id: "Templates",
			match: /(\|)([^\|]*)$/,
			search: function (term, callback) {
				//console.log("/* Template parameter choice search - start */");
				var text = getBeforeCursor();
				//console.log(" * Text before cursor: " + text);
				text = text.slice(0, text.length - term.length);
				//console.log(" * Text before term: " + text);
				var templateStartPos = getLastTemplateOpenBracketPos(text);
				var templateBody = text.slice(templateStartPos + "{{".length);
				//console.log(" * templateBody: " + templateBody);
				var templateName = templateBody.slice(0,templateBody.indexOf("|")); // Assumes stuff like {{Test|, not {{Test{{aTemplate}}|
				console.log(" * templateName: " + templateName);
				/* 
				 * Remove the templates [TODO: and tables] (there are no unmatched template brackets within this string?)
				 * - Loop a regex removal of {{..}} pairs, inside out
				 * - There should be no unmatched pairs due to having called 
				 *   getLastTemplateOpenBracketPos(text)
				 * - Use "?" in the regex to "lazy" match, or else it matches 
				 *   the biggest {{...}}, which renders the following method 
				 *   non-functional
				 * - The match will only have one "}}" at the end, due to ltr 
				 *   lazy regex search?
				 * - Then remove the rightmost/innermost {{...}} that includes 
				 *   this "}}", by reversing so the pair is on the left side of 
				 *   the string, lazy matching, then reversing back
				 */
				var templateBodyTrimmed = templateBody;
				while (templateBodyTrimmed != (templateBodyTrimmed = templateBodyTrimmed.replace(/\{\{[^]*?\}\}/g, function (match) {
						var matchInner = match.slice(2, match.length - 2); // Remove outer braces
						if (matchInner.includes("{{")) { 
							// Remove the last (inner) pair of "{{" and "}}"
							return match.split("").reverse().join("") // Reverse
									.replace(/\}\}[^]*?\{\{/, "") // Pairs become "}}...{{" when the string is reversed
									.split("").reverse().join(""); //Reverse back
						} else {
							return "";
						}
					})
				));
				
				// Count the number of "|" in text to determine which number 
				// parameter (e.g. "{{templateName|param1|param2|currentparam" )
				var paramNum = 0;
				for (var i=0; i<templateBodyTrimmed.length; i++) if (templateBodyTrimmed.charAt(i) == '|') paramNum++;
				console.log(" * paramNum: " + paramNum);
				var paramArray = [];
				if (templates[templateName] !== undefined) {
					if (typeof templates[templateName].getParams === "function") {
						paramArray = templates[templateName].getParams(paramNum);
					} else {
						if (templates[templateName][paramNum] !== undefined)
							paramArray = templates[templateName][paramNum];
					}
				}
				//console.log(" * paramArray: " + paramArray);
				autoComplete.currentHeader = templateName + " " + paramNum; // testing out header
				var nameArray = paramArray.filter(function(currentValue) { return currentValue.startsWith(term); });
				//console.log(" * nameArray: " + paramArray);
				//console.log("/* Template parameter choice search - end */");
				callback(nameArray); // List of possible completions ("names")
			},
			template: function (name) {
				var displayName = name;
				return displayName; // What to display in the list
			},
			replace: function (name) {
				var replacementString = "$1" + name;
				return replacementString; // What to replace the matched typed text with
			}
		};
		
		//console.log("/* createTemplateStrategy - end */");
		
		return templateStrategy;
		
	};
	
	/** Load Textcomplete then register the strategies */
	$(document).ready(autoComplete.getTextcompleteScript);
	
}
Advertisement