LJ Filter

By Xtina Last update Mar 12, 2008 — Installed 252 times. Daily Installs: 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 4, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name        LJ Filter
// @description Filters out user entries based on tags/keywords (present or absent).
// @namespace   http://the-xtina.livejournal.com/
// @include     http://*.livejournal.com/friends
// @include     http://*.livejournal.com/friends/*
// @include     http://*.livejournal.com/friends?*
// ==/UserScript==

/*
The original was created by lj:slobin, then updated by lj:dimrub.  I've updated
it here because I wanted this to work for all styles (or as many as I can get
away with), and I'm impatient.
*/

var DEBUG = false;

var DOCS = "# To only view posts with certain tags:\n"
         + "#   username: +tag1 +tag2\n"
         + "#\n"
         + "# To never see posts with certain tags:\n"
         + "#   username: -tag1 -tag2\n"
         + "#\n"
         + "# To include spaces in tags:\n"
         + "#   username: -tag~with~spaces\n"
         + "#\n"
         + "# To filter by keywords rather than tags,\n" 
         + "# use -- and ++ instead of - and +.\n";

var ITER = XPathResult.ORDERED_NODE_ITERATOR_TYPE;
var SNAP = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
var NODE = XPathResult.FIRST_ORDERED_NODE_TYPE;

var filter, errors, editWindow;

function debug(message)
{
  if (DEBUG) GM_log(message);
}

/*
'query' is a valid Xpath expression.
'root' is the node relative to which the query is performed (usually document).
'type' is one of the following:
- ITER = XPathResult.ORDERED_NODE_ITERATOR_TYPE;
- SNAP = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
- NODE = XPathResult.FIRST_ORDERED_NODE_TYPE;

This returns 'undefined' if the query has failed.
*/

function xpath(query, root, type)
{
  var res;
  try {
    res = document.evaluate(query, root, null, type, null);
  } catch (e) {
    debug("Failed to run \"" + query + "\", got exception <" + e.name + ">, error was: <" + e.message + ">");
  } finally {
    return res;
  }
}

function setRules(rules)
{
  GM_setValue("rules", escape(rules));
}

function getRules(defvalue)
{
  return unescape(GM_getValue("rules", defvalue));
}

function parseRules(rules)
{
  debug("parseRules");
  filter = new Object();
  errors = new Array();
  rules = rules.split("\n");
  for (var i = 0; i < rules.length; i++) {
    var line = rules[i];
    if (!line.length || line[0] == '#') continue;

    var pair = line.split(/:/, 2);
    var user = pair[0];
    var tags = pair[1];
    if (tags == undefined) {
      errors.push({line: i+1, message: "Line must be in this format: <username>: <list of tags/keywords>"});
      return;
    }
    filter[user] = new Object();
    var userFilter = filter[user];
    userFilter.has_positive = false;
    userFilter.positive = new Object();
    userFilter.negative = new Object();
    userFilter.positive_kw = new Object();
    userFilter.negative_kw = new Object();
    tags = tags.split(/ +/);
    for (var j = 0; j < tags.length; j++) {
      var tag = tags[j];
      tag = tag.replace(/~/g, " ");
      if (tag.length) {
        switch (tag[0]) {
        case "+":
          if (tag[1] == '+') {
            userFilter.has_positive = true;
            userFilter.positive_kw[tag.slice(2)] = true;
          } else {
            userFilter.has_positive = true;
            userFilter.positive[tag.slice(1)] = true;
          }
          break;
        case "-":
          if (tag[1] == '-') {
            userFilter.negative_kw[tag.slice(2)] = true;
          } else {
            userFilter.negative[tag.slice(1)] = true;
          }
          break;
        default:
          errors.push({line: i+1, message: "Unrecognized symbol at the beginning of a tag/keyword."});
          break;
        }
      }
    }
  }
}

function applyFilter()
{
  debug("applyFilter");

  var parents = Array();
  var elements = Array();
  var recognized = false;

  function remember(parent, element)
  {
    parents.push(parent);
    elements.push(element);
  }

/*
The description of a style consists of the following fields:
- 'name': Name of the style.
- 'entryXpath': The XPath to the entry's node (the uppermost node that the
whole single entry is contained within).
- 'userXpath': The XPath to the node containing the username, relative to the
entry's XPath above.
- 'tagXpath': The XPath to the node containing the tags for the entry, relative
to the entry's XPath above.
- 'textXpath': The XPath to the entry's text, relative to the entry's XPath
above.
- 'matchXpath': The XPath that, if returns true, uniquely identifies this
style.
- 'removeFunction': A function that given an entry node, removes it and any
other node pertaining to the entry.

This is a full list of the basic journal styles.  Items with slashes are
similar enough to not have to separate them.  The key:

- 'u': It works for a username only.
- '*': It works for both a user and a community, woo!
- 'x': I can't get it to work and I don't know *why*.  Nrr.

You can see the styles here:  http://www.livejournal.com/customize/

* 3 Column
* A Novel Conundrum
x A Sturdy Gesture [can't get the user]
* Bloggish
u Classic
* Clean and Simple
x Component
u Cuteness Attack
* Digital Multiplex (OSWD)
* Disjointed
* Expressive / Expressive Winter / Mixit
* Flexible Squares
* Generator
* Gradient Strip
u Haven [users in a post have span:ljuser; post text/header foo is together]
* Magazine
* Nebula
* Notepad / Punquin Elegant
u Opal (Libra OSWD)
* Dear Diary... / Quite Lickable
* Refried Paper
* Smooth Sailing
* Tabular Indent
u The Boxer [aren't we in the future yet?? containers!!]
u Tranquility II
x Unearthed
x Variable Flow

Currently, I'm testing them one at a time.  There may be conflicts with others,
frex if the content holder for one is the same as the other.  Let me know and
I'll fixinate it.

Testing involves checking against a user and a community, tags and text.
*/

  var styles = [

    // 3 Columns
    {
      name: '3 Columns',
      entryXpath: '//div[@class="comment_wrapper"]',
      userXpath: './/div[@class="comment_postedby"]//a/b/text()',
      tagXpath: './/div[@class="ljtags"]//text()',
      textXpath: './/div[@class="entrytext"]',
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.previousSibling.previousSibling);
        remember(par, entry.previousSibling);
        remember(par, entry);
        remember(par, entry.nextSibling);
        remember(par, entry.nextSibling.nextSibling);
      }
    },

    // A Novel Conundrum
    {
      name: 'A Novel Conundrum',
      entryXpath: '//div[@class="bodybox"]',
      userXpath: './/div[@class="author"]/a[last()]/text()',
      tagXpath: './/div[@class="ljtags"]//text()',
      textXpath: './/div[@class="body"]',
      removeFunction: function(entry) {
        par = entry.parentNode[3];
        remember(par, entry);
      }
    },

/*
    // A Sturdy Gesture
    {
      name: 'A Sturdy Gesture',
      entryXpath: "//div[@class='box']",
      userXpath: ".//span[@class='ljuser']//text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel=\'tag\']/text()",
      textXpath: ".//div[@class='entry']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },
*/

    // Bloggish
    {
      name: 'Bloggish',
      entryXpath: "//div[@class='alpha-inner']/div[@class='entry']",
      userXpath: ".//p[@class='poster']/span[last()]//b/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='entry-content']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

/*
    // Classic
    {
      name: 'Classic',
      entryXpath: "//span[@class='view_links2']/table",
      userXpath: ".//strong/a/text()",
      tagXpath: ".//p/a[@rel='tag']//text()",
      textXpath: ".//td[@class='entry']",
      removeFunction: function(entry) {
        par = entry.parentNode[3];
        remember(par, entry);
      }
    },
*/

    // Clean and Simple
    {
      name: 'Clean and Simple',
      entryXpath: "//div[@id='entries']/div[@class='day']/div[@class='entry']",
      userXpath: ".//span[@class='entryheading']/a/text()",
      tagXpath: ".//div[@class='entrytext']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='entrytext']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },

/*
    // Component
    {
      name: 'Component',
      entryXpath: "//table[tbody/tr/td/@class='entryHolderBg']",
      userXpath: ".//span[@class='ljuser']/a/b/text()",
      tagXpath: ".//p/a[@rel='tag']/text()",
      textXpath: ".//td[@class='entryHolderBg'][3]//td[@class='entry']/div[not(contains(@class, 'entry'))]", //div",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },
*/

    // Cuteness Attack
// There's a[@class=user], then there's a[@class=comm].  Hm.
    {
      name: "Cuteness Attack",
      entryXpath: "//div[@id='entries-content']/div[@class='ind-entry']",
      userXpath: ".//a[@class='user']/text()",
      tagXpath: ".//div[@class='ljtags']//text()",
      textXpath: ".//div[@class='entry-item']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },

/*    // Dear Diary... / Quite Lickable
    {
      name: 'Dear Diary... or Quite Lickable',
      entryXpath: "//div[@class='day']/div[@class='entry']",
      userXpath: ".//div[@class='entryposter']/span[last()]//b/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='entrycontent']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },
*/

    // Digital Multiplex (OSWD)
    {
      name: 'Digital Multiplex (OSWD)',
      entryXpath: "//table[@class='full_entry']",
      userXpath: ".//table[@class='full_entry_userpic']/tbody/tr/td/span[last()]//b/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//tbody/tr/td[2]",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.previousSibling.previousSibling.previousSibling.previousSibling.previousSibling);
        remember(par, entry.previousSibling.previousSibling.previousSibling.previousSibling);
        remember(par, entry.previousSibling.previousSibling.previousSibling);
        remember(par, entry.previousSibling.previousSibling);
        remember(par, entry.previousSibling);
        remember(par, entry);
      }
    },

    // Disjointed
    {
      name: 'Disjointed',
      entryXpath: "//table[@class='entries']",
      userXpath: ".//td[@class='altposter']/a/text()",
      tagXpath: ".//table[@class='currbox']/tbody/tr/td/div[2]/a/text()",
      textXpath: ".//td[@class='entrybox']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Expressive / Expressive Winter / Mixit
    {
      name: 'Expressive, Expressive Winter, or Mixit',
      entryXpath: "//div[@class='post-asset asset']",
      userXpath: ".//div[@class='user-icon']/span[last()]/span[@class='ljuser']//b/text()",
      tagXpath: ".//ul[@class='asset-tags-list']//a[contains(@href, 'tag')]/text()",
      textXpath: ".//div[@class='asset-content']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Flexible Squares - checked (sciuro)
    {
      name: 'Flexible Squares',
      entryXpath: "//div[@class='subcontent']",
      userXpath: ".//div[@class='userpicfriends']//a[last()]/font/text()",
      tagXpath: ".//a[@rel='tag']/text()",
      textXpath: ".//div[@class='entry_text']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
        remember(par, entry.nextSibling);
        remember(par, entry.nextSibling.nextSibling);
      }
    },

    // Generator
    {
      name: 'Generator',
      entryXpath: '//table[@class="entrybox"]',
      userXpath: './/a[@class="index"]//text()',
      tagXpath: './/a[@rel="tag"]/text()',
      textXpath: './/tbody/tr/td/table/tbody/tr[2]',
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Gradient Strip
    {
      name: 'Gradient Strip',
      entryXpath: '//div[@class="contentholder"]',
      userXpath: './/span[last()][@class="ljuser"]//b/text()',
      tagXpath: './/a[@class="tag"]/text()',
      textXpath: './/div[@class="contentbody"]',
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },

    // Haven
    {
      name: 'Haven',
      entryXpath: "//div[@class='content']/div[@class='entry']",
      userXpath: ".//span[@class='ljuser']//b/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='text']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.previousSibling.previousSibling);
        remember(par, entry.previousSibling);
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Magazine
    {
      name: 'Magazine',
      entryXpath: "//div[@class='H3Holder']",
      userXpath: ".//div[@class='Picture']/div/a/small/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//p",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },

    // Nebula
    {
      name: 'Nebula',
      entryXpath: "//div[@id='content']/div[@class='entry']",
      userXpath: ".//span[@class='subHeading']/span[last()]//b/text()",
      tagXpath: ".//span[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='entryText']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.previousSibling.previousSibling);
        remember(par, entry.previousSibling);
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Notepad / Punquin Elegant
    {
      name: 'Notepad or Punquin Elegant',
      entryXpath: "//table[@class='entry']",
      userXpath: ".//a[1]/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//tbody/tr/td[2]",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.nextSibling);
        remember(par, entry);
      }
    },

/*
    // Opal (Libra OSWD)
    {
      name: 'Opal (Libra OSWD)',
      entryXpath: "//div[@class='entries']/div[@class='entry']",
      userXpath: ".//a[@class='friendname']/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='entrytext']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },
*/

    // Refried Paper
    {
      name: 'Refried Paper',
      entryXpath: "//div[@class='entry']",
      userXpath: ".//span[@class='ljuser']//b/text()",
      tagXpath: ".//table[1]//table[1]//td[last()]/a/text()",
      textXpath: ".//p",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },

    // Smooth Sailing
    {
      name: 'Smooth Sailing',
      entryXpath: "//div[@class='entryHolder']",
      userXpath: ".//div[@class='entryUserinfo-username']/span[last()]//b/text()",
      tagXpath: ".//span[@class='entryMetadata-content']/a[contains(@href, 'tag')]/text()",
      textXpath: ".//div[@class='entryText']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.previousSibling.previousSibling);
        remember(par, entry.previousSibling);
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Tabular Indent
    {
      name: 'Tabular Indent',
      entryXpath: '//h3[@class="page-header"]/following-sibling::div',
      userXpath: './/div[1]/a[last()]/text()',
      tagXpath: ".//a[@rel='tag']/text()",
      textXpath: './/table',
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry.previousSibling);
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // The Boxer
    {
      name: 'The Boxer',
      entryXpath: "//table[@class='new']",
      userXpath: ".//a[1]/text()",
      tagXpath: ".//a[@rel='tag']/text()",
      textXpath: ".//div[@class='entrycontent']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
        remember(par, entry.nextSibling);
      }
    },

    // Tranquility II
    {
      name: 'Tranquility II',
      entryXpath: '//div[@class="ind-entry"]',
      userXpath: './/span[@class="ljuser"]//b/text()',
      tagXpath: './/div[@class="ljtags"]//text()',
      textXpath: './/div[@class="entry-item"]',
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },

/*
    // Unearthed
    {
      name: 'Unearthed',
      entryXpath: "//table[@class='DropShadow']",
      userXpath: ".//td[@class='BoxSideBarContents']/span[last()]/a[2]/b/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//td[@class='BoxContents']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    },
*/

/*
    // Variable Flow
    {
      name: 'Variable Flow',
      entryXpath: "//div[@id='page']/div[@class='day']/div[@class='entry']",
      userXpath: ".//h2[@class='entryheading']/span[2]//b/text()",
      tagXpath: ".//div[@class='ljtags']/a[@rel='tag']/text()",
      textXpath: ".//div[@class='entrytext']",
      removeFunction: function(entry) {
        par = entry.parentNode;
        remember(par, entry);
      }
    }
*/

  ];

  for (var i = 0; i < styles.length; i++) {
    var style = styles[i];
    debug("Style name: " + style.name);
    var allEntries = xpath(style.entryXpath, document, ITER);
    if (!allEntries) continue;
    while (entryNode = allEntries.iterateNext()) {
      if (!recognized) debug("The style is probably " + style.name + ".");
      recognized = true;

      var userNode = xpath(style.userXpath, entryNode, NODE);
      if (userNode) {
        var user = userNode.singleNodeValue.nodeValue;
        debug("LJ user: " + user);
      } else {
        debug("Didn't find a username!");
        continue;
      }

      var userFilter = filter[user];
      if (userFilter) {
        var positive = !userFilter.has_positive;
        var negative = false;
        var allTags = xpath(style.tagXpath, entryNode, ITER);
        var tagNode;
        while (allTags && (tagNode = allTags.iterateNext())) {
          var tag = tagNode.nodeValue;
          debug("Found tag: " + tag);
          if (userFilter.positive[tag]) positive = true;
          if (userFilter.negative[tag]) negative = true;
        }
        if (style.textXpath) {
          var textNode = xpath(style.textXpath, entryNode, NODE);
          if (textNode) {
            var text = textNode.singleNodeValue.innerHTML;
            debug("Entry text: " + text);
            for (var kw in userFilter.positive_kw) {
              debug("Looking up the keyword <" + kw + ">.");
              if (text.indexOf(kw) >= 0) {
                positive = true;
                debug("Found!");
              }
            }
            for (var kw in userFilter.negative_kw) {
              debug("Looking up the keyword <" + kw + ">.");
              if (text.indexOf(kw) >= 0) {
                negative = true;
                debug("Found!");
              }
            }
          }
        }
        if (!positive || negative) {
          debug("Removing the offending entry.");
          style.removeFunction(entryNode);
        }
      }
    }
    if (recognized) break;
  }

  for (var i = 0; i < parents.length; i++) {
     parents[i].removeChild(elements[i]);
  }
}

function editRules()
{
  debug("editRules");
  editWindow = window.open("about:blank", "_blank",
               "width=500, height=400, resizable=1");
  editWindow.document.writeln("<form name='edit' action='about:blank'>");
  editWindow.document.writeln("<textarea name='rules' cols='50' rows='15'>");
  editWindow.document.writeln(getRules());
  editWindow.document.writeln("</textarea>");
  if (errors.length) {
    for (var i = 0; i < errors.length; i++) {
      editWindow.document.writeln("<P>Error in line: " + errors[i].line + ": " + errors[i].message + "</P>");
    }
  }                 
  editWindow.document.writeln("<p><input type='submit' value='Submit'>");
  editWindow.document.writeln("</form>");
  editWindow.document.close();
  var form = editWindow.document.forms.namedItem("edit");
  form.addEventListener("submit", processForm, true); 
  editWindow.focus();
}

function processForm()
{
  debug("processForm");
  var form = editWindow.document.forms.namedItem("edit");
  var elem = form.elements.namedItem("rules");
  var rules = elem.value;
  rules = rules.replace(/\n*$/, "\n");
  setRules(rules);
  parseRules(rules);
  editWindow.close();
  if (errors.length) {
    editRules();
  } else {
    location.reload();
  }
}

GM_registerMenuCommand("Edit LJ filters", editRules);
setRules(getRules(DOCS));
parseRules(getRules());
applyFilter();