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 のそれと同様の画像で置き換えるようにしてみた。

akahukuplus-emoji

wasavi/0.6.580 released

リリースした。

Chrome: https://chrome.google.com/webstore/detail/wasavi/dgogifpkoilgiofhhhodbodcfgomelhe

Opera: https://addons.opera.com/ja/extensions/details/wasavi/

Firefox: https://github.com/akahuku/wasavi/raw/master/dist/wasavi.xpi

* * *

w/W/b/B コマンドがある。これらは vi 内の独自の文字の分類に従ってカーソルを移動させる。これ、Unicode における単語境界の仕様に準拠してもいいんじゃないかなあという気になりつつある。

a critical bug

wasavi をリリースしたばかりなのだが、Chrome 上のとある条件下でフリーズするという致命的なバグを直したので今月末辺りにまたリリースするかもしれない。フリーズと言ってもプロセッサのリソースを食いつぶすとかそういう類ではない。

これは 2 つのあまり関係なさそうな事柄が関係している。

まず、wasavi が保持するバッファは、各行の末尾に [cci]\n[/cci] の内容を持つテキストノードを保持している。これは、編集後に textarea に代入するために wasavi のバッファから単一の文字列を得る際、単にバッファの div 要素の textContent プロパティを参照するだけで済むようにするためだ。

次に、wasavi の input モードは div 要素の contentEditable 属性をオンにすることで実現している。

この 2 つの事柄が Chrome 上で絡みあうと不思議なことが起きる。IME を通した仮入力が最終的に空文字になり、かつカーソル行の div 要素の内容も空になった場合(たとえば空行で仮入力を開始したが、結局全部 backspace で消して仮入力を抜けた時…など)、前述の改行要素が Chrome によって勝手に削除されてしまうようなのだ。

wasavi のあらゆる編集機能はこの改行要素が存在することを前提としているので、ないとなるとあらゆるところでエラーが発生して満足に動かなくなる。

width of character

wasavi のステータス行にはカーソル位置の桁・行位置を表示しているのだけど、その桁位置とはなんぞやというと、これはカーソル位置の内部的な行頭からのオフセットをそのまま指しているわけではない。つまり内部的な桁位置と表示上の桁位置は違う。

まず内部で charWidth という変数があり、これは大元の textarea のフォント関連のスタイリングに基づく、[cci]0[/cci] の文字幅だ。技術的には span 要素に [cci]0[/cci] を入れた際の offsetWidth の値だ。

それを踏まえて表示上の桁位置を算出するには、span 要素に行頭からカーソルの物理位置までの文字列を入れ、その offsetWidth を出し、charWidth で割る。表示しているフォントが固定幅であればこれで特に問題ない。

ところが困ったことに、固定幅フォントでさえ、文字列全体の offsetWidth が charWidth の整数倍にならないことが多々あるのであった。つまりこれは、cssom では offsetWidth の型は long だ。しかし実際には、それは小数で保持されているのが原因なんだと思う。そういうわけで正しい表示上の桁位置が得られない。

うーんどうしよう。

 * * *

charWidth を算出する際に、100 個並べた [cci]0[/cci] の offsetWidth を 100 で割るようにした。つまり charWidth の精度を上げた。

wasavi/0.6.559 released

リリースした。

Chrome: https://chrome.google.com/webstore/detail/wasavi/dgogifpkoilgiofhhhodbodcfgomelhe

Opera: https://addons.opera.com/ja/extensions/details/wasavi/

Firefox 版は AMO へのアップを諦めた。その代わり github 上の開発版は署名済みになっているので Firefox41+ でもインストール時に警告されることはない: https://github.com/akahuku/wasavi/raw/master/dist/wasavi.xpi

 * * *

wasavi が各ブラウザ上でアップグレードされた時、http://appsweets.net/wasavi/ を開くようになっている。ここで実質的なユーザー数をカウントすることができるので、そうなっている。先頭のテキスト “wasavi は Chrome、Opera、Firefox 用のエクステンションです。” の文句のそれぞれのブラウザ名にカーソルを合わせるとユーザー数を表示する。Opera に関しては Presto Opera と Blink Opera を分けてカウントする。

現在のところ、

  • Chrome: 1615
  • Presto Opera: 0
  • Blink Opera: 57
  • Firefox: 29

とのこと。Chrome では Web Store での表示が 3500 人とかそんな感じなので、まあ数日でその辺りに達するのだと思う。それにしても偏りがすごい。Opera に関してはブラウザ自体のシェア比からこんなもんなのかなという気はするが、Firefox 版ユーザーが妙に少ない。

Presto Opera でのユーザーが 0 というのはとてもいいことだ。これはもはや Presto Opera を考慮して開発する必要が全くないということだ。