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 #5

GraphemeBreakProperty.txt、WordBreakProperty.txt、Scripts.txt から生成するデータの 1 エントリに従来 8 バイトを割いていたのを、5 バイトまで詰めてみた。

ただ、これで最適だというわけではない。npmjs.com 上のライブラリ grapheme-breaker では Trie 木の構造で GraphemeBreakProperty データを保持していて、そのサイズは約 3KB だ。同じデータが Unistring では約 6KB。すごい。

それはそれとして、そろそろ wasavi に組み込んでみたい。

Handling Unicode #4

やはり、漢字やタイ語云々についてはルールの中に組み入れるのはやめ、

  • 分割ルールで分割可能と判断され
  • 分割位置の左右の Scripts.txt 内のプロパティが Common でなく
  • 分割位置の左右の Scripts.txt 内のプロパティが同一である

場合には分割しないようにした。この拡張ルールは [cci]Unistring.getWords(str, useScripts)[/cci] の第 2 引数で有効にするかを指定できる。

それから、UAX#29 のルールだと、空白類を1文字ずつ分割してしまう。これはそういうようにデザインされているのかわからないが、wasavi に組み込むにあたってはとても不便そうだ。そういうわけで連続する空白類はひとまとまりの擬似的な単語として扱うようにした。

ちなみに Scripts.txt 併用ルールだと、連続する絵文字はひとまとめに扱われない。絵文字は Common スクリプトに含まれるからだ。でも、たとえば Chrome や Firefox で Ctrl+← や Ctrl+→ 使うとひとまとめにしてるんだよね。どうしようかな。

Handling Unicode #3

unistring に、UAX#29 における word boundary の定義に従って文字列を単語で分割するメソッドを追加したい。

そういうわけでやってみたところ、UAX#29 自身に書いてあることであるが、ドキュメントに例示されているアルゴリズムそのままだと、あまり実用にはならない。とてもありがちなことに、ラテン文字が処理のメインになっているのだ。なぜかカタカナだけは組み入れられているが……。ただこれは無理もないことで、日本語と中国語の場合は分かち書きをしないので単純なルールで分割することはできない。Mecab みたいな形態素解析プログラムの助けを借りなければならない。

しかしまあだとしても、例えばひらがなが1文字ずつ分割されたりするのは実際実用にならないわけで、ちょっとだけ拡張したい。

できれば実装自体は UAX#29 通りにして、分割のルールを独自に追加変更できるようなインターフェースを組み込もうかと思ったが、面倒そうなのでやめた。とりあえず UAX#29 で例示されているタイ語、ラオス語、クメール語、ミャンマー語、それから漢字とひらがなについて分割を禁止するようなルールを組み込んだ。ただこの辺はこれらのスクリプトに限定せず、WB14(なんでも分割可のルール)の直前に、同一スクリプト間は分割禁止、みたいなルールを追加するなど、ある程度一般化したほうがよいかもしれない。

そんなわけで WordBreakTest.txt の1489種のテストに全てパスするようになった。ちなみに書記素クラスタの方も GraphemeClusterTest.txt の402種のテストに全てパスする。

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 に似たメソッド群は提供してくれなかった。

tabqueue released

Opera12 の場合、各タブがアクティブになった順番を覚えていて、あるタブを閉じた時はその順番を逆にさかのぼることで、残ったタブのうちどれをアクティブにするかを判断する。これはとても賢い。しかし、Opera12 以外のタブは一切こういった動作をしない。

Firefox の場合は、まあいろいろあるんだと思うけど、とりあえず Tab Deque を入れることで同じ動作になる。

Chrome の場合はどうか。Chrome の場合は探しても見つからなかった。そんなわけで、ないものは作るの精神で、作った:

https://chrome.google.com/webstore/detail/tabqueue/pghkhbkcicjcmgobjcgcabpmngbljill

とりあえず虹裏でスレを立てて様子を見てみたのだが、TPC というものがすでにあるらしい。ほんとだ。

うーん、まあ、いいか!

a Surrogation

赤福プラスにおいて、絵文字を画像で表示する処理を追加したのだけど。

この絵文字というものはだいたいのところ、U+FFFF を超えるコードポイントを持っている。これはかなり面食らう。個人的には BMP を超える文字なんて誰が使うのかしら…などと高をくくっていたのである。しかし絵文字なんてキャッチーなものが収録され始めているわけで、ちゃんとやらないとこれは不味いのではないか? と不安になってきた。

ところが、wasavi ではこのへんの Unicode の異様にめんどくさい部分、つまりサロゲートペアと書記素クラスタの扱いはまだ一切何も考えていないのであった(更に輪をかけてめんどくさい bidi もだが)。しかしこれをちゃんとするとなると結構な大改造になる。どうしよう。

基本的には、バッファの内容の保持とそれを操作する機能は Buffer クラスが一元的に持っている。従って直すとしたらそれが主な対象になるのだけど、全てというわけではないので地道に探していじっていく他にない。