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 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 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);

});

});

//