MediaWiki:RecentChangesMultiple/code.js

//  - [ 0 / "+_itemsToAddTotal+" Recent Changes added] ";			_resultsNode.innerHTML = "";			gApp._rcmChunk(0, 99, 99);		}		MyApp.prototype._rcmChunk = function(pIndex, pLastDay, pLastMonth) {			var date = _recentChangesEntries[pIndex].date;			if(date.getUTCDate != pLastDay || date.getUTCMonth != pLastMonth) {				pLastDay = date.getUTCDate;				pLastMonth = date.getUTCMonth;				_resultsNode.innerHTML += " "+date.getUTCDate+" "+MONTH_DICT[date.getUTCMonth]+" "+date.getUTCFullYear+" ";			}			// Add to page			_resultsNode.appendChild(_recentChangesEntries[pIndex].toHTML);			if(_recentChangesEntries[pIndex].class_name == RecentChangeList.CLASS_NAME) {				_itemsAdded += _recentChangesEntries[pIndex].list.length;			} else {				_itemsAdded++;			}			if(++pIndex < _recentChangesEntries.length) { document.querySelector("#rcm-content-loading-num").innerHTML = _itemsAdded; setTimeout(function{ gApp._rcmChunk(pIndex, pLastDay, pLastMonth); }); } else { this._finishScript; } }		MyApp.prototype._finishScript = function { gUtils.removeElement(document.querySelector("#rcm-content-loading")); this._addRefreshButtonTo(_statusNode); $( "#rc-content-multiple .mw-collapsible" ).each(function{ $(this).makeCollapsible; }); (window.ajaxCallAgain || []).forEach(function(cb){ cb; }); }		//######################################		// Specific Helper Methods //######################################		MyApp.prototype._addRefreshButtonTo = function(pParent) { pParent.innerHTML += " "; gUtils.newElement("button", { innerHTML:"Refresh" }, pParent).addEventListener("click", function tHandler(e){				e.target.removeEventListener("click", tHandler);				gApp._refresh;			}); }		MyApp.prototype._getWikiFavicon = function(pWikiInfo) { var tFavicon = {url:"", image:""}; if(pWikiInfo.favicon) { if(pWikiInfo.favicon.indexOf(".") > -1) { tFavicon.url = "//"+pWikiInfo.favicon; } else { tFavicon.url = "http://vignette3.wikia.nocookie.net/"+pWikiInfo.favicon+"/images/6/64/Favicon.ico" }			} else if(pWikiInfo.url.indexOf(".wikia.") > -1 && pWikiInfo.url.split(".wikia.")[0].indexOf(".") <= -1) { tFavicon.url = "http://vignette3.wikia.nocookie.net/"+pWikiInfo.url.split(".wikia.")[0]+"/images/6/64/Favicon.ico" } else { tFavicon.url = FAVICON_BASE+pWikiInfo.url; }			tFavicon.image = gUtils.formatString(tFavicon.image, tFavicon.url); return tFavicon; }		return new MyApp; });	//######################################	// Specific Helper Methods	//######################################	gHelper = {};	// Returns if privilege "exists"	gHelper.checkPrivilege = function(pPrivilege, pRC) {		return true;	}	gHelper.sizeDiffText = function(pSizeDiff, pNoSummary) {		var html = "({1}{2}) ";		if(pSizeDiff > 0) {			html = gUtils.formatString(html, "mw-plusminus-pos", "+", pSizeDiff);		} else if(pSizeDiff < 0) {			html = gUtils.formatString(html, "mw-plusminus-neg", "", pSizeDiff);		} else {			var val = (pNoSummary ? "N/A " : pSizeDiff);			html = gUtils.formatString(html, "mw-plusminus-null", "", val);		}		return html;	}	//######################################	// Objects	//######################################	// Base class that doesn't want to work.	/*var RecentChangeBase = (function {		function RecentChangeBase { }		RecentChangeBase.prototype.toHTML = function { console.log("Override me!"); return gUtils.newElement("table"); }		RecentChangeBase.prototype.dispose = function { console.log("Override me!"); }		// Return if this wiki is marked with this privelege. RecentChangeBase.prototype.checkPrivilege = function(pPrivilege) { return true; }		// Get the text for the size diff RecentChangeBase.prototype.sizeDiffText = function(pSizeDiff, pNoSummary) { var html = "({1}{2}) "; if(pSizeDiff > 0) { html = gUtils.formatString(html, "mw-plusminus-pos", "+", pSizeDiff); } else if(pSizeDiff < 0) { html = gUtils.formatString(html, "mw-plusminus-neg", "", pSizeDiff); } else { var val = (pNoSummary ? "N/A " : pSizeDiff); html = gUtils.formatString(html, "mw-plusminus-null", "", val); }			return html; }		return RecentChangeBase; });*/	// ### RecentChange ### //	// Holds all info about a single change	var RecentChange = (function {		function RecentChange(pEntry, pDate, pWikiInfo, pFavicon) { this.class_name = RecentChange.CLASS_NAME; this.entry = pEntry; this.date = pDate; this.wikiInfo = pWikiInfo; this.href = "http://"+this.wikiInfo.url+this.wikiInfo.subdir; // link up to the point of the page name this.hrefNormal = this.entry.link.href.split("diff=")[0]; // Includes page name // can't split on ?diff= / &diff=, since some wikis that use the /dir/ format still return strinks in the index.php?title= format this.favicon = pFavicon; this.summaryDiffHTML = gUtils.newElement("div", { innerHTML:this.entry.summary.content }); gUtils.forEach(this.summaryDiffHTML.querySelector("p").querySelectorAll("a"), function(elem){ elem.href = "http://"+pWikiInfo.url+elem.getAttribute("href"); }); this.summaryText = this.summaryDiffHTML.querySelector("p").innerHTML; this.noSummary = false; // Check if the entry is part of a log this.logType = RecentChange.LOG_TYPE.NONE; // Default Object.keys(LOG_DETECTION_TEXT).some(function (key) {				if(gUtils.stringContainsAtLeastOneSubstringInArray(this.summaryText, LOG_DETECTION_TEXT[key])) {					this.logType = RecentChange.LOG_TYPE[key];					return true;				}				return false;			}, this); this.pageName = this.entry.title; // Page name including namespace this.uniqueID = this.pageName; // A unique ID is primarily important for "comments", since the atom feed doesn't display specific title. // Find out the type of change this is			if(this.logType != RecentChange.LOG_TYPE.NONE) { this.type = RecentChange.TYPE.LOG; }			else if(this.entry.title.indexOf("/@comment") > -1) { this.type = RecentChange.TYPE.COMMENT; // A "comment" can be a article comment, message wall post, or forum post. this.pageName = this.entry.title.split("/@comment")[0]; this.uniqueID = this.pageName+"/@comment"+this.entry.title.split("/@comment")[1]; // "Comments" can have 2 /@comments, the first one is what we care about. }			else { this.type = RecentChange.TYPE.NORMAL; }			this.isNewPage = gUtils.stringContainsAtLeastOneSubstringInArray(this.summaryText, NEW_PAGE_TEXT); this.sizeDiff = this.calcSizeDiff; }		//RecentChange.prototype = Object.create(RecentChangeBase.prototype); //## Constants ## RecentChange.CLASS_NAME = "RecentChange"; var SEP = " . . "; RecentChange.TYPE = Object.freeze({NORMAL:"normalChange", COMMENT:"commentType", LOG:"logChange"}); RecentChange.LOG_TYPE = Object.freeze({			NONE:"notALog",			BLOCK:"blockedUser",			CHAT_BAN:"chatBan",			DELETION:"deletedPage",			IMPORT:"importPage",			MERGE:"mergePage",			MOVED:"pageMoved",			// PATROL not used			PROTECTION:"pageProtectionChanged",			UPLOAD:"uploadedFile",			USER_AVATAR:"userAvatar",			USER_CREATION:"userCreated",			USER_RENAME:"userRenamed",			USER_RIGHTS:"userRightsChanged",			WIKI_FEATURES:"wikiFeatures"		}); //## Methods ## RecentChange.prototype.toHTML = function { var html = ""; switch(this.type) { case RecentChange.TYPE.LOG: { switch(this.logType) { default: { html += this.logTypeText; html += SEP; html += this.summaryWithUserDetails; }					}					break; }				case RecentChange.TYPE.COMMENT: case RecentChange.TYPE.NORMAL: default: { html += this._pageTitleText; html += SEP; html += gHelper.sizeDiffText(this.sizeDiff, this.noSummary); html += SEP; html += this.userDetails; if(this.summaryText != "") { html += " ("+this.summaryText+")"; } break; }			}			var tTable = gUtils.newElement("table", { className:"mw-enhanced-rc" }); gUtils.newElement("caption", {}, tTable).style.cssText = "background-image:url("+this.favicon.url+")"; var tRow = gUtils.newElement("tr", {}, tTable); gUtils.newElement("td", { innerHTML:this.favicon.image }, tRow); gUtils.newElement("td", { className:"mw-enhanced-rc", innerHTML:""				+''				+(this.isNewPage ? 'N ' : ' ')				+" "				+" "				+" "				+" "				+this.time				+" "			}, tRow); gUtils.newElement("td", { innerHTML:html }, tRow); return tTable; };		RecentChange.prototype.toHTMLRow = function { var html = ""; switch(this.type) { case RecentChange.TYPE.NORMAL: { if(this.entry.link.href.indexOf("diff=") <= -1) { //console.log(this.entry.link.href); //console.log(this.summaryText); console.log("Issue displaying "+this.pageName+"; most likely an undetect change type, due to something uncommon / uncheck case in other language."); html += " "+this.time+" " html += SEP; html += this.userDetails; if(this.summaryText != "") { html += " ("+this.summaryText+")"; } break; }					var hrefParamas = (this.entry.link.href.split("diff=")[1]).split("&"); var diffNum = hrefParamas[0]; var oldNum = hrefParamas[1].replace("oldid=",""); html += " "+this.time+" " html += " (cur";					if(!this.isNewPage) { html += " | prev"; }					html += ")"; html += SEP; html += gHelper.sizeDiffText(this.sizeDiff, this.noSummary); html += SEP; html += this.userDetails; if(this.summaryText != "") { html += " ("+this.summaryText+")"; } break; }				case RecentChange.TYPE.COMMENT: { if(this.entry.link.href.indexOf("diff=") > -1) { var hrefParamas = (this.entry.link.href.split("diff=")[1]).split("&"); var diffNum = hrefParamas[0]; var oldNum = hrefParamas[1].replace("oldid=",""); html += " "+this.time+" " html += " (<a href='"+this.hrefNormal+"diff=0&oldid="+diffNum+"'>cur</a>";						if(!this.isNewPage) { html += " | <a href='"+this.entry.link.href+"'>prev</a>"; }						html += ")"; html += SEP; html += gHelper.sizeDiffText(this.sizeDiff, this.noSummary); html += SEP; html += this.userDetails; if(this.summaryText != "") { html += " ("+this.summaryText+")"; } } else { // If no diff=, then it's something like "restored / removed reply" on a board thread. html += " "+this.time+" " html += SEP; html += this.summaryWithUserDetails; }					break; }				case RecentChange.TYPE.LOG: { html += " "+this.time+" " html += SEP; html += this.summaryWithUserDetails; break; }			}			var tRow = gUtils.newElement("tr", {}); gUtils.newElement("td", {}, tRow); // Blank spot for where favicon would be on a normal table gUtils.newElement("td", {}, tRow); // Blank spot for where collapsing arrow would be on the table gUtils.newElement("td", { className:"mw-enhanced-rc", innerHTML:""				+(this.isNewPage ? '<abbr class="newpage" title="This edit created a new page">N ' : ' ')				+" "				+" "				+" "				+" "			}, tRow); gUtils.newElement("td", { className:"mw-enhanced-rc-nested", innerHTML:html }, tRow); return tRow; }		RecentChange.prototype.time = function { return gUtils.pad(this.date.getUTCHours,2)+":"+gUtils.pad(this.date.getUTCMinutes,2); };		RecentChange.prototype.calcSizeDiff = function { if(this.type == RecentChange.TYPE.LOG) { return 0; } var charDiff = 0; if(this.summaryDiffHTML.querySelector("table.diff") != undefined) { var charCountOld = 0, charCountNew = 0; var changes = this.summaryDiffHTML.querySelectorAll(".diff-marker"); gUtils.forEach(changes, function(diffMarker){					var myDiff = diffMarker.nextElementSibling;					var myDiffChanges = myDiff.querySelectorAll(".diffchange");					if(diffMarker.innerHTML == " ") { return; }					else if(diffMarker.innerHTML == "−") {						if(myDiffChanges.length > 0) {							gUtils.forEach(myDiffChanges, function(diff){ charCountOld += diff.textContent.length; });						} else {							var diff = myDiff.querySelector("div");							if(diff == null) return;							if(myDiff.nextElementSibling.className != "diff-empty") return; // nothing changed on this line, ignore							charCountOld += diff.textContent.length;						}					}					else if(diffMarker.innerHTML == "+") {						if(diffMarker.previousElementSibling.className == "diff-empty") charCountNew++; // New line, add extra character						if(myDiffChanges.length > 0) {							gUtils.forEach(myDiffChanges, function(diff){ charCountNew += diff.textContent.length; });						} else {							var diff = myDiff.querySelector("div");							if(diff == null) return;							if(diffMarker.previousElementSibling.className != "diff-empty") return; // nothing changed on this line, ignore							charCountNew += diff.textContent.length;						}					}				}); charDiff = charCountNew - charCountOld; } else { var diff; //console.log(diff); if((diff = this.summaryDiffHTML.querySelector("div")) != null) { charDiff = diff.textContent.length; } else if((diff = this.summaryDiffHTML.querySelector("span[dir=auto]")) != null) { //console.log(this.entry.title); //console.log(this.summaryDiffHTML); this.noSummary = true; return 0; // } else { //console.log(this.entry.title); //console.log(this.summaryDiffHTML); return 0; // None of the above; possibly in error, but may be a "valid" reason. }			}			return charDiff; }		RecentChange.prototype._pageTitleText = function { var titleLink = "<a href='{0}'>{1}</a>"; if(this.type == RecentChange.TYPE.COMMENT && this.isNewPage) { titleLink = "Comment ("+titleLink+")"; } var diffLink = "diff"; if(!this.isNewPage) { diffLink = "<a href='{2}'>"+diffLink+"</a>"; } return gUtils.formatString(titleLink+" ("+diffLink+" | <a href='{0}action=history'>hist</a>)", this.hrefNormal, this.pageName, this.entry.link.href); }		RecentChange.prototype.userDetails = function { var username = this.entry.author.name; var blockText = gHelper.checkPrivilege("admin", this) ? " | <a href='{0}Special:Block/{1}'>block</a>" : ""; if(!IP_REGEX.test(username)) { return gUtils.formatString("<a href='{0}User:{1}'>{1}</a> (<a href='{0}User_talk:{1}'>wall</a> | <a href='{0}Special:Contributions/{1}'>contribs</a>"+blockText+")", this.href, username); } else { return gUtils.formatString("<a href='{0}Special:Contributions/{1}'>{1}</a> (<a href='{0}User_talk:{1}'>wall</a>"+blockText+")", this.href, username); }		}		// Meant for summaries that START with a username link RecentChange.prototype.summaryWithUserDetails = function { return this.userDetails + this.summaryText.slice(this.summaryText.indexOf("</a>")+4); // summary, plus everything after the username link }		RecentChange.prototype.logTypeText = function { var logTemplate = "(<a href='"+this.href+"Special:Log/{0}'>{1} log</a>)"; switch(this.logType) { case RecentChange.LOG_TYPE.BLOCK			:{ return gUtils.formatString(logTemplate, "block", "Block"); } case RecentChange.LOG_TYPE.CHAT_BAN			:{ return gUtils.formatString(logTemplate, "chatban", "chat ban"); } case RecentChange.LOG_TYPE.DELETION			:{ return gUtils.formatString(logTemplate, "delete", "Deletion"); } case RecentChange.LOG_TYPE.IMPORT			:{ return gUtils.formatString(logTemplate, "import", "Import"); } case RecentChange.LOG_TYPE.MERGE			:{ return gUtils.formatString(logTemplate, "merge", "Merge"); } case RecentChange.LOG_TYPE.MOVED			:{ return gUtils.formatString(logTemplate, "move", "Move"); } case RecentChange.LOG_TYPE.PROTECTION		:{ return gUtils.formatString(logTemplate, "protect", "Protection"); } case RecentChange.LOG_TYPE.UPLOAD			:{ return gUtils.formatString(logTemplate, "upload", "Upload"); } case RecentChange.LOG_TYPE.USER_AVATAR		:{ return gUtils.formatString(logTemplate, "useravatar", "User avatar"); } case RecentChange.LOG_TYPE.USER_CREATION	:{ return gUtils.formatString(logTemplate, "newusers", "User creation"); } case RecentChange.LOG_TYPE.USER_RENAME		:{ return gUtils.formatString(logTemplate, "renameuser", "User rename"); } case RecentChange.LOG_TYPE.USER_RIGHTS		:{ return gUtils.formatString(logTemplate, "rights", "User rights"); } case RecentChange.LOG_TYPE.WIKI_FEATURES	:{ return gUtils.formatString(logTemplate, "wikifeatures", "Wiki Features"); } case RecentChange.LOG_TYPE.NONE: default: { // nothing }			}			return ""; }		RecentChange.prototype.shouldGroupWith = function(pRC) { if(this.wikiInfo.url == pRC.wikiInfo.url && this.type == pRC.type && this.date.getUTCMonth == pRC.date.getUTCMonth && this.date.getUTCDate == pRC.date.getUTCDate) { switch(this.type) { case RecentChange.TYPE.NORMAL: { if(this.uniqueID == pRC.uniqueID) { return true; } break; }					case RecentChange.TYPE.COMMENT: { if(this.uniqueID == pRC.uniqueID) { return true; } break; }					case RecentChange.TYPE.LOG: { if(this.logType == pRC.logType) { return true; } break; }				}			}			return false; }		RecentChange.prototype.dispose = function { this.entry = null; this.date = null; this.wikiInfo = null; this.summaryDiffHTML = null; }		return RecentChange; });	// ### RecentChangeList ### //	// Holds grouped changes, be they logs or same-page edits.	var RecentChangeList = (function {		function RecentChangeList(pRC1, pRC2) { this.class_name = RecentChangeList.CLASS_NAME; this.list = [pRC1, pRC2]; Object.defineProperty(this, "newest", { get: function { return this.list[0]; }, enumerable: true }); Object.defineProperty(this, "oldest", { get: function { return this.list[this.list.length-1]; }, enumerable: true }); Object.defineProperty(this, "date", { get: function { return this.newest.date; }, enumerable: true }); Object.defineProperty(this, "uniqueID", { get: function { return this.newest.uniqueID; }, enumerable: true }); Object.defineProperty(this, "wikiInfo", { get: function { return this.newest.wikiInfo; }, enumerable: true }); Object.defineProperty(this, "logType", { get: function { return this.newest.logType; }, enumerable: true }); Object.defineProperty(this, "type", { get: function { return this.newest.type; }, enumerable: true }); }		//RecentChangeList.prototype = Object.create(RecentChangeBase.prototype); //## Constants ## RecentChangeList.CLASS_NAME = "RecentChangeList"; var SEP = " . . "; //## Methods ## RecentChangeList.prototype.toHTML = function { var html = ""; switch(this.type) { case RecentChange.TYPE.LOG: { html += this.newest.logTypeText; break; } case RecentChange.TYPE.NORMAL: case RecentChange.TYPE.COMMENT: default: { //html += this.list[0]._pageTitleText; html += "<a href='"+this.newest.hrefNormal+"'>"+this.newest.pageName+"</a>"; html += " ("+this.changesText+" | <a href='"+this.newest.hrefNormal+"action=history'>hist</a>)"; html += SEP html += gHelper.sizeDiffText(this.totalDiff, false); html += SEP; break; }			}			html += SEP; html += this._contributorsCountText; var tTable = gUtils.newElement("table", { className:"mw-collapsible mw-enhanced-rc mw-collapsed" }); // mw-made-collapsible gUtils.newElement("caption", {}, tTable).style.cssText = "background-image:url("+this.newest.favicon.url+")"; var tRow = gUtils.newElement("tr", {}, tTable); gUtils.newElement("td", { innerHTML:this.newest.favicon.image }, tRow); var td1 = gUtils.newElement("td", {}, tRow); gUtils.newElement("span", { className:"mw-collapsible-toggle", innerHTML:' <a title="Show details (requires JavaScript)" href="#"><img width="12" height="12" title="Show details (requires JavaScript)" alt="+" src="http://slot1.images.wikia.nocookie.net/__cb1422546004/common/skins/common/images/Arr_r.png"></a> <a title="Hide details" href="#"><img width="12" height="12" title="Hide details" alt="-" src="http://slot1.images.wikia.nocookie.net/__cb1422546004/common/skins/common/images/Arr_d.png"></a> ' }, td1); gUtils.newElement("td", { className:"mw-enhanced-rc", innerHTML:""				+(this.oldest.isNewPage ? '<abbr class="newpage" title="This edit created a new page">N ' : ' ')				+" "				+" "				+" "				+" "				+this.newest.time				+" "			}, tRow); gUtils.newElement("td", { innerHTML:html }, tRow); this.list.forEach(function(rc){				tTable.appendChild(rc.toHTMLRow);			}); return tTable; }		RecentChangeList.prototype._contributorsCountText = function { var contribs = {}, indx; this.list.forEach(function(rc){				if(contribs.hasOwnProperty(rc.entry.author.name)) {					contribs[rc.entry.author.name]++;				} else {					contribs[rc.entry.author.name] = 1;				}			}); var returnText = "[", total = 0, tLength = this.list.length; Object.keys(contribs).forEach(function (key) {				returnText += this._userPageLink(key) + (contribs[key] > 1 ? " ("+contribs[key]+"x)" : "");				total += contribs[key];				if(total < tLength) { returnText += "; "; }			}, this); return returnText + "]"; }		// For use with comments / normal pages RecentChangeList.prototype.changesText = function { var returnText = this.list.length+" changes"; if(this.type == RecentChange.TYPE.COMMENT) { // The use case of comments/threads having a "diff" link (as a RCList) are rare enough not to bother with, since they'd have to be the same post, // edited by two differant people (since atom feeds combine edits by same person), without any other posts on the "thread" edited, and with no new posts. } else if(this.type == RecentChange.TYPE.NORMAL) { var diffNum1, oldNum1, diffNum2, oldNum2; if(this.newest.entry.link.href.indexOf("diff=") > -1) { var hrefParamas = (this.newest.entry.link.href.split("diff=")[1]).split("&"); diffNum1 = hrefParamas[0]; oldNum1 = hrefParamas[1].replace("oldid=",""); }				if(this.oldest.isNewPage == false) { if(this.oldest.entry.link.href.indexOf("diff=") > -1) { var hrefParamas = (this.oldest.entry.link.href.split("diff=")[1]).split("&"); diffNum2 = hrefParamas[0]; oldNum2 = hrefParamas[1].replace("oldid=",""); }				} else { diffNum2 = oldNum2 = 0; }				returnText = "<a href='"+this.newest.hrefNormal+"diff="+diffNum1+"&oldid="+oldNum2+"'>"+returnText+"</a>"; }			return returnText; }		RecentChangeList.prototype._userPageLink = function(pUsername) { if(!IP_REGEX.test(pUsername)) { return gUtils.formatString("<a href='{0}User:{1}'>{1}</a>", this.list[0].href, pUsername); } else { return gUtils.formatString("<a href='{0}Special:Contributions/{1}'>{1}</a>", this.list[0].href, pUsername); }		}		RecentChangeList.prototype.totalDiff = function { var total = 0; this.list.forEach(function(rc){ total += rc.sizeDiff; }); return total; }		RecentChangeList.prototype.dispose = function { for (var i = 0; i < this.list.length; i++) { this.list[i].dispose; this.list[i] = null; };		}		return RecentChangeList; });	//######################################	// General Helper Methods	//######################################	var gUtils = {};	// Allows forEach even on nodelists	gUtils.forEach = function(collection, callback) { if(collection != undefined) { Array.prototype.forEach.call(collection, callback); } }	// http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript	gUtils.pad = function(n, width, z) {//Number, max padding (ex:3 = 001), what to pad with (default 0)		z = z || '0';		n = n + '';		return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;	}	// http://stackoverflow.com/a/4673436/1411473	gUtils.formatString = function(format) {		var args = Array.prototype.slice.call(arguments, 1);		return format.replace(/{(\d+)}/g, function(match, number) { return typeof args[number] != 'undefined' ? args[number] : match ;		});	}	// Creates a new HTML element (not jQuery) with specific attributes	gUtils.newElement = function(tag, attributes, parent) {		var element = document.createElement(tag);		if(attributes != undefined) {			for(var key in attributes)				element[key] = attributes[key];		}		if(parent != undefined) parent.appendChild(element);		return element	;	}	gUtils.removeElement = function(pNode) {		pNode.parentNode.removeChild(pNode);	}	// http://stackoverflow.com/a/5582621/1411473	gUtils.stringContainsAtLeastOneSubstringInArray = function(pString, pSubstringsArray) {		return pSubstringsArray.some(function(v) { return pString.indexOf(v) >= 0; });	}	$(document).ready(gApp.init); })(jQuery, document, mediaWiki); //