US.o script author helper

By Mindeye Last update Jun 1, 2009 — Installed 229 times. Daily Installs: 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0

There are 8 previous versions of this script.

// ==UserScript==
// @name          US.o script author helper
// @namespace     http://userscripts.org/scripts/show/36353
// @description   Shows how many new comments, installs and fans your scripts have got since you last checked
// @include       http://userscripts.org/home/scripts
// @include       http://userscripts.org/home/scripts?*
// ==/UserScript==

// Reverses a string
String.prototype.reverse = function() {
	return this.split("").reverse().join("");
};

// Transforms a number into a string with a coma as thousands separator
// If showSign is true, the plus (+) sign is appended to the positive numbers
// The double reverse is to workaround Javascript lack of lookbehind in regular expressions
Number.prototype.toCustomString = function(showSign) {
	var strNumber = this.toString();
	strNumber = strNumber.reverse().replace(/\d{3}(?!,|$|-)/g, "$&,").reverse();
	if (showSign) strNumber = ((this > 0) ? "+" : "") + strNumber;
	return strNumber;
};

// Runs a particular XPath expression p against the context node context (or the document, if not provided)
// Returns the results as an array
function $x(p, context) {
	if (!context) context = document;
	var arr = [], xpr = document.evaluate(p, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = 0, l = xpr.snapshotLength; i < l; i++) arr.push(xpr.snapshotItem(i));
	return arr;
}

// Returns only the first element of the array returned by $x (or null if the array was empty)
function $x1(p, context, docObj) {
	var nodeArray = $x(p, context, docObj);
	return (nodeArray.length > 0) ? nodeArray[0] : null;
}

// Creates a new node with the given attributes and properties (be careful with XPCNativeWrapper limitations)
function createNode(type, attributes, props) {
	var node = document.createElement(type);
	if (attributes) {
		for (var attr in attributes) {
			node.setAttribute(attr, attributes[attr]);
		}
	}
	if (props) {
		for (var prop in props) {
			if (prop in node) node[prop] = props[prop];
		}
	}
	return node;
}

// Returns the "pluralized" form of singularWord if it's necessary to express a timesNumber quantity
function adjustPlural(singularWord, timesNumber) {
	return singularWord + ((Math.abs(timesNumber) != 1) ? "s" : "");
}

// Creates a span with the difference number as its text and a CSS class according to its sign
// If formatString is provided, the span will present it as its text (with each %s replaced with the difference number)
function createDiffSpan(diffNumber, formatString) {
	if (!formatString) formatString = " (%s)"; // Default span format
	return createNode("span", {class: ((diffNumber > 0) ? "gsPosDiff" : "gsNegDiff")},
														{textContent: formatString.replace(/%s/g, diffNumber.toCustomString(true))});
}

// Calculates the difference between two dates and returns it as a "humanized" string
// It uses UTC functions because the timezone offset can affect the calculations
function getDateDiffString(dateNew, dateOld) {

	// Creates a date object with the difference between the two passed dates
	var dateDiff = new Date(dateNew.getTime() - dateOld.getTime());
	dateDiff.setUTCFullYear(dateDiff.getUTCFullYear() - 1970); // Substracts 1970 years to compensate Date.getTime's (Unix) epoch (1 Jan 1970 00:00:00 UTC)

	// Initializes the variables (timeunitsHash correlates the time units names with its corresponding method of the Date object)
	var strDateDiff = "", timeunitValue = 0;
	var timeunitsHash = {year: "getUTCFullYear", month: "getUTCMonth", day: "getUTCDate",
											 hour: "getUTCHours", minute: "getUTCMinutes", second: "getUTCSeconds", millisecond: "getUTCMilliseconds"};

	// Appends the time units values and its names to construct the string
	for (var timeunitName in timeunitsHash) {

		// Gets the time unit value by calling the corresponding method of the Date object
		// It substracts 1 for the days because they begin with 1 (the other time units begin with 0)
		timeunitValue = dateDiff[timeunitsHash[timeunitName]]() - ((timeunitName == "day") ? 1 : 0);

		// If the value isn't 0, appends this time unit to the string
		if (timeunitValue !== 0) {
			if ((timeunitName == "millisecond") && (strDateDiff.length !== 0)) continue; // Milliseconds won't be added unless the difference is less than 1 second
			strDateDiff += ((strDateDiff.length === 0) ? "" : ", ") + // Adds a comma as separator if another time unit has already been added
										 timeunitValue.toCustomString(false) + " " +
										 adjustPlural(timeunitName, timeunitValue);
		}

	}

	// Replaces the last comma with an "and" to humanize the string
	strDateDiff = strDateDiff.replace(/,([^,]*)$/, " and$1");

	return strDateDiff;

}

// Class for a base object used in several properties and methods of the GmScript and GmScriptContainer classes
function GmBase(baseComments, baseFans, baseInstalls) {
	this.comments = baseComments;
	this.fans = baseFans;
	this.installs = baseInstalls;
}

// Class for the script object
// Each script row will be parsed to an object of this class and added to a SOA
function GmScript(scriptId, scriptName, scriptComments, scriptFans, scriptInstalls) {
	this.id = scriptId; // Script id
	this.name = scriptName;	//Script name
	this.props = new GmBase(scriptComments, scriptFans, scriptInstalls); // Script's properties object
}

// Class for the scripts' container object
function GmScriptContainer(dataKeyName) {

	// Script objects array (SOA). The data will be retrieved from about:config
	this.scripts = this.getScriptsData(dataKeyName);

	// Totals object. Its properties are calculated according to the SOA
	this.totals = {_parent: this, // Private variable that holds a reference to the GmScriptContainer object for later use (because, when the getters or _sum are called, "this" will refer to the totals object)
								 _sum: function(propName) { // Private function that calculates the sum of "propName" in the scripts of the SOA
									 var totalItems = 0;
									 this._parent.scripts.forEach(function(s) {totalItems += s.props[propName];});
									 return totalItems;
								 },
								 get comments() {return this._sum("comments");}, // Comments getter
								 get fans() {return this._sum("fans");}, // Fans getter
								 get installs() {return this._sum("installs");}}; // Installs getter

}
// Returns the script object from the container's SOA with the passed id
// If no script is found, it returns null
GmScriptContainer.prototype.getScriptById = function(scriptId) {
	var scriptArrayFiltered = this.scripts.filter(function(s) {return (s.id == scriptId);});
	return (scriptArrayFiltered.length > 0) ? scriptArrayFiltered[0]: null;
};
// Calculates the differences between the totals of this container and the ones of containerOld
// Returns the results in a GmBase object
GmScriptContainer.prototype.calculateDiffs = function(containerOld) {
	var retObj = new GmBase(0, 0, 0);
	for (var propName in retObj) retObj[propName] = this.totals[propName] - containerOld.totals[propName];
	return retObj;
};
// Saves the SOA to about:config in JSON format
GmScriptContainer.prototype.saveScriptsData = function(dataKeyName) {
	GM_setValue(dataKeyName, uneval(this.scripts));
};
// Returns the SOA stored in about:config, converting it from JSON format
// If dataKeyName isn't passed, or it or its content evaluates to false, an empty array is returned instead
GmScriptContainer.prototype.getScriptsData = function(dataKeyName) {
	if (!dataKeyName) return [];
	return (eval(GM_getValue(dataKeyName, "null")) || []);
};

// Adds classes to colorize positive and negative differences
GM_addStyle(".gsPosDiff {color: green !important}");
GM_addStyle("th > .gsPosDiff {color: lime !important}"); // For better contrast with the th black background
GM_addStyle(".gsNegDiff {color: red !important}");

// Associative array used to match each script property name with its corresponding column number (cells collection) in a script row
var columnsHash = {comments: 3, fans: 4, installs: 5};

// Gets the scripts table (tbody)
var scriptTable = $x1("id('content')//table/tbody");
if (!scriptTable) return;

// Gets an array with all the rows that correspond to scripts
var scriptRows = $x("./tr[starts-with(@id, 'scripts-')]", scriptTable);
if (scriptRows.length === 0) return;

// Parses the script rows into script objects (GmScript class) and adds them to the SOA of a GmScriptContainer object. The old SOA is loaded into a container as well
// Then it compares each script object from the SOA with the corresponding script object from the old SOA, presenting the differences in the document
var containerNew = new GmScriptContainer(), containerOld = new GmScriptContainer("scriptData");
scriptRows.forEach(function(scriptRow) {

	// Finds the link to the script homepage
	var scriptLink = $x1("./td[@class='script-meat']/a", scriptRow);
	if (!scriptLink) return;

	// Extracts the script name and id (parsed to an integer) from the link
	var scriptName = scriptLink.textContent;
	var scriptId = scriptLink.href.match(/\/(\d+)$/);
	if (scriptId === null) {
		return;
	}
	else {
		scriptId = parseInt(scriptId[1], 10);
		if (isNaN(scriptId)) return;
	}

	// Extracts and parses the comments, fans and installs numbers from the apropiate cell of the script row
	var parseObj = new GmBase(0, 0, 0);
	for (var propName in parseObj) {
		var scriptPropCell = scriptRow.cells[columnsHash[propName]];
		parseObj[propName] = (scriptPropCell) ? parseInt(scriptPropCell.textContent, 10) : NaN;
	}
	if (isNaN(parseObj.comments) || isNaN(parseObj.fans) || isNaN(parseObj.installs)) return;

	// Creates a script object with the extracted data and adds it to the SOA
	var scriptObj = new GmScript(scriptId, scriptName, parseObj.comments, parseObj.fans, parseObj.installs);
	containerNew.scripts.push(scriptObj);

	// If there isn't any saved data, no comparison is necessary
	if (containerOld.scripts.length === 0) return;

	// Gets the corresponding script object from the old SOA
	var scriptObjOld = containerOld.getScriptById(scriptObj.id);
	if (scriptObjOld === null) return;

	// Compares each property (comments, fans and installs) of the script objects
	for (var propName in scriptObj.props) {

		// Calculates the difference between properties. If it's zero, there's nothing to indicate
		var propDiff = scriptObj.props[propName] - scriptObjOld.props[propName];
		if (propDiff === 0) continue;

		// Creates a difference span with the difference number
		var spanDiff = createDiffSpan(propDiff);

		// Finds the appropiate cell for this property using the scriptRow object (row) and the columnsHash information (column)
		var propCell = scriptRow.cells[columnsHash[propName]];
		if (!propCell) continue;

		// Appends the span to the cell
		propCell.appendChild(spanDiff);

	}

});
if (containerNew.scripts.length === 0) return; // If no script object has been added to the SOA, the script returns

// Saves the new SOA to about:config
containerNew.saveScriptsData("scriptData");

// Gets the headers' row of the script table
var scriptHeaderRow = $x1("./tr[th]", scriptTable);
if (!scriptHeaderRow) return;

// Calculates the differences between the totals of the new and old container, presenting the differences in the document
if (containerOld.scripts.length !== 0) {
	var diffObj = containerNew.calculateDiffs(containerOld);
	for (var propName in diffObj) {
		if (diffObj[propName] !== 0) scriptHeaderRow.cells[columnsHash[propName]].appendChild(createDiffSpan(diffObj[propName]));
	}
}

// Creates a paragraph with the totals and inserts it into the document before the script table
var totalsText = containerNew.scripts.length.toCustomString(false) + " " + adjustPlural("script", containerNew.scripts.length);
for (var propName in containerNew.totals) {
	if (propName.charAt(0) != "_") { // Private members should be ignored
		totalsText += " / " + containerNew.totals[propName].toCustomString(false) + " " + adjustPlural(propName.slice(0, -1), containerNew.totals[propName]);
	}
}
var totalsP = createNode("p", {class: "subtitle"}, {textContent: totalsText});
var scriptTableT = scriptTable.parentNode; // This is the "table" element
scriptTableT.parentNode.insertBefore(totalsP, scriptTableT);

// Gets the last check date (LCD) and saves the current one
var checkDate = new Date(), checkDateOld = parseInt(GM_getValue("lastCheck", null), 10);
checkDateOld = (isNaN(checkDateOld)) ? null : (new Date(checkDateOld));
GM_setValue("lastCheck", checkDate.getTime().toString());

// If there's a saved LCD, creates a paragraph with it (and its "humanized" difference with the current date) and insert it into the document before the script table
if (checkDateOld !== null) {
	var datesP = createNode("p", {class: "subtitle"},
															 {textContent: "Last check was " + getDateDiffString(checkDate, checkDateOld) + " ago (" +
																checkDateOld.toUTCString() + ")"});
	scriptTableT.parentNode.insertBefore(datesP, scriptTableT);
}