Meet Vivaldi

Vivaldi で赤福プラスを動かしてみた。
akahukuplus-on-vivaldi1
akahukuplus-on-vivaldi2
akahukuplus-on-vivaldi3
まだ一応動かないこともない、というレベル。

for Firefox #10

特に CSS Transition による再描画がカクカクになってしまう事があると書いたが、どうも iframe による広告がてんこ盛りのメインコンテンツで、おまけに 2 ちゃんねるのまとめが載ってある系のまとめサイトを開いてしまうとそういう状態になる気がする。iframe が多すぎると色々おかしくなるのは Presto Opera ではよく経験した(読み込みが終わらなくなるとか、スクロールがカクカクになるとか、不要な履歴が追加されてしまうとか)が、Firefox でも無問題というわけではないようだ。へぇ。

ううん。でもどうしようもないかも。

赤福プラスで、youtube へのリンクを見つけたら埋め込みのプレイヤーに展開するようにしているのだが、これが意外にメモリを食っている。about:memory で見てみると、ひとつあたりだいたい 8MB ほど。○○な動画教えてよ系のスレを開いてしまうとなかなか厳しい。

そこで、ドキュメントの scroll イベントで何かしてみることにした。つまり、埋め込みプレイヤーが、今現在見えている文書のスクロール位置から十分離れている場合は、プレイヤーを除去してしまう。逆にスクロール位置に近づいたらあらためて生成する。Twitter でも同じことをしている。

問題があるとすると、動画を見ていたが一時停止してまたあとで見よう…と思いつつ一旦遠く離れた位置にスクロールしてしまうと、問答無用でプレイヤーが削除されてしまうので一時停止したことも綺麗さっぱり忘れられてしまうことだが。

for Firefox #9

KeySnail の設定を行う。と言ってもとりあえず ~/.keysnail.js を書いて読み込ませるだけだ。また、そんなにガチガチにオレオレキーバインドしまくりというわけでもない。なんとなくそれっぽく hjkl が使えるとか、テキスト入力時に C-f とか C-b とかが使えれば良いのだ。

ただ、単にキーにただひとつの機能を割り当てるものの他に、いくつか特殊なのはある。

まず C-h に、履歴を戻る機能を割り当てる。ただし、戻れる履歴がない場合、つまり履歴の先頭まで戻った場合はタブを閉じる:

key.setViewKey('C-h', function (ev) {
. if (getBrowser().canGoBack) {
. . BrowserBack();
. }
. else {
. . BrowserCloseTabOrWindow();
. }
}, '戻る、または閉じる', false);

この時あらかじめ用意されている Firefox の機能を呼び出すために、BrowserBack とか、key.generateKey(引数は仮想キーコード) とか、goDoCommand(引数は内部コマンドの文字列表現) とか、undoCloseTab とか、命名規則から引数から何から何まで統一されていないカオスが揃っていて調べるのがとってもめんどくさい。

次にスペースキーに、ビューの高さの半分だけスクロールする機能を割り当てる。ただし、すでにスクロールできる末尾に達していた場合は何かをする。何かって何かといえば、Presto Opera に倣って言えば

  • [cci_html][/cci_html] な要素があればそれが指すアドレスへ移動する
  • 次へ とか next とか書いてあるそれっぽいリンクがあったらそのアドレスへ移動する
  • 画像っぽいものリンクをリストアップし、連番にソートし、順番にそのアドレスへ移動する。すべて辿ったら元のページに戻る

的な、よろしくやってくれる系の機能だ。ただこれ、link 要素程度ならともかく残りを実装するのかなり大変なので後回し。

それと、この機能は赤福プラスと少し関係する。赤福プラス側ではすでにスクロールできる末尾に達していた場合は続きを読むようにしているのだ。しかしスペースキーを KeySnail 側で消費してしまうと赤福プラス側のそれが呼ばれることはない。そこで、何かするのに先立って RequestMoreContent というカスタムイベントをドキュメントに対して発生させるようにした。そのイベントが preventDefault() されていなければ、何かする……というふうにすればよい。もちろん赤福プラス側ではそのイベントをリスンし、続きを読み、preventDefault() するのである。

key.setViewKey('SPC', function (ev, arg) {
. let w = twin(ev);
. let d = tdoc(ev);
. if (w.scrollY >= d.documentElement.scrollHeight - w.innerHeight) {
. . let customEvent = d.createEvent('CustomEvent');
. . customEvent.initCustomEvent('RequestMoreContent', true, true, {});

. . if (d.dispatchEvent(customEvent)) {
. . . // 何かする
. . }
. }
. else {
. . w.scrollBy(0, Math.floor(w.innerHeight / 2));
. }
}, 'ビューの半分の高さだけスクロールダウン', false);

for Firefox #8

というわけで、ぼちぼち Firefox 版の赤福プラスが動き始めてきたので常用し始めてみる。まず、Firefox といえばエクステンションでいかようにもできるブラウザということなのだが、逆に言うと素のままで使うのはなかなか厳しいということである。とは言うもののエクステンション漬けになるのも後々困りそうなので、できるだけ最小限のものに絞って入れてみる:

  • AdBlock Plus
  • Gmail Notifier
  • Hide Caption Titlebar Plus
  • KeySnail
  • TabDeque
  • YesScript
  • ブックマークを新しいタブで開く
TOWN AGE はいいアルバムだと思います

TOWN AGE はいいアルバムだと思います

Presto Opera でやっていたようなキーバインドは、その一切を KeySnail で行うようにした。これはこれで奥が深いので keysnail.js の内容はいろいろやってみる必要があるのだけどとりあえずそれはさておき。

現役のブラウザということで、ナウいページでもちゃんと表示できるという安心感はあるのだけど。Presto Opera に比べてブラウザの動作の表も裏も javascript で制御される割合が大きいためなのかどうか、なんというか、Presto Opera に比べて全体的に 1 割〜 2 割増しで動作が緩慢な気がする。レンダリング速度自体は速い。しかしたとえば何がしかのボタンを押してから反応が返ってくるまでの間のようなものは Presto Opera に比べても微妙に鈍くさい。

Presto Opera は javascript の動作速度こそ最前線のブラウザに比べて一歩落ちるものの、レンダリング自体はまだ結構速いし、アプリケーション全体の速度もなかなかチャキチャキしてたんだなあと再認識した。Firefox は全くその逆の特性になっている。

また、数時間使っていると、そのせいなのかあるいは別の理由があるのかレンダリングがかくかくになってしまうことがあるような気がする。かくかくというのはつまり CSS Transition のようにせめて 30fps で安定して再描画してくれないと困るものがコマ送りのようになってしまうのである。なにがトリガーなのかはいまいちわからないがとりあえず適当な間隔で再起動すれば直る…のだけど、これじゃ Presto Opera と変わらない(こっちはこっちでソケットの解放をし忘れるという盛大なバグがあってやっぱり適当な間隔で再起動が必要)。

あとは、IME のハンドリングがまともなのがうれしい。Linux 版の Presto Opera のそれはもう、なんというか、テスターに正拳ぶち込みたくなるくらいめっちゃくちゃのしっちゃかめっちゃかなので……。

for Firefox #7

  • [cci]disable-output-escaping[/cci] にいよいよ手を入れる。これは何かというと、XSL による変換を行う際にすでにマークアップされているであろう文字列をそのまま出力するというオプションなのだけど、Firefox が内蔵する XSLTProcessor はこれに対応していない。

    XSLProcessor へのオプションの与え方といったレベルではワークアラウンドはない、と思う。

    というわけでどうするかというと、マークアップ済みの文字列を要素の内容としてではなく、要素の data-doe 属性に収めて出力させる(doe は disable-output-escaping の略)。得たフラグメントをドキュメントに追加した後、querySelectorAll(“[data-doe]”) で抜き出し、それぞれを insertAdjacentHTML() で挿入させるようにした。

  • xpi を生成するよう Makefile を追加した。生成物は https://github.com/akahuku/akahukuplus/tree/master/dist に置いた。ちなみにちゃんとインストールできるかはまだ試していない。

for Firefox #6

Add-on SDK で作る拡張機能のうち、バックグラウンドで動くコードはモジュールとして作り、複数モジュールを用意した場合はそれぞれが非干渉になるようになっている。そういう仕組みとして固定されている。モジュールの機能を呼び出す場合は明示的にエクスポートした関数を通す。

で、そのエクスポートされた関数を呼び出した際に不思議なことが起きた。Kosian を初期化するために RegExp のインスタンスを含んだハッシュオブジェクトをオプション群として渡す。その RegExp インスタンス、どういうわけか instanceof RegExp が真にならないのである。なので一部の初期化が正しく行われない。相変わらず恐ろしく重くて遅い内蔵デバッガで覗いてみたりする分には普通に正規表現オブジェクトだし、Object.prototype.toString.call() してみても [cci][Object RegExp][/cci] が返ってくる。しかしながら instanceof RegExp は偽になる。

なんか嫌な感じ。

ちなみにモジュール内の関数で生成した正規表現オブジェクトをその場で判定すれば当然 instanceof RegExp は真になる。モジュール間での関数呼び出しの際に引数が何か操作されてる感じなのだ。嫌だなあ。なんか嫌な、品のないブラウザだ。

for Firefox #5

またしても window と unsafeWindow の違い。Add-on SDK のコンテントスクリプトにおいては、window はおなじみの Window ではない不思議なグローバルオブジェクトである。ページのグローバルオブジェクトとしては unsafeWindow を参照する。なぜそんな殺風景な命名なのかはわからない。contentWindow でいいと思うのだけど。

  • すでに何らかの動作にイベントリスナが結び付けられている時にそのリスナを呼び出したい際、リスナを直接呼び出すのではなく、コードから擬似的にイベントを発生させる手法をよく使う。リスナがスコープになくてもよくなるので。そういう場合、document.createEvent(…) して、init*Event(…) して、目的の要素に対して dispatchEvent(…) する。これが上手く行かない。init*Event() の引数内に view オブジェクトとして window を渡すとエラーになる。解法は window.unsafeWindow が存在する場合はそちらを渡す。
  • CustomEvent に対しても同様。
  • pushstate イベントをリスンしている。その状態で location.hash を更新すると Firefox でだけ pushstate イベントが発生する。どのブラウザで動作している場合hも、location.hash を更新するときは一時的に pushstate のイベントリスナを取り外すよう修正。

for Firefox #4

  • favicon に、スレ画像から canvas を経由して動的に生成した data スキームの文字列を割り当てているのだけど。ImageData オブジェクトのプロパティにアクセスすると Firefox ではよくわからないが権限の異なるゾーンをまたいでるよ的エラーになる。そんな内部構造から来る制限でエラー出されても困りますよ。

    似たような不具合によれば、new window.ImageData や canvas のコンテキストの createImageData() ではなく、new unsafeWindow.ImageData() とすると動く。ふーん。

  • CSS Transition を多用しているのだが、transition プロパティでショートハンド指定すると発生しない。transition-duration 等々で個別に指定すると発生する。なにそれ。まるで IE6 とかがやらかしそうなしょっぱいバグですけど……。

    それと、実際に確認したわけではないのだけれど、transitionend の発生するタイミングがおかしいとレポートしている blog の記事があったりする。本当ならひどいもんだね。

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();
}