Google Readerにはてなブックマーク件数とブックマーク用アイコンを表示

昨日スクリプトを改良して、ブックマーク登録用のアイコンも表示するようにしてみた。

installする

  • (2006.10.6 バグ修正: replace(/&/, "&") → replace(/&/g, "&"))
  • (2006.10.14 @includeにhttpsを追加, リンク先変更)
// ==UserScript==
// @name          GR+?B
// @namespace     http://d.hatena.ne.jp/nozom/
// @description   show ?B button and count in Google Reader
// @include       http://www.google.com/reader/view/*
// @include       https://www.google.com/reader/view/*
// ==/UserScript==

// original author is id:kusigahama
// see http://d.hatena.ne.jp/kusigahama/20051207#p1

(function() {
  var timerID;
  var busy = false;
  var url2count = new Object();

  String.prototype.htmlescape = function() {
    return this.replace(/&/g, "&amp;").replace(/</g, "&lt;");
  }

  function setBookmarkCount(targetNode, href, count) {
    var str = (count > 0 ? "" + count : "no") + " user" + (count > 1 ? "s" : "");
    var a = document.createElement("a");
    a.setAttribute('href', "http://b.hatena.ne.jp/entry/" + href);
    a.setAttribute('target', '_blank');
    a.appendChild(document.createTextNode(str));
    with (a.style) {
      fontSize = "0.9em";
      textDecoration = "none";
      if (count >= 5) {
        fontWeight = "bold";
        backgroundColor = "#fff0f0";
        color = "#f66";
      }
      if (count >= 10) {
        backgroundColor = "#ffcccc";
        color = "red";
      }
    }

    with (targetNode) {
      appendChild(document.createTextNode(" ("));
      appendChild(a);
      appendChild(document.createTextNode(") "));
    }
  }

  function setBookmarkCounts(titleArray) {
    for (var i = 0; i < titleArray.length; i++) {
      var href = titleArray[i].href;
      var title = titleArray[i].node;
      var titleStr = titleArray[i].titleStr;
      var count = url2count[href];
      if (count != null) {
        var node = document.createElement('span');
        node.className = 'hatena-bookmark-count';
        if (count > 0) {
          setBookmarkCount(node, href, count);
        } else {
          node.appendChild(document.createTextNode(' '));
        }
        var a = document.createElement('a');
        a.setAttribute('href', 'http://b.hatena.ne.jp/add?mode=confirm&is_bm=1&title=' + escape(titleStr) + '&url=' + escape(href));
        a.setAttribute('target', '_blank');
        var img = document.createElement('img');
        img.setAttribute('src', 'http://d.hatena.ne.jp/images/b_entry.gif');
        img.setAttribute('alt', titleStr);
        with (img.style) {
          borderWidth = '0px';
        }
        a.appendChild(img);
        node.appendChild(a);
        title.insertBefore(node, title.childNodes[1]);
      }
    }
  }

  function callXmlrpc(requestbody, titleArray) {
    const endpoint = "http://b.hatena.ne.jp/xmlrpc";
    function onload(response) {
      if (response.responseText.match(/<fault>/)) {
        clearInterval(timerID);
        alert("xmlrpc call failed: " + response.responseText + "\n" + "request: " + requestbody);
      } else {
        var pattern = /<name>([^<]+)<\/name>\s*<value><int>(\d+)/g;
        var m;
        while (m = pattern.exec(response.responseText)) {
          url2count[m[1]] = m[2];
        }
        setBookmarkCounts(titleArray);
      }
      busy = false;
    }

    // alert('xmlrpc call');
    GM_xmlhttpRequest({ method: "POST", url: endpoint, data: requestbody, onload: onload });
  }

  function greader_add_bookmark_count() {
    if (busy) return;

    var titles = document.evaluate('//h2[@class="entry-title"]', document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    if (! titles.snapshotLength) return;

    busy = true;

    var titleArray = new Array();
    var reqUrlArray = new Array();
    var reqUrlHash = new Object();
    for (var i = 0; i < titles.snapshotLength; i++) {
      var title = titles.snapshotItem(i);
      var nodes = title.childNodes;
      if ((nodes == null) ||
          (nodes[1] == null) ||
          (nodes[1].tagName != 'SPAN') ||
          (nodes[1].className != 'hatena-bookmark-count')) {
        var link = null;
        var titleStr = '';
        if (title.firstChild.tagName == 'A') {
          // entry-container (Expanded view or Collapsed item)
          link = title.firstChild;
          titleStr = link.firstChild.textContent;
        } else {
          // entry (List view)
          link = title.parentNode.parentNode.firstChild;
          if (link.tagName != 'A') link = null;
          titleStr = title.textContent;
        }
        if (link != null) {
          titleArray.push({ node: title, href: link.href, titleStr: titleStr });
          if ((url2count[link.href] == null) &&
              (! reqUrlHash[link.href])) {
            reqUrlHash[link.href] = true;
            reqUrlArray.push(link.href);
          }
        }
      }
    }
    if (titleArray.length == 0) {
      busy = false;
      return;
    }

    if (reqUrlArray.length == 0) {
      // all items are found in the cache
      setBookmarkCounts(titleArray);
      busy = false;
    } else {
      var request = '<?xml version="1.0"?>\n<methodCall>\n<methodName>bookmark.getCount</methodName>\n<params>\n';
      for (var i = 0; i < reqUrlArray.length; i++) {
        // avoid xmlrpc call failure in 'too many params' error
        if (i > 20) break;
        var href = reqUrlArray[i];
        request += "<param><value><string>" + href.htmlescape() + "</string></value></param>\n";
      }
      request += "</params>\n</methodCall>\n";
      callXmlrpc(request, titleArray);
    }
  }

  // be careful not to be too busy
  timerID = setInterval(greader_add_bookmark_count, 3000);
})();

今までGoogle Reader上で読んでいる記事をブックマークしたいと思ったときは一度元ページを表示してから登録用のブックマークレットを実行していたのだけど、これでその手間が省けるのでとても便利になった。