// ==UserScript== // @namespace http://snowplow.org/martin/greasemonkey // @name killfile // @description Provides a killfile for certain blogs. Covers livejournal, haloscan comments, most typepad blogs, most blogspot blogs, scienceblogs.com, and more as I add them. Trolls can be killfiled with a single click. // ==/UserScript== // // This is a script for greasemonkey 0.6, a plugin for the FireFox // web browser. (http://greasemonkey.mozdev.org/) // It likely will NOT work in earlier versions of // greasemonkey, nor will it work in any other browser. // // The intention of this script is to hide the comments of commentors // you, the reader, do not wish to hear from. In that respect, it's // like an old usenet killfile. It does not affect what other // visitors to the site will see, nor does it affect what you see // before the page finishes loading (due to some limitations in // Mozilla, you really don't want to rewrite HTML that's not entirely // there yet). This is not a tool meant for handling spam, only for // an individual comment reader to avoid having to see comments they // don't wish to see. // // This script is maintained by martin@snowplow.org (Daniel Martin) // This script is provided under the terms of the GNU General // Public License, version 2, which can be found at // http://www.gnu.org/copyleft/gpl.html // Specifically, note that this script comes with NO guarantees, // not even the implied guarantee of merchantibility or fitness for a // specific purpose. // // Please let me know of other blogs you'd like covered with this, // and I welcome patches. Future versions of this script may be // released under a license different from the GPL. // // Future enhancements of this script will hopefully include a // configuration page so that the user can add their own blog // configurations themselves. Email me if you'd like to be // notified of new versions. // Acknowledgements: // The GreaseMonkey folks, for putting together GM. // Stan Dyck, whose original killfile script for // the blog pharyngula was the inspiration for this script // (Although no actual code was borrowed) // You can see his script at // http://sgd.homelinux.net/greasemonkey/pharyngulakillfile.user.js var ourVersion = "20070423.1"; var ourURL = 'http://snowplow.org/martin/greasemonkey/killfile.user.js'; function upgrade() { window.open(ourURL,"killfile"); } GM_registerMenuCommand("Get latest killfile script", upgrade); function checkVersion() { GM_xmlhttpRequest({ method:"GET", url:"http://snowplow.org/martin/greasemonkey/versions.xml?killfile-"+ourVersion, headers:{ "User-Agent":"Greasemonkey/0.6", "Accept":"application/xml,text/xml", }, onload: function(responseDetails) { var parser = new DOMParser(); var dom = parser.parseFromString(responseDetails.responseText, "application/xml"); var programNodes = dom.getElementsByTagName('program'); if (!programNodes) { GM_log("Can't find program: " + responseDetails.responseText); } for (var i=0; i < programNodes.length; i++) { if (programNodes[i].getAttribute('id') == 'killfile') { var programNode = programNodes[i]; var version = programNode.getElementsByTagName("version")[0]; if (version.textContent > ourVersion) { alert("There's a new version of killfile available.\n" + "You are running version " + ourVersion + "\n" + "The latest available version is " + version.textContent + "\n" + "You can upgrade from the 'Tools->User script commands' menu"); } GM_setValue('lastVersionCheckTime',Math.round((new Date()).getTime()/1000)); } } } }); } function autoCheckVersion() { var nowsecs = Math.round((new Date()).getTime()/1000); var prevcheck = GM_getValue('lastVersionCheckTime',0); if (nowsecs > prevcheck + 60*60*24*3) { checkVersion(); } } function showComment(spot) { spot.childNodes[0].style.display = 'block'; spot.childNodes[1].style.display = 'none'; } function hideComment(spot) { spot.childNodes[0].style.display = 'none'; spot.childNodes[1].style.display = 'block'; } function reviewContent() { var trolllist = GM_getValue("Trolllist", ';'); var snap = document.evaluate("//div[@class='dtm_killfile_commentholder']", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (var i=0; i < snap.snapshotLength; i++) { var spot = snap.snapshotItem(i); if (trolllist.indexOf(";" + spot.getAttribute("dtm_killfile_user") + ";") > -1) { hideComment(spot); } else { showComment(spot); } } } function addTroll(troll) { var trolllist = GM_getValue("Trolllist", ';'); if (trolllist.indexOf(";" + troll + ";") < 0) { trolllist = trolllist + troll + ";"; GM_setValue("Trolllist", trolllist); } reviewContent(); } function delTroll(troll) { var trolllist = GM_getValue("Trolllist", ';'); if (trolllist.indexOf(";" + troll + ";") > -1) { trolllist = trolllist.replace(";" + troll + ";",";"); GM_setValue("Trolllist", trolllist); } reviewContent(); } function handleClick(evt) { var holderdiv = this; var evtar = evt.target; var clazz = evtar.getAttribute('class'); if (clazz) { if (clazz == "dtm_killfile_show") { evt.stopPropagation(); evt.preventDefault(); showComment(holderdiv); } else if (clazz == "dtm_killfile_hide") { evt.stopPropagation(); evt.preventDefault(); hideComment(holderdiv); } else if (clazz == "dtm_killfile_kill") { evt.stopPropagation(); evt.preventDefault(); addTroll(holderdiv.getAttribute('dtm_killfile_user')); } else if (clazz == "dtm_killfile_unkill") { evt.stopPropagation(); evt.preventDefault(); delTroll(holderdiv.getAttribute('dtm_killfile_user')); } } } // the basic scenario is essentially wordpress-like, so the // wordpress scenarios below are pretty short function basicScenario() { return { commenttopxpath: "//ol[@class='commentlist']/li", sigbit: ".//cite[1]", replaceXpath: "child::node()", mangleBefore: null, mangleAppend: null, tabXpath: null, precedingBit: '', followingBit: '', sigUserMatch: '$2', sigHrefMatch: '$1', aHrefAttribute: 'href', // sometimes title get sigpat() { var s = ('^\\s*' + this.precedingBit + '\\s*(?:]*?(?:\\b(?:' + this.aHrefAttribute + ')\\s*="([^>"]*)")?[^>]*>)?(\\S[^<]*[^\\s<]|[^\\s<])\\s*(?:)?\\s*' + this.followingBit + '.*'); return new RegExp(s,""); }, foreachComment: function(loopBody) { if (!loopBody) {return null;} var snap = document.evaluate(this.commenttopxpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (var i=0; i < snap.snapshotLength; i++) { loopBody(snap.snapshotItem(i)); } }, getUserspec: function(commentNode) { if (!commentNode) {return null;} var sigsnap = document.evaluate(this.sigbit, commentNode, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null ); var nd = sigsnap.singleNodeValue; if (nd == null) {return null;} var sigHTML = nd.innerHTML; if (!sigHTML) {sigHTML = nd.textContent;} else {sigHTML = sigHTML.replace(/\ /g, ' ');} sigHTML = sigHTML.replace(/\s+/g, ' '); // in case people match on attribute tags var sigre = this.sigpat; if (! sigre.test(sigHTML)) { progresslog("Didn't match: html " + sigHTML.toSource() + " and regexp " + sigre.toSource()); return null; } var user = sigHTML.replace(sigre,this.sigUserMatch); var href = sigHTML.replace(sigre,this.sigHrefMatch); return [user, escape(user) + "!" + escape(href)]; }, divHTML: '
' + ' ', divHTMLuser: function(user, userspec) { return this.divHTML.replace(/__SHORTUSER__/g,user).replace(/__USER__/g,userspec); }, getEmptyHolder: function(commentNode, user, userspec) { var ddiv = document.createElement('div'); ddiv.innerHTML = this.divHTMLuser(user, userspec); if (this.tabXpath) { var tabNode = document.evaluate(this.tabXpath, commentNode, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null); tabNode = tabNode.singleNodeValue; if (tabNode) { var tabtarget = ddiv.childNodes[1].childNodes[0]; tabtarget.insertBefore(tabNode.cloneNode(true),tabtarget.firstChild); } } ddiv.setAttribute("class", "dtm_killfile_commentholder"); ddiv.setAttribute("dtm_killfile_user", userspec); return ddiv; }, spanHTML: '[kill]' + '[hide comment]', spanHTMLuser: function(user, userspec) { return this.spanHTML.replace(/__SHORTUSER__/g,user).replace(/__USER__/g,userspec); }, mangleCommentContent: function(contentNode, user, userspec) { if (!contentNode) {return null;} var snap3 = null; var useBefore = true; if (this.mangleBefore) { snap3 = document.evaluate(this.mangleBefore, contentNode, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null); } else if (this.mangleAppend) { snap3 = document.evaluate(this.mangleAppend, contentNode, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null); useBefore = false; } if (snap3 && snap3.singleNodeValue) { var target = snap3.singleNodeValue; var cspan = document.createElement('span'); cspan.innerHTML = this.spanHTMLuser(user, userspec); if (useBefore) { target.parentNode.insertBefore(cspan,target); } else { target.appendChild(cspan); } return contentNode; } progresslog("Can't find insertion point for kill button"); progresslog(contentNode); progresslog(contentNode.childNodes[0]); return null; }, insertCommentHolder: function(commentNode, holderDiv) { if (! (commentNode && holderDiv)) {return null;} var snap2 = document.evaluate(this.replaceXpath, commentNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); if (snap2.snapshotLength == 0) {return null;} snap2.snapshotItem(0).parentNode.insertBefore(holderDiv,snap2.snapshotItem(0)); var contentNode = holderDiv.firstChild; for (var j=0; j < snap2.snapshotLength; j++) { var removedNode = snap2.snapshotItem(j);//.parentNode.removeChild(snap2.snapshotItem(j)); contentNode.appendChild(removedNode); } holderDiv.addEventListener('click', handleClick, true); return contentNode; }, handleComment: function(commentNode) { //progresslog("Comment found " + location.href); var us = this.getUserspec(commentNode); if (! us) {progresslog("Can't find user");return null;} var commentTwo = this.mangleCommentContent(commentNode, us[0], us[1]); if (!commentTwo) {progresslog("No kill button"); return null;} var ddiv = this.getEmptyHolder(commentTwo, us[0], us[1]); if (! ddiv) {progresslog("Can't get empty holder");return null;} var contentdiv = this.insertCommentHolder(commentTwo, ddiv); if (! contentdiv) {progresslog("Can't insert comment holder");return null;} }, manglePage: function () { var me = this; this.foreachComment(function (c) {me.handleComment(c)}); } }; } var kf_debug = GM_getValue("debug",0); function progresslog(logstr) { if (kf_debug) {GM_log(logstr);} } function wordpressScenario() { return { get mangleBefore() {return this.sigbit + '/following::*'}, __proto__:basicScenario() }; } function riotactScenario() { return { get mangleAppend() {return this.sigbit + '/..'}, precedingBit: 'Comment by ', followingBit: ' [-\u2014] ', commenttopxpath: "//ol[@id='commentlist']/li", __proto__:basicScenario() }; } function pandagonScenario() { return { sigbit: "span[1]", __proto__:wordpressScenario() }; } function livejournalScenario() { return { commenttopxpath: "//table[starts-with(@id,'ljcmt') and .//span[@class='ljuser']]", sigbit: "descendant::span[@class='ljuser'][1]", replaceXpath: ".", get mangleBefore() {return this.sigbit + "/following::*[1]";}, tabXpath: 'descendant::img[1]', precedingBit: '(?:.*?)?', __proto__:basicScenario() }; } function livejournalScenario2() { return { commenttopxpath: "//div[@class='comment_wrapper']", sigbit: ".//div[@class='comment_postedby']/*[1]", tabXpath: '', __proto__:livejournalScenario() }; } function livejournalScenario3() { return { commenttopxpath: "//div[starts-with(@id,'ljcmt') and child::div[@class='entry']]", replaceXpath: "./h3|./div[@class='entry']|./div[@class='talklinks']", tabXpath: '', __proto__:livejournalScenario() }; } function livejournalScenario4() { return { commenttopxpath: "//div[starts-with(@id,'ljcmt')]", replaceXpath: "child::node()", tabXpath: '', __proto__:livejournalScenario() }; } function livejournalScenario5() { return { commenttopxpath: "//td[@id='content']//table[@class='heading_bar']/following-sibling::div", get mangleAppend() {return this.sigbit + "/parent::node()";}, mangleBefore: null, __proto__:livejournalScenario4() }; } function livejournalScenario6() { return { commenttopxpath: "//table[@class='entrybox'][2]/tbody/tr/td/table/tbody/tr/td/div", __proto__:livejournalScenario5() }; } function ljfriendsScenario() { // If you want this, see the comments in "scenariolist" below return { commenttopxpath: "//div[@class='H3Holder']", sigbit: "descendant::a[1]/@href", replaceXpath: ".", sigpat: /(http:\/\/([\w-]+)\..*)/, sigUserMatch: '$2', sigHrefMatch: '$1', mangleAppend: "descendant::a[contains(@href,'livejournal.com/') and " + "substring-after(@href,'livejournal.com/')=''][1]/..", spanHTML: '[kill]