User:Suffusion of Yellow/mark-reverted.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. |
This user script seems to have a documentation page at User:Suffusion of Yellow/mark-reverted and an accompanying .css page at User:Suffusion of Yellow/mark-reverted.css. |
/*
* mark-reverted.js
*
* Highlights diffs and permalinks by status: live, reverted, or unknown.
* Should work on any page. Based on revision SHA1 only.
*/
// <nowiki>
(function() {
/* globals $, mw */
'use strict';
const MESSAGES = {
'mr-activate-text' : "Mark reverted",
'mr-activate-title' : "Highlight links by status (reverted, live, or unknown)",
'mr-link-unknown-title' : "This status of this edit could not be determined",
'mr-link-reverted-title' : "This edit has been reverted at least once",
'mr-link-live-title' : "This edit has identical text with the current revision",
'mr-link-error-title' : "An error occured while determining the status of this edit (see browser console)",
'mr-disallow-toomany': "There are $1 unique pages linked from here. Try again on a page with fewer links.",
'mr-warn-toomany' : "There are $1 unique pages linked from here. Continue?",
};
const WINDOW_SIZE = 30; // Always look AT LEAST this far ahead/back
/*
* It's possible for there to be 5000 unique titles linked from
* one user contributions page. That would be 10000 API requests!
* Set some sensible limits.
*/
const SOFT_PAGE_LIMIT = 100; // Prompt first
const HARD_PAGE_LIMIT = 1000; // Nope
const MAX_CONCURRENT_REQUESTS = 10;
const CSS_PAGE = "https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/mark-reverted.css&action=raw&ctype=text/css";
const API_USER_AGENT = "mark-reverted/0.1 (https://en.wikipedia.org/wiki/User:Suffusion_of_Yellow/mark-reverted.js)";
var api;
/*
* Silently ignore API errors, but log them to the console.
* We are making LOTS of requests and there's no need to panic
* if one goes missing. Results will be "good enough".
*/
function handleApiError(code, details) {
if (typeof code != 'string')
throw code; // Something went very wrong
if (code == "http" && details.textStatus == "abort")
return; // Aborted by user, not an error
console.log((code == "http") ?
"HTTP error: " + details.textStatus :
"API returned error \"" + code + "\": " + details.error.info);
}
/*
* Get a batch of revisions and mark all revisions as
* "live" if the rev_sha1 is same as the current rev_sha1
* "reverted" if:
* Some later revision has a rev_sha1, S
* AND some earlier revision has the same rev_sha1, S
* AND the revision itself does NOT have rev_sha1 S
* "unknown" otherwise
*/
async function getRevisions(state, revid, dir) {
let page = state.page, revlist = state.revlist;
let revmap = state.revmap, shamap = state.shamap;
let start = revid, idx = revmap.get(revid);
if (idx !== undefined) {
if (dir == "newer" ||
revlist.length - idx > WINDOW_SIZE ||
revlist[revlist.length - 1].parentid === 0)
return; // Fully cached
start = revlist[revlist.length - 1].revid; // Partly cached
}
let r = await api.get( {
action : 'query',
prop : 'revisions',
pageids : page.pageid,
rvprop : "ids|sha1",
rvstartid : start,
rvdir : dir,
rvlimit : WINDOW_SIZE
}).catch(handleApiError);
let revisions;
try {
revisions = r.query.pages[page.pageid].revisions;
if (dir == "newer")
revisions.reverse();
} catch(e) {
return;
}
for (let rev of revisions) {
if (revmap.get(rev.revid))
continue;
rev.status = "unknown";
revmap.set(rev.revid, revlist.length);
revlist.push(rev);
if (rev.sha1 !== undefined) {
if (revlist[0].revid == page.lastrevid &&
rev.sha1 == revlist[0].sha1)
rev.status = "live";
let last = shamap.get(rev.sha1);
if (last !== undefined)
for (let j = last; j < revlist.length - 1; j++)
if (revlist[j].status == "unknown" &&
revlist[j].sha1 !== rev.sha1)
revlist[j].status = "reverted";
shamap.set(rev.sha1, revlist.length - 1);
}
}
}
/*
* Mark all links for a given page.
* NOT concurrent; makes caching tricky
*/
async function markAllForPage(page, links) {
let state = {
page : page,
revlist : [],
revmap : new Map(),
shamap : new Map()
};
for (let rev of page.revisions) {
await getRevisions(state, rev.revid, "newer");
await getRevisions(state, rev.revid, "older");
}
for (let rev of page.revisions) {
let r = state.revmap.get(rev.revid);
let result = r !== undefined ? state.revlist[r].status : "error";
links.get(rev.revid).addClass("mr-" + result);
links.get(rev.revid).prop("title",
mw.msg("mr-link-" + result + "-title"));
}
}
/*
* Concurrently mark all links for all pages
*/
async function markAll(pages, links) {
let pending = [];
for(let [id, page] of pages) {
let idx = pending.length < MAX_CONCURRENT_REQUESTS ?
pending.length : await Promise.race(pending);
pending[idx] = markAllForPage(page, links).then(() => idx);
}
}
/*
* Find out what page is associated with each revision,
* and create a list of revisions for each page.
*/
async function getPageInfo(links) {
const BATCH_SIZE = 50;
let pages = new Map();
let revids = [...links.keys()];
for(let i = 0; i < revids.length; i += BATCH_SIZE) {
let response = await api.get({
action : 'query',
prop : 'revisions|info',
rvprop : "ids|timestamp",
revids : revids.slice(i, i + BATCH_SIZE).join("|")
}).catch(handleApiError);
if (!response.query || !response.query.pages)
continue; // All the revids were bad, perhaps?
for (let id in response.query.pages) {
let page = pages.get(id);
if (!page)
pages.set(id, response.query.pages[id]);
else {
page.revisions.push(...response.query.pages[id].revisions);
}
}
}
/*
* Sort by timestamp (newest first), then by revid (largest first),
* and remove duplicates
*/
for (let [id, page] of pages) {
let r = page.revisions.slice();
r.sort((a, b) => {
if (a.timestamp == b.timestamp)
return a.revid == b.revid ? 0 : a.revid > b.revid ? -1 : 1;
else
return a.timestamp > b.timestamp ? -1 : 1;
});
page.revisions = [];
for(let i = 0; i < r.length; i++)
if (i == 0 || r[i].revid != r[i - 1].revid)
page.revisions.push(r[i]);
}
return pages;
}
/*
* Extract revision ID from various forms of links,
* (...diff=prev&oldid=xxx, Special:Diff/xxx, etc.)
*/
function parseLink(link) {
let diff, oldid, match, p;
try {
p = new mw.Uri(link);
} catch(e) {
return null;
}
if (p.host !== mw.config.get('wgServerName'))
return null;
if (p.path == mw.config.get('wgScript')) {
diff = p.query.diff;
oldid = p.query.oldid;
} else if ((match = p.path.match(/^\/wiki\/Special:Diff\/([^\/]+)$/i))) {
diff = match[1];
} else if ((match = p.path.match(/^\/wiki\/Special:PermanentLink\/([^\/]+)$/i))) {
oldid = match[1];
} else if ((match = p.path.match(/^\/wiki\/Special:Diff\/([^\/]+)\/([^\/]+)$/i))) {
oldid = match[1];
diff = match[2];
}
switch(diff) {
case undefined:
case "prev":
return parseInt(oldid) || null;
case "cur":
case "next":
return null; // Not yet implemented
default:
return parseInt(diff) || null;
}
return null;
}
async function activate(event) {
event.preventDefault();
if (api)
api.abort();
else {
api = new mw.Api({
ajax: {
headers: {
'Api-User-Agent' : API_USER_AGENT
}
}
});
mw.loader.load(CSS_PAGE, "text/css");
}
let links = new Map();
/*
* Find any element with an associated revid, or any link
* that is NOT descended from an element with a revid
*/
let $elems =
$('#mw-content-text [data-mw-revid], #mw-content-text a:not([data-mw-revid] a)');
for (let e of $elems) {
let $elem, revid = $(e).data('mwRevid') || parseLink(e.href);
// Not a permalink or diff
if (!revid)
continue;
// No data-mw-revid in ancestor <li> on AbuseLog
if (mw.config.get('wgCanonicalSpecialPageName') == "AbuseLog")
$elem = $(e).closest('li');
else
$elem = $(e);
$elem.removeClass("external damaging mr-reverted mr-unknown mr-live mr-error");
if (!links.get(revid))
links.set(revid, $elem);
else
links.set(revid, links.get(revid).add($elem));
}
let pages = await getPageInfo(links);
if (pages.size > HARD_PAGE_LIMIT) {
alert(mw.msg('mr-disallow-toomany', pages.size));
return;
} else if (pages.size > SOFT_PAGE_LIMIT) {
if (!confirm(mw.msg('mr-warn-toomany', pages.size)))
return;
}
markAll(pages, links);
}
$.when(mw.loader.using( ["mediawiki.util",
"mediawiki.api",
"mediawiki.Uri"] ),
$.ready).then(() => {
mw.messages.set(MESSAGES);
$(mw.util.addPortletLink(
"p-tb",
"#",
mw.msg('mr-activate-text'),
't-markreverted',
mw.msg('mr-activate-title')
)).click(activate);
});
})();
// </nowiki>