User:Þjarkur/Highlight recently added text.js

$.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.org/wiki/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(``)

})

}

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(/[\u00A0-\u9999<>\&]/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[i]) {

console.warn('Went through too many characters!')

return null;

}

if (t !== characters[i].text) {

console.error('Could not highlight recently changed text')

console.warn(`Expected "${t}", got "${characters[i].text}"`)

console.log(`Surrounding: ${characters.map(i => i.text).slice(Math.max(0,i-5),i+5).join('')}`)

stop = true;

return null;

}

return characters[i++]

}).filter(Boolean)

if (stop) return;

var new_text = array.reduce((output, current) => {

var lastIndex = output.length - 1

if (!output[lastIndex]) {

return [current]

}

if (output[lastIndex].isAdding === current.isAdding) {

output[lastIndex] = {

...output[lastIndex],

text: output[lastIndex].text + current.text,

}

return output

} else {

return [

...output,

current,

]

}

}, []).map(x => {

if (x.isAdding) {

return '' + escape_html(x.text) + ''

} 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[page].revisions

/* Only callback if we weren't the most recent editor */

if (revisions.length === 0 || revisions[0].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[page].revisions

DiscardRevertedEdits(revisions, callback)

}

}).fail(function (error) {

console.log(error);

})

}

/*

Adapted from User:SD0001/hide-reverted-edits.js

*/

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[1];

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[1];

removed.push(revision.revid)

var i = index

var $rev = revisions[i++]

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[i++]

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[index + 1] ? revisions[index + 1].user : null

}

// Twinkle rollbacks

else if (rgx = /^Reverted (?:good faith|\d+) edits? by (.*?) \(talk\)/.exec(comment)) {

reverted_user = rgx[1];

// Old Twinke vandalism rollback

} else if (rgx = /^Reverted \d+ edits? by (.*?) identified as vandalism/.exec(comment)) {

reverted_user = rgx[1];

// 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[1];

// normal MediaWiki rollback and Huggle rollback

} else if (rgx = /^Reverted edits by (.*?) \(talk\)/.exec(comment)) {

reverted_user = rgx[1];

// ClueBot

} else if (['ClueBot NG', 'ClueBot'].includes(revision.user)) {

reverted_user = /^Reverting possible vandalism by (.*?) to version by/.exec(comment)[1];

// XLinkBot

} else if (revision.user === 'XLinkBot') {

reverted_user = /^BOT--Reverting link addition\(s\) by (.*?) to/.exec(comment)[1];

}

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[i++];

while ($rev.user === reverted_user) {

removed.push($rev.revid)

$rev = revisions[i++];

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 [current]

}

var last = output[output.length - 1]

if (last.user === current.user) {

output[output.length - 1] = current // Overwrite last

return output

} else {

return [

...output,

current,

]

}

}, [])

var last_ten = revisions.slice(0, 10)

callback(last_ten[last_ten.length - 1].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)

})