User:Evad37/TimestampDiffs.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.
/***************************************************************************************************
 TimestampDiffs --- by Evad37
 > Links timestamps to diffs on discussion pages
***************************************************************************************************/
/* jshint esnext:false, laxbreak: true, undef: true, maxerr: 999*/
/* globals console, document, $, mw */
// <nowiki>
$.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 [[Wikipedia: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 <a> 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);
	});
});
// </nowiki>