User:ProcrastinatingReader/TimestampDiffs.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:ProcrastinatingReader/TimestampDiffs. |
/***************************************************************************************************
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 && node.parentNode != null) {
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..*?$/, "");
// special cases:
basePageName = basePageName.replace(/\/IncidentArchive..*?$/, "/Incidents");
var isArchive = /\/(?:Incident)?Archive/.test(config.mw.wgPageName);
var apiQueryCount = 0;
var processTimestamps = async function(rvStartId) {
if (isArchive && !rvStartId) {
// get latest diff that may be an archive diff
await api.get({
"action": "query",
"format": "json",
"prop": "revisions",
"titles": config.mw.wgPageName,
"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("Archive API response did not contain any revisions");
}
var latestArchiveDiff = response.query.pages[0].revisions.find(elm => {
return /OneClickArchiver|Archiving \d+ discussion/.test(elm.comment);
})
rvStartId = latestArchiveDiff.revid;
});
}
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;
});
console.log(pageRevisions);
document.querySelectorAll(".tsdiffs-unlinked").forEach(function(timestampNode) {
var timestamp;
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 + ")";
linkTimestamp(timestampNode, href, title);
}
});
if ( apiQueryCount < 8 && 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>