vigor
http://vig.orous.org/
ソース
http://code.google.com/p/vigor-project/
あんまり出来は……よくない。
vi クローンを名乗るソフトウェアを試してみるとき、J を押して何が起きるかでだいたいの本気度が分かるよ。
vigor
http://vig.orous.org/
ソース
http://code.google.com/p/vigor-project/
あんまり出来は……よくない。
vi クローンを名乗るソフトウェアを試してみるとき、J を押して何が起きるかでだいたいの本気度が分かるよ。
前置き: vi では、削除やヤンク(コピー)した内容はレジスタと呼ばれる領域に格納される。これは vi 内で完結しているので、他のアプリケーションで参照することはできない。だが vim の場合はレジスタ名に「*」を用いるとクリップボードに対する読み書きになり、他のアプリケーションとの連携が可能になる。
で、ブラウザのエクステンションでクリップボードへのアクセスが可能かどうか。そういう API が提供されているかどうか。
Chrome でもクリップボードへのアクセスは可能だ。まず manifest.json の permission に “clipboardRead” や “clipboardWrite” を追加する。
クリップボードとの実際のやり取りは、専用の API は提供されておらず、IE 起源のあやふやなメソッド execCommand() を用いる。ドキュメントに適当な textarea 要素を生成し、その内容をバッファにする。
読み込むときは
var ta = document.getElementById('clipboard-buffer');
ta.value = '';
document.execCommand('paste');
// ta.value はクリップボードの内容になっている
書き込むときは
var ta = document.getElementById('clipboard-buffer');
ta.value = 'コピーしたい内容';
ta.select();
document.execCommand('copy');
ただ、いくつか制限がある。クリップボードとのアクセスはバックグラウンドスクリプトでしか許されない。コンテントスクリプトではできない。したがって読み書き共にバックグランドへの通信と組み合わせる必要があるのだが、組み合わせること自体は大した問題ではなく……問題は、Chrome の場合、異なるドキュメント同士の通信は必ず非同期で行われることだ。これはクリップボードへの書き込みはともかく、読み込みの際に面倒なことになる。コンテントスクリプト側で、こんな感じでバックグラウンドに対してクリップボードからの読み込みを要求する:
getClipboard: function () {
// 1
var result = '';
this.postMessage({type:'get-clipboard'}, function (req) {
// 2
result = req.data || '';
});
// 3
return result;
}
これは 1 -> 3 -> 2 の順で実行されるので、単にこのメソッドを呼び出すだけではクリップボードの内容を得られない。wasavi 側のコマンド実行処理自体を非同期な仕組みにしないといけない。
うーん。非同期ベースに書き換えるのはやぶさかではないけれど(大量の行に対して :s や :g を実行することを考えると、コマンドの実行を細切れにする、究極的には Web Worker に担当させるのはもっともな話だ)、そうすると将来 wasavi をプラグインによってスクリプタブルにした場合、プラグイン側も非同期を意識して書かないといけなくなる。これはちょっとお手軽感台なしだなーという気が。
いやそれ以前に自動テストのためのコードも全て非同期式に書き換えないといけないのだけど。自動テストでは
Wasavi.send('dd');
assertEquals('1\n2\n3', Wasavi.value);
みたいな感じなのだ。非同期にすると Wasavi.send() で送ったコマンドの実行が完了する前に assertEquals() に達してしまってテストが失敗する。
うーん! 困ったわぁ。
なんとか同期的な通信ができるか、コンテントスクリプト側でクリップボードの読み書きが行えればとりあえず解決するんだけどなー。
ぼちぼち各ブラウザの公式エクステンションサイトに公開していい頃かと思う。
それはそれとして、これからのロードマップを思い描いてみよう。
全部実装できるのかなー。
wasavi 本体の l10n は終わったが、まだオプションページとインストール時のメタ情報ファイルが残っている。
オプションページ
アーカイブ内の options.html をどうにかする。ところで勘違いしていたのだけど、Chrome で l10n 対象の文字列(__MSG_*__)が自動的に変換されるというのは、manifest.json と css ファイルだけだった。html ファイルは変換されない。しかしどのみち、Opera と Firefox では自前で変換しなければいけないので特に問題ではない。
オプションページにアタッチしたコンテントスクリプトで DOMContentLoaded イベントをリスンし、ページ内の __MSG_*__ を xpath で探し出し、変換後の文字列で置き換える。オプションページが ajax であれこれするページだともっとめんどくさいのだろうけど、いまのところはそうではないので、これだけ。
インストール時のメタ情報ファイル
このファイルの中で弄るのは具体的には、wasavi の説明文だ。これを l10n したい。”wasavi” という名前はまあ、別にどの言語でも同じでいいんじゃないかな?
{
"name": "wasavi",
"version": "0.3.147",
"description": "__MSG_wasavi_desc__",
"default_locale": "en_US",
"icons": {
"16": "icon016.png",
"48": "icon048.png",
"128": "icon128.png"
},
:
:
}
wasavi
vi editor for any web page.
:
:
vi editor for any web page.
web ページ上の vi エディタ
ところで 1 つ気になることがある。Chrome と Opera では、メッセージカタログはそれぞれのエクステンションがネイティブで提供する l10n の仕組みに沿って格納しているが、Firefox ではそうではない。本来はエクステンションのアーカイブの /locale/[localeCode].json に置くのを、/resources/wasavi/data/xlocale/[localeCode]/message.json という Firefox のあずかり知らない場所と形式で格納している。
これって AMO に登録する際にリジェクトの理由になったりするのかな? あるいは、リジェクトまでは行かなくても、エクステンションの一覧か何かで「このエクステンションはどの言語にもローカライズされてまへん」みたいな扱いになるかもしれない。
それはちょっと悔しいので、wasavi の説明だけは Add-on SDK 標準の形式で各言語のメッセージカタログを置くようにしてみた。実行時に参照されることはないのだけど、一応どの言語へ l10n されているかは分かる。これもやはりそのためのスクリプトを書き、ビルド時に自動的に実行されるようにした。
それから、Firefox でオプションページを開く手段として、アドオンバーにアイコンを追加するようにしていたのだけど、本当に単にオプションページを開くだけの機能しかなくてなんだかなーだった。しかし simple-prefs でアドオンの管理ページにボタンを追加できるようなのでそうした。
そうしたのだが、このボタンのラベルを l10n する手段がわからない。うーむこれはまあ分かったらいじることにしよう……。
l10n のだいたいの形が決まったので、その通りに組んでみた。
おさらいする。3 種のブラウザで同様に動く l10n の仕組みを wasavi に組み込むのが目的。3 種のブラウザがそれぞれ l10n の仕組みを備えているが、wasavi 上では Chrome のそれを基本にし、他のブラウザでは Chrome の仕組みをエミュレートする形。Chrome の l10n の仕組みとは、エクステンションのアーカイブ内に _locales/[localeCode]/messages.json といったファイルを配置し、その json ファイルにメッセージ ID とそれに対応する各言語のテキストを格納し、変換は、wasavi 実行中に動的に行うというもの。
それに加えて、wasavi 独自の仕組みがある。
deleted 1 line.
deleted 10 lines.
これには、以下のものが必要になる:
これらの情報もまた、messages.json に格納する。まず 1. は、Language Plural Rules を参考に生成する。例えば英語の場合は
Language Name Code Category Examples Rules English en one 1 one → n is 1;
other → everything elseother 0, 2-999;
1.2, 2.07…
ということである。変換関数は Rules を、変化パターンは Category を見ればいい。まず変換関数は、Rules に対応する javascript の式を書く。n が 1 の場合は ‘one’、それ以外は ‘other’(実際は、空文字で代用)を返すようにする。
{
"_plural_rule@function": {
"message": "n==1?'one':''"
}
}
そして、全メッセージ中で使用している複数形になりうる単語について、one パターンと other パターンのそれぞれを定義する。
{
"_plural_substitution": {
"message": "substitutions"
},
"_plural_substitution@one": {
"message": "substitution"
},
"_plural_line": {
"message": "lines"
},
"_plural_line@one": {
"message": "line"
},
"_plural_character": {
"message": "characters"
},
"_plural_character@one": {
"message": "character"
},
"_plural_operation": {
"message": "operations"
},
"_plural_operation@one": {
"message": "operation"
}
}
メッセージ ID は “_plural_” + word の基本形 + @ + 変化パターンとなる。この後ろに、通常のメッセージ群についてメッセージ ID と変換結果を全て記述する。
{
"invalid_boolean_value": {
"message": "Invalid boolean value",
"description": "Invalid boolean value"
},
:
:
:
"top_of_history": {
"message": "Top of history.",
"description": "Top of history."
}
}
messages.json 側でやることは以上。
で、Chrome ではメッセージ ID を変換するには chrome.i18n.getMessage() を呼ぶだけでよいのだが、他のブラウザではそうは行かない。
L10n, つまり Localization をやる段階になった。
再度各ブラウザに用意されている l10n(あるいは i18n)のリンクを引いておく。
l10n といってもやることはいろいろあると思うけど、wasavi においては、wasavi からのメッセージをユーザが求める言語に変換した上で出力することとしよう。ソース上では、l10n の対象になりそうなメッセージの文字列リテラルは、すべて関数 _ の呼び出しにラップしてある。したがって、変換処理は _ の内部に閉じ込めればいい。
Hacker News から。
http://mit.edu/~georgiou/www/vi/
vi 部分自体は Internet Connection, Inc. 製のそれがベースになっているが、多少バグフィクスしてある?
それから、:e で dropbox や box のファイルを開けて、:w でシームレスに保存できるようだ。
しまった! 同じことを考えてたのに先を越されたー!
最終的に 508 項目のテスト、2044 個のアサートとなった。Opera、Chrome、Firefox の各ブラウザにおいて、ペンディングになっている 2 点ほどを除いたその他のテストにパスした。
ex コマンド最後のテスト、@ コマンド。
@ コマンドは、指定のレジスタに格納されている文字列を ex コマンドとみなし、実行する。vi コマンドにも @ があり、vi コマンド版 @ は ex コマンドを含むことができるのでつまり上位互換ということになっている。ex コマンド版の @ はレジスタの内容をあくまで ex コマンドとして実行する。
[addresses]@register
レジスタが省略されたか、文字 @ が指定された場合は、最後に実行されたレジスタ名が用いられる。レジスタの実行がまだ 1 度も行われていない場合は、エラーになる。
addresses で指定された範囲の各行について、まず現在行( . )にセットされ、そしてその時点で @ コマンドが実行されたかのように、名前付きレジスタの内容が ex コマンドとして実行される。ex コマンドパーサは、行指向で保存されたレジスタの各行と、文字指向で保存されたレジスタの最終行以外の行は、末尾に改行がついたものとして扱う。
処理の間にエラーが発生した場合、または addresses で指定された行がそれを現在行としてセットする段階で存在しなかった場合、または addresses として複数行が指定されかつバッファの内容が(たとえば ex コマンド edit によって)置き換えられた場合、エラーとなり、エラーメッセージが表示され、このコマンドの実行は打ち切られる。
* * *
最後の段落以外は、特に問題はない。最後の段落は実はよくわからない。たとえば現在行の次の行を削除するような ex コマンド(+1d とか)を、複数行を対象に、@ コマンドで実行したとする。すると、仕様で言っているような addresses には含まれているが、レジスタの内容を実行する段階では存在しない行というのが確かに生まれる。仕様では、そういう行を見つけたら即エラーにすると言っている。それはどうなのか? その動作は global コマンドとも異なっている(global コマンドはマッチした行を覚えておくフェーズと、それらの各行に対してコマンドをは実行するフェーズで構成される。第 2 フェーズのループ内では、第 1 フェーズにおいて覚えておいた行が存在しなくてもエラーにせず、次の行へスキップする)。
また、レジスタに格納されている ex コマンドが明示的に addresses を含んでいる場合、@ コマンドに対する addresses とどう折り合いをつけるかは仕様では示されていない。置き換えるのか、マージするのか、レジスタのそれを優先するのか?
vim はこの点について、@ コマンドに付随するアドレスは addresses ではなく実質的に address であるという仕様にしているようだ。これはちょっと特殊で、コマンド自体は複数行の範囲を受け付けるが、実行はその先頭行のみに対して行われる。先頭行を現在行に設定しレジスタの内容を実行する、それで終わり。ヘルプを読む限りでは将来的には複数行に対応するかもねということになっているが、posix による定義が曖昧なのが解決しない限り対応は難しそうだ。
ということで、wasavi でもそれに従う。