/* * Thanks to Foxfirefey for Dreamwidth Dynamic Reading Page Expand/Collapse * on which this is somewhat based. */ // ==UserScript== // @name DW Entry Screening // @namespace wiring.io // @version 0.1 // @description Cuts entries based on keywords/journals (present or absent). Works with Core2 styles. // @include http://*.dreamwidth.org/* // @exclude http://www.dreamwidth.org/* // @exclude http://*.dreamwidth.org/manage* // @exclude http://*.dreamwidth.org/inbox* // @exclude http://*.dreamwidth.org/update* // @exclude http://*.dreamwidth.org/profile* // @exclude http://*.dreamwidth.org/calendar* // @exclude http://*.dreamwidth.org/tag* // @exclude http://*.dreamwidth.org/*.html* // @copyright 2011+, Lian (http://wiring.io/) // @require http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js // ==/UserScript== /** * TODO/Known Bugs: * - UI/UX improvements? * - multiple filtering-- order of action: whitelist > blacklist, don't cut/collapse if already hidden, etc * - BUG: fix cut text display so doesn't say entry contains if mode is set to include */ (function(){ /* ENUMS ---------------*/ var NodeState = { EXPANDED : { name: "expanded", value: 0 }, COLLAPSED : { name: "collapsed", value: 1 }, HIDDEN : { name: "hidden", value: 2 }, CUT : { name: "cut", value: 3 } }; var ListType = { KEYWORDS : { name: "Keywords", value: 0 }, USERS : { name: "Users", value: 1 }, COMMUNITIES : { name: "Communities", value: 2 } }; var FilterType = { INCLUDE : { name: "Include", value: 0 }, EXCLUDE : { name: "Exclude", value: 1 } }; /* SETTINGS -----------*/ // default state of all entries-- if you want to change this, you can start // with all entries collapsed/hidden/cut and whitelist the ones you want visible/open var globalState = NodeState.EXPANDED; // put classes of things you'd like to be hidden in 'collapsed' state here var collapseables = new Array("entry-content", "metadata", "userpic", "entry-management-links"); // fill in items with words/usernames you want to filter by // if you use EXCLUDE, the state will apply to whatever you list in items // if you use INCLUDE, the state will apply to everything BUT those items // you can add more filters or remove the examples you're not using var filters = [ { items: ["anything_you", "want_warning_of"], listType: ListType.KEYWORDS, filterType: FilterType.EXCLUDE, state: NodeState.CUT }, { items: ["anything_you", "don't_want_to_see"], listType: ListType.KEYWORDS, filterType: FilterType.EXCLUDE, state: NodeState.COLLAPSE }, { items: ["individual_name"], listType: ListType.USERS, filterType: FilterType.EXCLUDE, state: NodeState.COLLAPSED }, { items: ["community_name"], listType: ListType.COMMUNITIES, filterType: FilterType.EXCLUDE, state: NodeState.COLLAPSED } ]; /* MAIN ------------*/ var Utilities = function() { }; Utilities.prototype = { getNodeState: function (value) { switch(value) { case 0: return NodeState.EXPANDED; case 1: return NodeState.COLLAPSED; case 2: return NodeState.HIDDEN; case 3: return NodeState.CUT; } } }; // XPath Constructor var XPath = function() { this._path = "//div[@class='entry-content']"; this._resultType = XPathResult.UNORDERED_NODE_ITERATOR_TYPE; this.init(); }; XPath.prototype = { _path: null, _resultType: null, _result: null, _resultArray: null, init: function () { this._result = this.getResult(this._path, document, this._resultType); this._resultArray = this.getResultArray(this._result); }, getResult: function (path, scope, resultType) { return document.evaluate(path, scope, null, resultType, null); }, getResultArray: function (result) { var temp = result.iterateNext(), array = []; while(temp) { array.push(temp); temp = result.iterateNext(); } return array; } }; // Generic Filter object var Filter = function() { this._xpath = new XPath(); this._utils = new Utilities(); this.createEntryList(); }; Filter.prototype = { _xpath: null, _entryList: null, _savedEntryList: null, createEntryList: function () { this._entryList = {}; this._savedEntryList = {}; for (var i = 0; i < this._xpath._resultArray.length; i++) { var entry = new Entry(this._xpath._resultArray[i]); var storedValue = GM_getValue(entry._uid); if (storedValue) { entry.setState(this._utils.getNodeState(storedValue.value)); this._savedEntryList[entry._uid] = entry; } else { entry.setState(globalState); } this._entryList[entry._uid] = entry; } }, match: function (listType, type, nodeState, items) { var matches = {}; for (var x in this._entryList) { var entry = this._entryList[x]; var matchPattern = null; var entryMatches = []; for (var j = 0, len = items.length; j < len; j++) { switch(listType) { case "Keywords": matchPattern = entry._innerHtml.match(new RegExp(items[j], "i")); if (matchPattern) { console.log("keyword: " + items[j]+ " (" + entry._uid + ")");} break; case "Users": matchPattern = entry.matchPoster(items[j]); if (matchPattern) { console.log("user: " + items[j]+ " (" + entry._uid + ")");} break; case "Communities": matchPattern = entry.matchJournal(items[j]); if (matchPattern) { console.log("comm: " + items[j] + " (" + entry._uid + ")");} break; } matchPattern = (type == FilterType.EXCLUDE) ? matchPattern : !matchPattern; if(matchPattern) { entry._matchedArray.push(items[j]); entryMatches.push(items[j]); // todo: do I need both of these? } if ((entryMatches.length > 0) && (j == len-1) && !matches[entry._uid]) { entry.setState(nodeState); matches[entry._uid] = entry; console.log(entry); } } } return matches; } }; // Entry constructor var Entry = function(content, state) { this._content = content; this._state = state; this._node = this.getEntry(); this._innerHtml = this._content.innerHTML; this._url = this.getEntryLink(); this._matchedArray = []; this._matchedString = ""; this._poster = this.getPoster(); this._journal = this.getJournal(); this._uid = this.getEntryId(); this._hasLinks = this.insertInteraction(); }; Entry.prototype = { _content: null, _node: null, _state: null, _url: null, _matchedArray: null, _matchedString: null, _uid: null, _innerHtml: null, _poster: null, _journal: null, _hasLinks: false, setMatchedString: function() { for (var i = 0; i < this._matchedArray.length; i++) { var connector = ""; if (i !== 0) { connector = ", "; } this._matchedString += connector + this._matchedArray[i]; } }, getEntry: function() { return $(this._content).parents(".entry-wrapper"); }, matchPoster: function(poster) { return this._poster == poster; //return this._poster.match(new RegExp(poster + "$", "i")); }, matchJournal: function(journal) { return this._journal == journal; //return this._journal.match(new RegExp(journal + "$", "i")); }, getPoster: function() { var classes = this._node.attr("class"); var regex = new RegExp("poster-[a-z]*", "i"); return classes.match(regex)[0].split("-")[1]; }, getJournal: function() { var classes = this._node.attr("class"); console.log(this._journal); var regex = new RegExp(/journal-[a-z0-9\_]+/g); var jMatches = classes.match(regex); for (var i = 0, len = jMatches.length; i < len; i++) { if (jMatches[i] !== "journal-type") { return jMatches[i].split("-")[1]; } } return false; }, getEntryLink: function() { return this._node.find(".entry-permalink a").attr("href"); }, getEntryId: function() { return this._poster + this._node.attr("id").match(new RegExp("[0-9]*$"))[0]; }, setState: function(state) { switch(state){ case NodeState.CUT: this.cut(); break; case NodeState.COLLAPSED: this.collapse(); break; case NodeState.HIDDEN: this.hide(); break; default: this.expand(); break; } }, // sets up the links on the title of the entry to manually collapse/expand/hide an entry: mostly from foxfirefey's code insertInteraction: function() { var that = this; if (!this._hasLinks) { // This is so people can customize it to be [] or {} var link_left_bracket = "["; var link_right_bracket = "]"; // This is so people can customize to say "collapse" or "expand" or "delete" var link_collapse = "-"; var link_expand = "+"; var link_hide = "x"; // Create a span containing the collapse and hide link // foxfirefey's code: var span, collapse_link, collapse_link_text, hide_link, state_link_text; var link_span = document.createElement("span"); link_span.className = "entryCollapseState"; link_span.id = "entryCollapseState-" + this._uid; var state_link = document.createElement("a"); state_link.href = "#"; // Figure out which action link to create -- collapse or expand if ( this._state == NodeState.COLLAPSED ) { state_link.addEventListener('click', function(event) { event.stopPropagation(); event.preventDefault(); GM_deleteValue(that._uid); that.expand(); }, true); state_link_text = link_expand; state_link.className = "entry-expand-link"; state_link.title = "Expand entry"; } else { state_link.addEventListener('click', function(event) { GM_setValue(that._uid, NodeState.COLLAPSED); that.collapse(); event.stopPropagation(); event.preventDefault(); }, true); state_link_text = link_collapse; state_link.className = "entry-collapse-link"; state_link.title = "Collapse entry"; } state_link.appendChild( document.createTextNode(state_link_text) ); link_span.appendChild( document.createTextNode(" ")); link_span.appendChild( document.createTextNode(link_left_bracket) ); link_span.appendChild( state_link ); link_span.appendChild( document.createTextNode(link_right_bracket )); hide_link = document.createElement("a"); hide_link.href = "#"; hide_link.addEventListener('click', function(event) { GM_setValue(that._uid, NodeState.HIDDEN); that.hide(); event.stopPropagation(); event.preventDefault(); }, true); hide_link.title = "Hide entry"; hide_link.appendChild( document.createTextNode(link_hide) ); link_span.appendChild( document.createTextNode(" ")); link_span.appendChild( document.createTextNode(link_left_bracket) ); link_span.appendChild( hide_link ); link_span.appendChild( document.createTextNode(link_right_bracket )); this._node.find(".entry-title").append( link_span ); this._hasLinks = true; } else { this._node.find("span.entryCollapseState").remove(); this._hasLinks = false; this.insertInteraction(); } return this._hasLinks; }, // uses a DW-esque cut to cut an entry // can't use an actual dw cut atm ($.dw.cuttag) cut: function() { this._state = NodeState.CUT; this.setMatchedString(); this._content.innerHTML = 'Expand( entry text contains: '+ this._matchedString +' )'; this.initCut(); }, // make the button clickable initCut: function() { var linkButton = $(this._content).find("a.cuttag-action-before"); var hiddenContent = $(this._content).find("div.keyword-hidden-entry"); linkButton.bind("click", function(e) { e.preventDefault(); var isHidden = ($(this._content).find(".keyword-hidden-entry:hidden").length > 0); var buttonState = isHidden ? "expand" : "collapse"; linkButton.find("img").attr("src", "http://www.dreamwidth.org/img/" + buttonState + ".gif"); hiddenContent.toggle(); }); }, // completely hides an entry from view on the page hide: function() { this._state = NodeState.HIDDEN; this._node.hide(); }, // hides "collapseable" values in an entry instead of making a cut-- smaller footprint collapse: function() { this._state = NodeState.COLLAPSED; for ( var collapse_index in collapseables ) { var collapse = this._node.find("." + collapseables[collapse_index]); if (collapse.length > 0) { collapse.hide(); } } this.insertInteraction(); }, // expand collapsed entries expand: function() { this._state = NodeState.EXPANDED; for ( var expand_index in collapseables ) { var expand = this._node.find("." + collapseables[expand_index]); if( expand.length > 0 ) { expand[0].style.display = ''; } } this.insertInteraction(); } }; function clearCollapsed() { var listVals = GM_listValues(); for (var i = 0, len = listVals.length; i < len; i++) { var val = listVals[i]; var state = GM_getValue(val); if (state.value == NodeState.COLLAPSED.value) { GM_deleteValue(val); } } GM_notification("Cleared collapsed. Please refresh to see changes."); } function clearHidden() { var listVals = GM_listValues(); for (var i = 0, len = listVals.length; i < len; i++) { var val = listVals[i]; var state = GM_getValue(val); if (state.value == NodeState.HIDDEN.value) { GM_deleteValue(val); } } GM_notification("Cleared hidden. Please refresh to see changes."); } function clearAll() { var listVals = GM_listValues(); for (var i = 0, len = listVals.length; i < len; i++) { var val = listVals[i]; GM_deleteValue(val); } GM_notification("Cleared all. Please refresh to see changes."); } // Object to actually instantiate and pull everything together var Init = function() { var filter = new Filter(); GM_registerMenuCommand("Clear stored hidden entries.", clearHidden); GM_registerMenuCommand("Clear stored collapsed entries.", clearCollapsed); GM_registerMenuCommand("Clear all stored entries.", clearAll); for (var f = 0, len = filters.length; f < len; f++) { var current = filters[f]; filter.match(current.listType.name, current.filterType, current.state, current.items); } }; Init(); })();