Fold comments

By Johan Sundström Last update Jun 8, 2009 — Installed 198 times. Daily Installs: 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

There are 13 previous versions of this script.

// ==UserScript==  -*- coding:utf-8 -*-
// @name           Fold comments
// @namespace      http://code.google.com/p/ecmanaut/
// @url            http://userscripts.org/scripts/source/19344.user.js
// @description    Folds comments and the like by XPath expressions.
// @require        http://ecmanaut.googlecode.com/svn/trunk/lib/gm/$x$X.js
// @require        http://ecmanaut.googlecode.com/svn/trunk/lib/addmetainfo.js
// @include        http://*
// ==/UserScript==

var debug = GM_getValue("debug", false);

var wpcomments = ['id("comments")','following-sibling::*[last()]','../ol/li'];
var lolsumptin = {
  comment:['id("comments")',
           'following-sibling::*[not(.//textarea)][last()]',
           '//div[starts-with(@id,"div-comment-")]']
};

// Map site name to fold handlers. Indices mark what to fold, values how. The
// array lists xpath expressions to start (and optional end point, if another
// node) of what to fold, the optional third argument an expression used to
// count (and show) how many somethings were folded. Both latter expressions
// may be relative to the start node. (Alternatetively, an array with a single
// element enumerating all comments can be given, from which first, last and
// count expressions will be generated.)
var data = {
  "blog.wired.com":{
    comment:['//div[@class="comment"]'],
    categorie:['//img[contains(@src,"RRheadings/hed_categories")]',
               'following-sibling::div[1]', 'following-sibling::div/div/ul/li']
  },
  "\\.wordpress\\.com$":{
    comment:['id("comments")','id("respond")/preceding-sibling::*[1]',
             'following-sibling::dl[@class]/dd']
  },
  "intertwingly.net":{
    comment:['id("comments")','following-sibling::*[@class="comment"][last()]',
             'following-sibling::*[@class="comment"]']
  },
  "blog.jonudell.net":{ comment:wpcomments },
  "www.schneier.com":{
    comment:['//div[@class="commentform"]','following-sibling::div[last()]',
             '//div[@class="commentbody"]'],
    "archived link":['//h3[.="Recent Entries"]','following-sibling::ul[3]',
                     '../ul/li']
  },
  "webkit.org":{
    comment:wpcomments,
    "archive link":['//li[@class="subtitle" and .="Archives"]',
                    'following-sibling::li[last()]',
                    'following-sibling::li']
  },
  "userscripts.org":{
    comment:['//table[@class="comments posts wide"]',,'tbody/tr[@id]']
  },
  "www.antiwar.com":{
    comment:['id("comments")', '//div[contains(@class,"commentlist")]',
             '..//div[not(@id="addcomment") and starts-with(@class,"comment")]']
  },
  "www.guardian.co.uk":{
    "related article":['id("related-info")/h2',
                       'id("related-articles")',
                       'id("related-articles")/ul/li']
  },
  "www.independent.co.uk":{
    comment:['//div[@id="comments"]',,
	     './/div[@class="comments-body"]//div[@class="comment-body"]']
  },
  "ejohn.org":{
    comment:['id("comments")','id("commentlist")','id("commentlist")/li']
  },
  "radaronline.com":{
    comment:['//div[@class="entry-footer"]',,'.//div[@class="commentBody"]']
  },
  "www.expressen.se":{
    article:['(id("article")/following-sibling::*)[1]',
             'following-sibling::*[last()]',
             '../div[starts-with(@class,"slot ")]'],
    block:['id("rightcol")',,'div[starts-with(@class,"block")]'],
    left:['id("nav-wrap")',,'.//div[@class="contentpush"]']
  },
  "www.vk.se":{
    snippet:['//div[@class="column_tertiary"]',,'h5'],
    comment:['//h2[.="Kommentarer"]','(following-sibling::div)[1]',
             '//table[@class="commenttable"]'],
    teaser:['//div[@class="teaser"]','(//div[@class="teaser"])[last()]',
            '//div[@class="teaser"]'],
    telegram:['//div[@class="column_secondary"]',,'ul/li'],
    blahblah:['//div[@class="column_tertiary last"]'],
    menuitem:['id("mainmenu")','//ul[@class="submenu"]','li']
  },
  "\\.theoildrum\\.com$":{
    comment:['id("comments")','id("footer")','.//div[@class="comment"]']
  },
  "www.okcupid.com":{
    visitee:['id("lsRecent")','id("lsRecentContent")',
             'id("lsRecentContent")/a'],
    favorite:['id("lsFavorites")','id("lsFavoritesContent")']
              //'id("lsFavoritesContent")/ul[not(@style)]/li']
  },
  "paulgraham.com":{
    note:['//b[.="Notes"]',
          '//a[starts-with(@name,"f")][last()]/following-sibling::br[1]',
          '../a[starts-with(@name,"f")]'],
    comment:['id("dsq-comments-count")',
             'id("dsq-pagination")',
             '//*[@class="dsq-comment"]']
  },
  "www.codinghorror.com":{
    comment:['//div[@class="comments-head"]',
             '//div[@class="comments-body"][last()-1]',
             '//div[@class="comments-body"][position()<last()]']
  },
  "\\.blogspot\\.com$":{
    comment:['id("comments")',,'id("comments-block")//dt[starts-with(@id,"c")]']
  },
  "www.antipope.org":{
    comment:['id("comments")',,'div[@class="comment"]']
  },
  "blogg.aftonbladet.se":{
    comment:['id("comments")/h2','../dl[last()]','../dl']
  },
  "tianmi.info":{
    comment:['//*[name()="h3" and .="Kommentarer:"]',
             '//*[name()="a" and @name="form"]',
             '//*[name()="div" and @class="comment"]'],
    "old post":['//*[name()="h3"][*[@src="/blogge/img/artiklar.png"]]',
                '../*[name()="p"][following-sibling::*[1][name()="div"][*[name()="form"]]]',
                '../*[name()="p" and not(@class)][not(preceding-sibling::*[name()="div"][*[name()="form"]])]'],
    "left column filler":['//*[parent::*[@class="vänster"]][2]',
                          '//*[parent::*[@class="vänster"]][last()]',
                          '//*[parent::*[@class="vänster"]][position()>1]'],
    "right column filler":['//*[parent::*[@class="höger"]][5]',
                           '//*[parent::*[@class="höger"]][last()]',
                           '//*[parent::*[@class="höger"]][position()>4]']
  },
  "www.techcrunch.com":{
    //trackback:['id("trackbacks")','following-sibling::div',]
  trackback:['(//ol[@class="commentlist"])[1]',,'li'],
    comment:['(//ol[@class="commentlist"])[2]',,'li']
    //comment:['//ol[@class="commentlist"][preceding::h3[.="Comments"]]',,'li']
  },
  "pogue.blogs.nytimes.com":{
    comment:['id("blog_comments")',,'ul/li']
  },
  "blog.brokep.com":{
    comment:['id("comments")/h3','following::dl','id("comment_list")/dt']
  },
  "www.fourhourworkweek.com":{
    comment:['id("comments")',,'id("comment_list")/*']
  },
  "hackademix.net":{
    comment:['id("comments")','following::ol','(following::ol)[1]/li']
  },
  "adblockplus.org":{
    comment:['id("comment")','following::ol','(following::ol)[1]/li']
  },
  "extjs.com":{
    comment:['id("comments")','following::ol','(following::ol)[1]/li']
  },
  "dilbert.com":{
    comment:['//div[@class="CMT_CommentList"]',,'div']
  },
  "icanhascheezburger.com":lolsumptin,
  "totallylookslike.com":lolsumptin,
  "punditkitchen.com":lolsumptin,
  "engrishfunny.com":lolsumptin,
  "onceuponawin.com":lolsumptin,
  "ihasahotdog.com":lolsumptin,
  "roflrazzi.com":lolsumptin,
  "graphjam.com":lolsumptin,
  "failblog.org":lolsumptin,
  "code.google.com": {
    comment:['id("commentlist")//*[@class="artifactcomment"]']
  },
  "blog.chromium.org": {
    comment:['id("comments")',,'id("comments")//dt[starts-with(@id,"c")]']
  },
  "vimeo.com":{
    comment:['//ul[@id and @class="comments"]',,'//ul[@id and @class="comments"]/li[@class]/a[@name]']
  }
};

var togglers = []; // all fold/unfold functions
var divs = []; // all folded sections
var host = location.hostname.toLowerCase();
var hide = data[host];
if (hide)
  debug && console.info("Folding to rule %x: %x", host, hide);
else
  for (var re in data)
    if (/[^a-z0-9.-]/i.test(re) &&
	(host.match(re) ||
	 /^xn--/.test(host) && IDNAtoASCII(host).match(re))) {
      hide = data[re];
      debug && console.info("Folding to regexp rule %x: %x", re, hide);
      break;
    }
try {
  for (var unit in hide) {
    var args = hide[unit];
    if (args.length == 1 && $x(args[0]).length > 0) {
      args[2] = args[0]; // count
      args[1] = '('+ args[0] +')[last()]';
    }
    args[3] = unit;
    fold.apply( this, args );
  }
} catch(e) {}

if (divs.length) {
  document.body.addEventListener("click", unfoldOnIntraLinkClick, true);
}

function IDNAtoASCII(hostname) {
  var a = document.createElement("a");
  a.href = "http://" + hostname + "/";
  return a.hostname;
}

function unfoldOnIntraLinkClick(e) {
  function unfoldFolded(div, n, all) {
    if (div) {
      togglers[n](!!"unfold");
    }
  }
  var a = $X('ancestor-or-self::a[1]', e.target);
  if (a) {
    debug && console.log("Unfold %s", a.hash);
    hasAnchor(a.hash).forEach(unfoldFolded);
  }
}

function getYPos(node, y) {
  y = (y || 0) + node.offsetTop;
  if (node.offsetParent)
    return getYPos(node.offsetParent, y);
  return y;
}

function hasAnchor(hash, div) {
  function hasId(root) {
    return $x('count(.//*[@id="'+ id +'" or @name="'+ id +'"])', root) && root;
  }
  function hasXPath(root) {
    var node = $X(xp);
    var root_contains_node = 1<<4;
    return (root.compareDocumentPosition(node) & root_contains_node) ? 1 : 0;
  }
  if (!hash) return [0];
  var haystack = div ? [div] : divs;
  var id = (hash || "").replace(/^#/, "");
  var xp = /^xpath:(.*)/i.match(id);
  if (xp) {
    xp = decodeURIComponent(xp[1]);
    return haystack.map(hasXPath);
  }
  return haystack.map(hasId);
}

// Seeks out the start and end node, folds the content (including the start and
// end node) and annotates the folded section with the count and type of things
// it purportedly contains, as well as how many (vertical) pagefuls it spanned.
//
// As a bonus, it uses the "count" XPath expression to mark up the page with the
// part of the pagination microformat that tracks items. (This makes my keyboard
// shortcuts script feeding off of those able to key through a comment at a time
// when unfolded, without shifting eye focus much, making skimming much easier.)
function fold( from, to, countXPath, unit ) {
  var r = document.createRange();
  var f = typeof from == "string" ? $X( from ) : from;
  var t = typeof to == "string" ? $X( to, f ) : to || f;
  var p = f.parentNode;
  var count = countXPath ? $x(countXPath, f).length : "";
  if (debug) {
    console.info("Folding %d %x to %x; %scommon parent", count, f, t,
                 p==t.parentNode ? "" : "un");
  }
  if (count === 0) return null;
  if (!f || !t || f.parentNode != t.parentNode)
    throw new Error(!f?"No from node":!t?"No to node":"Different parent nodes");

  addMeta("items-xpath", countXPath);

  r.setStartBefore(f);
  r.setEndAfter(t);
  var dy = getYPos(t) + t.offsetHeight - getYPos(f);
  var pagefuls = Math.round(dy / innerHeight);
  var div = document.createElement("div");
  r.surroundContents(div);

  var title = "Toggle "+ count +" "+ (unit || "comment") + (count==1?"":"s");
  if (pagefuls > 0)
    title += " ("+ pagefuls +" pageful"+ (pagefuls==1?"":"s") +")";
  addToggleBefore(title, div, null, div);
  var toggler = addToggleBefore(title, p, div, div);
  if (!hasAnchor(location.hash, div)[0]) toggler(); // fold unless referenced
  togglers.push(toggler);
  divs.push(div);
  return div;
}

function addToggleBefore(title, p, node, div) {
  function toggle(state) {
    if ("boolean" != typeof state)
      state = !!div.style.display;
    div.style.display = state ? "" : "none";
  }

  var a = document.createElement("a");
  a.addEventListener("click", toggle, false);
  a.style.cursor = "pointer";
  a.innerHTML = title;

  p.insertBefore(a, node);
  return toggle;
}