Jump to content

User:Mxn/TimestampDiffs.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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.2",
		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>