User:Þjarkur/Highlight recently added text.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:Þjarkur/Highlight recently added text. |
$.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(`<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(/[\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 '<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[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)
})