$.ready.then(function () {
setTimeout(function () { // Delay to prevent other plugins from clashing
if (
mw.config.get('wgAction') !== 'view' ||
mw.config.get('wgDiffOldId') || // Set on diff pages
mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId') ||
mw.config.get('wgNamespaceNumber') === 14 || //Category
!mw.config.get('wgArticleId') ||
$('html').hasClass('ve-active') // VisualEditor
) return;
var settings = {
color: 'rgba(108, 255, 18, 0.09)', // Faint green
useInMainspace: true,
...(window.highlightRecentlyAddedTextSettings || {}),
}
if (!settings.useInMainspace && mw.config.get('wgNamespaceNumber') === 0) return;
/* Find last seen revision */
var lastSeenRevision = GetLastSeenRevision()
SaveLastSeenRevision()
function run() {
findGoodOldID(oldid => {
if (oldid == mw.config.get('wgCurRevisionId')) {
console.log('Not highlighting text, no recent changes')
return;
}
console.log(`Checking changes since https://en.wikipedia.orghttps://wikifreehand.com/en/Special:Diff/${oldid}/cur`)
getOldversion(oldid, function (old_html) {
$.when(mw.loader.getScript('https://en.wikipedia.org/w/index.php?title=User:%C3%9Ejarkur/Cacycle%27s_diff_(without_omissions).js&action=raw&ctype=text/javascript')).then(function () {
var old_text = getText($(old_html))
var new_text = getText($('body').find('.mw-parser-output').clone())
if($('html').hasClass('ve-active')) return; // VisualEditor has been turned on in the meantime
var diffHtml = $((new WikEdDiff()).diff(old_text, new_text))
diffHtml.find('.wikEdDiffDelete').remove()
console.log(`${diffHtml.find('.wikEdDiffInsert').length} text additions found`)
highlightCharacters(FindAdditions(diffHtml))
})
})
$('head').append(`<style>.recent_addition { background: ${settings.color}; }</style>`)
})
}
function getOldversion(oldid, callback) {
var api = new mw.Api();
api.get({
action: 'parse',
oldid: oldid,
format: 'json'
}).done(function (data) {
callback($.parseHTML(data.parse.text))
}).fail(function (error) {
console.log(error);
})
}
var ignore = '.reference, .noprint, .mw-cite-backlink, .mw-editsection, .toc, style, script, .navbox, .reply-link-wrapper, .scriptInstallerLink'
/*
Convoluted way to find text nodes to match up with our later method
*/
function getText(html) {
var returns = ''
function TraverseAndFindText(input) {
$(input).contents().each(function () {
if (this.nodeType === Node.TEXT_NODE) {
returns += $(this).text()
} else {
if (!$(this).is(ignore)) {
TraverseAndFindText(this)
}
}
})
}
TraverseAndFindText(html)
return returns
}
function FindAdditions(input) {
var returns =
TraverseAndFindAdditions(input, false, function (character) {
returns.push(character)
})
return returns
}
function TraverseAndFindAdditions(input, isAdding, callback) {
$(input).contents().each(function () {
if (this.nodeType === Node.TEXT_NODE) {
var text = $(this).text()
text.split('').forEach(t => {
callback({
isAdding,
text: t
})
})
} else {
var newIsAdding = isAdding
if ($(this).hasClass('wikEdDiffInsert')) {
newIsAdding = true
}
TraverseAndFindAdditions(this, newIsAdding, callback)
}
})
}
function escape_html (input) {
return input.replace(//gim, function(i) {
return '&#'+i.charCodeAt(0)+';';
});
}
function highlightCharacters(characters) {
var i = 0;
var stop = false;
if (!characters.find(i => i.isAdding)) {
return console.log('No text added since the revision checked')
}
characters = characters.filter(i => i.text !== '\n')
function TraverseAndHighlight(input) {
if (stop) return;
$(input).contents().each(function () {
if (this.nodeType === Node.TEXT_NODE) {
var text = $(this).text()
var array = text.split('').map(t => {
if (stop) return;
if (t === '\n') {
return {
isAdding: false,
text: t,
}
}
if (!characters) {
console.warn('Went through too many characters!')
return null;
}
if (t !== characters.text) {
console.error('Could not highlight recently changed text')
console.warn(`Expected "${t}", got "${characters.text}"`)
console.log(`Surrounding: ${characters.map(i => i.text).slice(Math.max(0,i-5),i+5).join('')}`)
stop = true;
return null;
}
return characters
}).filter(Boolean)
if (stop) return;
var new_text = array.reduce((output, current) => {
var lastIndex = output.length - 1
if (!output) {
return
}
if (output.isAdding === current.isAdding) {
output = {
...output,
text: output.text + current.text,
}
return output
} else {
return [
...output,
current,
]
}
}, ).map(x => {
if (x.isAdding) {
return '<span class="recent_addition">' + escape_html(x.text) + '</span>'
} else {
return escape_html(x.text)
}
}).join('')
$(this).replaceWith(new_text)
} else {
if (!$(this).is(ignore)) {
TraverseAndHighlight(this)
}
}
})
}
TraverseAndHighlight($('body').find('.mw-parser-output'))
}
function findGoodOldID(callback) {
if (lastSeenRevision) {
/*
Check that we didn't just submit our own text
*/
var api = new mw.Api();
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvlimit: '1',
rvprop: 'user',
format: 'json',
}).done(function (data) {
var pages = data.query.pages
for (page in pages) {
var revisions = pages.revisions
/* Only callback if we weren't the most recent editor */
if (revisions.length === 0 || revisions.user != mw.config.get('wgUserName')) {
callback(lastSeenRevision)
}
}
}).fail(function (error) {
callback(lastSeenRevision)
console.log(error);
})
return
}
/*
If none, find last 50 edits.
Only do this for mainspace.
*/
if (
mw.config.get('wgNamespaceNumber') !== 0
// mw.config.get('wgCategories').includes('Non-talk pages that are automatically signed')
) {
return;
}
var api = new mw.Api();
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvlimit: '50',
rvprop: 'ids|timestamp|user|comment|size|tags',
format: 'json',
}).done(function (data) {
var pages = data.query.pages
for (page in pages) {
var revisions = pages.revisions
DiscardRevertedEdits(revisions, callback)
}
}).fail(function (error) {
console.log(error);
})
}
/*
Adapted from ]
*/
function DiscardRevertedEdits(revisions, callback) {
var lastEditByCurrentUser = revisions.find(r => {
return r.user == mw.config.get('wgUserName')
})
if (lastEditByCurrentUser) {
return callback(lastEditByCurrentUser.revid)
}
var removed =
revisions.forEach(function (revision, index) {
var rgx;
var comment = (revision.comment && revision.comment.replace(/\+?\|(]+)\]\]/g, '$1')) || ''
// Plain MediaWiki undo with untampered edit summary
if (rgx = /^Undid revision (\d+) by/.exec(comment)) {
var reverted_rev_id = rgx;
var $reverted_rev = revisions.find(r => r.revid == reverted_rev_id)
if(!$reverted_rev) return;
// just to confirm that the edit isn't a partial revert, find the byte count changes for the
// two edits: if they add up to 0, then this is a full revert (in all likelihood)
var diffbytes1 = revision.size;
var diffbytes2 = $reverted_rev.size;
if (diffbytes1 + diffbytes2 === 0) {
removed.push(revision.revid)
removed.push($reverted_rev.revid)
}
// 'Restore this version' reverts using Twinkle or popups or pending changes reverts
// TW: Reverted to revision 3234343 by ...
// popups: Revert to revision 34234234 by ...
// PC tool: Revereted 3 pending edits by Foo and Bar to revision 3243432 by ...
} else if (rgx = /^Revert(?:ed)? (?:\d+ pending edits? by .*?)?to revision (\d+)/.exec(comment)) {
var last_good_revision_id = rgx;
removed.push(revision.revid)
var i = index
var $rev = revisions
if (parseInt(last_good_revision_id) > parseInt($rev.revid) ||
parseInt(last_good_revision_id) < 100) { // sanity checks
return true; // revision id given has to be wrong
}
while ($rev.revid != last_good_revision_id) {
removed.push($rev.revid)
$rev = revisions
if ($rev && $rev.length === 0) {
callback(last_good_revision_id)
break; // end of page history in current view
}
}
} else {
var reverted_user;
// Reverts tagged as "Rollback"
if (revision.tags.includes('mw-rollback')) {
reverted_user = revisions ? revisions.user : null
}
// Twinkle rollbacks
else if (rgx = /^Reverted (?:good faith|\d+) edits? by (.*?) \(talk\)/.exec(comment)) {
reverted_user = rgx;
// Old Twinke vandalism rollback
} else if (rgx = /^Reverted \d+ edits? by (.*?) identified as vandalism/.exec(comment)) {
reverted_user = rgx;
// STiki vandalism rollbacks, and all reverts using MediaWiki rollback, Huggle, Cluebot have the "Rollback" tag added
// and hence would have been handled above. The regex checks here are to account for old reverts done before the
// "Rollback" tag was introduced
// STiki AGF/normal/vandalism revert
} else if (rgx = /^Reverted \d+ (?:good faith )?edits? by (.*?) (?:identified as test\/vandalism )?using STiki/.exec(comment)) {
reverted_user = rgx;
// normal MediaWiki rollback and Huggle rollback
} else if (rgx = /^Reverted edits by (.*?) \(talk\)/.exec(comment)) {
reverted_user = rgx;
// ClueBot
} else if (.includes(revision.user)) {
reverted_user = /^Reverting possible vandalism by (.*?) to version by/.exec(comment);
// XLinkBot
} else if (revision.user === 'XLinkBot') {
reverted_user = /^BOT--Reverting link addition\(s\) by (.*?) to/.exec(comment);
}
if (reverted_user) {
// page history shows compressed IPv6 address (with multiple 0's replaced by ::)
// though rollback edit summaries use the uncompressed form (though with leading 0's removed)
if (mw.util.isIPv6Address(reverted_user)) {
reverted_user = reverted_user.replace(/\b(?:0+:){2,}/, ':').toLowerCase();
}
removed.push(revision.revid)
var i = 0
var $rev = revisions;
while ($rev.user === reverted_user) {
removed.push($rev.revid)
$rev = revisions;
if ($rev.length === 0) break; // end of page history (in current view)
}
}
}
});
/* Filter out */
revisions
.filter(r => !removed.includes(r.revid))
.reduce((output, current) => {
if (output.length === 0) {
return
}
var last = output
if (last.user === current.user) {
output = current // Overwrite last
return output
} else {
return [
...output,
current,
]
}
}, )
var last_ten = revisions.slice(0, 10)
callback(last_ten.revid)
}
function GetLastSeenRevision() {
return window.localStorage.getItem('last_seen_' + mw.config.get('wgArticleId'))
}
function SaveLastSeenRevision() {
window.localStorage.setItem('last_seen_' + mw.config.get('wgArticleId'), mw.config.get('wgRevisionId'));
}
// Reset: window.localStorage.setItem('last_seen_' + mw.config.get('wgArticleId'), '')
run()
}, 100)
})