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(` .recent_addition { background: ${settings.color}; } `)      }) }

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