livedoor Reader で特定のサイトや記事の本文を非表示にする

折り畳みの自動化

フィードに全文掲載云々という問題が言われたりしますが、それとは逆に、全文掲載してくれているんだけれど、もの凄く長文かつ大量の更新が行われるような (しかも読みにくかったりする) フィードもあるのではないかと思います。

そんなフィードを、畳んだ状態で表示して読みたいものだけをブラウザを開いて読む、という方法を考えてみました。

おなじみフックです:

register_hook("before_printfeed", function(feed) {
  var link = feed.channel.link;
  var fold_it = /(?:jp.rubyist.net\/magazine)/;
  (fold_it.test(link) ? addClass : removeClass)("right_body", "compact");
})

これだけです。

ただ、これだと正規表現にマッチしないフィードは強制的に折り畳み解除されてしまいますので、手作業 (c キー) での折り畳み状態を保てなくなります。

実際の利用においては殆ど支障無いかも知れませんが、もうちょっと丁寧に、スクリプトで畳んだのかどうかを記憶するようにしてみましょう:

register_hook("before_printfeed", function(feed) {
  var self = arguments.callee;
  var link = feed.channel.link;
  var fold_it = /(?:jp.rubyist.net\/magazine)/;

  if (fold_it.test(link)) {
    if (!hasClass("right_body", "compact")) {
      self.i_folded = true;
      addClass("right_body", "compact");
    }
  }
  else if (self.i_folded) {
    self.i_folded = false;
    removeClass("right_body", "compact");
  }
})

「この関数が折り畳んだ」ということを "i_folded" という変数で記憶しています。

変数を関数自身 (arguments.callee) のプロパティーとして保存しているんですが、これは C の static や perl の our に相当するテクニックと言って良いでしょう。外のスコープに変数を作るよりも、一つの関数で自己完結させたい場合に使える方法です。

コンテンツ・フィルタ

不用な記事を削除してしまう、ということも同様にフックで行うことが出来ます:

register_hook("before_printfeed", function(feed) {
  var del_it = /ngword/;
  feed.items.forEach(function(item) {
    if (del_it.test(item.body))
      item.body = "Aboned!";
  });
});

これは書き換えの例ですが、削除はと言いますと、記事を配列 (feed.items) 自体から削除してしまうと過去記事の表示がずれてしまうので良くないです。

なので、逆に表示された記事を消す、というアプローチを考えましょう。表示「後」のイベントを使ってみます:

register_hook("after_printfeed", function(feed) {
  var del_it = /ngword/;
  feed.items.forEach(function(item) {
    if (del_it.test(item.body))
      close_item(item.id);
  });
});

"close_item" というのが記事を削除する関数です。LDR 内部では未使用であるにもかかわらず、存在しています。まさにこういう目的のために用意してくれているかのようです。

ただ、"after_printfeed" と言っても完全に表示が終わってから発生するわけではないんですね。「遅延表示」が行われるためです。ですから、表示されるのを待ってから削除する必要があります:

register_hook("after_printfeed", function(feed) {
  var del_it = /ngword/;
  feed.items.forEach(function(item) {
    if (del_it.test(item.body))
      (function(id) {
        var self = arguments.callee;
        try {
          close_item(id);
        }
        catch (e) {
          setTimeout(function(){self(id)}, 500);
        }
      })(item.id);
  });
});

記事毎に一人ひとり削除屋 (無名関数) をディスパッチする、というイメージです。

が、まだ若干の不備が残っています。未読数が表示数を超えて存在した場合の対策が無いんです。つまり、永久に setTimeout で再帰呼び出しされる関数が生じる可能性があるわけです (表示の途中で過去記事や別のフィードに移った場合も同様です)。

また、サイト毎に NG ワードを指定できるようにするなど、工夫の余地はいくらでもありそうです。続きはお任せします。


関連記事:
livedoor Reader で記事のリンク先を書き換える


追記 [20060716]:

記事を close_item で削除してしまうと、それ以降の記事の番号 (get_active_item 関数が返す offset 値) がずれてしまうことに気付きました。

これは、offset の値を利用する部分で不具合が生じる可能性がある、ということです。例えば

$('item_count_' + item.offset)

のようなものが正しく機能しなくなります。

これから LDR 本体の動作に支障が出ないか調査したいと思います。

と思ったら見つかりました。私は使っていないんですが、現在記事をハイライトする関数が正常に機能しなくなると思います。

他にも同様の問題が生じる Greasemonkey スクリプトがあるはずです。ご注意ください。

回避策としては、

  • 削除しない
  • offset の代わりに id を利用するようにスクリプトを変更する

のいずれかの方法が考えられます。


追記 [20060720]:

?B の、タグ「それPla」を含む注目エントリー (の RSS) で以下の記事を拝見しました:
小野和俊のブログ:はてなブックマークにフィルター機能が欲しい
また君か。@d.hatena - はてなブックマークの注目エントリを読まなすぎる

LDR 登場以前からも「?B の RSS を任意のタグでフィルタリングしたい」という要望はちらほら目にしていましたが、上記の様なスクリプトを作れば実現可能になります (それGre)。

上では本文 (body) のみのフィルタリングの例でしたが、他にも以下のような記事データを利用することが出来ます:

  • category (タグ)
  • title (記事タイトル)
  • link (記事URL)
  • author (著者)

(category はスペース区切りの文字列です。author はブックマークの RSS には無いかも知れません)

これらを元に、

if (/NGtag1|NGtag2/.test(item.category))
  close_item(item.id);

のように、読みたくないものにマッチした場合は削除すれば良いわけです。

一応 id:matakimika さんのアイデアから一つだけ具体例を考えてみました。人気エントリーからタグ「ハルヒ」と「web2.0」の両方を含む記事を削除する Greasemonkey スクリプトです:

// ==UserScript==
// @name      LDR Contents Filter
// @namespace http://d.hatena.ne.jp/reinyannyan/20060714/
// @include   http://reader.livedoor.com/reader/*
// @version   0
// ==/UserScript==

with (unsafeWindow) {
  register_hook("after_printfeed", function(feed) {
    var link = feed.channel.link;
    var exclude, ands, ors;

    if (/b.hatena.ne.jp\/hotentry/.test(link)) {
      ands = [ /web2\.0/, /ハルヒ/ ];
      exclude = function(item) {
        // 配列.every で AND 検索
        return ands.every(function(p) { return item.category.match(p) });
      };
    }
    //else if (他のサイト)...

    if (!exclude) return;

    feed.items.forEach(function(item) {
      if (exclude(item))
        (function(id) {
          var self = arguments.callee;
          self.fails = self.fails || 0;   // 削除の失敗を記録 (無限再帰を防ぐ)

          try {
            close_item(id);
          }
          catch (e) {
            if (self.fails++ > 30) return;
            setTimeout(function(){ self(id) }, 500);
          }
        })(item.id);
    });
  });
}

未テストです (ひょっとして日本語は Unicode エスケープする必要あります?) が、是非参考にしていただきたいと思います。