Jump to content

User:Gary/script installer source.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// ====================================================================================================
// ============================================== api.js ==============================================
// ====================================================================================================

/**
 * The callback response that retrieves a script's "associated script".
 */
associatedScriptCallback = function(response)
{
	if (!response['query'] || !response['query']['pageids'] || response['query']['pageids'][0] == -1) return false;
	markAssociatedScript(otherPage);
	
	// set cookie
	setConvertedCookie('associated-scripts', [ pageName, otherPage ]);
};

/**
 * Gets the edit token for the INSTALL_PAGE, then sends return data to another function for processing.
 */
beginScriptInstallation = function(pageToInstallTo, scriptName)
{
	var api = sajax_init_object();
	api.open('GET', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?format=json&action=query&prop=info&indexpageids=1&intoken=edit&titles=' + pageToInstallTo, true);
	// now that we used GET, get the token from the results
	api.onreadystatechange = function()
	{
		if (api.readyState == 4)
		{
			if (api.status == 200)
			{
				var response = eval('(' + api.responseText + ')');
				var tokenPage = response['query']['pages'][response['query']['pageids'][0]];
				sendScriptInstallation(pageToInstallTo, tokenPage['edittoken'], scriptName);
			}
			else
				errorMessage('EXT_TOK');
		}
	};
	api.send(null);
};

/**
 * Gets the backlinks to a page, which we later process and then call "installations".
 */
function getInstallations(callback, page, blcontinue, printToLog)
{
	wikiApi(callback, 'action=query&rawcontinue=&list=backlinks&bltitle=' + page + '&bllimit=500&blfilterredir=nonredirects&blnamespace=2' + (blcontinue ? '&blcontinue=' + blcontinue : ''), printToLog);
};

/**
 * Get the user's page where their scripts are installed to.
 */
getInstallPage = function(scriptName, skinPage, markInstalled, functionToRun)
{
	if (skinPage == scriptName)
	{
		// current script is the script where we install to
		var install = document.getElementById('install-this-script');
		var node = document.createElement('span');
		node.className = 'si-heading';
		node.id = 'install-this-script';
		node.innerHTML = '<a href="' + createLink(libraryPage) + '">Your scripts</a> are installed here';

		return install.parentNode.replaceChild(node, install);
	}
	
	var api = sajax_init_object();
	api.open('GET', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?format=json&action=query&prop=revisions&titles=' + skinPage + '&rvprop=content&indexpageids=1', true);
	api.onreadystatechange = function()
		{
			if (api.readyState == 4)
			{
				if (api.status == 200)
				{
					var response = eval('(' + api.responseText + ')');
					var pageToInstallTo = response['query']['pages'][response['query']['pageids'][0]];
					
					if (typeof pageToInstallTo['revisions'] == 'undefined')
					{
						errorMessage('NO_REV');
						return;
					}
					
					existingScriptContent = pageToInstallTo['revisions'][0]['*'];
					
					if (!functionToRun) 
						indicateScriptInstalled(scriptName, pageToInstallTo['title'], existingScriptContent, markInstalled);
					else 
						functionToRun(pageToInstallTo['title'], existingScriptContent);
				}
				else if (typeof(failedToConnectToAPI) == 'function')
					failedToConnectToAPI();
			}
		};
	api.send(null);
};

/**
 * The callback used for the Script Library to get all of a user's scripts.
 * 
 * Also, the callback for finding how many users have a script installed
 * with blcontinue (for when we reached the 500 max limit on results)
 */
scriptInstallationsCallback = function(response)
{
	if (!response['query'] || !response['query']['backlinks']) return;
	var userCount = countUsersFromJSON(response['query']['backlinks']).length;
	doScriptInstallation(userCount, (response['query-continue'] ? formatBlContinue(response['query-continue']['backlinks']['blcontinue']) : ''));
};

/**
 * With the token, use POST to submit a page edit.
 */
function sendScriptInstallation(pageToInstallTo, token, scriptName)
{
	// Script is already in script page
	if (findExistingScript(scriptName, existingScriptContent) && siSettings['checkIfScriptIsInstalled'])
	{
		alert('The script is already installed.');
		return;
	}
	else 
		indicateScriptIsInstalling();
	
	var api = sajax_init_object();
	var text = existingScriptContent + (existingScriptContent ? '\n' : '') + importScriptTypes[scriptType] + '(\'' + scriptName + '\'); // [[' + scriptName + ']]';
	
	// FIXME Indicate "Installing stylesheet" if that is the case.
	var editSummary = siEditSummary('Installing script [[' + scriptName + ']]');
	
	var parameters = 'action=edit&title=' + encodeURIComponent(pageToInstallTo) + '&text=' + encodeURIComponent(text) + '&token=' + encodeURIComponent(token) + '&summary=' + encodeURIComponent(editSummary) + '&format=json';
	api.open('POST', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php', true);
	
	// tell the user what has happened; either success or failure
	api.onreadystatechange = function()
		{
			if (api.readyState == 4)
			{
				if (api.status == 200) 
				{
					// TODO Update the Script Library cookie by adding this script.
					alert('The script "' + scriptName + '" was installed successfully to "' + pageToInstallTo + '".');
					location.reload(true);
				}
				else 
					errorMessage('AL_RES_STAT');
			}
		};
		
	api.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
	api.setRequestHeader('Connection', 'keep-alive');
	api.setRequestHeader('Content-length', parameters.length);
	api.send(parameters);
};

/**
 * Callback response for number of backlinks to a single script.
 */
singleScriptInstalls = function(response)
{
	if (!response['query'] || !response['query']['backlinks']) return;
	var userCount = countUsersFromJSON(response['query']['backlinks']).length;
	doSingleScriptInstalls(userCount, (response['query-continue'] ? formatBlContinue(response['query-continue']['backlinks']['blcontinue']) : ''));
};

/**
 * Wrapper for Wikipedia's API.
 */
function wikiApi(callback, parameters, printToLog)
{
	var url = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?callback=' + callback + '&format=json&' + parameters;
	importScriptURI(url);
	if (printToLog) siLog(url);
};

// ====================================================================================================
// ============================================ cookies.js ============================================
// ====================================================================================================

/**
 * Create a cookie.
 */
function createCookie(name, value, days)
{
	// override the "days" parameter (for now)
	var days = ageOfCookies;
	
	// convert 'value' hash to string
	if (value instanceof Array)
	{
		var newValues = [];
		
		for (var i = 0; i < value.length; i++)
		{
			if (value[i] instanceof Array)
				newValues.push(value[i].join(','));
		}
		
		var value = newValues.join(encodeURIComponent(';'));
	}
	
	if (days)
	{
		var date = new Date();
		date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
		var expires = '; expires=' + date.toGMTString();
	}
	else 
		var expires = '';

	// don't set the cookie if it has the same value as the one that already exists
	if (readCookie('si-' + name) == value) return false;
	
	var name = 'si-' + name;
	document.cookie = name + '=' + value + expires + '; path=/';
};

/**
 * Delete a cookie.
 */
function deleteCookie(name)
{
	createCookie(name, '', -1);
};

/**
 * Get a cookie, then convert it to a hash.
 * 
 * @param {string} name Name of cookie, without the preceding "si-".
 * @param {boolean} asArray Return an array instead.
 * @param {boolean} fromSetter True if retrieved from the cookie setter function;
 * 		necessary so that that function can still work properly when SI cookies are disabled.
 * @return {object} Returns the cookie laid out as an associative array.
 */
function getConvertedCookie(name, asArray, fromSetter)
{
	var cookieName = 'si-' + name;
	var cookie = readCookie(cookieName);
	if (!cookie || (!siSettings['enableSICookies'] && !fromSetter)) return [];

	var allowed = ['associated-scripts', 'installed-by', 'script-library'];
	
	if (!has(allowed, name)) return (asArray ? [] : {});
	
	var scripts = decodeURIComponent(cookie).split(';');
	if (asArray) var result = [];
	else var result = {};
	var length = 0;
	
	for (var i = 0; i < scripts.length; i++)
	{
		var script = scripts[i].split(',');
		
		if (name == 'script-library')
		{
			if (asArray) result.push([script[0], script[1], script[2]]);
			else result[script[0]] = [script[1], script[2]];
		}
		else
		{
			if (asArray) result.push([script[0], script[1]]);
			else result[script[0]] = script[1];
		}
		length++;
	}
	
	if (typeof(result) == 'object')
	{
		// include the length
		result['length'] = length;
	}
	
	return result;
};

/**
 * Reads a cookie.
 */
function readCookie(name)
{
	var nameEQ = name + '=';
	var ca = document.cookie.split(';');
	for(var i = 0; i < ca.length; i++)
	{
		var c = ca[i];
		while (c.charAt(0) == ' ') c = c.substring(1, c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
	}
	return null;
};

/**
 * Sets a converted cookie.
 * 
 * @param {string} name Name of cookie
 * @param {array} value The item to add to the existing cookie.
 */
function setConvertedCookie(name, value)
{
	// get the cookie first, only keep maximum 50 items in the cookie
	var convertedCookie = getConvertedCookie(name, true, true);
	var convertedCookieHash = getConvertedCookie(name, false, true);
	
	// FIXME Don't set this if old script library cookie is the same as the new one.
	// Set a cookie every time the Script Library loads
	if (name == 'script-library')
	{
		return createCookie(name, (convertedCookie.length > 0 ? convertedCookie : value));
	}
	
	// if new cookie already exists in existing convertedCookie
	// AND the values are the same, then skip this
	if (convertedCookieHash[value[0]] && convertedCookieHash[value[0]] == value[1]) return true;
	
	// if the new cookie already exists, then remove it first
	if (name == 'installed-by')
	{
		for (var i = 0; i < convertedCookie.length; i++)
		{
			if (convertedCookie[i][0] == value[0])
			{
				convertedCookie.splice(i, 1);
				break;
			}
		}
	}
	
	if (convertedCookie.length >= 50)
	{
		// remove the first item
		convertedCookie.shift();
		
		// add newest item at the end
		convertedCookie.push(value);
	}
	else
	{
		// add newest item at the end
		convertedCookie.push(value);
	}

	return createCookie(name, convertedCookie);
};

// ====================================================================================================
// ============================================= doers.js =============================================
// ====================================================================================================

addTitleTooltips = function()
{
	if (typeof(tooltipText) == 'undefined')
		return false;
	
	// apply TITLE attribute to the ID as well
	for (var name in tooltipText)
	{
		var id = document.getElementById('si-' + name);
		if (id) id.title = stripHTML(tooltipText[name].replace(/<br \/>/g, ' '));
	}
};

/**
 * Create the list of scripts installed for a user's Script Library
 */
function buildUserLibrary(title, content)
{
	var scripts = installedScripts(content);
	doUserLibrary(scripts);
	setConvertedCookie('script-library', scripts);
};

function countUsersFromJSON(users)
{
	// TODO +1 user if current user is not included. 
	// remove the forced = 1 from other functions then, but keep the link unlinking.
	var newUsers = [];
	for (var i = 0; i < users.length; i++)
	{
		// determine if the title ends in .js and is a skin page
		// strip .js first
		var title = users[i]['title'];
		var parts = title.split('/');
		if (parts.length != 2) continue;
		title = parts[1];
		if (!title.indexOf('.')) continue;
		title = title.substring(0, title.indexOf('.'));
		if (allSkinsHash[title]) newUsers.push(users[i]);
	}
	
	return newUsers;
};

/**
 * Used on the Script Library.
 * Executed directly when a cookie is found.
 * Otherwise, executed after contacting API and after scriptInstallationsCallback.
 * Determines which node to update with the help of the "callback-counter" node.
 *
 * @param {integer} userCount Number of users.
 * @param {object} blcontinue An object containing blcontinue information, if available.
 */
function doScriptInstallation(userCount, blcontinue)
{
	if (userCount == 0) userCount = 1;
	
	// get the current counter
	var counter = document.getElementById('callback-counter');
	var currentCount = counter.firstChild.nodeValue;
	
	// insert the number of installations
	var installs = document.getElementById('installation-' + currentCount);
	
	// check if "installs" exists; if not, then fall back to script counter
	if (!installs)
	{
		var scriptCounter = document.getElementById('si-script-counter');
		var scriptNode = document.getElementById(scriptCounter.removeChild(scriptCounter.firstChild).firstChild.nodeValue);
		currentCount = scriptNode.getElementsByClassName('si-script-counter')[0].firstChild.nodeValue;
		installs = document.getElementById('installation-' + currentCount);
	}
	else
	{
		// replace the counter
		counter.replaceChild(document.createTextNode(parseInt(currentCount) + 1), counter.firstChild);
	}
	
	var script = installs.parentNode.parentNode.id;
	
	// insert the new number of users
	var currentUsers = installs.firstChild.nodeValue;
	userCount = userCount + (isNaN(parseInt(currentUsers)) ? 0 : parseInt(currentUsers));
	var numberOfUsers = document.createTextNode(userCount);
	
	installs.replaceChild(numberOfUsers, installs.firstChild);

	// pluralize if necessary 
	var plural = document.getElementById('installation-plural-' + currentCount);
	if (userCount == 1) 
	{
		plural.replaceChild(document.createTextNode(' installation'), plural.firstChild);
		
		// unlink installation-link-i since we are forcing the number
		var span = convertIdToSpan('installation-link-' + currentCount);
		span.title = 'Only YOU have this script installed';
	}
	
	// connect to API again to get more backlinks on the same script
	if (blcontinue)
	{
		var scriptCounter = document.getElementById('si-script-counter');
		var newScriptCounter = scriptCounter.appendChild(document.createElement('div'));
		newScriptCounter.className = 'si-hidden';
		newScriptCounter.appendChild(document.createTextNode(wgFormattedNamespaces[blcontinue['namespace']] + ':' + blcontinue['title']));
		
		// get API
		getInstallations('scriptInstallationsCallback', wgFormattedNamespaces[blcontinue['namespace']] + ':' + blcontinue['title'], blcontinue['full']);
	}
	else
	{
		// update the cookie
		setConvertedCookie('installed-by', [script, userCount]);
	}
};

function doSingleScriptInstalls(numberOfUsers, blcontinue)
{
	// TODO Do some "faking" by adding 1 if the user has this script installed 
		// AND the number of installs is 0? Then remove the link to backlinks?
	var Xpeople = document.getElementById('installed-by-X-people');

	// delink the link to the script's backlinks
	if (numberOfUsers == 0)
		var span = convertIdToSpan('installed-by-link');
	
	// singularize "people" to "person" for 1 person
	if (numberOfUsers == 1)
		document.getElementById('people-plural').replaceChild(document.createTextNode(' person'), document.getElementById('people-plural').firstChild);
		
	// calculate number of users by adding it with existing number
	var currentNumberOfUsers = Xpeople.firstChild.nodeValue;
	numberOfUsers = numberOfUsers + (isNaN(parseInt(currentNumberOfUsers)) ? 0 : parseInt(currentNumberOfUsers));
		
	// insert the number of users
	Xpeople.replaceChild(document.createTextNode(numberOfUsers), Xpeople.firstChild);
	
	// get API
	if (blcontinue)
		getInstallations('singleScriptInstalls', wgFormattedNamespaces[blcontinue['namespace']] + ':' + blcontinue['title'], blcontinue['full']);
	// update the cookie
	else
		setConvertedCookie('installed-by', [(scriptData && scriptData['page'] ? scriptData['page'] : pageName ), numberOfUsers]);
};

function failedToConnectToAPI()
{
	// Mark "Checking script" as failed
	var installThisScript = document.getElementById('install-this-script');
	if (installThisScript)
		installThisScript.replaceChild(document.createTextNode('Failed to connect to the API'), installThisScript.firstChild);
};

/**
 * Formats blcontinue tokens
 */
function formatBlContinue(origBlcontinue)
{
	var blcontinue = origBlcontinue.split('|');
	return { 'namespace': blcontinue[0], 'title': blcontinue[1], 'id': blcontinue[2], 'full': origBlcontinue };
};

hideTooltip = function(tooltip)
{
	if (typeof timerIsRunning != 'undefined' && timerIsRunning === true) timerIsRunning = false;
	var tooltip = document.getElementById('tooltip-' + tooltip);
	if (tooltip) return tooltip.parentNode.removeChild(tooltip);
};

function indicateScriptInstalled(scriptName, pageToInstallTo, content, markInstalled)
{
	var install = document.getElementById('install-this-script');
	
	// current script is already installed
	// TODO This should use a cookie for checking if script is already installed or not.
	if (findExistingScript(scriptName, content) && markInstalled !== false)
	{
		var node = document.createElement('span');
		node.className = 'si-heading';
		node.id = 'install-this-script';
		node.innerHTML = 'You already <a href="' + createLink(libraryPage) + '">installed this ' + scriptType + '</a>';
	}
	// current script is not yet installed
	else
	{
		var node = document.createElement('a');
		node.className = 'si-heading';
		node.href = 'javascript:beginScriptInstallation(\'' + pageToInstallTo + '\', \'' + scriptName + '\');';
		node.id = 'install-this-script';
		node.title = 'This will install the script to [[' + pageToInstallTo + ']]';
		node.appendChild(document.createTextNode('Install this ' + scriptType));
	}
	
	install.parentNode.replaceChild(node, install);
};

function indicateScriptIsInstalling()
{
	// replace "Install this script" with "Installing..."
	var span = document.createElement('span');
	span.className = 'si-heading';
	span.id = 'install-this-script';
	
	// FIXME Indicate "Installing stylesheet" if that is the case.
	span.appendChild(document.createTextNode('Installing script...'));
	
	var install = document.getElementById('install-this-script');
	install.parentNode.replaceChild(span, install);
};

function markAssociatedScript(otherPage)
{
	var relatedPage = document.getElementById('si-related-page');
	var a = document.createElement('a');
	a.href = createLink(otherPage);
	a.id = 'si-related-page';
	a.appendChild(document.createTextNode('Related ' + oppScriptType));
	return relatedPage.parentNode.replaceChild(a, relatedPage);	
};

function parseScriptData()
{
	var scriptDataId = document.getElementById('script-data');
	if (!scriptDataId) return false;
	
	var data = {};
	for (var i = 0; i < documentationData.length; i++)
	{
		var docData = document.getElementById('script-data-' + documentationData[i]);
		if (docData && docData.firstChild && docData.firstChild.nodeValue) data[documentationData[i]] = docData.firstChild.nodeValue.trim();
	}	
	
	return data;
};

showTooltip = function(event, tooltip, extras)
{
	// if tooltip already exists due to a previous hover, then don't create it again
	if (document.getElementById('tooltip-' + tooltip)) 
	{
		hideTooltip(tooltip);
		return;
	}
	
	if (extras) extras = eval(extras);
	else extras = [];
	var coords = getMouseCoordinates(event);
	var text = tooltipText[tooltip];
	if (!text) text = '';
	
	// apply extras
	if (tooltip == 'verified')
	{
		if (extras[0] === true) text = text.replace('<b>Verified:</b>', '<b class="highlight">Verified:</b>');
		else if (extras[0] === false) text = text.replace('<b>Unverified:</b>', '<b class="highlight">Unverified:</b>');
	}
	
	var div = document.createElement('div');
	div.id = 'tooltip-' + tooltip;
	div.className = 'si-tooltip';
	div.style.left = (coords[0] + 5) + 'px';
	div.style.top = (coords[1] + 5) + 'px';
	
	// add the close button
	div.innerHTML = '<div class="si-close-tooltip" title="Close this box">[<a href="#" onclick="hideTooltip(\'' + tooltip + '\'); return false;">X</a>]</div>';
	div.innerHTML += text;
	
	body.appendChild(div);
};

function sortScripts(first, second)
{
	if (first[1]) var a = first[1];
	else var a = getBasePage(first[0]).capitalize();
	if (second[1]) var b = second[1];
	else var b = getBasePage(second[0]).capitalize();
	
	if (a < b) return -1;
	else if (a > b) return 1;
	else return 0;
};

// ====================================================================================================
// ============================================ general.js ============================================
// ====================================================================================================

function addClass(element, newClass)
{
	if (element.className)
	{
		var classes = element.className.split(' ');
		classes.push(newClass);
		return element.className = classes.join(' ');
	}
	else return element.className = newClass;	
};

String.prototype.capitalize = function(string)
{
	return this.replace(/(^|\s)([a-z])/g, function(m, p1, p2) { return p1 + p2.toUpperCase(); } );
};;

function checkURLParam(param)
{
	if (!document.location.search || document.location.search.length == 0) return false;
	var params = document.location.search.substr(1).split('&');
	for (var i = 0; i < params.length; i++)
	{
		var paramParts = params[i].split('=');
		if (paramParts[0] == param) return true;
	}
	return false;
};

function convertArrayToHash(array)
{
	var hash = {};
	for (var i = 0; i < array.length; i++) hash[array[i]] = true;
	return hash;
};

function convertIdToSpan(id)
{
	var node = document.getElementById(id);
	var span = document.createElement('span');
	span.id = id;
	var children = node.childNodes;
	
	for (var i = children.length - 1; i >= 0; i--)
		span.insertBefore(children[i], span.firstChild);
	
	node.parentNode.replaceChild(span, node);
	return span;
};

function createLink(title)
{
	var title = title.trim();
	if (title.indexOf('http://') == 0) return title;
	else return wgScript + '?title=' + encodeURIComponent(title);
};

function createLinkForBacklinks(script)
{
	return mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=Special:WhatLinksHere/' + script + '&namespace=2&hideredirs=1&hidetrans=1&limit=50';
};

function errorMessage(code)
{
	alert(standardErrorMessage + ' (' + code.toUpperCase() + ')');
};

function generateTooltip(name, extras)
{
	return '<span class="si-question-mark">(<a href="#" onclick="showTooltip(event, \'' + name + '\', \'' + (extras ? extras : '') + '\'); return false;">?</a>)</span>';
};

function has(haystack, needle)
{
	if (haystack instanceof Array)
	{
		for (var i = 0; i < haystack.length; i++)
		{
			if (haystack[i] == needle)
				return true;
		}
	}
	
	return false;
};

function hasClass(element, classToCheck)
{
	if (typeof(element) == 'undefined' || !element.className) return false;
	var classes = element.className.split(' ');
	for (var i = 0; i < classes.length; i++)
		if (classes[i] == classToCheck) return true;
	return false;	
};

function isANumber(number)
{
	return !isNaN(parseInt(number));
};

function isUnsafe()
{
	if (typeof(unsafeWindow) != 'undefined') return true;
	else return false;
};

function siLog(message, alert)
{
	if (typeof(console) != 'undefined' && alert !== true) console.log(message);
	else window.alert(message);
};

String.prototype.ltrim = function(stringToTrim)
{
	return this.replace(/^[\s|\n]+/, '');
};

function objectLength(obj) 
{
    var size = 0, key;
    for (key in obj) if (obj.hasOwnProperty(key)) size++;
    return size;
};

String.prototype.pluralize = function(count, plural)
{
	if (plural == null) var plural = this + 's';
	return (count == 1 ? this : plural)
};

function removeClass(element, oldClass)
{
	if (!element.className) return false;
	var classes = element.className.split(' ');
	var newClasses = [];
	for (var i = 0; i < classes.length; i++)
	{
		if (classes[i] != oldClass)
			newClasses.push(classes[i]);
	}
	
	return element.className = newClasses;	
};

function reverseHash(oldHash)
{
	var newHash = {};
	for (var key in oldHash) newHash[oldHash[key]] = key;
	return newHash;
};

String.prototype.rtrim = function(stringToTrim)
{
	return this.replace(/[\s|\n]+$/, '');
};

function sanitizeHTML(data)
{
	return data.replace(/</g, '&lt;').replace(/>/g, '&gt;');
};

function stripHTML(html)
{
	var tmp = document.createElement('div');
	tmp.innerHTML = html;
	return tmp.textContent||tmp.innerText;
};

String.prototype.trim = function(stringToTrim)
{
	return this.replace(/^[\s|\n]+|[\s|\n]+$/g, '');	
};

String.prototype.truncate = function(maxLength, truncateFromStart)
{
	if (this.length <= maxLength) return this;
	
	if (truncateFromStart)
		return '...' + this.substring(this.length - maxLength + 3, this.length);
	else
		return this.substring(0, maxLength - 3) + '...';
};

// ====================================================================================================
// ============================================ getters.js ============================================
// ====================================================================================================

function findAssociatedScript()
{
	var relatedPage = document.getElementById('si-related-page');
	if (!scriptData['page'] || !relatedPage) return false;
	
	var split = scriptData['page'].split('/');
	var last = split[split.length - 1];
	if (last.indexOf('.') == -1 || !linkExtensions[last.substring(last.indexOf('.') + 1, last.length)]) return false;
	otherPage = scriptData['page'].substring(0, scriptData['page'].indexOf('.', scriptData['page'].indexOf(last)) + 1) + reverseLinkExtensions[oppScriptType];
	
	var associatedScripts = getConvertedCookie('associated-scripts');
	if (associatedScripts[pageName]) markAssociatedScript(otherPage);
	else wikiApi('associatedScriptCallback', 'action=query&prop=info&indexpageids=1&titles=' + otherPage.replace(/ /g, '_'));
};

function findExistingScript(name, content)
{
	// FIXME This function doesn't actually find importScript; only the script name itself. Not necessarily a problem, though.
	var searchedText = ('\n' + content + '\n').replace(/\\n/g, '\n');
	if (searchedText.indexOf(name) == -1) return false; // didn't find the script
	else if (searchedText.search(new RegExp('\n(.*?)//(.*?)' + name)) != -1) // found it commented out
	{
		searchedText = searchedText.replace(new RegExp('\n(.*?)//(.*?)' + name + '(.*?)\n', 'g'), '\n$1\n'); // remove the commented out versions
		if (searchedText.search(new RegExp('\n(.*?)' + name)) == -1) return false; // we can no longer find it
		else return true; // we found it even though the commented out copies are removed, so it's definitely installed
	}
	else return true; // it's found and it's not commented out
};

function getBasePage(page)
{
	var pages = page.split('/');
	if (!pages[1]) return false;
	var hasExt = pages[pages.length - 1].indexOf('.');
	if (hasExt == -1) return false;
	return pages[pages.length - 1].substring(0, hasExt);
};

function getIsViewingSkin()
{
	var basePage = getBasePage(pageName);
	
	for (var i = 0; i < allSkins.length; i++)
		if (allSkins[i] == basePage) return true;
	
	return false;
};

function getMouseCoordinates(e)
{
	var posx = 0;
	var posy = 0;
	if (!e) var e = window.event;
	if (e.pageX || e.pageY) 	
	{
		posx = e.pageX;
		posy = e.pageY;
	}
	else if (e.clientX || e.clientY) 	
	{
		posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
		posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
	}
	
	return [posx, posy];
};

function getOppScriptType()
{
	if (scriptType == 'stylesheet') return 'script';
	else return 'stylesheet';
};

function getScriptMetadata(maxLength)
{
	// get comments classes
	var classes = {};
	classes['multi'] = content.getElementsByClassName('coMULTI');
	classes['single'] = content.getElementsByClassName('co1');
	var existingMatches = {};
	
	for (var commentClass in classes)
	{
		if (classes[commentClass].length > 100) var shorterLength = 100;
		else var shorterLength = classes[commentClass].length;
		
		for (var i = 0; i < shorterLength; i++)
		{
			var singleClass = classes[commentClass][i];
			var value = singleClass.firstChild.nodeValue;
			
			// find metadata in the element
			existingMatches = hasScriptMetadata(value, existingMatches, maxLength);
			if (existingMatches['done']) break;
		}
		if (existingMatches['done']) break;
	}
	
	delete existingMatches['done'];
	return existingMatches;
};

function getScriptType(data)
{
	if (data['type'])
	{
		for (var name in scriptClasses)
			if (scriptClasses[name] == data['type']) return data['type'];
	}
	var pre = document.getElementsByTagName('pre')[0];
	if (!pre) return false;
	var classes = pre.className.split(' ');
	var className = classes[0];
	return scriptClasses[className]
};

function hasAssociatedScript(findScript)
{
	var script = document.getElementById('mw-script-doc');
	if (!script) return false;
	var links = script.getElementsByTagName('a');
	if (links.length >= 3) return links[2].href;
	else if (isViewingSkin && links.length >= 2 && !hasClass(links[1], 'new')) return links[1].href;
	else return false;
};

function hasDocumentation()
{
	var docs = document.getElementById('mw-script-doc');
	if (!docs) return false;
	if (docs.getElementsByClassName('new').length > 0) return false;
	// get the documentation page
	else if (!isViewingSkin) return docs.getElementsByTagName('a')[1].href;
};

function hasScriptMetadata(string, existingMatches, maxLength)
{
	var string = ('\n' + string + '\n').replace(/\\n/g, '\n');
	
	for (var i = 0; i < singleScriptData.length; i++)
	{
		if (existingMatches[singleScriptData[i]]) continue;
		var matches = string.match(new RegExp('@' + singleScriptData[i] + '\\s+(.*?)\n'));
		if (matches) existingMatches[singleScriptData[i]] = sanitizeHTML(matches[1].trim());
		else continue;
		if (isANumber(maxLength)) existingMatches[singleScriptData[i]] = existingMatches[singleScriptData[i]].truncate(maxLength);
	}
	
	if (singleScriptData.length == objectLength(existingMatches)) existingMatches['done'] = true;
	return existingMatches;
};

function installedScripts(content)
{
	var searchedText = ('\n' + content + '\n').replace(/\\n/g, '\n');
	
	// remove comments with an importScript type
	for (var type in importScriptTypes) 
		searchedText = searchedText.replace(new RegExp('\n(.*?)//(.*?)' + importScriptTypes[type] + '(.*?)\n', 'g'), '\n$1\n');

	// collect importScript types, line by line
	var matches = [];
	var lines = searchedText.split('\n');
	
	for (var type in importScriptTypes)
	{
		if (type == 'external') continue;
		
		for (var i = 0; i < lines.length; i++)
		{
			var pattern = new RegExp('(' + importScriptTypes[type] + '\\s*\\(\\s*[\'|"].*?[\'|"]\\s*\\))');
			var match = lines[i].match(pattern);
			if (match)
			{
				match = match[1].replace(/.*['|"](.*?)['|"].*/, '$1');
				var verified = verifiedScripts[match];
				
				if (verified) matches.push([match, verified['name'], verified['documentation']]);
				else matches.push([match]);
			}
		}
	}
	
	return matches.sort(sortScripts);
};

function isSiInProduction()
{
	for (var setting in siSettings)
	{
		if (!siSettings[setting])
			return false;
	}
	
	return true;
};

// ====================================================================================================
// ============================================== init.js =============================================
// ====================================================================================================

/**
 * @fileoverview Initiating the script
 */

/*
	@name Installer or Script Installer or Easy Script Installer?
	@description Simplifies the installation and configuration process for user scripts.
*/

if (typeof(isUnsafe) == 'function' && isUnsafe())
{
	addPortletLink = unsafeWindow.addPortletLink;
	appendCSS = unsafeWindow.appendCSS;
	importScriptURI = unsafeWindow.importScriptURI;
	sajax_init_object = unsafeWindow.sajax_init_object;
	skin = unsafeWindow.skin;
	wgCanonicalNamespace = unsafeWindow.wgCanonicalNamespace;
	wgPageName = unsafeWindow.wgPageName;
	wgScript = unsafeWindow.wgScript;
	wgScriptPath = unsafeWindow.wgScriptPath;
	wgServer = unsafeWindow.wgServer;
	wgUserName = unsafeWindow.wgUserName;
	wgFormattedNamespaces = unsafeWindow.wgFormattedNamespaces;
};

if (typeof(ScriptInstaller) == 'undefined') ScriptInstaller = {};

/**
 * This is the only function loaded in ONLOAD.
 * This script is intended to work on two pages: script pages (ending in .js) and script documentation pages.
 * Primarily these two, anyway.
 */ 
function scriptInstaller()
{
	if (typeof(isSiInProduction) != 'function' || typeof(doDocumentationPage) != 'function')
		return false;
	
	/*
		Useful param disablers:
		
		?installations: does not retrieve number of installations for scripts
	*/
	
	// Settings. These all default to TRUE.
	// FIXME The following settings should be different when this script is in beta, and especially production mode.
	siSettings = {};
	siSettings['checkIfScriptIsInstalled'] = 1; // If false, don't check if a script is installed
	// FIXME When this is true, the installations for the Script Library are in the wrong order.
	siSettings['enableSICookies'] = 0; // If false, then cookies are disabled, so no cached data is retrieved. Data is still cached, however.
	siSettings['useRealLibraryLink'] = 1; // If false, links to the sandbox
	siSettings['useSkinPageForInstalls'] = 0; // If false, uses test page instead
	
	siIsInProduction = isSiInProduction();
	
	siScriptName = 'Script Installer'; // the current script's name
	maxLengthForScriptMetadata = 250; // maximum length for script metadata on single script pages
	ageOfCookies = 365; // age for cookies; set to 365, but in reality we still update cookies behind the scenes when they are different from newly acquired data
	
	// tooltip text
	tooltipText = 
	{
		'installed-by': 'Only users that install this script on a <a href="' + createLink('Wikipedia:Skin#Customisation (advanced users)') + '">personal JavaScript</a> page are counted (including yourself).',
		'metadata': '<b>Script metadata:</b> Information about the script that is provided by the script itself.', // Hence, <a href="' + createLink('Metadata') + '">metadata</a>.
		'number-of-scripts': '<b>Scripts installed here:</b> The number of scripts found on this page that are imported with importScript().<br /><br />Does not include scripts installed using importScriptURI(), which are typically scripts not located on the <a href="' + createLink('English Wikipedia') + '">English Wikipedia</a> and are therefore limited in the amount of information that can be obtained about them.',
		'personal-user-script': '<b>Personal user script:</b> A script that contains all the scripts installed by a given user. Installing this will install all scripts that the user has installed themselves.',
		'verified': '<b>Verified:</b> The script has been tested. It is known to work.<br /><b>Unverified:</b> The script has <em>not</em> been tested.'
	};
	
	// useful variables
	installPage = (siSettings['useSkinPageForInstalls'] || (!siSettings['useSkinPageForInstalls'] && wgUserName != 'Gary King') ? 'User:' + wgUserName + '/' + skin + '.js' : 'User:Gary King/scripts.js');
	
	// 'class used in PRE': 'what to call the current script\'s type'
	scriptClasses = { 'css': 'stylesheet', 'javascript': 'script' };
	importScriptTypes = { 'script': 'importScript', 'stylesheet': 'importStylesheet', 'external': 'importScriptURI' };
	
	body = document.getElementsByTagName('body')[0];
	content = document.getElementById('content');
	if (!content) return;
	
	// the following are used for highestBox, in this order
	jsfile = document.getElementById('jsfile');
	jsWarning = document.getElementById('jswarning');
	clearCache = document.getElementById('clearprefcache');
	
	standardErrorMessage = 'An error occurred. Please try again later.';
	pageName = wgPageName.replace(/_/g, ' ');
	documentationData = ['page', 'type'];
	scriptData = parseScriptData();
	scriptType = getScriptType(scriptData);
	oppScriptType = getOppScriptType();
	allSkins = ['chick', 'standard', 'cologneblue', 'modern', 'monobook', 'myskin', 'nostalgia', 'simple', 'vector'];
	allSkinsHash = convertArrayToHash(allSkins);
	isViewingSkin = getIsViewingSkin();
	libraryPage = (siSettings['useRealLibraryLink'] ? 'Wikipedia:Script Library' : 'User:Gary King/My Scripts');
	linkExtensions = {'css': 'stylesheet', 'js': 'script'};
	reverseLinkExtensions = reverseHash(linkExtensions);
	singleScriptData = ['name', 'description'];
	jumpToNav = document.getElementById('jump-to-nav');
	usersWithBetaAccess = [/*'Gary King', 'Gary Queen'*/];
	
	// only allow verified users to use this script, for now
	if (!siIsInProduction && usersWithBetaAccess.length > 0)
	{
		var hasAccess = checkIfUserCanAccessSI();
		if (!hasAccess)
		{
			if (wgUserName != null)
				alert('You do not have access to the \"' + siScriptName + '\" script while it is in beta mode.');
			return false;
		}
	}
	
	// TODO Disable this script with a config setting, for myself only so I can test this script in Greasemonkey while it still exists in my skin.js.
	
	// add settings, if different from default, to siteSub. Only on Script Library and single script pages, and only if still in testing mode.
	if (!siIsInProduction && (wgUserName == 'Gary King') && (pageName == libraryPage || (clearCache && scriptType))) addSISettingsToPage();
	
	// add portlet link for script library
	if (addPortletLink) mw.util.addPortletLink('p-tb', createLink(libraryPage), 'Script Library', 't-script-library', 'Go to the Script Library');
	
	// identify documentation pages
	if (wgCanonicalNamespace != '' && wgCanonicalNamespace != 'Template') doDocumentationPage();
	
	// replace imported scripts using importScript and importScriptURI with clickable links for easier access to them
	// this works on not only monobook.js, but on script documentation pages, etc. as well
	if (wgCanonicalNamespace != '') linksReplaced = replaceImportedScriptsWithLinks();

	// do script library stuff if viewing the library page
	if (pageName == libraryPage) doScriptLibrary();
	
	// we are viewing an actual script page
	if (clearCache && scriptType)
	{
		// a global variable; "currentScript' should eventually be replaced with 'pageName'
		currentScript = pageName;
		
		// now that we know we are viewing a script, add the install message box
		addInstallMessageBox(pageName);
	
		// check if script is already installed or not, and replace text appropriately
		getInstallPage(pageName, installPage, (siSettings['checkIfScriptIsInstalled'] ? true : false), false);
	}
};

if (typeof(isUnsafe) == 'function' && isUnsafe())
{
	unsafeWindow.addTitleTooltips = addTitleTooltips;
	unsafeWindow.associatedScriptCallback = associatedScriptCallback;
	unsafeWindow.beginScriptInstallation = beginScriptInstallation;
	unsafeWindow.hideTooltip = hideTooltip;
	unsafeWindow.scriptInstallationsCallback = scriptInstallationsCallback;
	unsafeWindow.sendScriptInstallation = sendScriptInstallation;
	unsafeWindow.showTooltip = showTooltip;
	unsafeWindow.singleScriptInstalls = singleScriptInstalls;
};

if (typeof(isUnsafe) == 'function' && typeof(isSiInProduction) == 'function'/* && typeof(verifiedScripts) != 'undefined'*/)
{
	// On wiki
	if (typeof(addOnloadHook) != 'undefined')
	{
		if (typeof(siSettings) == 'undefined')
		{
			addOnloadHook(scriptInstaller);
			addOnloadHook(addTitleTooltips);
		}
	}
	// Off wiki
	else
	{
		if (typeof(siSettings) == 'undefined')
		{
			scriptInstaller();
			addTitleTooltips();
		}
	}
}// ====================================================================================================
// ============================================= layout.js ============================================
// ====================================================================================================

/**
 * Add the install message box to a single script page
 */
function addInstallMessageBox(currentScript)
{
	var boxes = [jsfile, jsWarning, clearCache, jumpToNav.nextSibling];
	
	for (var i = 0; i < boxes.length; i++)
	{
		if (boxes[i]) 
		{
			var highestBox = boxes[i];
			break;
		}
	}
	
	if (!highestBox) return false;
	
	var div = document.createElement('div');
	div.id = 'si-message-box';
	
	var checking = document.createElement('span');
	checking.className = 'si-heading si-loading';
	checking.id = 'install-this-script';
	checking.appendChild(document.createTextNode('Checking if script is already installed...'));

	// if viewing a user's personal script, then indicate so
	var isViewingSkinNode = document.createElement('div');
	isViewingSkinNode.className = 'si-text';
	isViewingSkinNode.id = 'is-viewing-skin';
	if (isViewingSkin && currentScript != installPage) isViewingSkinNode.innerHTML = '<span id="si-careful">Careful!</span> This is a <a href="' + createLink('Wikipedia:Skin#Customisation (advanced users)') + '">personal user script</a> ' + generateTooltip('personal-user-script') + '.';
	
	// if there are scripts installed on this page, then indicate how many there are
	var replaced = document.createElement('div');
	replaced.id = 'si-number-of-scripts';
	
	// FIXME This says "X scripts installed here" even though they could be stylesheets installed here, too.
	if (typeof(linksReplaced) == 'object' && linksReplaced.length > 0)
		replaced.innerHTML = '<b>' + linksReplaced.length + ' ' + 'script'.pluralize(linksReplaced.length) + '</b> installed here ' + generateTooltip('number-of-scripts');
	
	// FIXME Don't show the following if we're viewing the user's skin.js
	var verified = document.createElement('div');
	verified.id = 'si-verified';
	verified.className = 'si-verification-status ' + (verifiedScripts[currentScript] ? 'si-verified' : 'si-not-verified');
	verified.innerHTML = (verifiedScripts[currentScript] ? 'Verified' : 'Unverified') + ' ' + generateTooltip('verified', '[verifiedScripts[currentScript] ? true : false]');
	
	var installedByCookie = getConvertedCookie('installed-by');
	if (installedByCookie[currentScript]) var numberOfInstallers = installedByCookie[currentScript];
	else var numberOfInstallers = '';
	
	// FIXME Don't show the following if we're viewing the user's skin.js
	var installedBy = document.createElement('div');
	installedBy.id = 'si-installed-by';
	installedBy.className = 'si-installed-by';
	installedBy.innerHTML = ' Installed by <a href="' + createLinkForBacklinks(currentScript) + '" id="installed-by-link"><span class="unknown" id="installed-by-X-people">?</span> <span id="people-plural" class="si-people">people</span></a> ' + generateTooltip('installed-by');
	
	// also insert other text, below the heading created above
	var text = document.createElement('div');
	text.id = 'script-description';
	text.className = 'si-text';

	// create menu
	var menuItems = [];

	// find documentation
	var documentation = hasDocumentation();
	if (documentation) menuItems.push('<a href="' + documentation + '" id="si-documentation">Documentation</a>');
	else if (!scriptData) menuItems.push('<span id="si-documentation">No documentation</span>');
	
	// find stylesheet
	var script = hasAssociatedScript();
	if (script) menuItems.push('<a href="' + script + '" id="si-related-page">Related ' + oppScriptType + '</a>');
	else menuItems.push('<span id="si-related-page">No ' + oppScriptType + '</span>');
	
	text.innerHTML = menuItems.join(' · ');
	
	// get the script's metadata
	var metadata = getScriptMetadata(maxLengthForScriptMetadata);
	var metadataArray = [];
	for (var data in metadata) metadataArray.push('<span class="si-metadata-name">' + data.capitalize() + '</span>: <span class="si-metadata-data">' + metadata[data] + '</span>');
	
	// add script metadata
	var metadata = document.createElement('div');
	metadata.id = 'si-metadata';
	if (metadataArray.length > 0) metadata.innerHTML = '<b>Script info ' + generateTooltip('metadata') + ':</b> ' + metadataArray.join(' · ');
	
	// order of the message box parts
	var parts = [checking, isViewingSkinNode, replaced, verified, installedBy, text, metadata];
	var partsWithNoFollowingBullet = [checking, isViewingSkinNode, text, metadata];
	
	for (var i = 0; i < parts.length; i++)
	{
		if (!parts[i].firstChild) continue;
		div.appendChild(parts[i]);
		
		var addBullet = true;
		for (var j = 0; j < partsWithNoFollowingBullet.length; j++)
		{
			var noBullets = partsWithNoFollowingBullet[j];
			if (noBullets == parts[i])
			{
				addBullet = false;
				break;
			}
		}
		
		if (addBullet) div.appendChild(document.createTextNode(' · '));
	}
	
	// insert the message box into the page
	highestBox.parentNode.insertBefore(div, highestBox);
	
	// get number of installations for this script
	if (numberOfInstallers) 
		doSingleScriptInstalls(numberOfInstallers);
	else 
		getInstallations('singleScriptInstalls', currentScript);
	
	// check if associated script exists via a callback, only for documentation pages
	findAssociatedScript();
};

/**
 * Adds the script's settings, if different from default, to siteSub
 */
function addSISettingsToPage()
{
	// Disabled settings in $siScriptName: ...
	var disabledSettings = [];
	
	for (var setting in siSettings)
	{
		if (!siSettings[setting])
			disabledSettings.push('<u>' + setting + '</u>');
	}
	
	var string = '<strong>Disabled settings</strong> in <em>' + siScriptName + '</em>: ' + disabledSettings.join(', ') + '. ';
	document.getElementById('siteSub').innerHTML = string + document.getElementById('siteSub').innerHTML + '.';
};

/**
 * Check if the user has access to this script while its in beta mode
 */
function checkIfUserCanAccessSI()
{
	for (var i = 0; i < usersWithBetaAccess.length; i++)
	{
		if (usersWithBetaAccess[i] == wgUserName)
			return true;
	}
	
	return false;
};

function doDocumentationPage()
{
	if (!scriptData) return false;
	
	addInstallMessageBox(scriptData['page']);
	getInstallPage(scriptData['page'], installPage, (siSettings['checkIfScriptIsInstalled'] ? true : false), false);
};

/**
 * Draw the Script Library.
 */
function doScriptLibrary()
{
	// we are viewing the script library page
	var myLib = document.getElementById('my-scripts');
	if (!myLib) return false;
	
	// heading
	var heading = document.createElement('h2');
	heading.className = 'installed-scripts-heading';
	heading.innerHTML = 'My Scripts';
	myLib.appendChild(heading);
	
	// descriptive text
	var text = document.createElement('span');
	text.className = 'installed-scripts-description';
	text.innerHTML = 'Your <span id="si-number-of-scripts">scripts</span> ' + generateTooltip('number-of-scripts') + ' are installed at <strong><a href="' + createLink(installPage) + '">' + installPage + '</a></strong>. <span id="si-verified">Script names in <strong>bold</strong> have been <strong>verified</strong> ' + generateTooltip('verified') + '.</span><br />' + tooltipText['installed-by'] + ' You have the following scripts installed:<br />';
	myLib.appendChild(text);
	
	// installed scripts
	var scripts = document.createElement('div');
	scripts.id = 'si-installed-scripts';
	
	var loading = document.createElement('span');
	loading.id = 'loading';
	loading.className = 'si-scripts-are-loading si-loading';
	loading.appendChild(document.createTextNode('Loading...'));
	
	scripts.appendChild(loading);
	myLib.appendChild(scripts);
	
	var scriptLibraryCookie = getConvertedCookie('script-library', true);
	
	// create the list of scripts
	if (scriptLibraryCookie.length > 0)
	// Script Library cookie already exists
	{
		doUserLibrary(scriptLibraryCookie);
		
		// FIXME Should be updating the cookie when the script library contents have changed.
			// Is it comparing the old list of scripts with the one existing in the cookie?
		var scripts = installedScripts(content);
		setConvertedCookie('script-library', scripts);
	}
	else
	// Script Library cookie does not exist, so create it.
	{
		getInstallPage(false, installPage, false, buildUserLibrary);
	}
	
	// script gallery
	var galleryHeading = document.createElement('h2');
	galleryHeading.className = 'si-script-gallery';
	galleryHeading.innerHTML = 'Script Gallery';
	myLib.appendChild(galleryHeading);
};

function doUserLibrary(scripts)
{
	var installed = document.getElementById('si-installed-scripts');
	if (!installed) return;
	
	// remove loading text
	installed.removeChild(document.getElementById('loading'));
	var numberOfScriptsInstalled = scripts.length;
	
	var counter = document.createElement('span');
	counter.className = 'si-hidden';
	counter.id = 'callback-counter';
	counter.appendChild(document.createTextNode(0));
	installed.appendChild(counter);
	
	// create counter with a script name
	var scriptCounter = document.createElement('span');
	scriptCounter.id = 'si-script-counter';
	scriptCounter.className = 'si-hidden';
	installed.appendChild(scriptCounter);
	
	for (var i = 0; i < scripts.length; i++)
	{
		var script = scripts[i][0];
		var scriptName = scripts[i][1];
		var docs = scripts[i][2] ? scripts[i][2] : '';
		var node = document.createElement('div');
		node.className = 'script-library-item';
		node.id = script.replace(/ /g, '_');
		
		// truncate the script name from the beginning if it's too long
		var scriptLink = '<a href="' + createLink(script) + '">' + (scriptName ? scriptName : script).truncate(50, true) + '</a>';
		
		// bold the script's name if it's verified
		if (scriptName) scriptLink = '<strong>' + scriptLink + '</strong>';
		
		if (docs) var documentation = '(<a class="si-documentation-link" href="' + createLink(docs) + '">documentation</a>)';
		else var documentation = '';
		
		var installations = '<a class="script-installations" href="' + createLinkForBacklinks(script) + '" id="installation-link-' + i + '"><span id="installation-' + i + '">?</span> <span id="installation-plural-' + i + '">installations</span></a>';
		
		node.innerHTML = '<span class="script-name">' + scriptLink + ' ' + documentation + '</span>' + installations + '<span class="si-hidden si-script-name" id="si-script-name-' + i + '">' + script + '</span><span class="si-script-counter si-hidden">' + i + '</span>';
		installed.appendChild(node);
		
		// get number of installations
		if (checkURLParam('installations')) continue;
		
		var installedByCookie = getConvertedCookie('installed-by');
		if (installedByCookie[script]) doScriptInstallation(installedByCookie[script]);
		else getInstallations('scriptInstallationsCallback', script);
	}
	
	var numberOfScripts = document.getElementById('si-number-of-scripts');
	numberOfScripts.replaceChild(document.createTextNode(numberOfScriptsInstalled + ' scripts'), numberOfScripts.firstChild);
};

function replaceImportedScriptsWithLinks()
{
	// find .js links in code
	var scriptLinks = content.getElementsByClassName('st0');
	var replacedLinks = [];
	
	for (var i = 0; i < (scriptLinks.length > 250 ? 250 : scriptLinks.length); i++)
	{
		// trim the script text to get just the actual name
		var sL = scriptLinks[i];
		if (sL.childNodes.length > 1 || !sL.firstChild.nodeValue) continue;
		var nodeValue = sL.firstChild.nodeValue;
		var open = nodeValue.trim().replace(/^(['|"]).*/g, '$1').trim();
		var close = nodeValue.trim().replace(/^['|"].*(['|"])/g, '$1').trim();
		var title = nodeValue.trim().replace(/^['|"]|['|"]$/g, '').trim();

		// check if this is an imported script
		var prevSibling = sL.previousSibling.previousSibling;
		if (prevSibling && prevSibling.nodeValue) var functionName = prevSibling.nodeValue.trim();
		else var functionName = '';
		
		var isLegit = false;
		for (var importType in importScriptTypes)
		{
			if (importType == 'external') continue;
			if (functionName == importScriptTypes[importType])
				isLegit = true;
		}
		if (!isLegit) continue;
		
		// create the link to replace the existing text with
		var a = document.createElement('a');
		a.href = createLink(title);
		a.appendChild(document.createTextNode(title));
		
		var span = document.createElement('span');
		span.appendChild(document.createTextNode(open));
		span.appendChild(a);
		span.appendChild(document.createTextNode(close));
		
		sL.replaceChild(span, sL.firstChild);
		
		replacedLinks.push(title);
	}
	
	return replacedLinks;
};

// ====================================================================================================
// ============================================== misc.js =============================================
// ====================================================================================================

/**
 * Create an edit summary
 */
function siEditSummary(text)
{
	return text + ' with [[WP:Script Installer]]';
};