Some essential changes

wasavi 0.6.387 を github に上げた。このビルドには、いくつかの重要な変更がある。

まず、Firefox 版が Github 上で起動しない問題。前述の通り、これは wasavi をページ上に出す際の iframe に割り当てる src のドメインの問題だ。Github の CSP が data: スキームを許可していない。

そこで、iframe の src へは [cci]about:blank?wasavi-frame-source[/cci] というアドレスを割り当てることにした。当然中身は空っぽなので PageMod を経由して中身を差し込む。ちなみに about: スキームも CSP は許可していないのだが、結局 CSP の記述を実際にどう利用するかはブラウザが判断することであり、いろいろな例外があるわけで、すなわち about: 系は許されるのであり。このへんは実際に試してみないとわからないのだった。

実は Github で起動しない理由は他に、Hotkey モジュールに定義したショートカットよりもページスクリプトが消費するキー入力イベントが優先されるというのもある。しかし Firefox 版で Hotkey モジュールを使った理由というのは、それがページスクリプトよりも優先されるはずだからなのであり、実際その当時はその通り動作した。しかし現在はそうではないのである。しかし、まあ、Firefox というものは、大体において、そういうものなのでしょう、きっと。これは単に Chrome や Presto Opera と同様、keydown のフック機構を経由してキー入力を監視する方式に切り替えればいい。このくらいでは泣かないぞ。

* * *

insert モードと replace モードを総称して input モードと呼ぶ。従来はそれを実現するために、カーソル位置に重ね合わせる形で透明な textarea を配置し、それが受けたイベントを wasavi へ転送するというわけのわからない複雑さを備えた謎の機構を経由していた。なんでこんなめんどくさいことをしているのかといえば、すなわち Presto Opera のためだ。あの苦労してこさえた擬似 composition イベントは、textarea がフォーカスを持っている状態じゃないと正しく生成できないのである。

しかし、まあ、Presto Opera でありますよ。そろそろ引退させたほうがいろいろな方面が幸せになるのですよ。

というわけで input モードでは div 要素の contenteditalbe 属性を利用するようにした。これにより結構な量のコードをばっさり削除できるのである。おまけに入力のオーバーヘッドも減り、Selenium のテストも速くなり、word wrapping なども CSS での指定をそのまま使用でき、悪いものはないのである。強いて言えば前述の通り Presto Opera で IME を通した入力ができなくなるが、Presto Opera 上で wasavi を使っている人間なんて地球上にただ数人いるかいないかであり特に問題ではない(とはいえ最低限おかしくならない程度の処理は入れたが、まあ完全ではない)。

Some topics

  • [cci]cfx run[/cci] で wasavi のデバッグ用 Firefox を起動する。そうすると console への出力がそのまま端末にリダイレクトされるのだが。Firefox 自身が持っている chrome:// なんちゃらドメインの各種 javascript ソースから発生するログがあまりに多すぎる。たいていは strict モードにまつわるエラーで、中には本気でこれバグってるんじゃないの? 系のエラーメッセージもあったりする。オブジェクトの存在しないプロパティにアクセスしてたり、null のオブジェクトに触ってたり。

    ちょっと、ひどい。直したほうがいいんじゃないですか。

  • Blink Opera の wasavi では、ページフックスクリプトをいうものを有効にしていたのだが。すべて除去した。

  • Firefox 版の wasavi では、たとえば github 上で動作しない。これはなぜかといえば github のサーバから送られてくるレスポンスヘッダ内の CSP が原因だ。Firefox 版の wasavi は data スキームの内容を src にした iframe だ。しかし github のページの CSP は data スキームの iframe を許可しない。

    これ、どうしたものかなー。もしかしたら解決の糸口になるかもしれないものを一応メモ。

    Firefox Addon SDK: Loading addon file into iframe
    http://stackoverflow.com/questions/21082162/firefox-addon-sdk-loading-addon-file-into-iframe

Up and Down Quickly

github 上の wasavi の issue で、[cci]j[/cci] [cci]k[/cci] が重い というものがあるのだが、いくらなんでもそれらのキーの機能が重かったら即気が付くわけで、従ってまったく手元で再現せず手を付けられないでいた。

しかし、ふともしかして [cci]:set jkdenotative[/cci] してるんじゃないの? と聞いてみたところその通りであるらしい。なるほど。すべての謎が解けた。

本来 [cci]j[/cci] と [cci]k[/cci] は、改行までの 1 行(物理行)を単位としてカーソルを上下に移動させる。一方、[cci]gj[/cci] と [cci]gk[/cci] は、レンダリングされた結果における折り返し行を単位とする。ここで、もしユーザーが改行までの 1 行が十分に長いテキストを頻繁に編集するのなら、[cci]j[/cci] [cci]k[/cci] は折り返し行単位で移動したほうがカーソルの移動は圧倒的に楽である。

そういうわけで、[cci]jkdenotative[/cci] オプションは [cci]j[/cci] [cci]k[/cci] と [cci]gj[/cci] [cci]gk[/cci] の機能を交換するか否かを指定する。まあ [cci]:map[/cci] でもいいんだけど。

で。この折り返し行単位の移動が確かに重いのである。従って [cci]:set jkdenotative[/cci] とすると、[cci]j[/cci] [cci]k[/cci] でのカーソルの移動がめちゃ重くなるわけだ。

よしわかった。やってやろうじゃん。100 倍速くしてやる。

折り返し行単位でのカーソルの移動を行うためには、折り返し行のそれぞれの先頭位置が、物理行のどの位置に対応するかというマッピング情報が必要になる。wasavi における物理行はすなわち div 要素なのだけど、しかし DOM にはそのマッピング情報を得る手段がない。そういうわけで、かなり力技を使わざるを得ない。物理行のある位置 n が属する次の折り返し行の先頭の物理位置を得るには、n から物理行の末尾に向かってループする。ループ内では 1 文字ずつカーソル位置を span でくくり、getBoundingClientRect() を呼び出す。これが前回の呼び出し位置と top プロパティが異なっていればそれが折り返し行の区切りになる。従来のやり方はこれであり、そして、すんごく重い。getBoundingClientRect() が重い。そういうわけでできるだけこのメソッドの使用回数を抑えるか、あわよくばゼロにする必要がある。

getBoundingClientRect() が重いのは、それがページ全体における要素の位置を計算するからだと思う。display:static な要素の場合、その要素の前のすべてを再計算しないといけない(レンダリングの結果でそれぞれの要素がそれを保持していないんだろうか? という気もするが)。

それはそれとして、とにかく別の方法を使わなければならない。ループを用いるのは同じだが、先立ってカーソル位置をまず span でくくり、これを left とする。また、カーソル位置から物理行末尾までの文字列を別の span でくくり、これを right とする。

ループでは right の先頭を left の末尾へ追加するという処理を行い、その都度 left の offsetHeight をチェックし、ループ直前のそれよりも増えていれば、折り返し行の区切りに達したことになる。offsetHeight はその要素だけで再計算が完了するはずなので、getBoundingClientRect() よりは速い。

また、ループは 2 つのステージで構成するようにする。第 1 ステージでは、ループカウンタの増分は 1 ではなくループごとに 2 倍して、ガンガン先に進める。この場合、「次の」折り返し行を超えた先へ到達してしまう可能性があるので、そのようである場合(初期 offsetHeight + 1 行分の lineHeight よりも offsetHeight が大きければ)増分を 1/2 してやり直したりする微調整は必要。

いずれにしてもカーソル位置の次の折り返し行に達した場合は第 2 ステージへ移り、増分を 1 に固定して正確な折り返し行の区切り位置を見つけ出す。これによりオーダーは O(n) から O(log2 n) になる、はず。

やってみたところ、修正前は [cci]10j[/cci] するのに 1600ms ほどかかっていたものが、160ms 程度まで短くすることができた。うーんさすがに 100 倍速は無理だった。また、折り返し行の区切り位置を見つけ出すループに関しては、たとえば単純ループが 38 回かかるところを 14 回で済んでいる。これはちょっと多いが、第 1 ステージは 6 回程度で抜けているのでオーダーの計算としては合ってる。第 2 ステージは更に最適化できるかもしれない。

というわけで折り返し行単位のカーソル上下移動が実用的になったと思う。最初からそう組んどけよ! と言われればはいその通りですと言わざるを得ないのだが。

change a IME state

あらゆるところですでに怨嗟の声が書き込まれまくっているので、特に新しく書くこともないのだけれど、iBus 1.5 で IME の状態を切り替えるという話。

つまるところ、IME のオン・オフを切り替えるために従来は IME 内の状態(”直接入力” と “ひらがな”)を操作していた。それが、システムに登録されたキーボードレイアウト(たとえば、US キーボードと mozc が有効なキーボード)自体を切り替えるようになったのである。”IME が有効なキーボードレイアウト” という概念が不自然で、この辺の移行が混乱しているのが文句の原因らしいのである。

個人的には、方式が変わること自体は別に文句はない。ちょこちょこっと設定しなおして、右 Alt キーで IME の状態をトグルできればそれで良いのです。ただ困るのはいずれの方式にしても微妙にストレスフルな不具合というか何と言うかがあるという点で:

まず iBus 1.5 デフォルトの、キーボードレイアウト自体の切り替え方式にした場合。この場合、レイアウトの切り替えに使うキーは iBus の設定ウィンドウ中に登録する。

  • 良い点: レイアウトの状態(≒ IME の状態)をシステムアイコンに反映させられる
  • 悪い点: iBus の設定の中の “すべてのアプリケーション間で同じインプットメソッドを共有する” をオンにすると、このようにブラウザ上で文章を書きつつ端末上の vim に切り替えた場合など、明らかに IME がオフであるべきアプリケーションでも IME がオンになっている状況が多発してとてもストレスが貯まる。
    そこで前述の設定をオフにすれば、レイアウトの状態はアプリケーションごとに独立するのでそれは解決するのだが、そのかわり IME をオンにした時の最初のプリエディットの最初の文字を取りこぼす現象が多発してこれはこれでものすごくストレスが貯まる。

そんなわけで、じゃあ “すべてのアプリケーション間で同じインプットメソッドを共有する” はオフにした上で、IME の状態はやっぱり IME 自身が管理したほうがいいんじゃないの? と思い、右 Alt キーは mozc が反応するように設定しなおしたのだが。

そうするとシステムアイコン上のキーボードレイアウトは常に単に mozc のアイコンが表示されるだけになり、現在の入力モードがわからない。いや入力モード自体は iBus のプロパティパネルとやらに表示されるのだが。これが表示される位置がなかなかめっちゃくちゃで、また表示を自動にすると表示から数秒後に自動的に消えてしまうし、常に表示にすれば常に邪魔くさいわけで、つまりフロート式のプロパティパネルという仕様自体が間違っていると思うのです。

iBus のプロパティパネルのシステムアイコン版が、システムアイコンのキーボードレイアウトアイコンの隣に表示されればそれが最も現実的な解だと思うのだけれど。それをするためにどこの設定をどーすればいいのかわからないわけでストレスが貯まる。

ストレスをいくら貯めたところでポイントには変換できないのである。どうにかならないのかしらん。

Input mode revised

キーボード周りで Qeema を利用することにしたが、それにより若干従来のキーボードマネージャからインターフェースが変わった部分がある。この際なので、input モード周りのキー入力のハンドリングのおさらいをしたい。

モードが insert / replace へ遷移することにより、カーソル位置にまず I ビームを表示させる必要がある。この I ビームは、textarea 要素のそれを流用する。ちなみに input モード時に textarea 要素を流用するのは I ビームの見た目がほしいからではなく、composition events は、activeElement が編集可能な要素じゃないと発生しないからである。

textarea 要素のその位置は、折り返し行単位のカーソル行の上にぴったり重ね合わせるようにする。また、ここでカーソル行の前後に 2 つの span を生成する。

1 つは、折り返し行の先頭から I ビームまでの文字列で、これを leading と呼んでいる。leading span は visibility:hidden にする。その代わり、textarea の value 要素を leading で初期化する。これにより、カーソル行のカーソル位置と input 用 textarea の I ビームの位置が(だいたい)一致する。だいたいというのは…理屈の上では、margin やら padding やらを合わせれば wasavi の行の div と textarea で揃えればぴったり重ね合わさるはずが、ブラウザによってはそうならないものもあるのである。ただまあ、ずれるとしても数ピクセルなので大したことではない。ただ I ビームの先頭位置を を textarea の内容で調節するのはもう 1 つの問題がある。つまり色分けできないのである。現状では wasavi は色分けしないのでいいのだけど、将来必ず困ってしまうのであるのであるが、それはさておき。

もう 1 つの span は I ビーム位置に挿入する空の span で、compositionupdate や compositionend イベントで送られてくるプリエディット文字列を入れる場所だ。このようにして入力用 textarea とプリエディットの状態を同期させることによりインライン入力っぽいような動作をさせている。実際に IME から一気に送出された文字列を挿入する処理は、IME 経由以外の方法で入力された文字と共通する。この仕掛けは qeema の恩恵による。

composition events を正しくリスンし正しく相応しい処理を行うのは、アジア圏の言語のためだけではない。Compose キーを併用した文字の入力は、たとえば Firefox では composition events を経由するので(それを踏まえて qeema でもそうしてある)、より広範囲の言語に対応するために必須の処理だ。

ところで Project Spartan なる新生 IE は、javascript ベースの拡張をサポートするそうだ。Presto Opera のサポートを漸次的に終了すると同時に、Spartan の動向をウォッチする必要があるかもしれない。

Some spices for Qeema

Qeema によってキーボード周りを wasavi と赤福プラスで共通化したのだけど、キーボードの扱い方は両者でかなり異なる。

赤福プラスはいくつかのキーボードショートカットを定義しているが、それ以外に関しては手を付けない。一方 wasavi は全てのキー入力をリスンし、全てのキー入力を消費し(つまりデフォルトアクションをキャンセルし)、自前の処理を行う。この中で、全てのキー入力というのは composition events によって発生したものも含まれる。

ところが、composition events というのは単なる通知イベントなので、DOM の定義上はキャンセルできないのである。なので、qeema 側で compositionstart の時点での入力文字列を覚えておき、compositionend で復帰するようにしている。

ついでに、Presto Opera での composition events エミュレーションをできるだけシンプルな構造に書きなおした。まあ、いまさらこれをブラッシュアップしたところで誰も幸せにならないんですけどね……。

Signing an extension

Introducing Extension Signing: A Safer Add-on Experience
http://blog.mozilla.org/addons/2015/02/10/extension-signing-safer-experience/

より安全なアドオン体験を提供するため、拡張機能に署名を導入します
https://dev.mozilla.jp/2015/02/extension-signing-safer-experience/

ほう。

さらに、Add-on SDK 製のエクステンションに関しては、近い将来 cfx は用いられなくなり、node.js ベースのツールへ移行するというもある。

for Firefox #12

というわけで赤福プラスが Firefox でも動くようになって、また常用ブラウザ自体も Firefox に移行できた。まだメモ機能の代替品を見つけていないのだけど。

それはさておき、赤福プラスは wasavi とソースレベルではいろいろなものを共有している。例えば Kosian(バックエンドのエクステンションの抽象化ライブラリ)、Qeema(キーボード周りのライブラリ)、フロントエンドのエクステンションラッパー、Brisket(ビルドスクリプト群)と言ったものだ。

これらもいろいろと更新したわけなので、wasavi のほうに戻り、それらの共有部品を最新のものに置き換えてみたい。また、wasavi も Firefox 上で常用することになる。これはとっても大事だ。Firefox 版の wasavi というのは、これはこれでけっこう苦労して作ったのだが、まっさらのプロファイルでひと通りテストが通ればそれでおっけーということにしている。しかし世の Firefox で実際のところ拡張をひとつも入れてないまま稼働してるのなんて早々ないだろう。もうちょっと使い込んだプロファイルで常用しないといけない。

つまり、この度の常用ブラウザの移行は丁度いいチャンスなのである。

qeema

赤福プラスはいくつかのショートカットキーを受け付けるので keydown や keypress イベントをリスンするのだが、もう言うまでもないのだが、このあたりのイベントのブラウザごとの相違は壮絶なものがあるのである。そのへんを吸収している、wasavi が持っている keyManager を持ってきたい。件の、使用するショートカットを表明する処理もこれに入れてしまいたい。

というわけで、wasavi からそれを抜き出して別のリポジトリに仕立てた。キーボードのマネージャーなので略して qeema とした。ひき肉。
https://github.com/akahuku/qeema

ただひとつ問題があるのである。これを赤福プラスや wasavi に取り込む際、単に git のサブモジュールを使うわけにはいかない。というのは、qeema.js 自体は素の javascript ソースなのだが、これを Presto Opera で inject script として扱うには、ヘッダを付けなければいけない。


// ==UserScript==
// @name frontend of akahukuplus
// @include http://*.2chan.net/*/*.htm
// @include http://*.2chan.net/*/*.htm?*
// @include http://*.2chan.net/*/res/*.htm
// @include http://*.2chan.net/*/res/*.htm?*
// @exclude http://dec.2chan.net/up/*
// @exclude http://dec.2chan.net/up2/*
// ==/UserScript==

同じソースでも wasavi に取り込む際は @include 節がまた違った指定になるわけで、いずれにしてもサブモジュールそのままの状態で使えるわけではない。

うーんなにかうまい方法がないのかな。

for Firefox #11

赤福プラスに関して、大体のところは Firefox でも動くようになった。ただ、やはりまだキーバインドについて詰め切れていないところがある。

たとえばスペースキーに対して、通常の状態では

  • ページ前提のスクロール領域の末尾にすでにいるなら、続きを読む
  • そうでなければ、何もしない(つまり、ブラウザのデフォルトアクションに委ねる)

というハンドラを割り当てている。

一方、lightbox を実行している場合は

  • 表示している画像をスクロールさせる。ブラウザのデフォルトアクションは常に preventDefault() する

とさせている。

何が違うのか。前者は、もともとブラウザが持っているアクションを置き換えて、ちょっとした tweak を行っている。従ってその置き換えがエクステンションによって無効にされたとしても、必ずしも致命的な不具合にはならない。すなわちエクステンションによるキーバインディングよりも優先度が低い。

一方で後者は、ブラウザの機能とは全く別のものだ。この処理をエクステンションによって無効にされると、lightbox により画像を表示している裏で単にページがスクロールするだけになってしまう。全く機能が破綻してしまうということだ。従ってこのケースはエクステンションによるキーバインディングよりも優先度が高くなければならないのである。

エクステンションによるキーバインディングが行われても構わないか・あるいは困るかという表明を、なんとかエクステンション側に対して伝えられないだろうか。それを両者で共有し連携すれば辻褄が合う。

表明については、documentElement の data-なんとか属性に優先度の高いキーバインディングを DOMTokenList 形式で列挙すればよいのではないか。で、.keysnail.js の登録したハンドラ側では、登録したキーバインディングについてドキュメント側でその表明が行われていない場合にのみ自前の処理を行えばよい、はずだ。

 * * *

ということでやってみた。[cci][/cci] とした文書を用意する。.keysnail.js の preserve 領域に

function tdoc (ev) {
return ev.originalTarget.ownerDocument;
}

function twin (ev) {
return ev.originalTarget.ownerDocument.defaultView;
}

function dispatch (stroke, ev, handler) {
var doc = tdoc(ev);
if (!doc) return;

var m = doc.documentElement.getAttribute('data-prior-keys');
if (m && (' ' + m + ' ').indexOf(stroke) >= 0) {
var ev2 = key.stringToKeyEvent(stroke, true, 'keypress', true);
if (!doc.dispatchEvent(ev2)) {
return;
}
}

handler(doc, twin(ev), ev);
}

とし、実際のスペースキーに対するハンドラは

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

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

とする。これにより、html 要素の data-prior-keys 属性に表明されているキーストロークは、いったんページのスクリプトに処理を丸投げする。ただし丸投げしたとしてもページスクリプト側で preventDefault() されていない場合は、keysnail 側のハンドラも実行する。一方、data-prior-keys 属性に表明されていないキーストロークは、即 keysnail で処理する。

丸投げの部分は、本来真面目にやろうとすると keydown/keyup もエミュレートしないといけなかったり、さらにキーリピート絡みも考えなければならなかったり、もっと言えば本当は key プロパティも正しくセットされるように initKeyEvent() ではなく KeyboardEvent() の方を使わないといけなかったりとかなり大規模になるのだが、まあ、とりあえず。

でも基本的にこのやり方、つまり文書側が使用する特定のキーストロークを外部に対して表明するという仕組みはかなり、いいと思う。こんな場末のブログとエクステンションだけが使うにはもったいない感じがする。どこか有名所の javascript のフレームワークとかがパクってくれないだろうか。