修正が完了し、全テストも通った。晴れて wasavi は beyond BMP enabled になった。
Tag Archives: vi
Handling Unicode #11
様々な箇所で必要なら Unistring を使用するようにする修正が完了しつつある。実際には Buffer クラスの他、様々な箇所で Position クラスの col プロパティを直接インクリメント・デクリメントしており、それを修正していくことになる。
このアプローチはつまり、基本的には文字列が UTF-16 のシーケンスであることを意識した上で、各箇所で論理的な文字単位と UTF-16 のインデックスとを相互変換するということで、割と煩雑だ。
一方、異なるアプローチも考えられる。文字のインデックスは常に論理的な文字単位をベースにし、Buffer クラス内でレンダリングする際に Unistring を使う。おそらくは、こちらが正しい。ただ現在はレンダリングはブラウザ任せなので、やりたいけどそうはいかない。これは将来の課題だ。
ところで javascript で構築した vi という点でいろいろな人が作った諸々を見てみると、おそらくサロゲートペアと結合文字列を意識した動作をするものはない。たとえば CodeMirror の vim バインディングはなかなか良く出来ているが、上記のトピックを正しく処理しない。その点で wasavi のアドバンテージが 1 つ増えたわけで、これは誇っていいと思う。
Handling Unicode #10
他のモーションも修正する。
- [cci]|[/cci] このコマンドは、与えられたカウントを桁数とみなしてカーソルをそこへ移動させる。従来はカウンタを単純に UTF-16 シーケンスのインデックスとして使用していたが、charWidth 変数(文字の平均幅をピクセル単位で表す)×カウンタの位置に最も近い書記素クラスタのインデックスを算出するようにした。
- [cci]ga[/cci] このコマンドは、カーソル位置の文字のコードポイントを表示する。書記素クラスタ全体を対象とするように修正。
- [cci]x/X[/cci] このコマンドは、カーソル位置の前方、あるいは後方に向かってカウント分文字を削除する。これを書記素クラスタ単位で削除するように修正。ただ、後方削除が実際にどういった削除を行うかは方法が 2 通り考えられる:
- 一度に書記素クラスタ全体を削除する – vim など
- 基底文字、結合文字単位で削除する – Chrome や Firefox の textarea など
とりあえず vim に合わせてみる。
その他、モーション以外に編集コマンド、bound モード、line_input モードが残っている。なかなか先は長い。
Handling Unicode #9
[cci]motionUpDown[/cci]。
テキストエディタはカーソルの行位置・桁位置を管理保持する。このとき桁位置に関して、メモ帳のようなシンプルなエディタ以外はたいてい、カーソルの本来の桁位置とは別に架空の桁位置を持っている。この架空の桁位置は、カーソルが水平移動した時にカーソルの本来の桁位置に同期する。カーソルが垂直移動した時は変更されず、かつカーソルの桁位置は架空の桁位置に最も近い位置に算出される。
これで何が良くなるのかというと:
aaaaaaaaaaa
bbbbbbbbbbb
なんてテキストの先頭行の最終桁にカーソルがあった時、最終行の最終桁近辺を編集したくなったとする。そこで、下矢印キーを 2 回押すわけだが。架空の桁位置がないと中間の行でカーソル位置が行頭に固定されてしまい、最終行に達した時にはわざわざ最終桁へ移動させる手間が増えてしまう。架空の桁位置の仕組みがあると、最終行にカーソルが移動した時その桁位置は架空の桁位置に最も近い位置に再生されて、無駄なカーソル移動をしなくて済むというわけだ。このとき、絶対に等幅のフォントでしか描画しない!というわけでなければ、架空の桁位置はピクセル単位で保持することになる。そういうわけで、「あるピクセル位置に最も近い、文字列上の桁位置」というのを算出する処理が必要になる。
処理の内容は、素朴に考えれば、文字列の先頭から1文字ずつ切り出すループを設け、その部分文字列の offsetWidth を出し、それが基準ピクセル位置を超えていたならば、超える直前の offsetWidth と比較して距離が近い方を採用し、それに対応するループカウンタが結果の桁位置になる……という感じになる。
しかしこのまま組むと結構遅い。offsetWidth というのはそんなに軽くないメソッドなので、100 文字あったら 100 回 offsetWidth を呼ぶというのは避けなければならない。
で、実は [cci]motionUpDown()[/cci] はすでにそうなっていて(offsetWidth の呼び出しが算出される桁位置の log2 〜 2log2 で収まるようになっている)、それを Unistring を使うように修正する必要があり、そうした。
この修正は、折り返し行単位でのカーソルの上下移動とも関わるのでこれで終わりではない。
* * *
そういうわけで、その辺の諸々を更新。
Handling Unicode #8
次に、[cci]f/F/t/T[/cci] コマンド。つまり [cci]motionNextWord()[/cci] と [cci]motionPrevWord()[/cci]。
これらのコマンドの本質的な部分では、特に書記素クラスタなどのことを考える必要は実はあまりない。キーボードから直接入力できる文字の中に結合文字が基本的にない。OS の digraph 機構等々を経由してもし入力できたとしても、コマンドは書記素クラスタの先頭文字との一致を判定するので、結合文字そのものにマッチすることがない。
これは不便といえば不便かもしれない。「Circumflex 付きの任意の文字をサーチしたい」などといった要件はないわけではないと思う。でもそういうのは [cci]/ ?[/cci] コマンドが担当するべきかなあ。微妙。
ただ、[cci]t/T[/cci] コマンドは入力された文字の位置へカーソルをおいた後 1 文字進めたり戻ったりする仕様なので、その時は 1 書記素クラス多分前後させねばならない。
次に、[cci]^/$[/cci] コマンド、つまり [cci]motionLineStart()[/cci] と [cci]motionLineEnd()[/cci] だが、これこそ書記素クラスタのことを考える必要は全くない。何を単位にしようが文字列の先頭と末尾は同じだ。
めんどくさいのは [cci]j/k[/cci] すなわち [cci]motionUpDown()[/cci] だ。
Handling Unicode #7
順当に行けば、次は [cci]w/W/e/E/b/B[/cci] なのだが、数が多いのでこれが意外と面倒くさい。
これらのコマンドは、Unicode 関連を抜きにしてもいじる必要があった。従来は、カーソルをジャンプさせるべきテキストの切れ目をオンザフライで走査していた。このとき、[cci]w/W/e/E[/cci] なら走査の方向は順方向、[cci]b/B[/cci] なら逆方向なのだが、一部の文字は走査の方向によって切れ目が変化してしまうという問題があった。
従ってオンザフライではなく、一旦常に同一方向でテキストを走査して切れ目を貯めこみ、次にそれを利用するという形にしたかったのだ。そして、「テキストを走査して切れ目を貯めこむ」という処理は UAX #29 で述べられている word boundary そのものなので、ついでにそれにも準拠したいなあということなのであった。
そういうわけで [cci]w/W/e/E/b/B[/cci] コマンドを全て Unistring が切り出した単語の情報を利用するように書き換えた。
ちなみに割とどうでもいいような、それでいて非常に重要なことのようなトピックとして、UAX #29 のルールに則ると濁点・半濁点付きの半角カナにおいてそれらの 2 文字(UTF-16 単位で)が 1 つの書記素クラスタとして扱われるというものがある。従ってカーソルが濁点・半濁点だけを指すということがなくなるし、また削除するとしたら書記素クラスタ単位になる。これはなかなか目からウロコな仕様で、改めて考えてみるとこちらのほうが確かに自然なのだが、日本産のテキストエディタでこういう動作をするものは多分なかったと思うのですんなり受け入れられるか少し不安な感じはする。
Handling Unicode #6
そういうわけでぼちぼちと wasavi に組み込み始める。
まずは簡単そうな [cci]motionLeft()[/cci] からとりかかってみよう。
function motionLeft (c, count) {
// カウントの値を正規化
count || (count = 1);
// 現在のカーソル位置
var n = buffer.selectionStart;
// 現在行の書記素クラスタ群
var clusters = getGraphemeClusters(n);
// カーソルが位置する書記素クラスタのインデックス
var clusterIndex = clusters.getClusterIndexFromUTF16Index(n.col);
// 移動できないならメッセージを生成
if (c != '' && clusterIndex <= 0) {
requestNotice({silent:_('Top of line.')});
}
// 書記素クラスタ単位でカウント分戻る
// ただし、0 未満にはしない
if (clusters.length) {
n.col = clusters.rawIndexAt(Math.max(
clusterIndex - count, 0));
}
// バッファのカーソル位置に上書き
buffer.selectionStart = n;
// キー名をコマンド列に追加
prefixInput.motion = c;
// 水平移動なので、"記憶された水平位置" は更新する必要がある
invalidateIdealWidthPixels();
// コマンド実行完了
return true;
}
とこんなように Unicode のめんどくさい部分は全て Unistring が面倒を見てくれる。
Handling Unicode
wasavi が正しくサロゲートペアや書記素クラスタと言った、Unicode のめんどくさいトピックを正しく処理できるようにするためにはどこをどう直したらいいのか考えている。
もちろん、これらの Unicode のめんどくさい部分が関わる個別の箇所に個別の処理を書き加えるのは正しくない。それらを統一的に処理するクラスなり関数を設けて、個別の部分では単にそれを利用するだけにしたい。
ここで、1 つのクラスを作ってみたい。このクラスは文字列を引数に取り、サロゲートペアを解決しつつ UCS4 のコードポイントの配列を生成し、さらにそれを書記素クラスタで分割する。このクラスは String に似たメソッドを持ち、String を操作するように書記素クラスタの配列を操作することができる。
例えば Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞ という文字列は UTF-16 の文字が 75 個も並んでる複雑なシーケンスだ。この中から G の部分だけ抜き出す、だとか、「ユーザが認識する文字」の数、つまり 6 を得ると言った処理は、String に対する操作では不可能なのだが、このクラスを使うことで
var us = new Unistring("Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞");
us.length; // 6 が返る
us.substr(3, 1).toString(); // G̴̻͈͍͔̹̑͗̎̅͛́ が返る
us.charAt(3); // G が返る
と言ったように簡単に操作できるようになる。
というわけで、書いてみた。
実は、UTF-16 シーケンスを書記素クラスタに分割する javascript のライブラリというのはすでにあるのだが(ZALGO! もこのライブラリのドキュメントから取った)、純粋に分割するだけで String に似たメソッド群は提供してくれなかった。
a Surrogation
赤福プラスにおいて、絵文字を画像で表示する処理を追加したのだけど。
この絵文字というものはだいたいのところ、U+FFFF を超えるコードポイントを持っている。これはかなり面食らう。個人的には BMP を超える文字なんて誰が使うのかしら…などと高をくくっていたのである。しかし絵文字なんてキャッチーなものが収録され始めているわけで、ちゃんとやらないとこれは不味いのではないか? と不安になってきた。
ところが、wasavi ではこのへんの Unicode の異様にめんどくさい部分、つまりサロゲートペアと書記素クラスタの扱いはまだ一切何も考えていないのであった(更に輪をかけてめんどくさい bidi もだが)。しかしこれをちゃんとするとなると結構な大改造になる。どうしよう。
基本的には、バッファの内容の保持とそれを操作する機能は Buffer クラスが一元的に持っている。従って直すとしたらそれが主な対象になるのだけど、全てというわけではないので地道に探していじっていく他にない。
cfx to jpm #3
wasavi も 0.6.580 から jpm でビルドするようにした。ただこれらのツールは、ビルド時だけではなく実行時にも影響を及ぼす。大昔は Add on SDK のライブラリは個々の拡張に同梱されていたが(このため wasavi でも Firefox 版だけやたらサイズがでかくなるという問題がかつてあった)最近は SDK のライブラリは Firefox 自身が保持するようになっている。困ったことにこのライブラリが、cfx でビルドされたかあるいは jpm かで微妙に動作を変える。
たとえば cfx の [cci]require()[/cci] に比べて jpm のそれはより commonjs に準拠しているようになっていて、cfx では基準のディレクトリが常に lib なのだが(正確には、エントリポイントスクリプトの dirname かもしれない。未確認)、jpm では require() を実行したソースが位置するパスが基準になるのである。つまり lib/foo とか lib/bar とかだったりと可変なのだ。
これで何が困るのかといえば、wasavi や akahukuplus はソースの共通化のために require() の polyfill を定義しているのだが、ある関数を呼び出した際にその呼び出し元のソースファイルのパスを得るという標準的な方法がないことだ。
標準的な方法がないということは、標準的ではない方法を使わざるを得ないということで、具体的には [cci](new Error).stack[/cci] が返す文字列を取得して解析するしかない。しかしこれは非常に脆弱で、各ブラウザベンダがこのプロパティが返す文字列の内容をちょっとでも変えたら即破綻する。文字列ではなく、もっと構造化されたオブジェクトでスタックフレーム情報を返してくれればもうちょっとましなのだけど…。
さて、cfx から jpm への移行で最後に残ったのは赤福プラスだ。これも移行してみた。また、最近は絵文字が unicode のコードポイントにやたら導入されている状況を鑑みたり鑑みなかったりしつつ、コメント中の絵文字は twitter のそれと同様の画像で置き換えるようにしてみた。