- favicon に、スレ画像から canvas を経由して動的に生成した data スキームの文字列を割り当てているのだけど。ImageData オブジェクトのプロパティにアクセスすると Firefox ではよくわからないが権限の異なるゾーンをまたいでるよ的エラーになる。そんな内部構造から来る制限でエラー出されても困りますよ。
似たような不具合によれば、new window.ImageData や canvas のコンテキストの createImageData() ではなく、new unsafeWindow.ImageData() とすると動く。ふーん。
- CSS Transition を多用しているのだが、transition プロパティでショートハンド指定すると発生しない。transition-duration 等々で個別に指定すると発生する。なにそれ。まるで IE6 とかがやらかしそうなしょっぱいバグですけど……。
それと、実際に確認したわけではないのだけれど、transitionend の発生するタイミングがおかしいとレポートしている blog の記事があったりする。本当ならひどいもんだね。
Author Archives: akahuku
for Firefox #3
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] が最大の障壁だと考えていたのだが、そこに至る前にもけっこうな壁があるのである。ああ。
for chrome
赤福プラスを Chrome でも動くようにちょこちょこっと調整している。
まず Kosian の HEAD に追従させるべくバックエンドの微調整。これは特に問題はない。めんどくさいのはフロントエンドで、フロントエンド側というのは開発の極々初期に html のレイアウトを Chrome や Firefox で確認したくらいで、それ以降の全ては Presto Opera でしか動かしていないのであった。特に、XSLTProcessor という素敵な処理系の動きが Chrome でも Opera と同様なのかどうか動かしてみないとまったくわからない。
などと予想していたのだが、割とあっさり動いた。Chrome が内包する XSLTProcessor は xsl:import や xsl:include に対応していないなどの罠があるらしいが、今のところそれらは使っていないので大丈夫。ただ XSLTProcessor#translateToFragment() に失敗した場合、エラー情報を含んだ xml 文書を返してくれるのはどうなんですか。普通に素直に例外出していいんじゃないのかな。
次は Firefox で動かすようにするのだが、むしろこっちが本命なのだが(個人的な常用ブラウザを Presto Opera から Firefox に移行させつつあるので)、これまでの移植の経験から鑑みるに、たぶんこっちはいろいろとめんどくさい問題が出まくる。例えば [cci]disable-output-escaping[/cci] とか。
exodus from presto opera #2
そろそろ本格的に Presto Opera から脱出しようと考えている。いよいよ様々なサイトが、異様に重かったり、満足に動かなかったり、「オメーのブラウザには見せてやんねー!」されたり、果ては固まったりするからだ。また、特に Linux 版は IME のハンドリングが根本的にめっちゃくちゃなのもさすがに耐えられなくなってきている。
脱出して何に移行するかは 3 つの選択肢がある: Chrome か、Blink Opera か、Firefox かだ。一方で、以前にも何度か書いたが、Presto Opera では以下のようなカスタマイズや機能使用をしていた。つまり移行先のブラウザでもできるだけ同じようなことを出来るとすごくうれしい。
- ブラウザと統合されたメーラ/フィードリーダ
- ビュー上でのキー操作のカスタマイズ
- アドレスバー上でのキー操作のカスタマイズ
- ブラウザと統合されたメモ機能
ブラウザと統合されたメーラ/フィードリーダ
これは Presto Opera の特徴でもあるのだけど、実際のところメールの読み書きは gmail で行っているので、メーラに関しては notifier 系のエクステンションを入れればそれで済んでしまうかもしれない。フィードリーダはまあいろいろあるのでどのブラウザに移行するかという問題とは直接関わらない。つまるところメーラ関連は移行に関してあまり大きな障壁にはならない。
ビュー上でのキー操作のカスタマイズ
移行先のいずれのブラウザも、Presto Opera が備えていたようなキーバインディングの編集機能は持っていない。従って似たようなことをさせるにはエクステンションを入れるしかないのだが、幸いそれぞれのブラウザに某かのエクステンションがある(Blink Opera には今のところなかった気がするが、まあ要するに Chrome なので、何とかなるのである)。ただし Chrome 系は、http や https 以外のスキームのページでエクステンションが動かないとか、純正エクステンションストアでエクステンションが動かないとか謎の制限がたくさんあるので実際のところぐぬぬとなるシーンが少なくない。
アドレスバー上でのキー操作のカスタマイズ
同じキー操作のカスタマイズでもこちらが問題だ。Chrome 系はアドレスバーに対するカスタマイズは極々制限されたものしか許していない。むしろアドレスバーはあんまりいじらせたくないという確固たる意志すら感じる。キー操作に関しては、Firefox のほうが自由度は圧倒的に高い。
ブラウザと統合されたメモ機能
意外と盲点だったが、けっこう使っていた。Opera のメモ機能というのは、メモ機能と表現するより他にないシンプルなものだ。しかしそれは悪いことではなく必要最小限な機能に絞ったよいデザインと言える。
- メモを追加、編集、削除できる
- メモを操作するためのペインがタブとして開く。またパネル内に開くこともできる
- メモ同士の関係性はツリー構造である
- Opera Sync による同期の対象である
- 編集可能な要素上のコンテキストメニューにメモの構造のツリーが現れ、そのまま挿入できる
テキストベースのメモ機能というのはおそらくどのブラウザのエクステンションにもたくさんあると思うが、Opera のメモ機能にできるだけ近い操作性のものってあるんだろうか。たくさんあるだけに試すのが大変だ。そして、たぶんないと思う。どうしたものか。
その他
たとえばリンクを含んだ文字列上のドラッグ時の振る舞いは Presto Opera が未だに最も賢い。これは Firefox にそういうように動作させるエクステンションがあるようだ。ただ、いずれにしても javascript でのエミュレーションであり、ページコンテキストのスクリプトが何かやってる場合にそれと上手く共存させられるのかどうかは保証できない気がする。このへんは移行先のブラウザの流儀に慣れたほうがよいのかもしれない。
Presto Opera はどのタブを最後にアクティベートさせたか、のスタックを保持していて、タブをクリックした時タブをそのスタックの末尾に追いやる、いわば最小化動作を行わせることができる。これもたぶん Firefox にはそういうのがあるだろうけど、Chrome ではタブクリックのイベントをエクステンションから取れないのでエミュレートは不可能だと思う。
Chrome はマシンのリソース(CPU パワーや、メモリ)をふんだんに使うブラウザとして有名だ。これは非力なノート上ではつまり、何かするたびファンが回りまくるということを意味する。はっきり言ってとてもうるさい。
まとめ
このように俯瞰してみると、拡張性という意味では Chrome は Firefox に一歩以上劣っているようだ。あるいは優劣ではなく両ブラウザで拡張性のベクトルが違うと言ったほうがよいのかもしれない。しかしそれを踏まえても、Opera からの移行をスムーズに行えそうなのは Firefox のようである。というわけでそうしてみたい。
execution #3
ex コマンドの executor をごそっと書き直した。また、global コマンドの内容もそれに合わせた。
基本的には想定したコードそのままなのだが、ひとつ思いがけないことがあった。個々の ex コマンドはアドレスを前置することができる。このとき、絶対指定以外の行番号は現在のカレント行から相対的に決定される。それで、現在は ex コマンドのソース文字列をパースする段階でアドレスも評価し、実際の行番号を得ているのだが、一方で global コマンドの入れ子コマンドはいったん生成した中間形式を繰り返し再利用する形になっている。つまり、アドレスの情報が最初に評価した状態から変更されないのである。アドレス情報だけは実行ごとにダイナミックに生成しないといけない。
とりあえずあんまり綺麗ではない手法で何とかしたが(ex コマンドを実行した後にアドレス情報を破棄する。再度実行した時アドレス情報が無効なら再生成する)、これはパースの段階では単に文法的な誤りがないことだけを判断し、評価は実行直前まで遅延させたほうがいい。これはそのうち直す。
次は s コマンド。
a suddenly farewell #6
何やら回線の IPv6 関係が変わったせいでネットへの接続が物凄く遅くなった件。NTT から借りたホームゲートウェイではなく、buffalo 製の無線 LAN 親機をルータにすることでとりあえず解決したのだが。
この buffalo 製の WHR-G300N のルータ機能がなかなか曲者なのである。つまりとっても遅くなるときがあるというか、詰まる感じがするのである。調子のいい時はいいんだけど。たぶん Wi-Fi 周りをさばくのと LAN のルータとして振る舞うのと両方こなすだけの処理能力がないのかもしれない。
さて、IPv6 といえばそれを有効にするには ISP に対して有料のオプションを追加契約する必要があるというのがここ数年のセオリーだったと思っていたら、去年辺りから追加費用なしで IPv6 化できるようになっているようである。というわけで、@nifty に対して IPv6 化を申請して、そうなった。
最終的にルータはホームゲートウェイ側に戻し、buffalo のアレは無線 LAN のアクセスポイントに専念してもらうことにした。変な詰まり感はなくなり、亀が踊る様子も見ることができ、それでいて追加費用はなしとは、この世の春ではないか(近いうちにトラブルが起こるフラグ)。
execution #2
s コマンドについてもまとめておきたい。
s コマンドは global に似ていて、前段の処理と後段の処理が分かれている。前段で正規表現にマッチした箇所を覚えて、後段でそれらを置換する。したがって中間形式でも、ユーザがアクセスできる ex コマンドは前段、実際に仕事をするのは後段というように分けるのが自然だ。後段を latter-subst と名付けよう。
s コマンドは c オプションを付加するかどうかで動作がまるっきり変わる。すなわち、置換ごとにユーザに動作を確認するインタラクティブなモードと、一気に置換するモードだ。後者の場合は、latter-subst を生成する必要はない。前段の処理の最後に置換処理を呼び出して終わりである。したがって latter-subst は c オプションが付加されている場合のみ生成される。
c オプションが付加されると、モードが ex_s_prompt になる。また、置換に必要な情報は SubstituteWorker インスタンスに格納され、保持される。そのモードでのキー入力によって適切な動作を行う。現在は、modeHandler が SubstituteWorker を直接制御しているのだが、これは latter-subst に移譲したほうがよいだろう。つまり ExCommandExecutor のプロパティとして現在のプログラムカウンタが指す ex コマンドオブジェクト的なものを参照できないといけない。
なかなか固まってきた。そろそろ実装したい。
execution
ex コマンドの実行処理を再考したい。
実行処理の核となるループは単純だ。
- ソース文字列は ex コマンド列である
- ソース文字列から ex コマンドをひとつ取り出す。このとき、ex コマンドの区切りは改行だったり、[cci]|[/cci] だったりコマンドごとに異なる。この違いを吸収するのはパーサの仕事だ
- 取り出した ex コマンドから引数を解析する
- コマンド毎のハンドラを呼び出す
- ハンドラがエラーを返すか、ソース文字列の最後まで実行するまで繰り返す
しかし単純ではないものもある。まず、動作が非同期に行われるコマンドがある。
- クリップボードからの読み込み
- dropbox などからのファイルの読み込み
これらのコマンドを実行した場合、いったん実行ループをそこで終了し、待機状態にする必要がある。ex コマンド実行中ではあるが、待機中ということだ。そして、それぞれの処理が完了した時点で実行ループを再開する。なお、待機状態のタイムアウトや、[cci]^C[/cci] を特別なシグナルとして扱う処理は必要だ。
もうひとつ面倒なのは、ex コマンドが入れ子に実行される場合がある。global、v、および edit コマンドなども該当する。
現在の ex コマンド実行クラスである ExCommandExecutor は、非同期処理の機構は基本的には実装済みであるものの、先の記事の通り global の入れ子としてのコマンド群の実行が非同期処理に対応していない。これをどうにかするのがこのミッションのステートメントだ。
ExCommandExecutor はまずソース文字列を解析し、ひとつの ex コマンドにひとつのオブジェクト(引数とハンドラを保持する)が対応するその配列を生成する。ソースそのものではなくいわば中間形式にして保持するのは、非同期の完了時に呼ばれる仮想的・擬似的な ex コマンドを動的に ex コマンド列中に挿入する必要があるためだ。ソース文字列のままだと挿入するポイントをつかみにくい。なお擬似というのは中間形式の状態では存在するが、あくまで内部的に使用するものであって、ユーザーが直接使用することはできないということだ。
それを踏まえて入れ子コマンドを考えるに、中間形式も入れ子の構造にする必要はない。たとえば配列の [n] 番目が edit でその引数に読み込み完了後に実行される ex コマンドを指定した場合、時系列的にはその入れ子のコマンド群は [n+1] 番目のコマンドの前に実行されるわけなので、単に一次元の配列のまま [n] の直後、[n+1] の直前に新しい中間形式を挿入すればよい。あくまで一次元の配列のまま考えばよい。
ただしそうは言っても、global の場合はもっと複雑になる。[n] 番目が global だったとき、[n] の直後に入れ子コマンドの中間形式を挿入するのは同じだが、先頭には擬似 ex コマンドとして global の処理の後段、つまり与えられた正規表現にマッチする全ての行に対して入れ子コマンドを実行するループを制御する処理を司る中間形式 latter-global-head が入る。また末尾にもまた擬似 ex コマンドとして、入れ子コマンドの実行後の分岐を行う処理を司る中間形式 latter-global-bottom が入る。latter-global-head は引数とハンドラの他に
- 正規表現にマッチした全行への参照
- 入れ子の ex コマンドの個数
も保持する必要がある。latter-global-bottom は
- 入れ子の ex コマンドの個数
を持つ。
そして、実際にこの中間形式を実行する際は、
- global: 次に実行されるべき latter-global-head を挿入する
- バッファから正規表現にマッチする行を抜き出し、latter-global-head に保持させる
- 入れ子のコマンドの中間形式を latter-global-head の直後に挿入する
- 続けて latter-global-bottom を挿入する
- latter-global-head、latter-global-bottom に挿入した中間形式の個数を保持させる
- latter-global-head: マッチしている行が残っているなら、何もしない。すなわち直後の中間形式を実行させる
- マッチしている行が残っていないなら、中間形式のプログラムカウンタ的なものに入れ子の ex コマンドの個数を加算し、実行をスキップさせる
- latter-global-bottom: 入れ子のコマンドが実行されるとその最後にこの中間形式が実行される。プログラムカウンタを latter-global-head へ戻す
というような、逐次処理・分岐処理・繰り返し処理を備えたちょっとした構造化言語として振る舞わせる必要がある(これは一見、ex コマンドの実行処理部がそのままスクリプティングの実行エンジンに転用できそうな感じがするが、その予定はない)
それから、入れ子のコマンドでは特定のコマンドが使用を制限されることに気をつけなければならない。global の入れ子コマンドは global と v を使うことができない、と POSIX は定義している。これはわかる。しかし不思議なことに、edit コマンドに付加できるコマンドにはそういう制限はないように思える。つまり edit の入れ子に edit したりすることが許される。[cci]:edit +edit\ B A[/cci] としたとき、最終的に読み込まれるのは B だ。これにどういう意味があるのかはわからないが、まあそういうふうに動作するように組んでみよう。