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 クラスが一元的に持っている。従って直すとしたらそれが主な対象になるのだけど、全てというわけではないので地道に探していじっていく他にない。

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 における単語境界の仕様に準拠してもいいんじゃないかなあという気になりつつある。

cfx to jpm #2

などといいつつ、jpm の動作をいろいろ見ている。

cfx と同様、[cci]jpm xpi[/cci] とすることで xpi を生成することができる。ただし、[cci]–pkgdir[/cci] オプションや [cci]–update-link[/cci] オプションがどういう訳か削除されている。前者は、xpi をアーカイブする際のベースディレクトリを指定する。これがないということになっても、単に [cci]cd && jpm xpi[/cci] などとすればよいのでそれほど問題ではない。しかし後者が問題だ。

[cci]cfx xpi[/cci] においては以下のオプションが有効で:

  • –update-link: 拡張を自動更新する際の最新の xpi ファイルの場所を指定する。このオプションを指定すると、cfx は update.rdf を生成する
  • –update-url: 拡張を自動更新する際の更新情報を示す RDF ファイルの場所を指定する。この情報は xpi 内の install.rdf に埋め込まれる

この違いはとてもわかりにくい。前者は [cci]–location-of-latest-xpi[/cci]、後者は [cci]–location-of-update-manifest-rdf[/cci] などと脳内で変換する必要がある。

で。jpm では前述の通り [cci]–update-link[/cci] オプションが削除されている。従って、jpm は update.rdf を一切生成しない。そのため jpm ベースの開発サイクルでは update.rdf を自前で生成しなければならなくなっている。なぜこのような仕様変更が行われたのかはよく分からないが、自前で update.rdf を制御する場面というのは、つまり拡張を AMO 以外の場所で配布する状況ということだ。AMO に載せるのなら気にする必要はない。つまり update.rdf の生成機能が取り除かれているのは野良拡張の配布をやめろという暗黙的な圧力なのかもしれない。

この手の悪意に満ちた嫌がらせはこれだけではない。拡張に自前で署名を施すためのツールとして McCoy というものがあるのだが、これもあからさまに使いにくい。そして、「使いにくいけど将来的には直すので期待してね!」なんつったりしちゃったりしてるのだが、もちろん今現在になっても使いやすくなってはいない。

一方で Mozilla は

インストールを Mozilla の配布チャンネルに限定することは余計な制約であると私たちは考えます

とかなんとか言っている。なーんか、言ってることとやってることが全然違いますね。こんなことばかりやってると、加速度的に信用を失うと思うんですけど。

それはさておき、update.rdf のテンプレートから任意の箇所を package.json の値で置換するような適当な javascript のスクリプトを書いた。javascript で書いたのは、jpm 自体が node.js で動かすスクリプトなのでそれに合わせて。

テンプレートとしてこういうものを用意して






  • {{version}}


    {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
    {{engines.firefox.minVersion}}
    {{engines.firefox.maxVersion}}
    https://example.org/example.xpi






  • package.json とテンプレートを両方読んで、プレースホルダ({{〜}} の部分)を package.json の該当する値で埋める。package.json に値が存在しなければ、プレースホルダを含む要素自体を空に置き換えるようにした。

    とりあえず webliopane fx40fixed を jpm でビルドするようにしてみた。

    cfx to jpm

    Firefox の拡張を Add on SDK ベースで作る際、cfx というツールを使うことになる。これは python で書かれたスクリプトだ。このツールが、拡張の基本的なディレクトリ構成を作成したり、テストしたり、xpi をビルドするといった開発のサイクルをサポートしてくれる。

    しかし、Mozilla 的にはこの cfx は既に deprecated である。この後継は jpm というツールになっている。jpm とは Node.js で動かす javascript のスクリプトだ。たぶん JetPackManager ということなのだろう。じゃあ cfx は何の略なんだろ。CuddleFisheXtension?

    ところでなぜ cfx がダメで、jpm に移行しなければならないかという理由はよく分からない。jpm だと npm ベースなので更新が簡単だとか、activate/deactivate する必要がないとか細々した理由はあるようだけど、しかしそれらは別に技術的に cfx で実現不可能なものとは思えない。技術的な理由じゃないとすると、Mozilla 内の政治的な理由なのかもしれない。

    ともあれ基本的には両者とも jetpack の仕組みに準じたツールであるので、移行したとしてもそれほど開発サイクルに変更はない。両者の違いはせいぜい 1 ページに収まる程度のものだ。

    しかしながらじゃあすぐ移行しようという気にはなぜかならない。どういうわけか jpm で作った拡張は AMO に載せてもらえないとかいう期間が長かったからかもしれない(長かったというか、今もそうなのだと思う)。この措置は本当に意味が分からないが、企業や団体や組織の行動のうち、外部から見て何の意味があるのかよく分からないものはやはり政治的な理由が根底にあったりするものなので、まあそういうことなんだろう。

    ここまでつらつらと書いてきて、なんだか Mozilla という団体がまともなブラウザも作れず、シェアも右肩下がりなくせして、内部の政治闘争だけは一生懸命なろくでもない奴らに思えてきたが、たぶん、きっと、おそらく、そんなことはないのです。

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