PrintWhatYouLike というサービスをご存じでしょうか?
ブックマークレットとして実行すると、ページ上のコンテンツ(HTML 要素)を
自由に削除したりできるサービスです。
Evernote の Web Clipper と組み合わせると最強。
ブログ記事の本文部分だけを抜き出してクリップできるので、
あとで Evernote 上で検索したときのノイズが劇的に減らせます。
参考)
Evernote と PrintWhatYouLike の組み合わせが便利すぎる!
-- マインドマップ1年生 plus ライフハック!
さて、とっても便利な PrintWhatYouLike ですが、
先日(7月のどこか)からうまく動かなくなってしまいました。
ブックマークレットを実行するとツールバー(サイドバー)は
表示されるのですが、コンテンツ編集用の赤色ボーダーも
要素クリック時のポップアップも表示されません。
あと、ツールバー上の "close" ボタン「X」も。
いろいろ調べた結果、とりあえず動くようにする代替ブックマークレットが
作れたので、ここで公開します。
⇒ PrintWhatYouLike(Seaoak版)
※ ブックマークバーに Drag&Drop すれば使えます
公式ブックマークレットの代わりに使えます。当然ながら非公式です。
この非公式ブックマークレットの中では、公式のと同じ JavaScript プログラムを
PrintWhatYouLike.com から読み込んで、問題のある箇所だけ修正して、再実行しています。
そのうち公式に修正版がリリースされる(またはこっそり JavaScript プログラムが
差し替えられる)ハズなので、それまでの「つなぎ」としてお使いください。
問題になりそうな処理は仕込んでいないので大丈夫だと思いますが、
例によって無保証ということでお願いします。
以下、技術的な詳細です。
=====
■ 1. 公式ブックマークレットが動かなくなった原因
ブラウザのコンソールを見ると、null
に設定されてる変数のプロパティを
読もうとしてエラーになっていることがわかります。エラーになった時点で
プログラムの実行が中断されて、その結果として従来の動作ができなく
なっているものと推測されます。
公式ブックマークレットの中で読み込んでいる JavaScript ファイルを
見てみると、次のようなコードが含まれています。
if(nextPage.nextLink.finalScore>17000){
この nextPage.nextLink
には、直前である関数の返値を代入しているのですが、
その値が null
になっているようです。
とりあえず、次のように修正してみたところ、動くようになりました。
if(nextPage.nextLink&&nextPage.nextLink.finalScore>17000){
変数名から推測するに、新しく組み込まれた機能の "PrintWhatYouZip" で
「次のページ」をたどろうとしたとき、その「次のページ」が見つからないと
この変数に null
が設定されてしまうようです。
=====
■ 2. ブックマークレットで動的にパッチを当てる
ブックマークレットで読み込んでいる JavaScript プログラムを
「その場で」書き換えて実行する方法を考えます。
そんなことをしなくても、公式の JavaScript プログラムをローカル保存して、
修正して、修正版を適当な Web サーバにアップロードしておいて、
そのファイルを参照するブックマークレットを作る方が簡単です。
しかし、
・公式の JavaScript プログラムのライセンスが不明
・他の人にも安心して使ってもらうのが難しい
・公式版の更新に追従できない
・自分で用意できる Web サーバだと遅い(読み込みに時間がかかる)
などなどの問題があります。
さて、問題の JavaScript プログラムは、動的に生成した script
要素で
読み込まれています。したがって、読み込まれた時点でソースコードの字面を
書き換えられれば一番簡単です。しかしながら、src
属性を指定した script
要素の
ソースコードを参照する方法がありません(text
属性を読んでみても「空」)。
一方で、問題の JavaScript プログラムを眺めてみたところ、
問題があるのは _pgzpInitPwyl()
という関数です。
この関数はグローバルな名前空間で定義されています。
つまり、ブックマークレットからも可視。
すなわち、ブックマークレットからも書き換え可能!
というわけで、次のような方法を考えました。
1. 関数 _pgzpInitPwyl()
のソースコードを得る
2. 得られたソースコードを書き換える
3. ソースコードを eval()
して関数オブジェクトに変換
4. 得られた関数オブジェクトをシンボル _pgzpInitPwyl
に設定
関数のソースコードは .toString()
で得られます。
関数オブジェクトへの変換およびシンボルへの設定は、
ひとつの eval()
でまとめてできます。
具体的には、以下のようなコードになります:
var code = _pgzpInitPwyl.toString();
code = code.replace('hogehoge', 'foobar');
code = '_pgzpInitPwyl = (' + code + ')';
eval(code);
=====
■ 3. 修正したコードを実行させる
上記のように関数の定義を動的に書き換えることが可能なのですが、
問題は「書き換えられるのは最初に実行された後」ということです。
src
属性を指定した script
要素の場合、
読み込み直後(実行される前)に処理を割り込ませることができません。
script
要素の onload
イベントハンドラが呼ばれるのは、
残念ながら読み込んだコードが実行された後です。
また、onbeforescriptexecute
イベントという便利そうなモノがあるのですが、
Firefox でしか実装されていないみたいです(未確認)。
さらに、IE8 では script
要素の onload
イベントハンドラは
実装されていないようです(IE9 では実装されているようです)。
幸いにして、問題の JavaScript プログラムは、エラーで死んだ後でも、
上記パッチを当てて再実行すれば動作してくれます。ただし、最初の
(エラーになった)実行でツールバー(サイドバー)が開いた状態に
なっているので、再実行は2回繰り返す必要があります。
「一度閉じて、開き直す」というわけですね。
最終的に、次のような方法を採用しました:
1. 「コード書き換え+再実行2回」を1回だけ実行する、という独自関数を定義。
2. script
要素の onload
イベントハンドラとして独自関数を設定する。
3. IE8 対策として setInterval()
で定期的に DOM を監視して、
問題の JavaScript が実行されたのを検知したら clearInterval()
して
独自関数を実行する。
ちょっと hacky ですが、まぁ、動きます。
=====
■ 4. 完成したブックマークレット
javascript : (function ()
{
var timer;
var handler = function ()
{
if (window['ppw'] && ppw['bookmarklet'] && timer)
{
window.clearInterval(timer);
timer = null;
if (! _pgzpInitPwyl) {
throw 'can not modified (by Seaoak)';
}
var regexp = /\bif\s*\(\s*nextPage.nextLink.finalScore\s*>\s*17000\s*\)\s*\{/;
var code = _pgzpInitPwyl.toString();
if (! regexp.test(code)) {
return;
}
code = code.replace('_pgzpInitPwyl', '');
code = code.replace(regexp, 'if(nextPage.nextLink&&nextPage.nextLink.finalScore>17000){');
code = '_pgzpInitPwyl = (' + code + ')';
eval(code);
var toggle = function ()
{
ppw.bookmarklet.toggle();
};
window.setTimeout(toggle, 100);
window.setTimeout(toggle, 100);
}
};
if (window['ppw'] && ppw['bookmarklet']) {
ppw.bookmarklet.toggle();
}
else
{
window._pwyl_home = 'http://www.printwhatyoulike.com/';
window._pwyl_pro_id = null;
window._pwyl_bmkl = document.createElement('script');
window._pwyl_bmkl.setAttribute('type', 'text/javascript');
window._pwyl_bmkl.setAttribute('src', window._pwyl_home + 'static/compressed/pwyl_bookmarklet_10.js');
window._pwyl_bmkl.setAttribute('pwyl', 'true');
window._pwyl_bmkl.onload = handler;
timer = window.setInterval(handler, 100);
document.getElementsByTagName('head')[0].appendChild(window._pwyl_bmkl);
}
})();
ソースコードの整形&色づけは、こちらを使わせていただきました。
=====
■ 5. まとめ
先日から PrintWhatYouLike が使えなくて非常に困っていたのですが、
ソースコードを眺めてみたらすぐに原因がわかって、
そこで「動的パッチ」というアイディアを思いついたので、
ひと晩で実装してみました。
JSON 以外で eval()
を使ったのは初めてだったので、
その挙動に悩みました。特に、IE8 だけ挙動が違ったのが罠でした。
いちおう動作確認したのは以下のブラウザです:
・Google Chrome 13.0.782.109 beta-m
・Firefox 5.0
・Opera 11.50
・Internet Explorer 8
OS は Windows 7 Professional 32bit です。
ちょっと長い記事になってしまいましたが、今回のコードは
PrintWhatYouLike の開発元が対応してくれたら無用の長物と
化してしまうので、ブログネタにしてみました。
ツッコミ歓迎です~