Skip to content

Instantly share code, notes, and snippets.

@Eklei
Last active November 1, 2018 01:09
Show Gist options
  • Save Eklei/3b070126300b63dfa56acfd51c03b799 to your computer and use it in GitHub Desktop.
Save Eklei/3b070126300b63dfa56acfd51c03b799 to your computer and use it in GitHub Desktop.
Retrieves SCP names and places them in page titles. Also puts tooltips on any instance of an SCP or experiment ID. The script will automatically create or update its cache by fetching the necessary index pages (including joke, explained, and archived SCPs) at most once every 3 days. The cache will also be created or updated whenever you visit an…
// ==UserScript==
// @author Eklei
// @name SCP Name Tracker
// @namespace eklei@fhqwhgads
// @description Retrieves SCP names and places them in page titles. Also puts tooltips on any instance of an SCP or experiment ID. The script will automatically create or update its cache by fetching the necessary index pages (including joke, explained, and archived SCPs) at most once every 3 days. The cache will also be created or updated whenever you visit an index page manually. As an added bonus, this script can defeat some memetic censoring (optional, see toggle at top of script). The automatic fetching only works on big boy browsers that are compliant with web standards from at least 2011. If you have some outdated mobile browser that isn't compliant, you can still generate a cache by visiting each index page manually.
// @version 2.4
// @include http://www.scp-wiki.net/*
// @include http://scp-wiki.wikidot.com/*
// @grant none
// ==/UserScript==
var breakMemeticCensoring = true; //Some pages have fake blackout/[REDACTED]/etc. Change this setting to false if you want to maintain the magic and the mystery.
console.log('*** SCP Name Tracker ***');
var regretfullyNecessaryStylesheet = document.createElement('style'); //Because only Firefox supports CSS3 text-decorations.
memeticStylesheet = 'span[style*="background-color: #000000"][style*="color: #000000"]\n' +
', span[style*="background-color:#000000"][style*="color:#000000"]\n' +
', span[style*="background-color: black"][style*="color: black"]\n' +
', span[style*="background-color:black"][style*="color:black"]\n' +
'{ color:#404040!important; }\n' +
'span[style*="color: transparent"], span[style*="color:transparent"]\n' +
', span[style*="color: white"], span[style*="color:white"]\n' +
', span[style*="color: #ffffff"], span[style*="color:#ffffff"]\n' +
', span[style*="color: #fefefe"], span[style*="color:#fefefe"]\n' +
', span[style*="color: #fcfcfc"], span[style*="color:#fcfcfc"]\n' +
', span[style*="color: #fdfdfd"], span[style*="color:#fdfdfd"]\n' +
'{ color:#808080!important; opacity:0.5!important; }\n' +
'span[style*="font-size: 0"], span[style*="font-size:0"]\n' +
'{ font-size:50%!important; line-height:1.41em!important; color:#c0c0c0!important }\n' +
'span[style*="font-size:0%"]:before\n' +
'{ content:""; font-size:0; line-height:0; display:block; width:0; height:0 }\n' +
'li > span[style*="font-size:0%"]\n' +
'{ display:block }\n' +
'.collapsible-block-link,\n';
regretfullyNecessaryStylesheet.innerHTML = (breakMemeticCensoring ? memeticStylesheet : '') +
'abbr, .hover, .hover:hover { border-bottom:0.1em dotted; }\n' +
'abbr { text-decoration:none; }\n';
document.body.appendChild(regretfullyNecessaryStylesheet);
var scpIndexPages = [ '/scp-series', '/scp-series-2', '/scp-series-3', '/scp-series-4', '/joke-scps', '/archived-scps', '/scp-ex' ];
function getFetchablePages() { //Because xhr is asynchronous, we need to decide which pages are fetchable in advance.
if (getFetchablePages.isAlreadyUsed) //This function is going to get hit for every yet-uncached SCP.
return false;
getFetchablePages.isAlreadyUsed = true;
var timestamp;
var fetchablePages = [];
for (var i=0; i<scpIndexPages.length; i++) {
timestamp = retrieveItem('timestamp for ' + scpIndexPages[i]);
if (!timestamp || timestamp < Date.now() - 432000000) //Can be fetched again after 5 days have passed since last caching.
fetchablePages.push(scpIndexPages[i]);
}
return fetchablePages;
}
function xhrFetchIndex(url, callback) {
xhrFetchIndex["visited " + url] = true;
storeItem('timestamp for ' + url, Date.now());
//console.log('XMLHttpRequest started: ' + url);
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function(){
callback(url, this.response);
});
xhr.open('GET', url);
xhr.responseType = "document";
xhr.send();
}
function xhrCallback(url, doc) { //Because xhr is asynchronous, we have no idea which page will be the last to finish loading.
if ('object' != typeof doc)
console.log("Yo, your browser is garbage (probably outdated). For automatic index fetching, you need a browser compliant with web standards from at least 2011, because it requires support for XMLHttpRequest responseType = document (i.e. HTML DOM). The entire script could be overhauled to regex the plain responseText rough and raw, but that would be a lot slower for all browsers, and a lot more code, so I'm not doing it. Sorry but not sorry.");
else
cacheSeriesIndex(url, doc);
xhrFetchIndex.count--;
if (xhrFetchIndex.count === 0) { //This must be the last fetch to finish, so the condition has been met to...
xhrFetchIndex.isCurrentlyFetching = false;
initializeNameTracker(); //Try processing the page once more with the fresh cache.
}
//console.log('XMLHttpRequest finished: ' + url);
}
function retrieveSCPName(scpIdentifier) {
if (xhrFetchIndex.isCurrentlyFetching)
return false;
var fetchablePages = getFetchablePages()
if (fetchablePages && fetchablePages.length) {
xhrFetchIndex.isCurrentlyFetching = true;
xhrFetchIndex.count = fetchablePages.length; //Set up condition for callback, so it only proceeds after the last asynchronous xhr is done.
for (var i=0; i<fetchablePages.length; i++) //We're fetching all the indexes at once because there's no reliable way to know which one we need.
xhrFetchIndex(fetchablePages[i], xhrCallback);
} else {
return retrieveItem(scpIdentifier);
}
}
function retrieveItem(identifier) {
return localStorage.getItem('*** fhqwhgads *** ' + identifier.toLowerCase());
}
function storeItem(identifier, string) {
localStorage.setItem('*** fhqwhgads *** ' + identifier.toLowerCase(), string);
}
function cacheSeriesIndex(url, doc) {
storeItem('timestamp for ' + url, Date.now());
//console.log('*** Attempting to cache SCP series: ' + url);
var links = doc.getElementById('page-content').querySelectorAll('a[href^="/scp-"]');
for (var i=0, l=links.length; i<l; i++) {
var scpNumber1 = /\/SCP-(.*-J(?:\S){0,3}|\d\d\d\d?(?:-ARC|-CU)?(?:-EX)?)/i.exec(links[i].href);
if (!scpNumber1 || !scpNumber1[1]) continue;
var scpNumber2 = /SCP-(.*-J(?:\S){0,3}|\d\d\d\d?(?:-ARC|-CU)?(?:-EX)?)/i.exec(links[i].innerHTML);
if (!scpNumber2 || !scpNumber2[1]) continue;
var scpName = /<\/a> - (.*)/.exec(links[i].parentNode.innerHTML);
if (!scpName || !scpName[1]) continue;
storeItem("SCP-"+scpNumber1[1], scpName[1]);
if (scpNumber1[1] != scpNumber2[1]) //Joke SCP with a mobius double format screw. Save both identifiers.
storeItem("SCP-"+scpNumber2[1], scpName[1]);
}
console.log('*** Finished caching SCP series: ' + url);
}
function enhanceItemPage() {
if (window.isAlreadySCPNameEnhanced) return; //We're flagging this on the window object in case Tampermonkey restarts (as for an update)
//console.log('*** Attempting to append SCP name to title...');
var scpNumber = /\/SCP-(.*-J(?:\S){0,3}|\d\d\d\d?(?:-ARC|-CU)?(?:-EX)?)/i.exec(location.pathname);
if (!scpNumber || !scpNumber[1]) return;
var scpName = retrieveSCPName("SCP-"+scpNumber[1]);
if (!scpName) return;
var scpTitle = document.getElementById('page-title');
scpTitle.innerHTML = scpTitle.innerHTML.trim() + ": " + scpName;
document.title = scpTitle.textContent.trim() + ' - SCP Foundation';
console.log('*** Finished appending SCP name to title.');
window.isAlreadySCPNameEnhanced = true;
}
var skipTags = { 'style': 1, 'script': 1, 'iframe': 1 };
function recurseReplace(haystack, needle, callback) {
var child, tag;
for (var i = haystack.childNodes.length - 1; i >= 0; i--) {
child = haystack.childNodes[i];
if (child.nodeType == 1) { //ELEMENT_NODE
tag = child.nodeName.toLowerCase();
if (!(tag in skipTags)) {
recurseReplace(child, needle, callback);
}
} else if (child.nodeType == 3) { //TEXT_NODE
replaceRegex(child, needle, callback);
}
}
}
function replaceRegex(text, needle, callback) {
var match, matches=[];
while ( (match = needle.exec(text.data)) ) {
matches.push(match);
}
for (var i=matches.length-1; i>=0; i--) {
match = matches[i];
text.splitText(match.index);
text.nextSibling.splitText(match[0].length);
callback(text, match[0], match[1]);
}
}
function replaceCallback(originalText, fullMatch, captureGroup) {
if (originalText.parentNode.nodeName.toLowerCase() == 'abbr')
return; //We're on a second pass where some abbrs have already been applied.
var abbr = document.createElement('abbr');
var spcName = retrieveSCPName("SCP-"+captureGroup);
if (spcName)
abbr.title = spcName.replace(/<\/?span[^>]*>/ig, '');
else
return;
abbr.innerHTML = fullMatch;
originalText.parentNode.replaceChild(abbr, originalText.nextSibling);
}
function initializeNameTracker() {
if (/^\/(?:scp-series-\d?|joke-scps|archived-scps|scp-ex)\/?$/.test(location.pathname)) {
cacheSeriesIndex(location.pathname, document);
} else if (/^\/scp-/.test(location.pathname)) {
enhanceItemPage();
}
//console.log('*** Attempting to insert SCP tooltips...');
recurseReplace(document.body, /\b(?:SCP[ -]|Experiment(?: Log)? )(.*-J(?:\S){0,3}|\d\d\d\d?(?:-ARC|-CU)?(?:-EX)?)\b/ig, replaceCallback);
console.log('*** Finished inserting SCP tooltips.');
}
initializeNameTracker();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment