file i/o #2

jsOAuth で気になる点がいくつかある。

  1. fetchRequestToken()、fetchAccessToken() というメソッドが用意されていて、その名のとおりの働きをするのだが、しかしリクエストが GET 固定なのだ。dropbox では該当する api は POST で投げることになっている(といいつつ、GET で投げても受け付けてはくれる……今のところは)のでちょっと困る。

    そういうわけでこれらのメソッドを使わず、下位のメソッド post() を呼ぶしかない。上記の fetch*() でリクエストメソッドを指定できたら嬉しいなー

  2. リクエストを投げた際にエラーが帰ってきた場合、http のステータスで何のエラーか判断したいのだが、request() は xhr.status を返してくれない。返してくれたら嬉しいなー
  3. まだ Firefox では動かしていないが、ソースを見る限り、XMLHttpRequest を生成する箇所が

    } else if (typeof require !== 'undefined') {
    // CommonJS require
    try {
    XHR = new require("xhr").XMLHttpRequest();
    } catch (e) {
    // module didn't expose correct API or doesn't exists
    if (typeof global.XMHttpRequest !== "undefined") {
    XHR = new global.XMLHttpRequest();
    } else {
    throw "No valid request transport found.";
    return null;
    }
    }
    }

    というような感じなので、たぶん Add on SDK 下ではたぶんエラーになる。SDK では

    require("api-utils/xhr");

    なのだ。Add on SDK に対応してもらえるか、xhr のファクトリを外に出してくれると嬉しいなー

file i/o

ひとまず dropbox から任意のファイルを読み出すことを考える。

dropbox の REST api を呼び出すには、OAuth が必要だ。そこでまず javascript 版の OAuth ライブラリをぐぐってみる。

あたりが有名? コードをざっとみた感じ、jsOAuth の方がよさそうなのでこっちを使ってみる。

OAuth に手をつける前に、wasavi 側の都合を考える。やはり同期・非同期の仕組みがここでも必要になる。読み込む処理はバックグラウンド側で行うためだ。そこで、コマンドを非同期で行うかどうかをレジスタ * を参照しているかだけではなく、それぞれのコマンドごとにフラグを持たせる。今回の場合は ex コマンド read を新設し、multiAsync フラグをつけておく。

読み込む処理の流れは以下のようになる:

  1. ex コマンド read が入力される
  2. コマンド エグゼキュータが非同期モードで開始する
  3. read コマンド本体が実行される
  4. バックグラウンドへ入力されたファイルパスを送信する。ex コマンドの実行はいったんここで中断する。しかしながら、依然として ex コマンドの実行中という状態には変わらない。この間 vi コマンドのキー入力がされても無視する
  5. ここからバックグラウンド側の処理: dropbox に対してリクエストトークンを要求する
  6. リクエストトークンを得たら、ブラウザの新しいタブで dropbox 上のapi 使用の認可ページを表示する。ユーザは wasavi に対して、dropbox 内のファイルのアクセスを許可する
  7. ページは指定したコールバックページにリダイレクトされる。コールバックページが表示された = ユーザが許可したことをバックグラウンド側で認識する
  8. dropbox に対してアクセストークンを要求する
  9. アクセストークンを得たら、dropbox に対してアカウント情報を要求する
  10. アカウント情報を得たら、確かに wasavi が使用する dropbox アカウントであることを確認する

  11. 確認が取れたら、dropbox に対して入力されたファイルパスの内容を要求する
  12. 内容を得たら、wasavi へ送信する
  13. ここから wasavi 側の処理: wasavi 側で内容を受信したら、バッファへ書き込む。これで read コマンドが完了
  14. 後続する ex コマンドを実行するため、コマンド エグゼキュータの実行を再開する
  15. 全てのコマンドを実行し終えたら、コマンド実行中のフラグを降ろす。通常の vi コマンドモードへ戻る

という感じ。

このほか、

  • バックグラウンド側で何らかのエラーが発生した場合も、wasavi 側への送信は行う。エラーメッセージを送信する
  • 取得したアクセストークンはバックグラウンドが保持するローカルストレージかセッションストレージに保存しておく。セッションのほうがいいかなあ?
  • アクセストークンを取得済みであれば、1. ~ 8. はスキップしてよい
  • アカウント情報の確認をどうするか? 各ユーザが dropbox へログインする際のアカウント名を wasavi のオプションで登録しておき、それと比較するということになるのだが、dropbox の場合送られてくるアカウント ID は謎の数字っぽいんだよなー。

なお javascript 版の dropbox api ラッパーというものもあるのだけど、これは使わない。最終的には dropbox だけでなく SkyDrive や Google Drive あたりにも対応させたいので、抽象化したクラスを自分で書いたほうがいいと思う。

stand alone

ロードマップのうち、まず stand alone form に手をつけてみる。

通常 wasavi が動作する場合は、任意の web ページに iframe を生成し、http://wasavi.appsweets.net/ の内容を表示する。このページ自体は単にプレースホルダで、内容はほとんど空っぽだが、この URL に対してエクステンション内のスクリプトが注入されて wasavi を構築する。

上記のアドレスを、iframe の内容ではなくブラウザのトップレベルとして表示した場合は、stand alone form として動作するようにする。

とまあ、そういう風に動作させること自体は別に難しくはないのだが、今の状態では編集したテキストを読み込んだりも書き込んだりもどうすることもできないので(ただしクリップボード経由のやりとりは可能)これでは実用にならない。

というわけで、オンラインストレージとの接続部分を作ることになる。作るというと嘘になるかもしれない。この辺ってすでにいろいろライブラリありそうだからそれらを比較研究した上で使わせてもらおうかなーという魂胆だ。

clipboard access #2

とりあえずこんな形にした:

  • キーボードからの入力によって vi/ex コマンドが直接実行される際に
  • レジスタを示すプリフィクス “* が入力されているか、レジスタに * を指定した ex コマンドである場合

コマンドの実行にさきがけてクリップボードの値を読み込み、そのコールバック内でコマンドを非同期的に実行する。コマンドの実行は setTimeout(command, 0) で独立したイベントキューに登録され、個別に行われる。ex コマンドは | で連結できるが、それもはやりコマンドごとに個別に実行される。

ということで、普通に使う分にはクリップボードを参照しながらの編集ができるようになる。

普通ではない使い方というのは vi コマンド @ や、ex コマンド @、または global/v で指定したサブコマンド、および外部インターフェースから vi コマンドを実行する場合で、これらはキーボードからの入力によって直接実行されるわけではない(つまり、実行のコンテキストがネストされている)ので、クリップボードの読み込みはできない。ネストされたコマンドは常に同期的に実行される。

バックグラウンドとの通信を同期的に行う仕組みがあれば別にこんなことはしなくて済むのだがなー。

clipboard access

前置き: vi では、削除やヤンク(コピー)した内容はレジスタと呼ばれる領域に格納される。これは vi 内で完結しているので、他のアプリケーションで参照することはできない。だが vim の場合はレジスタ名に「*」を用いるとクリップボードに対する読み書きになり、他のアプリケーションとの連携が可能になる。

で、ブラウザのエクステンションでクリップボードへのアクセスが可能かどうか。そういう API が提供されているかどうか。

  • Opera
    Opera では、クリップボードへのアクセスはできない。まあ Opera だからね!
  • Firefox
    clipboard モジュールが提供されている。読み書きともにできる。ただし後述するような Chrome と同じ課題はある。
  • Chrome
    いつも 3 つのブラウザの機能を比較するときは Chrome を最初に持ってくるのを、今回は一番最後なのは訳がある。

    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() に達してしまってテストが失敗する。

うーん! 困ったわぁ。

なんとか同期的な通信ができるか、コンテントスクリプト側でクリップボードの読み書きが行えればとりあえず解決するんだけどなー。

a loadmap

ぼちぼち各ブラウザの公式エクステンションサイトに公開していい頃かと思う。

それはそれとして、これからのロードマップを思い描いてみよう。

  • オンラインストレージとの連携。:read file で dropbox とかからファイルを読み込んだり、:write file で書き込んだりする。ただし、:edit は stand alone form(後述)のときのみ有効。
  • stand alone form。通常 wasavi は、あるページの textarea に寄生というか共生する使い方をする。これを symbiotic form とするならば、対して stand alone form があっていい。たとえば appsweets.net 内の特定の パスにアクセスすると自動的に wasavi がページに最大化する形で起動する。この形態では独立したテキストエディタとして振る舞う。オンラインストレージ上のファイルの編集、保存が可能。
  • syntax hilighting。ただ、symbiotic form の場合にどうやってファイルタイプを決定するか?
  • scripting。バッファを編集するためのインターフェースを公開し、後付の javascript で制御できるようにする。プラグインスクリプトをどこに置いてどうアクセスするか?
  • theming。今は決めうちで背景白の文字黒の…としているが、これもまたテーマを後付できるようにして、実行時に選択できるように。

全部実装できるのかなー。

Fürstentum L10n #4

wasavi 本体の l10n は終わったが、まだオプションページとインストール時のメタ情報ファイルが残っている。

オプションページ
アーカイブ内の options.html をどうにかする。ところで勘違いしていたのだけど、Chrome で l10n 対象の文字列(__MSG_*__)が自動的に変換されるというのは、manifest.json と css ファイルだけだった。html ファイルは変換されない。しかしどのみち、Opera と Firefox では自前で変換しなければいけないので特に問題ではない。

オプションページにアタッチしたコンテントスクリプトで DOMContentLoaded イベントをリスンし、ページ内の __MSG_*__ を xpath で探し出し、変換後の文字列で置き換える。オプションページが ajax であれこれするページだともっとめんどくさいのだろうけど、いまのところはそうではないので、これだけ。

インストール時のメタ情報ファイル
このファイルの中で弄るのは具体的には、wasavi の説明文だ。これを l10n したい。”wasavi” という名前はまあ、別にどの言語でも同じでいいんじゃないかな?

  • Chrome
    Chrome では manifest.json がメタ情報に当たるが、前述の通り自動的な変換の対象なので、単に説明文を __MSG_wasavi_desc__ と置き換えるだけ。

    {
    "name": "wasavi",
    "version": "0.3.147",
    "description": "__MSG_wasavi_desc__",
    "default_locale": "en_US",
    "icons": {
    "16": "icon016.png",
    "48": "icon048.png",
    "128": "icon128.png"
    },
    :
    :
    }
  • Opera
    Opera では、config.xml 内に description 要素を置き、xml:lang を指定した上でそれぞれの言語の説明文を格納する。ソース中のメッセージカタログを走査して説明文を取り出し、xml 内に注入するスクリプトを書き、ant の build.xml でそれを呼び出すようにした。最終的にはこうなる:



    wasavi
    vi editor for any web page.
    :
    :
    vi editor for any web page.
    web ページ上の vi エディタ

  • Firefox
    Firefox では、install.rdf を弄る。ここに詳しい。

    ところで 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 する手段がわからない。うーむこれはまあ分かったらいじることにしよう……。


ということでこんな感じに。

Fürstentum L10n #3

l10n のだいたいの形が決まったので、その通りに組んでみた。

おさらいする。3 種のブラウザで同様に動く l10n の仕組みを wasavi に組み込むのが目的。3 種のブラウザがそれぞれ l10n の仕組みを備えているが、wasavi 上では Chrome のそれを基本にし、他のブラウザでは Chrome の仕組みをエミュレートする形。Chrome の l10n の仕組みとは、エクステンションのアーカイブ内に _locales/[localeCode]/messages.json といったファイルを配置し、その json ファイルにメッセージ ID とそれに対応する各言語のテキストを格納し、変換は、wasavi 実行中に動的に行うというもの。

それに加えて、wasavi 独自の仕組みがある。

  • メッセージへのパラメータの埋め込み。平たく言うと sprintf() だ。これは Chrome が提供する i18n にも用意されているが、wasavi 側で行う。メッセージ中の {0} といった部分は、パラメータで置き換えられる(数値はパラメータのインデックス)。
  • メッセージ中、複数形になりうる名詞の変換。1 行削除したときは
    deleted 1 line.
    それ以上の行を削除したときは
    deleted 10 lines.
    のように自然な形のメッセージを生成する。ソース上は、メッセージは “deleted {0} {line:0}.” と記述する。{word:number} の形式だ。ここでも number はパラメータのインデックス。パラメータの値によって word を自然な形に変化させ、波かっことその中身全体を、生成した word で置き換える。

    これには、以下のものが必要になる:

    1. パラメータを引数にとって、変化パターンを返す関数
    2. 各変化パターンに対応する word の変化形

    これらの情報もまた、messages.json に格納する。まず 1. は、Language Plural Rules を参考に生成する。例えば英語の場合は

    Language Name Code Category Examples Rules
    English en one 1 one → n is 1;
    other → everything else
    other 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() を呼ぶだけでよいのだが、他のブラウザではそうは行かない。

  • Opera
    フォルダベースの l10n 機能を利用する。background の起動時に XHR でエクステンションアーカイブ内の /messages.json を読み込む。実際は、メッセージカタログは locales/[localeCode]/messages.json に配置してあり、自動的にロケールに応じたファイルが参照される。wasavi 起動時にメッセージカタログの内容を wasavi.js へ送信する。wasavi.js 側でメッセージが必要になった際は、自分でカタログの中から探し出す。
  • Firefox
    メッセージカタログは data/xlocale/[localeCode]/messages.json へ配置する。現在のロケールを得るために api-utils/l10n/locale モジュールの getPreferedLocales() を使う。また、有効なロケール群から現在のロケールに最もふさわしいロケールを抜き出すためにも、上記のモジュールが提供する findClosestLocale() メソッドを使う。これ以外に、標準的な l10n の機能は使わない。
    その他の仕組みは、Opera と共通。

というわけで、とりあえず日本語ロケールのメッセージを作ってみた。