User:Þjarkur/Highlight recently added text.js

In this article we are going to delve into the topic of User:Þjarkur/Highlight recently added text.js, analyzing its different aspects and repercussions in today's society. User:Þjarkur/Highlight recently added text.js has been the subject of debate and controversy in recent times, arousing the interest of researchers, academics and citizens. Throughout the next lines, we will explore its origin, evolution and its influence in different areas, as well as the possible implications it has for the future. From its origins to the present day, User:Þjarkur/Highlight recently added text.js has played a fundamental role in shaping the world we know, and it is important to understand its scope and consequences in order to address it appropriately.
$.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)
})