// ==UserScript== // @name 4chan Filter, Opera edition // @author Anonymous // @version 1.3a // @description The 4chan filter extension, rewritten to work with Opera. Inspired by the various filter extensions on userscripts.org for Firefox. // @ujs:documentation http://sector-5.net/archives/4chan-filter-userscript-for-opera/ // @ujs:download http://files.sector-5.net/4chan-filter.js // ==/UserScript== if (/^http:\/\/boards\.4chan\.org\/\w{1,3}\//.test(window.location.href)) { // Pack everything into an anonymous function block to avoid functions and // variables from leaking out. (function() { /// Saves the currently selected values. /// @return Associative array with all settings function saveSettings() { // Checkboxes setValue('4chan-filter-hidetext', document.getElementById('4chan-filter-hidetext').checked, 0); setValue('4chan-filter-forcedanonymous', document.getElementById('4chan-filter-forcedanonymous').checked, 0); setValue('4chan-filter-stickywindow', document.getElementById('4chan-filter-stickywindow').checked, 0); // Input boxes setValue('4chan-filter-name', document.getElementById('4chan-filter-name').value, 0); setValue('4chan-filter-tripcode', document.getElementById('4chan-filter-tripcode').value, 0); setValue('4chan-filter-email', document.getElementById('4chan-filter-email').value, 0); setValue('4chan-filter-subject', document.getElementById('4chan-filter-subject').value, 0); setValue('4chan-filter-comment', document.getElementById('4chan-filter-comment').value, 0); setValue('4chan-filter-image', document.getElementById('4chan-filter-image').value, 0); // Update global settings settings = restoreSettings(); return settings; } /// Loads the settings from previous sessions /// @return Associative array with all settings function restoreSettings() { /// Split the string by semicolon and precompile strings to /// regular expressions. var createRegExpArray = function(str) { if (str.length == 0) return []; else { var stringArray = str.split(';').filter(function (obj) { if (obj.length > 0) return true; else return false; }); var regexpArray = stringArray.map(function (obj) { return new RegExp(obj); }); return regexpArray; } } var array = new Object(); // Checkboxes array['hidetext'] = getValue('4chan-filter-hidetext', false); array['forcedanonymous'] = getValue('4chan-filter-forcedanonymous', false); array['stickywindow'] = getValue('4chan-filter-stickywindow', true); // Input boxes array['name'] = createRegExpArray(getValue('4chan-filter-name', '')); array['tripcode'] = createRegExpArray(getValue('4chan-filter-tripcode', '')); array['email'] = createRegExpArray(getValue('4chan-filter-email', '')); array['subject'] = createRegExpArray(getValue('4chan-filter-subject', '')); array['comment'] = createRegExpArray(getValue('4chan-filter-comment', '')); array['image'] = createRegExpArray(getValue('4chan-filter-image', '')); return array; } /// Apply settings, filtering the page function applySettings(settings, root) { if (!root) root = document.body; filter_filterPosts(root, settings); if (settings['hidetext']) filter_hideText(root); if (settings['forcedanonymous']) filter_forcedAnonymous(root); } /// Restores the original page by replacing document.body with its cloned copy. function resetModifications(settings) { resetPending = true; document.body = documentClone.cloneNode(true); document.body.appendChild(createUserInterface(settings)); applyStyles(settings); resetPending = false; } /// Apply style modifications to the control window function applyStyles(settings) { if (settings['stickywindow']) document.getElementById('4chan-filter').style.position = 'fixed'; else document.getElementById('4chan-filter').style.position = 'absolute'; } /// Stores a setting in a local cookie. /// Originally from `Emulate Greasemonkey functions' by TarquinWJ, /// http://userjs.org/scripts/browser/enhancements/aa-gm-functions function setValue(cookieName, cookieValue, lifeTime) { if( !cookieName ) { return; } if( lifeTime == "delete" ) { lifeTime = -10; } else { lifeTime = 31536000; } document.cookie = escape( cookieName ) + "=" + escape( cookieValue ) + ";expires=" + ( new Date( ( new Date() ).getTime() + ( 1000 * lifeTime ) ) ).toGMTString() + ";path=/"; } /// Loads a setting from local cookies. /// Originally from `Emulate Greasemonkey functions' by TarquinWJ, /// http://userjs.org/scripts/browser/enhancements/aa-gm-functions function getValue(cookieName, oDefault) { var cookieJar = document.cookie.split( "; " ); for( var x = 0; x < cookieJar.length; x++ ) { var oneCookie = cookieJar[x].split( "=" ); if( oneCookie[0] == escape( cookieName ) ) { var footm = unescape( oneCookie[1] ); if (footm == 'true') return true; else if (footm == 'false') return false; else return footm; } } return oDefault; } /// Deletes a local settings cookie. /// Originally from `Emulate Greasemonkey functions' by TarquinWJ, /// http://userjs.org/scripts/browser/enhancements/aa-gm-functions function deleteValue(oKey) { setValue( oKey, '', 'delete' ); } /// Filter functions /// Hides all text within posts. function filter_hideText(root) { var posts = root.getElementsByTagName("blockquote"); for (var i = 0; i < posts.length; ++i) { var text = posts[i].innerHTML; // Delete all posts without interboard-link (/rs/) or URL if (!(text.indexOf("http://") != -1 || text.indexOf(">>>/") != -1)) { while (posts[i].hasChildNodes()) posts[i].removeChild(posts[i].firstChild) } } } /// Changes every name to Anonymous, removes every email link /// except for sage and strips every tripcode. function filter_forcedAnonymous(root) { // Remove names var nameTags = document.evaluate( "descendant-or-self::span[@class='commentpostername'] |" + "descendant-or-self::span[@class='postername']", root, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0; i < nameTags.snapshotLength; ++i) { var nameTag = nameTags.snapshotItem(i); if (nameTag.childNodes.length > 0) { var textContent = nameTag.childNodes[0]; while (textContent != null && textContent.nodeType != 3) // Text node textContent = textContent.childNodes[0]; textContent.nodeValue = "Anonymous"; } } // Remove tripcodes var tripTags = document.evaluate( "descendant-or-self::span[@class='postertrip']", root, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, nameTags); for (var i = 0; i < tripTags.snapshotLength; ++i) tripTags.snapshotItem(i).textContent = ""; // Remove emails var emailTags = document.evaluate( "descendant-or-self::a[@class='linkmail']", root, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, nameTags); for (var i = 0; i < emailTags.snapshotLength; ++i) { var node = emailTags.snapshotItem(i); if (node.href != 'mailto:sage') { // Remove all child nodes and move them one level up in the DOM tree while (node.hasChildNodes()) { var child = node.firstChild; node.removeChild(child); node.parentNode.insertBefore(child, node); } // Remove the now empty element node.parentNode.removeChild(node); } } } /// Filters posts according to the values set in the configuration window. function filter_filterPosts(root, settings) { var filterCount = 0; var posts = root.getElementsByClassName('reply'); for (var i = posts.length - 1; i >= 0; --i) { var postFiltered = false; // Check for matching name if (postFiltered == false && settings['name'] != null && settings['name'].length > 0) { var nameTags = posts[i].getElementsByClassName('commentpostername'); if (nameTags.length > 0) { for (var j = 0; j < settings['name'].length; ++j) { // Work down towards the actual text node //var textContent = nameTags[0].childNodes[0]; //while (textContent != null && textContent.nodeType != 3) // Text node // textContent = textContent.childNodes[0]; if (settings['name'][j].test(nameTags[0].textContent)) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } } } // Check for matching tripcode if (postFiltered == false && settings['tripcode'] != null && settings['tripcode'].length > 0) { var tripcodeTags = posts[i].getElementsByClassName('postertrip'); if (tripcodeTags.length > 0) { for (var j = 0; j < settings['tripcode'].length; ++j) { if (settings['tripcode'][j].test(tripcodeTags[0].textContent)) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } } } // Check for matching email if (postFiltered == false && settings['email'] != null && settings['email'].length > 0) { var emailTags = posts[i].getElementsByClassName('linkmail'); if (emailTags.length > 0) { for (var j = 0; j < settings['email'].length; ++j) { // Skip 'mailto:' in emailTags[0].href (hence the 7) if (settings['email'][j].test(emailTags[0].href.substring(7))) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } } } // Check for matching subject if (postFiltered == false && settings['subject'] != null && settings['subject'].length > 0) { var subjectTags = posts[i].getElementsByClassName('replytitle'); if (subjectTags.length > 0) { for (var j = 0; j < settings['subject'].length; ++j) { if (settings['subject'][j].test(subjectTags[0].textContent)) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } } } // Check for matching comment if (postFiltered == false && settings['comment'] != null && settings['comment'].length > 0) { var commentTags = posts[i].getElementsByTagName('blockquote'); if (commentTags.length > 0) { for (var j = 0; j < settings['comment'].length; ++j) { if (settings['comment'][j].test(commentTags[0].textContent)) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } } } // Check for matching image name if (postFiltered == false && settings['image'] != null && settings['image'].length > 0) { /* var imageTags = document.evaluate("span[@class='filesize']/span[@title]", posts[i], null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); if (imageTags.snapshotLength > 0) { for (var j = 0; j < settings['image'].length; ++j) { if (settings['image'][j].test(imageTags.snapshotItem(0).title)) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } }*/ var imageTag = document.evaluate("span[@class='filesize']", posts[i], null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (imageTag != null) { for (var j = 0; j < settings['image'].length; ++j) { if (settings['image'][j].test(imageTag.textContent)) { posts[i].parentNode.parentNode.removeChild(posts[i].parentNode); postFiltered = true; ++filterCount; break; } } } } } // Update filter display if (filterCount > 0) { var title = document.getElementById('4chan-filter-titletag'); if (title != null) title.textContent = 'Images: ' + imageCount + ' Posts: ' + (postCount - filterCount) + ' (' + postCount + ')'; } } /// Create the user interface /// @return DocumentFragment object function createUserInterface(settings) { // Top level container var topContainer = document.createElement('div'); topContainer.id = '4chan-filter'; topContainer.style.right = '5px'; topContainer.style.top = '20px'; topContainer.style.width = '235px'; //topContainer.style.backgroundColor = '#D6DAF0'; topContainer.style.border = '1px solid black'; /// Utility function to find out if a node is descendant node of another node function isChildOrSame(child, parent) { while (child) { if (child == parent) return true; else child = child.parentNode; } return false; } // Mouse-over shows the settings... topContainer.onmouseover = function(event) { if (!isChildOrSame(event.relatedTarget, event.currentTarget)) { var innerContainer = document.getElementById('4chan-filter-inner'); innerContainer.style.display = 'block'; } } // Mouse-out saves settings. topContainer.onmouseout = function(event) { if (!isChildOrSame(event.relatedTarget, event.currentTarget)) { var innerContainer = document.getElementById('4chan-filter-inner'); innerContainer.style.display = 'none'; var settings = saveSettings(); applyStyles(settings); } }; // Title tag var title = document.createElement('h4'); title.id = '4chan-filter-titletag'; title.style.textAlign = 'center'; title.textContent = 'Images: ' + imageCount + ' Posts: ' + postCount; topContainer.appendChild(title); // Inner container - This container only expands when the mouse hovers over the control panel var innerContainer = document.createElement('div'); topContainer.appendChild(innerContainer) innerContainer.id = '4chan-filter-inner'; innerContainer.style.display = 'none'; innerContainer.style.textAlign = 'right'; innerContainer.style.padding = '5px'; /// Creates a new input field and appropriate label, /// and adds it to parentContainer var createInputField = function(id, parentContainer) { var label = id.charAt(0).toUpperCase().concat(id.substring(1)) var labelTag = document.createElement('label'); labelTag.setAttribute('for', '4chan-filter-' + id); labelTag.textContent = label + ':'; labelTag.style.marginRight = '5px'; labelTag.style.verticalAlign = 'middle'; var field = document.createElement('input'); field.id = '4chan-filter-' + id; field.type = 'text'; field.style.margin = '1px'; field.style.verticalAlign = 'middle'; if (settings[id] != null) { stringArray = settings[id].map(function(obj) { regexpString = obj.toString(); // Strip the leading and trailing / character return regexpString.substring(1, regexpString.length - 1); }); field.value = stringArray.join(';'); } parentContainer.appendChild(labelTag); parentContainer.appendChild(field); parentContainer.appendChild(document.createElement('br')); } /// Create a new checkbox and appropriate label and /// adds it to parentContainer. var createCheckbox = function(id, title, parentContainer) { var label = document.createElement('label'); label.setAttribute('for', '4chan-filter-' + id); label.textContent = ' ' + title + ': '; var checkbox = document.createElement('input'); checkbox.id = '4chan-filter-' + id; checkbox.type = 'checkbox'; if (settings[id]) checkbox.checked = true; parentContainer.appendChild(label); parentContainer.appendChild(checkbox); parentContainer.appendChild(document.createElement('br')); } // Hide text createCheckbox('hidetext', 'Hide text', innerContainer);; // Forced Anonymous createCheckbox('forcedanonymous', 'Forced Anonymous', innerContainer); // Setting fields createInputField('name', innerContainer); createInputField('tripcode', innerContainer); createInputField('email', innerContainer); createInputField('subject', innerContainer); createInputField('comment', innerContainer); createInputField('image', innerContainer); // Sticky window createCheckbox('stickywindow', 'Sticky window', innerContainer); // Command box var cmdBox = document.createElement('p'); innerContainer.appendChild(cmdBox); cmdBox.style.margin = '10px 5px 5px 5px'; cmdApply = document.createElement('a'); cmdApply.textContent = 'Apply changes'; cmdApply.style.marginLeft = '5px'; cmdApply.onclick = function(e) { var settings = saveSettings(); applySettings(settings); applyStyles(settings); } cmdBox.appendChild(cmdApply); cmdReset = document.createElement('a'); cmdReset.textContent = 'Reset page'; cmdReset.style.marginLeft = '5px'; cmdReset.onclick = function (e) { resetModifications(settings); }; cmdBox.appendChild(cmdReset); var lowerBox = document.createElement('p'); innerContainer.appendChild(lowerBox); lowerBox.style.margin = '10px 5px 5px 5px'; lowerBox.style.textAlign = 'center'; var explanation = document.createElement('small'); lowerBox.appendChild(explanation); explanation.appendChild(document.createTextNode( 'Note: Separate values with semicolon. ' + 'All text is interpreted as ')); var link = document.createElement('a'); link.href = 'https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp'; link.target = '_blank'; link.textContent = 'regular expression'; explanation.appendChild(link); explanation.appendChild(document.createTextNode('.')); var fragment = document.createDocumentFragment(); fragment.appendChild(topContainer); return fragment; } var postCount, imageCount, documentClone; var documentClone = null; var resetPending = false; // Restore all previous settings var settings = restoreSettings(); document.addEventListener('DOMContentLoaded', function(e) { // Restore all previous settings //const settings = restoreSettings(); // Clone document in order to be able to restore filtered posts // without reloading the page. documentClone = document.body.cloneNode(true); postCount = document.getElementsByClassName('reply').length + document.getElementsByClassName('postername').length; imageCount = document.evaluate("//img[@align='left']", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength; // Create the user interface and add it to the document document.body.appendChild(createUserInterface(settings)); // Apply settings (i.e. filter posts by criteria specified in settings) applySettings(settings); applyStyles(settings); }, false); document.addEventListener('DOMNodeInserted', function(e) { if (e.target.nodeType == 1 && resetPending == false) { applySettings(settings, e.target); } }, false); })(); }