User:Evad37/TimestampDiffs.js

/*************************************************************************************************** TimestampDiffs --- by Evad37 > Links timestamps to diffs on discussion pages /* jshint esnext:false, laxbreak: true, undef: true, maxerr: 999*/ /* globals console, document, $, mw */ // $.when(	mw.loader.using(["mediawiki.api"]),	$.ready ).then(function {	// Pollyfill NodeList.prototype.forEach per https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach	if (window.NodeList && !NodeList.prototype.forEach) {		NodeList.prototype.forEach = Array.prototype.forEach;	}

var config = { version: "1.1.3", mw: mw.config.get([			"wgNamespaceNumber",			"wgPageName",			"wgRevisionId",			"wgArticleId"		]), months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] };

// Only activate on existing talk pages and project pages var isExistingPage = config.mw.wgArticleId > 0; if ( !isExistingPage ) { return; }	var isTalkPage = config.mw.wgNamespaceNumber > 0 && config.mw.wgNamespaceNumber%2 === 1; var isProjectPage = config.mw.wgNamespaceNumber === 4; if ( !isTalkPage && !isProjectPage ) { return; }

mw.util.addCSS(".tsdiffs-timestamp a { color:inherit; text-decoration: underline dotted #6495ED; }" );

/**	 * Wraps timestamps within text nodes inside spans (with classes "tsdiffs-timestamp" and "tsdiffs-unlinked"). * Based on "replaceText" method in https://en.wikipedia.org/wiki/User:Gary/comments_in_local_time.js	 * * @param {Node} node Node in which to look for timestamps */	var wrapTimestamps = function(node) { var timestampPatten = /(\d{2}:\d{2}, \d{1,2} \w+ \d{4} \(UTC\))/g; if (!node) { return; }

var isTextNode = node.nodeType === 3; if (isTextNode) { var parent = node.parentNode; var parentNodeName = parent.nodeName;

if (['CODE', 'PRE'].includes(parentNodeName)) { return; }

var value = node.nodeValue; var matches = value.match(timestampPatten);

// Manipulating the DOM directly is much faster than using jQuery. if (matches) { // Only act on the first timestamp we found in this node. If				// there are two or more timestamps in the same node, they // will be dealt with through recursion below var match = matches[0]; var position = value.search(timestampPatten); var stringLength = match.toString.length; var beforeMatch = value.substring(0, position); var afterMatch = value.substring(position + stringLength);

var span = document.createElement('span'); span.className = 'tsdiffs-timestamp tsdiffs-unlinked'; span.append(document.createTextNode(match.toString));

parent = node.parentNode; parent.replaceChild(span, node);

var before = document.createElement('span'); before.className = 'before-tsdiffs'; before.append(document.createTextNode(beforeMatch));

var after = document.createElement('span'); after.className = 'after-tsdiffs'; after.append(document.createTextNode(afterMatch));

parent.insertBefore(before, span); parent.insertBefore(after, span.nextSibling);

// Look for timestamps to wrap in all subsequent sibling nodes var next = after; var nextNodes = []; while (next) { nextNodes.push(next); next = next.nextSibling; }				nextNodes.forEach(wrapTimestamps); }		} else { node.childNodes.forEach(wrapTimestamps); }	};	wrapTimestamps(document.querySelector(".mw-parser-output"));

// Account for Comments in local time gadget document.querySelectorAll(".localcomments").forEach(function(node) {		node.classList.add("tsdiffs-timestamp", "tsdiffs-unlinked");	});

/**	 * Wraps the child nodes of an element within an  tag, * with given href and title attributes, and removes the * `tsdiffs-unlinked` class from the element. * 	 * @param {Element} element * @param {string} href * @param {string} title */	var linkTimestamp = function(element, href, title) { var a = document.createElement("a"); a.setAttribute("href", href); a.setAttribute("title", title); element.childNodes.forEach(function(child) {			a.appendChild(child);		}); element.appendChild(a); element.classList.remove("tsdiffs-unlinked"); };

/**	 * Formats a JavaScript Date object as a string in the MediaWiki timestamp format: * hh:mm, dd Mmmm YYYY (UTC) * 	 * @param {Date} date * @returns {string} */	var dateToTimestamp = function(date) { var hours = ("0"+date.getUTCHours).slice(-2); var minutes = ("0"+date.getUTCMinutes).slice(-2); var day = date.getUTCDate; var month = config.months[date.getUTCMonth]; var year = date.getUTCFullYear; return hours + ":" + minutes + ", " + day + " " + month + " " + year + " (UTC)"; };

var api = new mw.Api( {		ajax: {			headers: { 				"Api-User-Agent": "TimestampDiffs/" + config.version + 					" ( https://en.wikipedia.org/wiki/User:Evad37/TimestampDiffs.js )"			}		}	} );

// For discussion archives, comments come from the base page var basePageName = config.mw.wgPageName.replace(/\/Archive..*?$/, "");

var apiQueryCount = 0; var processTimestamps = function(rvStartId) { apiQueryCount++; return api.get({			"action": "query",			"format": "json",			"prop": "revisions",			"titles": basePageName,			"formatversion": "2",			"rvprop": "timestamp|user|comment|ids",			"rvslots": "",			"rvlimit": "5000",			"rvStartId": rvStartId || config.mw.wgRevisionId		}).then(function(response) {			if (!response || !response.query || !response.query.pages || !response.query.pages[0] || !response.query.pages[0].revisions) {				return $.Deferred.reject("API response did not contain any revisions");			}			var pageRevisions = response.query.pages[0].revisions.map(function(revision) { var revisionDate = new Date(revision.timestamp); var oneMinutePriorDate = new Date(revisionDate - 1000*60); revision.timestampText = dateToTimestamp(revisionDate); revision.oneMinutePriorTimestampText = dateToTimestamp(oneMinutePriorDate); return revision; });

document.querySelectorAll(".tsdiffs-unlinked").forEach(function(timestampNode) {				var timestamp;				var timestampTitle;				if (timestampNode.tagName === "TIME") {					timestamp = dateToTimestamp(new Date(timestampNode.dateTime));					timestampTitle = timestampNode.title;				} else if (timestampNode.classList.contains("localcomments")) {					timestamp = timestampNode.getAttribute("title");				} else {					timestamp = timestampNode.textContent;				}

// Try finding revisions with an exact timestamp match var revisions = pageRevisions.filter(function(revision) {					return revision.timestampText === timestamp;				}); if (!revisions.length) { // Try finding revisions which are off by one miniute revisions = pageRevisions.filter(function(revision) {						return revision.oneMinutePriorTimestampText === timestamp;					}); }

if (revisions.length) { // One or more revisions had a matching timestamp // Generate a link of the diff the between newest revision in the array, // and the parent (previous) of the oldest revision in the array. var newerRevId = revisions[0].revid; var olderRevId = revisions[revisions.length-1].parentid || "prev"; var href = "/wiki/Special:Diff/" + olderRevId + "/" + newerRevId;

// Title attribute for the link can be the revision comment if there was // only one revision, otherwise use the number of revisions found var comment = revisions.length === 1 ? revisions[0].comment : revisions.length + " edits"; var title = "Diff (" + comment + ")"; if (timestampTitle) { title += "\n" + timestampTitle; }

linkTimestamp(timestampNode, href, title); }			});

if ( apiQueryCount < 5 && document.getElementsByClassName("tsdiffs-unlinked").length ) { return processTimestamps(pageRevisions[pageRevisions.length-1].revid); }		});	};

return processTimestamps .catch(function(code, error) {		mw.notify("Error: " + (code || "unknown"), {title:"TimestampDiffs failed to load"});		console.warn("[TimestampDiffs] Error: " + (code || "unknown"), error);	}); }); //