LDR で記事データを動的に加工する -> 全文取得への応用

これまで、イベント・フックを使ってフィード表示を制御する方法について何度か考えてきました:

今回も同様の主旨なんですが、テンプレート・クラス (/js/template.VERSION.js にあります。toString の使い方など、非常に勉強になります) を読んでいて、これまでと違う手法に気付いたのでご紹介したいと思います。


本来、フィード記事は以下の様な構造になっており:

{
  "items": [
    {
      "enclosure": null,
      "link": "http://hatena.g.hatena.ne.jp/hatenarss/20060823/1156305225",
      "enclosure_type": null,
      "author": "hatenarss",
      "body": "\n\t\t<div>\n\t\t\t<p>未読の記事のみを表示する設定の状態で、フィード毎のエントリー一覧のページを閲覧した場合、「次のXX件」のリンク先が正しくなかったので修正いたしました。</p>\n\t\t\t<p><a href=\"http://i.hatena.ne.jp/idea/11690\">idea:11690</a>にて御指摘いただきありがとうございました.</p>\n\t\t</div>\n",
      "created_on": 1156305225,
      "modified_on": 1156305225,
      "id": "6992277",
      "title": "次のXX件のリンク先の不具合を修正しました",
      "category": ""
    }
  ]
}

その各キーの値 (文字列・数値リテラル) が、テンプレートの対応箇所に埋め込まれることで表示が行われます。

例えばリンク先の修正の例では、この "link" の値を表示の直前 (before_printfeed) に修正することで適切な表示を行っていました。

この「値」が、どうやら関数でも良いらしい、というのが今回気付いたことです。

つまり、こんなことが可能になるのです:

item.body = function() {
  GM_xmlhttpRequest({
    method: "GET",
    url: item.link,
    onload: function(res) {
      /* res.responseText を整形 -> item.body に代入 */

      // 表示
      var body = $("item_body_"+item.id);
      body.innerHTML = '<div class="body">'+item.body+"</div>";
      fix_linktarget(body);
    }
  };
  return "Now printing...";
};
item.body.isFunction = true;  // Firefox 用おまじない

全文を提供しないフィード記事の "body" に対し、事前に上のような関数を割り当てておくことで、表示の際にはまず "Now printing..." が出力されます。そして、バックグラウンドで記事 HTML の取得が行われ、適切に全文が出力される、というわけです。

(注: キーによっては関数にしてしまうとまずいものもあります。その辺りはソースをよく読んで確認してください)

全体としては、以下のような形で実装することになると思います:

function item_modifier(feed) {
  // 記事データ (body, link, etc.) 書き換え関数を返す (feed.channel.link 等の値に応じて)
  var iter;

  // 上の例なら、このような関数を返すことになります
  iter = function(item) {
    item.body = function() {
      // ...
      return "Now printing...";
    };
    item.body.isFunction = true;
  };

  return iter;
}

function modify_items(feed) {
  if (feed.items_modified) return;

  var iter = item_modifier(feed); // 記事書き換え関数を取得
  // 書き換え実行
  iter && feed.items.forEach(iter);

  feed.items_modified = true;
}

register_hook("before_printfeed", modify_items);

必要を感じる方は是非試してみてください。


参考:
m4i::diary - livedoor Reader で EntryFullText


追記:

実装の際の注意点を幾つか挙げておきます。

1. HTML 中の相対リンクの絶対化

body.innerHTML への代入前に BASE.href の値を一時的に記事 URL に書き換え、

document.getElementsByTagName("base")[0].href = URL;

その後元に戻す

document.getElementsByTagName("base")[0].removeAttribute("href");

というテクニックが使えると思います。

ただ、現状 LDR では にあるべき タグが 要素中にあり、IE ではこの方法はうまく行きませんでした ( を一旦削除し、 に付け替える必要がありました)。

(追追記: ちょっと試してみたんですが、これは良くない方法だったかも知れません。BASE.href が書き換わった瞬間に、ブラウザが LDR 側の、相対指定になっている画像をそちらへ取得しに行ってしまいます。大量の無効なリクエストを投げてしまうことになり、かなり迷惑です。

尤もこれは IE での挙動で、他のブラウザではどうなのか分かりません。)


2. 記事コンテナの取得失敗への対策

var body = $("item_body_"+item.id);

の部分でエラーが発生することがあります。したがって、この関数の内部全体を try..catch 文で囲み、setTimeout で再帰呼び出しする仕組みが必要でしょう。その際、永久ループを避けるため、許容するエラー回数の上限を定めておくべきです。(具体的な方法は id:reinyannyan:20060714:p1 が参考になるかと思います)


3. 記事 URL が "&" 等の特殊文字を含んでいる場合

item.link にはエスケープされた値 (& 等) が入っています。GET する url の値は常に item.link.unescapeHTML() としておきましょう。