for Firefox #3

akahukuplus-on-firefox

とりあえずここまで動いた。右の方の表示が変なのが悪名高き [cci]disable-output-escaping[/cci] だ。これをどうするか。

for Firefox #2

例えば続きを読んだ際に、最新のレス数を反映させたりする。それを行うには、簡単に言えば
レス
みたいなマークアップに対して、javascript で

document.getElementById('replies-total').textContent = '100';

などとすればいいわけだ。

しかし実際にはこの手のコードは赤福プラスの中にはない。実際のマークアップは
レス
などと binding 属性が付加してある。それで、汎用的な再バインド関数を呼ぶことにより、binding 属性に応じて自動的に更新させるようになっている。再バインド関数内では、バインドの接頭辞が [cci]xpath:[/cci] であればふたばのサーバが返した html をスクレイピングした結果の中間 xml からの xpath の結果、あるいは [cci]template:[/cci] であれば中間 xml をパラメータにして与えた XSL の変換の結果を用いて要素の内容を置換する。

この記事の話題はその、文書の一部分を更新するために呼ばれる XSL 変換だ。文書の一部といっても、変換に際して使用する一時的な文書は html 要素から始まるまっとうなそれである。つまり





とこんな感じ。この変換結果を、XSLTProcessor#transformToFragment() を用いて DocumentFragment として得る。

この時、返ってくる要素のツリー構造が Firefox とその他で微妙に違う。Presto Opera と Chrome は、DocumentFragment の子要素として body 要素の子要素を割り当てる。一方 Firefox は、単純に DocumentFragment の子要素として html 要素を割り当てる。

どちらが正しいのだろうか。正しさというのは違うか。そもそも ブラウザの javascript から利用できる XSLTProcessor のインターフェースは誰が考えて管理してるのかよくわからない。とりあえず Opera と Chrome の動作のほうが、そのまま単純に DocumentFragment を appendChild() の引数にできるので楽だし、何より多数派だ。一方で、動作としては Firefox のほうが素直ではある。body 要素を特別扱いします的な例外中の例外動作は何なんだろうか。DocumentFragment はあくまで断片なので html/head/body といった大物は対象にしない、みたいなルールがあるのかもしれない。しかし誰がその仕様を決めているのかよくわからない。

そんなわけで、得た DocumentFragment が body 要素を持っていたら、さらに body の子要素を DocumentFragment としてくくり出すようにした。くくり出す時に Range を使うのだが、これがちゃんと DocumentFragment にも作用するのか確証は持てない(DocumentFragment は Document のいわばサブクラスなので Range による操作も理屈の上では互換性があるはずなのだが、DocumentFragment に対する操作というのはけっこうどのブラウザも怪しい、あるいは怪しい時期があったのだ)。でもまあ、とりあえず動いているようだ。

function fixFragment (/*DocumentFragment*/f) {
var bodies = f.querySelectorAll('body');
if (bodies.length == 0) return f;
var r = document.createRange();
r.selectNodeContents(bodies[0]);
return r.cloneContents();
}

for Firefox

そんなわけで、Chrome で動かすのは割とすんなり行ったので、いよいよ Firefox 上で動かしてみるのだけれども。

やっぱり一筋縄では行かない。例えば、赤福プラス ver.3 以降というのはいろいろな場面で XSLTProcessor が大回転するのだが。XSLTProcessor に食わせる xml は、そのソースが最初にページをロードした際は body.innerHTML、XHR で続きを読んだ際はふたばのサーバが返す html そのものと微妙に違う。なので、それぞれがほぼ同じものじゃないとちょっと困るのである。そして、Presto Opera と Chrome の innerHTML は期待通りの動作をする。つまり body.innerHTML を参照したとしても、元のソース文字列に出来るだけ同じものを返してくれる。

が、どういうわけか Firefox の場合は innerHTML は勝手に属性の定義順を書き換えたものを返してくれたりするようなのだ。どういうポリシーがあってそんなことをしでかしてくれるのかはよくわからない。html から中間 xml を生成する処理の内容はすなわち、ソース文字列を正規表現でパースするのである。したがって属性の定義順が不定だとか、パターンで表現できる範囲を超えられるとちょっと困るのである。

現役の web ブラウザが複数ある中で、それぞれが得手不得手があるのは自然なことである。しかしあくまで個人的な感想ですが、Presto Opera に関してそういう場面に出くわした場合、「おうおうお前はこれは苦手なのかうんうんかわいい奴め」という気分になるのだけれど(Presto Opera がもはや現役ではないという事情もあるのだが)、Firefox に対しては「てめーこの程度のこともできねーのかよ! 何年 web ブラウザやってんだアホッ!」と思う。あくまで個人的な感想です。

Firefox の XSLTProcessor に関しては、以前の記事にあるように [cci]disable-output-escaping[/cci] が最大の障壁だと考えていたのだが、そこに至る前にもけっこうな壁があるのである。ああ。

Strange Firefox

自動テストを Firefox でも行ったところ、不思議なことに Firefox でだけ失敗するテストがある。[cci]:[/cci] で行入力モードへ移行し、[cci]^V[/cci] でリテラル入力状態にした後、[cci]escape[/cci] キーを押して状態を途中でキャンセルするというものだ。そして、正しくリテラル入力状態がキャンセルされ、最後の行入力の状態から変化がないことをテストする。

これが Firefox でだけ失敗する。行入力バッファがクリアされてしまうのである。つまり勝手に空っぽになってしまうのだ。なにこれ。というかなんだこれ。

調べてみたところ、[cci]escape[/cci] キーを押されたことで発生するハンドラの中では、最後までバッファが勝手に変更されることはない。そのハンドラが終了した直後に勝手にクリアされてしまうようだ。つまり wasavi のコードがバグってるということではなくて、Firefox の動作がなんかおかしい。

もともと、wasavi 側ではキー入力ハンドラ内で 1ms 後に発生するタイマを仕掛け、そこで擬似的にバッファ変更イベント的なものを発生させている(これはインクリメンタルサーチで必要なのだ)。Firefox で動いている場合に限り、そのタイマの中でつじつまを合わせることにした。具体的にはキー入力ハンドラの最後にバッファの内容を data- なんとか属性にコピーしておき、タイマの中でその属性値と現在のバッファの value が違っていたらコピーしていた内容で復帰させるのである。もちろんこんな処理は本来は不要で、あまりに何をやっているのか意味不明なので、さすがにくどいコメントも付けておいた。

もしかしたら、このバグなのかもしれない。しかし件のバグは RESOLVED なので関係ないのかなあ?

とこのように、Firefox でだけ発生するバグを見つけると大変なのである。なにしろ拡張を華麗にデバッグするナウいデバッガがないので、古き良き…ではない古く悪しき printf デバッグでのたうちまわるしかないのだ。Firebug とかは、あくまで web ページのスクリプトのデバッガなので拡張は対象外なのです。

正確に言えば、最近の Firefox は Firebug ではないビルトインのデバッガを備えるようになっている。これが本当に最近の話だから困る。しかもそのデバッガが拡張をデバッグ対象にできるようになったのはさらに最近の話なので本当に困る。ちょっと実装するの遅すぎですよ。

ちなみにそのデバッガであるが、ステップ実行するのに 1 〜 2 秒かかったりステップ実行の単位がソースコードの 1 行ではなく副作用完了点的な何かなのか、複数回ステップ実行しないと停止行が進まなかったり、デバッガが動いている間 CPU を 80% 近く酷使してファンを全力全開にしてくれたり、まあなんといいますか、まだまだまったくもって人様にお出しできない状態なのだった。ひどいかひどくないかで言えば、かなり滅茶苦茶にひどい。

Firefox といえば OSS のブラウザの雄でであり、かつ長い歴史を持った老舗であるわけなのだが、デバッガ周りに関してなんでこんなにもあまりにも質の低いことをしているのか、ちょっとどういうことなのかよくわからない。どういうことなの。どういうことなんでしょうか。

play a sound #2

SDK のドキュメントをつらつらを眺めてみると、HiddenFrame なるものを見つけた。これは要するに、バックエンド側で保持することのできる iframe のようなものらしい。HiddenFrame 内に構築される window はごくごく普通のそれであり、Audio コンストラクタも持っている。

そういうわけで、Firefox においては、HiddenFrame 経由で Audio 要素を得ることができた。子供の window で生成したオブジェクトをそのまま親の側で使用できてしまうのがかなり気持ち悪いが、たぶん、きっと、だいじょうぶ。

play a sound

wasavi を実行中、音を出す場合がある。つまりビービー鳴らすモードを持つ vi と同様、必要なときにエラーベルを出す。もし「この音うるせーよ!!」と思われる場合は、ワークアラウンドとして [cci]:set bellvolume=0[/cci] とすれば、とりあえずミュート状態にはなる(例えばこの方がやっている方法だ)。将来的にはビジュアルベルに切り替わったりする気遣いをするかもしれない。

さてこの時に出す音は Audio 要素を使っていて、フロントエンド側で鳴らしている。音源は wasavi 起動時にバックエンドから送られてくる ogg または mp3 データである。

しかし、これは割と無駄なのではないか。複数のタブで wasavi を動かしているとして、各タブがもともと同じデータである音源のコピーを個別に抱えている状態だ。現状は 1 秒未満しか発音しないビープ音なのでデータ自体は数 KB だ。したがってものすごく容量を食っているというわけではないが、やっぱり無駄だ。

これを、Audio 要素をバックエンドで保持するようにして、フロントエンドで音が必要になったら発音リクエストを投げるだけにすれば、そういう無駄は解消できる。ということでやってみて、Opera と Chrome では思ったとおりになったのだけど、Firefox が問題だ。Firefox の Add on SDK 上のバックエンドは、Window ではない不可思議なサンドボックスオブジェクトがグローバルになっていて、つまり Audio 要素をバックエンド側で使えないのだった。

こういう場合、たいてい [cci]Cc[/cci] や [cci]Ci[/cci] といった Firefox 特有の黒魔術をごにょごにょして何とかなったりするものなのだが(Kosian では XHR や Blob や FormData についてごにょごにょしている)、黒魔術はしょせん黒魔術であり、あんまり使いたくない。なにしろ、AMO にアップする際の機械的チェックにすら「おい黒魔術使ってんぞ、気をつけろよな!」などと言われる始末である。禁止されているわけではなく、「気をつけろ」なのがミソではあるのだが。

さて、サウンドプレイヤについてもこの黒魔術を通して Audio 要素っぽいものを得ることはできるようなのだけど

var {Cc, Ci} = require("chrome");
var sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
var uri = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService)
.newURI(self.data.url(...), null, null);

sound.play(uri);

これで得られる nsISound は Audio 要素とは全然異なるものであって、たとえばボリュームの指定とかはできないようなのである。

kosian is here #3

kosian ベースの wasavi を Firefox で動かすべくあちこち直している。

Chrome と Presto Opera のエクステンションの仕組みは、もちろん同じではないにしても、いとこ程度には基本的な構造は似通っているので、一方が動けば他方はそれに合わせて直すだけでいいので楽なのだ。

問題は Firefox の Add-on SDK なのである。このプラットフォームで作る拡張機能というのは、かなり異質だ。Chrome や Opera の遠い親戚の、隣に住んでいるバングラデシュ人くらい異質だ。

Add-on SDK でいわゆる content script 的なものを動かすには、PageMod API を使う。このとき、どの URL で content script をアタッチし、あるいはどの URL ならアタッチしないかという include と exclude の情報が必要になる。wasavi の場合フロントエンドは agent と wasavi 本体にコードがわかれていてアタッチの仕方が若干複雑なので、exclude 機能は必須である。で、どちらを指定する機能も、Chrome と Opera は当然持っている。

Add-on SDK の PageMod は include の指定しかできないのである。意味が、わからない。そして、最新のドキュメントを見ると exclude 機能は Firefox 32 で実装されるよ! 乞うご期待! という扱いになっているのである。意味がわからない。

どうするか。include には URL がマッチすべき文字列か正規表現オブジェクトを渡すことになっている。ここで PageMod が求める正規表現オブジェクトとは必ずしも RegExp のインスタンスである必要はなく、test() と exec() メソッドを備えていてそれぞれがそれぞれの働きをすれば通してくれるので、今まで wasavi ではそのように振る舞う独自のオブジェクトを渡していた。この時点でちょっとアクロバティックである。

で、Firefox 30 でそのコードを動かしてみると見事に動かない。なんと include に渡されたものが文字列か RegExp インスタンスかどうかのチェックを行うようになったのである。このチェック自体は正しい。しかし前述の通り Firefox 30 の PageMod には exclude 機能はない。意味がわからない。どうしろというのか。

仕方がないのでさらにアクロバティックなことをして(互換性はないのに constructor を RegExp にする)動かすようにしたのだが、もちろんかなり嫌なコードである。なお、さらに PageMod 内部で摩訶不思議な RegExp インスタンスのキャッシュ機構が働いているらしく、toString() も上書きしないとダメだったりする。本当にがっかりさせられる。

最初から exclude 機能をつけてくれればこんな変なことをする必要はないのだし、そもそも include 機能があって exclude はないという API のデザインセンスの意味がわからない。

こういう記録が残るブログであまり穏やかではないことは書きたくないのだけど、Add-on SDK チームってバカなんかじゃないかと思う。

wasavi for Firefox is available

1月19日に AMO へアップデートを申請した wasavi 0.5.281 が今日フルレビューを通った。まるまる3週間かかったことになる。AMO からの素敵なお便りメールを受信する設定にすると、

Most updates are being reviewed within 2 weeks.

などと高らかに謳っている自画自賛メールがやたら届くわけだが、それはあくまで平均の数値であって実際にはなかなかそう上手くは行っていない。

別に Mozilla のそういうスタンスがダメだというわけではない。Chrome のように機械的にチェックするだけとか、Opera のようにおめーそれ体裁しかみてねーだろ! といったスタンスに比べたら、時間がかかってもひと通りソースコードを見てもらえるというのは良いことだ。とても良いことだ。

それにしてもちょっとレビューに時間がかかり過ぎじゃないですかね……ということなのだ。Mozilla って別に爪に火灯すような財政状況でもないと思うんだけど、なんでレビュアーを増やさないのかな。

a difficult AMO #5

AMO の中の人の手元でもサーバーエラーになるそうなので、bugzilla に登録してもらった。

うーんこちらで直接登録してもよかった気がするが。まあでも中の人……というか「the Add-ons Developer Relations Lead for Mozilla」という肩書きのおかげか、あるいはアサインとかも最初から割り当てられてたせいか、すぐに修正されたようだ。あとは、そのパッチがレビューされて実際に適用されるのを待つだけということかな。

長かったなあ。

関係ないけど、wasavi の preliminary / full review をしてくれた方は Pentadactyl の開発者の一人ということで、vi 繋がりなのかと思ってたら、単にものすごく多くのアドオンをレビューしているだけだった。なーんだ。

beauty is relative #2

relativenumber を実装してみたが、カーソルから上の領域の行番号が負の符号付きになる件。これはこれでしかたないかなーと思っていたところ、コメント欄で間接セレクタを使えばいいと教えていただいた。

あー、間接セレクタね! うんうん間接! 間接……間接セレクタってなんだっけ?

バカにしないでくれる!? 知ってるわよそれくらい!!(ググりながら)

というわけで無事 vim の relativenumber と同様の見た目になった。ありがとうありがとう。