0.4.169 released

http://appsweets.net/wasavi/

  • textarea / input 要素がフォーカスを得たときに自動的に wasavi が起動するオプションを追加した

オプションの変更は、各ブラウザのエクステンション管理ページ的なものから辿れるようにしてあるが、実は既に起動している適当な wasavi で
:options
と打っても設定ページを開くことができる。

quick activating #2

dropbox から返答があり、無事承認された。

 * * *

で。

自動的に起動した wasavi が、これまた自動的に終了するには 2 つの方法がある。まず、ユーザが明示的に wasavi から他の要素にフォーカスを移した場合。これはフォーカス可能な要素に限らず、単に body をクリックした場合も含まれる。いずれにしても、wasavi がフォーカスを失う時点での編集内容が textarea に反映され、wasavi は終了する。フォーカスはユーザが指定した要素にある。

次に、wasavi の起動中にコマンドモードで tab を押下した場合。この場合も wasavi のその時点の編集内容が textarea に反映され、wasavi は終了するのだが、前者との違いは、wasavi が拡張する対象となる textarea の次の tabindex を持つ要素にフォーカスされることだ。

つまり両者の違いは、wasavi の終了後にどの要素にフォーカスが移るかだ。前者は移行先要素をユーザが明示し、後者は暗黙的なので explicit deactivation と implicit deactivation と区別することができると思う。

で。

問題は、後者の implicit deactivation において、ある要素の、sequential focus navigation に沿った上での次の要素を求める手段が、標準的な API では提供されていないということだ。HTMLElement#nextFocusingElement みたいなのがあれば一発なんだけど。

sequential focus navigation というのは、PC 上のブラウザにおいては、tab キーを押すことで順繰りに、適当な要素にフォーカスが移っていく操作のことだ。適当な要素というのは、既定では

  • href 属性を備えた a 要素
  • href 属性を備えた link 要素
  • disabled ではない button 要素
  • disalbed ではなく、かつ type が hidden ではない input 要素
  • disabled ではない select 要素
  • disabled ではない textarea 要素
  • disalbed ではない command 要素
  • draggable 属性を備えた要素。ただし、ユーザーエージェントがポインティングデバイスを使うことなくドラッグ開始することを許可している場合
  • エディティング・ホスト
  • ブラウジング・コンテキスト・コンテナ

これに加えて、tabindex でインデックスが 0 と明示してある要素、および 0 以上の値が明示してある要素が sequential focus navigation の対象となる。前者は前後関係は文書順に従って自動的に決められるが、後者は順番は指定された tabindex に従う。

フォーカスを受け取れる要素は、以下の順になる:

  1. tabindex が 0 より大きい要素群(同一の tabindex では文書順)
  2. tabindex が無効な(指定されていないか、パース結果がエラーとなる)要素群
  3. tabindex が 0 以下と明示してある要素群
  4. このうち sequential focus navigation は 1. と 2. の要素群が対象になる。

    結局のところ、それらの要素群を抜き出し、wasavi の拡張対象となる要素を探し出し、その次の要素にフォーカスを移せばいい。

    ……すごく面倒くさいです! ブラウザ自身が持っている情報なのに、わざわざ javascript で算出するのはとても無駄です。

quick activating

コンシューマキー/シークレットを blowfish で暗号化して保存するようにしたバージョンで dropbox へ再申請した。

0.4.163 を某所でアナウンスしたところ

Ctrl+enter無しでデフォでオンになると嬉しい

というコメントがあった。

これはつまり、textarea 要素にフォーカスを移した時点で自動的に wasavi が起動してほしいということだと思う。確かにそのほうが、いちいち ctrl+enter するよりも使いやすい場面はたくさんあるだろう。textarea そのものが vi のインターフェースを備えたものとして扱えた方が、vi に慣れきった層にとってはこの上ないメリットになると考えられる。よしやろう是非やろう。

しかし隠れたデメリットをあげてみると、意外に少なくない:

  1. 実際の web ページ上の textarea 要素は、実際に送信されるフォームの部品である。一方、wasavi の実体は textarea の上にかぶせた完全に別の要素である。wasavi が起動してテキストを編集したとしても、それを :write するまでは textarea の値は更新されない。シームレスに起動したとしても、あくまでも別の要素の別の内容を編集していることには変わらない。もしシームレスに wasavi を起動させると、適当にテキストを編集 -> おもむろに送信ボタンをクリック -> 内容が送信されてねぇ! という事故が非常に高い確率で起こりうる。
  2. web ブラウザネイティブのフォーム部品は、tab キーを押すことでフォーカスを順繰りに遷移させることができる。wasavi が自動的に起動する際、すべてのキー入力は wasavi が処理する。マップされていないキー入力は何も起こらないとしても、wasavi が消費する。この動作はフォーカスの遷移とかち合うわけだが、どうするか?

1. 自動起動させる設定にした場合、wasavi 上で編集するごとに即 textarea 要素を更新する。あるいは wasavi がフォーカスを失ったら即 textarea 要素を更新する。いずれにしても :write は意味を持たなくなる。
2. 自動起動させる設定にした場合、wasavi のコマンドモードでの tab キーは予約される。標準のマップも、リマップもできなくなる。tab キーを押すと wasavi は終了し、対象の textarea 要素の次のフォーム部品へフォーカスが移動する。

こんな感じで解決できるかな?

the secret only between you and me

dropbox の API を使用するアプリケーションには Production status というものがある。最初にアプリケーションを登録した段階では development で、アプリケーションを登録したアカウントしか使用できない。その後 dropbox へ申請することで production に移行し、誰でも使えるようになる。

ということで申請したら、却下された。

理由は OAuth のコンシューマキーとコンシューマシークレットを plaintext で保存してんじゃねーよ! せめて難読化しろやボケが! というものであった。とてももっともな話である。

過去の記事で考察したように、ブラウザのエクステンション(javascript なのでソースを見るのは容易い)にどうやって OAuth のコンシューマキー等を含めるかは難しい。

エクステンション内ではなく、適当なサーバを用意し、その中で OAuth を用いた API の使用認可を得るようにすればコンシューマキーの置き場所について考える必要はない、が、適当なサーバが提供する REST api を真に wasavi だけが使えるようにするための認可が更に必要になる。いずれにしろエクステンションの中に何らかの鍵を含めなければならず、結局同じことだ。ならば中継サーバを使う必要性はあまりない。

ということでエクステンションに鍵を難読化した上で収めるようにしなければならないのだが、難読化や暗号化したところでデバッガを立ち上げて復号した直後にブレークポイントを張るだけで真の内容が見られるのだからあんまり意味ないよなーと思っていた。思っていたが、dropbox 側で難読化しろと言うのだから、そういう方向にしようと思う。

とりあえず難読化というか、暗号化しよう。blowfish あたりでいいだろう。

Javascript Blowfish Library
https://github.com/takezoh/blowfish.js

コンシューマキーとコンシューマシークレットを収めた json ファイルを blowfish で暗号化し、base64 してそれをエクステンション内に収める。バックグラウンドの起動時にそれを復号する。

building a xpi #2

最新のソースをビルドしてみたのだが、Firefox で動かない。そもそもバックグラウンド部に当たる main.js が実行されていないか、あるいは途中でエラーになってるようだ。しかしエラーコンソールには何も出ていない。

動かない原因を探るのにも、Firefox + Add-on SDK では一苦労だ。まず Firebug でデバッグできない。できることはまずそうな箇所に console.log() をちりばめるくらいである。

そんなわけで原因は require() しているところのようだ、と分かりかけてきたのだけど…… cfx run で実行してみる分には普通に動くのだ。しかし xpi をビルドしてそれを Firefox にインストールしてみると動かない。先の記事の通り、必要なライブラリの有無の問題? と xpi の中を覗いてみても問題なさげ。

そういうわけでいろいろ試行錯誤してみたところ、意味が分からないことに console.log() を付加したり外したりするごとに main.js の最後まで通常通り実行されたり、途中で止まったり再現性のあまりない動作をすることに気が付いた。

うー、これは javascript のレベルでのバグじゃなくて Firefox のバイナリ自身のバグじゃないのかしら。ちなみにやはり先の記事の通り、main.js は minify してある。もしかしてこれが……? と、試しに minify せずに xpi をビルドしてみると、これが普通に動くのだ。えーそういうことなのか。

しかし minify しないと、xpi のサイズが 300KB 超になってしまう。Add-on SDK による xpi は SDK のコードも含むため、ただでさえサイズ大きめ(Chrome と Opera のエクステンションのサイズが 100KB 程度なのに対して、xpi は 200KB くらいになる)なのに、minify をしないとさらに増えてしまうわけだ。もっとも 300KB というサイズ自体が、21 世紀の今現在においては全然大したことのない大きさではあると思うが、Chrome/Opera/Firefox のそれぞれのエクステンションを並べたときに Firefox の xpi だけ異常に巨大なのはやはり不自然だ。

しかし minify すると動かないのだから背に腹は変えられない。300KB のままで公開することにした。

調べてみるとそもそも SDK 自体が jetpack と呼ばれていたものを一旦キャンセルした後作り直したものだったり、api の仕様がプロセス分離モデルを前提としていたのにプロセス分離モデル自体がキャンセルされたので無駄に複雑なものになってしまったり、デバッガが使えなかったり、IE の唯一のカウンターパートという立ち位置だったころは Firefox って超クールでナウいブラウザなイメージだったのだけど、最近はなんだか迷走続きでかなりイケてないなーと思いました。Firefox がんばれ超がんばれ。

building a xpi

Firefox + Add-on SDK で 最終的に .xpi ファイルをビルドするには、

$ cfx xpi --pkgdir=/path/to/package --update-link=http://path/to/update-link --update-url=http://path/to/update-url

などとする。

cfx は、エクステンションを構成するファイルと、Add-on SDK が提供するライブラリをかき集めて .xpi ファイルを生成する。後者は、低位なライブラリ api-utils と高位な addon-kit の 2 種類あるのだが、addon-kit に関してはエクステンションが参照しているものだけを抽出して、.xpi のサイズの肥大化を防いでいる。なかなか賢いですね。

ただ、どうやってエクステンションが参照している addon-kit ライブラリを抜き出しているのかというと、$(addon-sdk)/python-lib/cuddlefish/manifest.py の中で、javascript ソースを正規表現レベルで走査している。

REQUIRE_RE = r"(?
という正規表現で、まさに grep している。javascript の構文解析をしているわけではない。

これがこうなっているのは、つまりこれでたいていの場合良いだろうということなんだと思うが、そうでもない場合もある。wasavi のパッケージは js ファイルを minify している。つまりパッケージ内の javascript ソースはものすごく長い 1 行のテキストなのだ。これが根本的な原因だと思うのだけど、参照しているライブラリの抜き出しがうまく行ったり行かなかったりする(たいていはうまく行かない)。

minify しなけりゃいいじゃんという気もするが、もしかしたら将来 closure compiler を使うかもしれないのでやっぱり cfx 側が変則的な javascript ソースにも対応してもらったほうがいいのではないかと思う。

とりあえずの回避策としてこうしている:

  • エクステンションで参照しているすべてのライブラリを内部的に require するダミーのモジュールを作る。lib/dummy-require.js とか。

    var self = require('self');
    var pageMod = require('page-mod');
    var tabs = require('tabs');
    var simplePrefs = require('simple-prefs');
    var clipboard = require('clipboard');
    var timers = require('timers');

    var locale = require('api-utils/l10n/locale');
    var xhr = require('api-utils/xhr');

    exports.hello = function () {
    return 'it is dummy module';
    }

    このソースだけは、minify しない。

  • 他の javascript ソースのどれか 1 つで、上記のソースを require する。ソースのできるだけ先頭の方がいいかもしれない。main.js あたりの先頭で:

    if (typeof window == 'undefined') {
    window = this;
    window.jetpack = {};
    require('./dummy-require');
    }

    のような感じ。require するだけで、別に何かに使ったりするわけではない。

    この状態で cfx xpi すると、必要なライブラリが正確に抜き出されるようだ。

    うーんまあ割とどうでもいい tips だな!

file i/o #10: final!

Opera をはじめ、Chrome と Firefox でも dropbox に対する読み書きを行えるようにした。

Chrome でも動くようにするのは、ぜんぜん難しくないのだ。んが、Firefox で動かすのがなかなか面倒。他のブラウザが、エクステンションといいつつも実態は html ページなのに対して、Firefox (Add-on SDK) で動くエクステンションのコードというのはグローバルオブジェクトが [object Proxy] だかなんだかという見慣れた window とはちょっと違う何かだったりする。

で、jsOAuth.js という独立したライブラリを参照する際、Opera と Chrome ではスタートアップとなる html ページにいつものよーに script 要素を書くだけでよいのだが、Firefox の場合は CommonJS に従ったルールで lib ディレクトリに置く必要がある。jsOAuth は Node.js & CommonJS コンパチブルだと謳っているので、まあ Firefox でもうまくいけばそのまま動くのだろうなどと思っていたら、甘かった。

CommonJS に則った方式で再利用可能なモジュールを書く場合、

exports.foo = function () {
console.log('hello, world');
};

などと exports に対して登録する形になる。jsOAuth もその先頭で

var exports = exports || this;
exports.OAuth = (function (global) {
:
})(this);

とやっている。これで、CommonJS 下ではエクスポート、一般的な javascript では window に OAuth オブジェクトが結び付けられる。めでたしめでたし……のはずが、なぜか Firefox ではいくら再利用する側で

var OAuth = require('./jsOAuth');

とやっても読み込まれない。正確には、読み込まれるのだが(つまり jsOAuth.js 自体は評価されるのだが)、再利用できない。OAuth は undefined のままなのだ。

結論から書くと

typeof window == 'object' && eval('var exports = this;');
exports.OAuth = (function (global) {
:
})(this);

のようにする必要がある。SDK が提供する javascript の実行環境は前述の object Proxy だか sandbox だかの、管理された空間みたいなのだが、その空間のグローバルに提供される exports やら require() やらはちょっと特殊な扱いっぽいのだな。なのでおそらく、var exports なんてしちゃうと本来の exports を「管理された空間上の exports 」で覆ってしまって役に立たなくなるということなのだと思う。したがって、exports の polyfill を定義するのは本当に window 下で実行されているときだけに厳密に分けないといけない。

そもそも require とか exports とか、どこでどう定義されているのかと SDK のソースをいろいろ探ってみたのだけど、どうも見つけられなかったのは内緒。いやまあ packages/api-utils/lib/loader.js とか cuddlefish.js とかがキーだと思うんだけど……。なんか bootstrap から追っていってもいきなり require() とか使い出してる感じだしよく分からなかった。

このほか、XMLHttpRequest も SDK 専用品を使わなければならなかったり(しかも upload プロパティまだ実装してまへんテヘペロ とかいう未完成品だったりする)、Firefox はなかなかいろいろと勝手が違うのだった。

file i/o #9: file, again

ex コマンド file でパス名を指定する。ところでおもしろいことに vim の場合、そのパス名がディレクトリそのものかどうかのチェックとかは行わない。
:file ../
なんてやると、素直にパス名が ../ になる。この状態で保存しようとしても、当然エラーになる。

これ、たとえば新しいパス名がディレクトリ部分しか含んでおらず、かつ現在のパス名がセットされている場合は変更前のファイル名を持ち越して使ってくれればいいのになーということでそうした。/foo/bar/baz.txt を開いて
:file ..
とするとパス名は /foo/baz.txt になる。この状態で
:file bar/
とすると /foo/bar/baz.txt になる。

こういったパス名のハンドリングは javascript でやるのはなんか新鮮だ。そしていろいろ考えることがある。

まず dropbox へ保存するとして、使えない文字があるのかどうか? windows の場合ファイル名に「:」を含めることはできない(もっとあったと思うけど忘れた)。「foo:bar.txt」なんかを dropbox へ保存できたとして、windows の dropbox クライアントでそれを同期するとどうなるのか? また保存できないとして、それを事前に知ることはできないのか? ちなみに cygwin でファイル操作するとなんと「:」を含めたファイル名を作れるのだが、どういうことか U+F03A という不思議な文字に置換される。私用領域。

相対パスの解決。さすがにこれを javascript で書いた例はぐぐっても見つからない。というよりむしろ、javascript じゃなくても相対パスを解決するアルゴリズムみたいなのがなかなか見つからない。これは考えてみれば当然で、相対パスの解決なんてシステムが提供するライブラリなりシステムコールで行うのが正道中の正道であって自前でやる方がおかしい。そんな中で URI に関して言えば、RFC1808 が URI 中の相対パスの解決アルゴリズムを記述している。とりあえずそれを参考にしつつそれっぽく解決するようにした。

file i/o #8: file

ex コマンド、file。
f[ile][file]
引数 file が指定された場合、現在のパス名を file へ変更する。

file コマンドはファイルの情報を表示する。ファイルが現在のパス名を持っている場合、それを含めて表示する。持っていない場合は、現在のパス名がないことを表示する。

編集バッファが行を持っている場合、現在の行番号と行の総数を含めて表示する。持っていない場合、編集バッファが空であることを表示する。

編集バッファが最後に書き込まれてから変更されている場合、そのことを含めて表示する。readonly オプションがセットされている場合、そのことを含めて表示する。

メッセージはその他のここに定義されていない項目を含めてもよい。

ということで、:f したり ^G したりすることで表示されるあれは割と実装によってまちまちだ。

  • vim 7.3 (香り屋版)
    "wasavi.js" [変更あり] 11622 行 --33%--
    "[無名]" --バッファに行がありません--
  • vim 7.3 (cygwin)
    "wasavi.js" 行 1 (全体 11622) --33%-- col 1
    "[無名]" --バッファに行がありません--
  • nvi 1.79
    wasavi.js: modified: line 2 of 11622 [33%]:
    /tmp//vi.ujMcQJZSaX: unmodified: empty file

表示する項目を大まかに分けると、

  • ファイル名。ファイルシステム上のパス名に割り当てられたそれか、割り当てられていないことを示す文字列
  • フラグ的なもの。変更済み、読み取り専用、等々。
  • バッファの状態。バッファ中のカーソル位置、またはバッファが空であることを示す文字列

ということになると思う。ということで、
"ファイル名" [フラグ的なもの] バッファの状態
ということにする。フラグが複数の場合はカンマで区切る。

こんな感じか。
とりあえずファイル操作系の ex コマンドはこれでだいたい実装したことになる。実はファイル名を入力する際の補完機能も作らないといけないのだが……。

file i/o #7: off-line

dropbox へ保存する際、15 秒程度の遅延を仕掛けている。タイマを設定し、保存対象となるテキストを適当なバッファへ覚えておき、15 秒後に保存処理を走らせる。その 15 秒以内に再度保存を要求された場合は単にバッファだけを更新する。

これは短時間のうちに何度も dropbox へアクセスするのを抑えるためだ。この、保存に際してバッファを経由する仕組みはオフラインアプリケーション化に繋がるかもしれない。バッファをローカルの何らかの永続的ストレージに持たせる。バッファから dropbox への送信はオンライン時にのみ行う。オフ -> オンラインに遷移した場合は自動的に溜まったバッファを送信する。そんな感じにすると常時接続ではないネットワーク環境でもそれなりにストレスレスな編集ができるかもしれない。

まあすぐそういう風にするわけじゃないけど。一応覚えておこう。