イベントの抜け道

windowの共有の次は、スクリプト間のイベント伝播だ。GM_AutoPagerizeLoaded、GM_MinibufferLoadedのような専用イベントを用意しても、スクリプト間では伝播されないようになったので(同じスクリプト内ならイベントも伝播する。MinibufferとLDRizeを同じスクリプトにつっこんだら動く、というのもそういうことだ)、どうにかしてこれを伝播可能にしたい。

似たような事例というのは案外転がっているもので、ググっていたらこういうの(http://wp.serpere.info/archives/1107)に行き当たった。



XPCNativeWrapperの外側でスクリプトを評価する関数。Firebugも同じことをしている。unsafeWindowを触らないので安全。


function evalInPage(fun) {

location.href = "javascript:void (" + fun + ")()";
}



SmartLDR更新 – 素人がプログラミングを勉強するブログ


これは面白い!Function.prototype.toString が関数のソースコードを返すことを利用した、巧妙にして簡潔なハック。


次のように引数を渡せるように改良すればさらに強力になる(引数に渡せるのはJSON化可能な値のみ)。


function evalInPage(func, args) {

var argStr = JSON.stringify(args || []);

location.href = "javascript:void "+ func +".apply(null,"+ argStr +")";

}




GM_xmlhttpRequest を使って別ドメインから得たデータを、unsafeWindow 上のライブラリを使って表示する、といったコードが安全かつ自然に書けるようになる。

http://wp.serpere.info/archives/1107

つまり、スクリプトから実行してうまくいかないんなら、ページ側で実行してもらいましょう、という話。今回のスクリプトにしても、元ページの初期化関数にタダ乗りするようにしたのは、このページを見たおかげ。

課題とか

こいつを上手くMinibufferあたりに組み込んでやると、イベントの発火は可能になる。ページ側での実行になるのでLDRize等の他のスクリプトからイベントを参照することも可能だ、というところまでは確認した。イベントの授受は元々非同期前提の動作なので、そのへんも問題ない。

残る問題はAutoPagerizeのほう。普通に動かすだけなら今でも問題ないが、イベント伝播も元通りにしようとするといささか困った点が出てくる。GM_AutoPagerizeLoadedとGM_AutoPagerizeNextPageLoadedのふたつのイベントはごく普通のイベントなので問題はない。
今回APIを使ったのもそのあたりが原因で、MutationイベントであるAutoPagerize_DOMNodeInsertedをページ側で実行してもらおうとしたとき、発火対象ノードを一意に定める必要が出てくる。このイベントは実質AutoPagerizeAPIみたいなもんで、これに対してaddEventListnerを設定することで新しく追加されたノードをいじれて、非常に使いやすい。

ノードそのものはJSON化不可なので、引数にしたところで意味がない。思いつく範囲では、ページ側に渡すスクリプトに、セレクタを利用するquerySelectorや、XPathを使うevaluateなどを用いたノード特定用の部品を組み込めばいいのだろうが……。少なくとも、テキストノードを選択することがうまく出来ないquerySelectorのほうは、選択肢から除外して良さそうだ。

AutoPagerizeもwindow共有不可の問題にはやっぱり影響受けてて、そのあたりちょっと考えてみましたよ

社長が訊く

任天堂のサイトにあるゲーム製作の舞台裏紹介ページ「社長が訊く」。現行のAutoPagerizeだとscriptタグで追加されるムービーへのリンクが変なことになるので、それをどうにかするスクリプトを書いた。

おそらくgreasemonkey版のAutoPagerizeでないと動かないので悪しからず。しかも以下に示す対策も必要。

AutoPagerizeにもsharedObjectを処方

ユーザースクリプト間のwindowの共有が問題となって、minibufferやLDRizeの動作に差し障りが出るのは既出の通り。対策もまた既出。他にもReblogCommandのような双方に依存するスクリプトがあれば、記事(id:aiwendi:20111115:1321338264) tでリブログ復活 - aiwendilの日記 のようにすれば解決する。

ということで、sharedObjectが用意できている環境ならば、windowの共有自体は下のようにすれば済む。行のオフセットが5000とかやたらアホみたいな数値になっているのは、ローカルにごりごり書いたSITEINFOの分なのでお気になさらぬよう。

@@ -5255,8 +5002,6 @@ if (typeof(window.AutoPagerize) == 'undefined') {
     }
     window.AutoPagerize.launchAutoPager = launchAutoPager

+    sharedObject.AutoPagerize = window.AutoPagerize
+
     var ev = document.createEvent('Event')
     ev.initEvent('GM_AutoPagerizeLoaded', true, false)
     document.dispatchEvent(ev)

window.AutoPagerizeを全部sharedObject.AutoPagerizeに書き換えなくても、AutoPagerize側の準備が出来た時点(つまり、GM_AutoPagerizeLoadedイベントの発火直前)で、sharedObjectにもコピーを作ってやればいい、というわけ。

AutoPagerizeAPI(自分用メモを兼ねて)

で、AutoPagerizeスクリプトの連携に話は移るわけだが、今回書いたスクリプトAutoPagerizeAPIhttp://autopagerize.jottit.com/apis_(ja))を利用している。

APIs

Filters

別のGreasemonkey Scriptから、AutoPagerizeの動作の途中に処理を挟むことができる。

Filter 実行タイミング 引数 用途
addDocumentFilter 次のページを読み込んで Document 作成した完了直後で、 Document からpageElement を切り出す前であり、「さらに次のページ」の URL 取得前 htmlDoc ( GM_xmlhttpRequest で取得したソースを元に作成した Document オブジェクト), this.requestURL (次のページとして取得した URL ), this.info (使用している SITEINFO ) nextLink, pageElement の取得の補助、 SITEINFO の動的な変更など
addFilter 次のページの pageElement を挿入後 pageElement ( htmlDoc から getElementsByXPath ( this.info.pageElement , htmlDoc で切り出した) 要素の配列。要素自身だと間違われやすいがあくまで配列) 継ぎ足しした部分を他の Script から操作したい場合など
addRequestFilter 次ページ取得のリクエストを送信する直前 GM_XHR用リクエストオプション リクエストのURLやオプションを変更する
addResponseFilter 次ページ取得のレスポンス取得時 レスポンス、URL レスポンスの内容を変更する

launchAutoPager

AutoPagerizeにsitoinfoを渡して実行させることができる。

引数はsitoinfoを格納した配列。

http://autopagerize.jottit.com/apis_(ja)

window.AutoPagerize下にあるAPIなので、現状、どうしてもwindowの共有が必須になる。今回スクリプトを書くのに参考にしたhttp://userscripts.org/scripts/show/48753にしても、sharedObject.AutoPagerizeを使うように書き換えて動かしているので。

まあ、windowの共有の代替策が見つかったところで、実のところまだ問題は残っているが。

Re:LDRizeをFirefox8+greasemonkey0.9.12以降で動かす

tyoroさん(id:tyoro1210 はてなが本拠じゃなさそうなのでこの呼称はどうか)がこっちの記事(id:t_f_m:20111111:1320962283)の6時間前にもっと有意義な記事を上げておられました。
Greasemonkey0.9.12以降でMinibufferとLDRizeが動かない理由とか - tyoro.exe

こっちの対策のほうが色々できて確実だと思われます。相変わらずイベントが伝わらないので、AutoPagerize依存だったり、minibufffer、LDRize依存のスクリプトは上手く動きませんが。AutoPagerizeもアドオン版に変えれば依存スクリプトが動くんだろうけど、自前のSITEINFOを手軽に書けるから抜け出せないんだよなあ。

一点つっこんでおくと、vimperator向けのパッチの

      if (Cc.injectScripts.toSource().search('sharedWindow') == -1) {

は、

      if (Cc.injectScripts.toSource().search('sharedObject') == -1) {

だと思われます。追記済みかどうかの判定なので。

LDRizeをFirefox8+greasemonkey0.9.12以降で動かす

g:vimperator:id:yutamoty:20111109:1320826826
はてなグループ
動かす方法は既にあるが、greasemonkey0.9.11以下が必要で、かつFirefox4.0以降+greasemonkey0.9.1以降用の対策も必要。

軽く確認したところ、動かない原因はLDRizeを起動させるGM_Minibuffer_Loadedイベントが伝わっていないことだったので、手抜きの解決。

minibufferの末尾にLDRizeの中身をコピペしてやればいい。

なお、greasemonkey自体のGM_openInTab関数の動作が直っているので、window.openに頼らなくて済む。

Re:Re:XPathの動作にまつわる試行錯誤

以前書いた記事(id:t_f_m:20110321:1301004931 XPathの動作にまつわる試行錯誤 - 近江在住)の内容を検証・仕様確認してくださった方がいらっしゃったので、この記事はそれへの返答となります。
tumblr:xkansanさんありがとうございます。

検証の要点

問題となったXPath、サイト構造とほぼ同様のテスト環境によるテスト。

この結果を見て疑問に思ったことは、

  1. id(“target-ul”)/li[2][substring-before(self::li/a/@href, “p”) = substring-before(preceding-sibling::li/a/@href, “n”)]/a という XPath で 「記事を読む」 の a ノードが選択されるのはなぜか
  2. id(“target-ul”)/li[3][substring-before(self::li/a/@href, “p”) = substring-before(preceding-sibling::li/a/@href, “n”)]/a という XPath でなにも選択されないのはなぜか

という2点。

Re: XPathの動作にまつわる試行錯誤 - xK.memo

問題の切り分け。それぞれの解題は以下。

空文字問題

1. について

これは、 li[2] の述語で空文字同士の比較が行われているため。 以下の2つの XPath とその結果を見比べればわかると思う。

分かってしまえば、こちらは至極単純。

substring-before(self::li/a/@href, "p")

を実行した結果も

substring-before(preceding-sibling::li/a/@href, "n")

を実行した結果も、共に空文字となり、空文字同士の比較は真。当然である。

仮にこれを[1][2]のような文字付加に依らず回避するならば、

id("target-ul")/li[substring-before(self::li/a/@href, "p")][substring-before(self::li/a/@href, "p") = substring-before(preceding-sibling::li/a/@href,"n")]/a

のように空文字そのものの真偽を判定してやれば良いだろうか。かなり冗長だが。

ノードと文字列

2. について

(中略)

これまでは substring-before の中でノードセットが文字列に変換される際には1つのノードしかノードセットに含まれなかったのでノードセットの文字列化について特に触れなかったのだけれど、この場合のように複数のノードを含むノードセットはどのように文字列化されるのか。

これは http://www.w3.org/TR/xpath/#section-String-Functions に書いてある。

A node-set is converted to a string by returning the string-value of the node in the node-set that is first in document order.

文書順で一番最初のノードの文字列値がノードセットの文字列値になるとある。 この場合にあてはめると

つまり、仕様に従ってノードから文字列(正しくはノードセット、ノード集合から文字列)に変換しているだけなので、別にバグというわけではない。何となく、文字列セットのようなものに変換されて、全て比較に回されるのではないかと思っていたのだが、世の中そんなに甘くなかった。

これを回避するための方策は

なのでノードセットを文字列化するときには、そのノードセットに1つのノードのみが含まれているようにするのがよいのでは。

とあるし、実際そうするしかない。

id("target-ul")/li[substring-before(self::li/a/@href, "p") = substring-before(preceding-sibling::li/a/@href,"n")]/a

の中の

preceding-sibling::li/a/@href

の部分が唯一のノードを返せばよい。少し冗長になるが、

preceding-sibling::li[contains(a/@href,"n")]/a/@href

として、一意に存在するノードを拾い上げれば良い。

対策

というわけで t_f_m さんが書こうとしていた XPath は、 id(“MainContent”)/div[@class=”pager”]/div/ul/li[substring-before(substring-after(self::li/a/@href,”/photos/”),”-p”) = substring-before(substring-after(preceding-sibling::li [1] /a/@href,”/news/”),”-n”) and substring-before(substring-after(self::li/a/@href,”/photos/”),”-p”) != “” ]/a みたいに書いたらいいのではないか、と思う。少し不格好かもしれないけれど。

あるいは、ページ構造によっては id(“MainContent”)/div[@class=”pager”]/div/ul/li [last()] [substring-before(substring-after(self::li/a/@href,”/photos/”),”-p”) = substring-before(substring-after(preceding-sibling::li [1] /a/@href,”/news/”),”-n”)]/a と書けるかもしれない。

『出来るだけマルチバイト文字をXPathとして使いたくない』という前提があるので、こういう書き方をせざるを得ないのだろうな、と思う。文字列も集合として取り扱えるのなら、ここまでトリッキーな書き方をしなくて済むのだろうけれど。まあ、この実装の利点も何となく分かるので、不満というほどでもない。

で、一意にノード同士を比較することが出来るなら元の問題もそこまで大きく騒がなくて済んだので、意味を失わない程度に書き換えるなら、自分はこうするかなあ、と。

id("MainContent")/div[@class="pager"]/div/ul/li[contains(a/@href,"photos")][substring-before(substring-after(a/@href,"/photos/"),"-p") = substring-before(substring-after(preceding-sibling::li[contains(a/@href,"news")]/a/@href,"/news/"),"-n")]/a

xkansanさんの書いてくれた例と大差はないが、自分なりのこだわりというヤツ。

締めに

元記事執筆以降も、何件か似たような失敗と、条件次第ではすべての要素が比較対象となるわけではないらしい、という事には薄々気付いていたものの、具体的な仕様についてはどこにまとまっているのかよく分からなかったので触れずにいた。今回、こうして仕様レベルの動作も含めた検証を見ることで、横着してばかりではいけないな、と痛感した。

実際のところ、動くSITEINFO自体は既に出来ているので、検証の確認と今後に役立てるべき反省材料という以上のものは無いのだが、XPathの仕様に触れることで推奨される手法、あるいは推奨されない手法、バッドノウハウなどを整理できる。他の仕様も、時間を見て確認していく予定。

余談もろもろ

githubにはログインできてるのにgistにはログインできない

何故……?

2011/09/15 15:07 追記

パスワードを勘違いしてただけでした。

LDRizeのバグはどこに報告するの

ということでどなたかuserscripts.orgのLDRizeを修正できる人までお伝えください。

LDRizeの#gm_ldrize_pinlistの横幅を制限

ついでに自分用の使いやすくする設定。これもLDRizeに直接ぶち込んでる。
ピンを立てた時のバルーンが、ものによってはものすごく横に長くなってしまい、画面中央付近までかぶさって見づらくなる状況を回避する。

@@ -345,6 +345,7 @@ LDRize.prototype = {
  if(CSS_HIGHLIGHT_LINK) css += "\n.gm_ldrize_link {" + CSS_HIGHLIGHT_LINK + "}";
  if(CSS_HIGHLIGHT_PINNED) css += "\n.gm_ldrize_pinned {" + CSS_HIGHLIGHT_PINNED + "}";
  css += ".gm_ldrize_iframe { min-height:200px; position:fixed; bottom:0px; left:0px; right:0px; }";
+ css += "#gm_ldrize_pinlist { max-width:300px; }";
  return css;
   },
   initPinList: function(){

モノはココ。
https://gist.github.com/1209259