Editing rich text with wasavi #2

続き。

しかし、最初の状態には確かに戻らないが、markdown された文字列

line1

line2

line3

---

![...] [...](...)

を再度マークアップすれば、とりあえず

line1
line2
line3
---

という形になり、とりあえず見た目は元に戻る。たぶんその状態で送信しても gmail 側は受け入れるだろうし、それならそれでいいような気もしてきた。そもそも別にメールを編集する際にリッチテキストを用いるとして、その書式に厳密な規格なんてないのだ。とりあえずこの方針で行ってみたい。このだんらく「とりあえず」多すぎだ。

となるとまず必要なのは、DOM 要素を markdown にしたり、逆に戻したりする素敵な javascript のライブラリだ。

ただしその前にいくつか考えることがある。前の記事での新規メールの DOM の構造を見ると、たとえば dir 属性が与えられた div 要素や、width/height が与えられた img、target が与えられた a 要素などが存在している。これらの属性は、普通に markdown に落とすとすべて削がれてしまう。div はともかくとして、img や a でそれらの情報が欠落すると問題だ。

そこで、それらの要素については標準的な markdown から外れ、いわば元の要素へのリンクという形にしてみてはどうかと思う。このような img 要素があったとして


markdown にする際は



という独自の書式にするのだ(あるいはより markdown に寄せて [cci]![](id=”foo”)[/cci] みたいな形でもいいが、寄せてはあるが markdown の仕様とは違うのでかえって紛らわしいかもしれない)。これをマークアップする際は id を元に、wasavi の編集対象となっている要素から対応する img 要素を探しだし、再利用するのである。これにより width だろうが height だろうが、あるいは突き詰めればイベントハンドラでさえ wasavi 編集前後で正しく再生される。

このような特別な扱いを必要とする要素はとりあえず a、img、object、embed でいいと思う。

もう一つ言うと上の例ではパラグラフをマークアップすると div になっているが、これも普通は p になる(かもしれない)わけでそのへんも呼び出し側で固定できると嬉しい。

ということで、このへんの要件を満たしてくれる javascript の markup/markdown ライブラリを探してみることにしよう。もしも無ければ、例によって自分で書くことになる。

Editing rich text with wasavi

さてそのめんどくさい issue 120 だ。

wasavi は textarea や input 要素の他に、contenteditable 属性が与えられた任意の要素を対象にすることができる。できるのだが、問題がある。当然ながら contenteditable な要素ではその要素が内包できるあらゆる要素を内包できる。一方で、wasavi はあくまでプレーンテキストエディタであるので、編集できるのは純粋にその中のテキスト部分だけである。従って編集後にそれを対象の要素へ上書きすると、元の要素に与えられていたマークアップの情報はほぼすべて失われる。

これは仕方がない。wasavi がリッチテキストエディタになれば解決するといえばするが、そういう方向に持っていくつもりは今の所ない。従って、contenteditable な要素を wasavi で編集する際は割りきって使うしかないのである……と考えていたのだが。

件の issue は、gmail でメールを wasavi で編集すると書式付きの署名が壊れてしまうのでそれを弄らないようにしてくれ、というものだ。なるほど。これはたしかに不便な話だ。

どうしたものかな。

まずは wasavi と contenteditable な要素間でのテキストのやりとりで、テキスト以外のマークアップ情報が失われるという問題について考えるに、100%ではないにしても最も情報を保存しつつやりとりできる方法は、テキストではなく要素の innerHTML を編集対象にすることだと思う。しかしこれが万人に使いやすいのかというとかなり疑問だ。

次善の策として、DOM の構造を markdown に変換するようにしてみてはどうか。これは割と悪くない気がする。

ただし、これによって上記の署名が壊れる問題は解決しない。gmail で新規メールを編集している時:
wasavi-gmail-2

その DOM の構造は:
wasavi-gmail-1

これを markdown に変換すると div 要素の構造が失われる。つまり、再度 markdown をマークアップしたとき最初の状態に戻らない。

Multiple Instances #2

そういうわけで、同時に複数の wasavi を起動できるようにした。

最も大掛かりにコードの変更を受けたのは agent.js で、一旦バラバラにして組み直したような格好だ。しかしこれでコード内の役割分担が割と明確になったので結果的にはよかった。

次に控えている issue はちょっと難しい。

Multiple Instances

issue #118

現状の実装だと、あるページに textarea が複数あった場合、wasavi の起動は排他的だ。つまり複数の wasavi を同時起動することはできない。この制限を取り払ってほしいという issue。

これを解決するとして、最も影響を受けるのはエージェントだ。まず wasavi を起動する際にフレームごとにユニークな ID を振るようにして、メッセージングの際はそれをやりとりするようにしないといけない。特にメッセージを受信した場合はフレーム ID を元に複数の wasavi のインスタンスから正しい宛先を探しだし、適切にディスパッチする必要がある。

これはこれとして、もともとエージェントの構造は若干とりとめなく関数を書き連ねてた形になっていたのが気になっていたので、大掛かりに見直す切っ掛けとしては良い。

vi flavored browser UX

テストのために VimFx を入れたり外してたりしたわけだが、普段はこの手の、ブラウザのインターフェースを vi 風味にする拡張というものを使っていない。正確には、Keysnail によって最低限の vi っぽいショートカット(hjlk とかその程度)を付け足してはいるが、その程度だ。hit-a-hint などのナウい機能は使わなくても何とかなっている。

これはなぜかと改めて振り返ってみると:

  • トラックポイント付きのキーボードを常用しているので、そもそもポインティングデバイスとキーボードの併用にストレスがあまりない
  • wasavi の動作テストをするにあたって、特定の拡張の影響を排除したい
  • この手の拡張を入れると、web ページが個別に定義するキーボードショートカットと拡張自身のショートカットのどちらを優先するかという問題が必ずつきまとう。この手の問題に煩わされたくない

ということだろうと思う。

そういうわけで個人的な要求としては、「ブラウザのナビゲーションにおいて vi っぽいショートカットは多少はあれば嬉しいけど、おおがかりに vi っぽくはしなくていいし、してほしくない」ということなのである。現在常用している Firefox においては、それは Keysnail で自分でちまちまとスクリプトを書けば対応できる。一方で Chrome はどうだろうか。Chrome も常用とはいかないが、それなりに使っている。

まず vi flavored なエクステンションはどうだろうか。Chrome におけるその手のエクステンションを列挙してみると:

…なんでこんなにあるんだ?

Compatibility to VimFx #3

色々やりとりしてみた所、何やら思いがけない方向に進んだ。

まず、wasavi 側で VimFx を意識した処理をする必要はなくなった。逆に、VimFx 側で wasavi の iframe を contenteditable な要素として扱ってもらえるようになった。のだが、途中から wasavi が起動中に blur させる方法がないのが問題、という話になり、正直なところへぁ? なんのこと? という気分に。

どうも VimFx の作法として、編集可能な要素がフォーカスを持っている場合は VimFx は自身のキーバインドを使用しないというものがあるようだ(逆に言うと、編集可能な要素の振る舞いを上書くことは VimFx ではできない)。ただし、編集可能な要素上で esc キーを押すとフォーカスを外す、つまり VimFx のキーバインドが再度有効になるようにはなっている。したがって、例えば通常の textarea で編集中に他のタブに切り替え、必要なものをコピー、戻って貼り付けなどという一連の動作をすべてキーボードで行うには、

  • textarea 上で esc
  • gt (次のタブへ)
  • yf 等々で必要なものをコピー
  • gT (前のタブへ)
  • gi (先頭、あるいは最後にフォーカスした編集可能要素に再度フォーカス)
  • wasavi 上で [cci]”*p[/cci]

というようなキーボード操作になる。で、これを可能とするために、wasavi も esc で blur して欲しいということのようだ。

というわけで、そういう風にした。ただ、この常にこの仕様で動作させて良いのか? という懸念はある。

  • vi 使いの中にはノーマルモードに戻るために esc を連打する人もいる
  • そうでなくても、うっかりノーマルモード上で esc を押してしまうことは有り得る
  • あくまで VimFx の作法である。VimFx がインストールされていれば、wasavi がフォーカスを失っても即 [cci]gi[/cci] で戻すことはできるが、インストールされていなければポインティングデバイスで wasavi をクリックし直すしかない

なので、新しいオプション [cci]esctoblur[/cci] を導入し、これがオンの場合のみに blur 処理を行うようにした。

Compatibility to VimFx #2

動作するようにはなったのだが、やはりイベントハンドラの実引数として渡された何かを取っておいてハンドラのスコープの外で使うというやり方はちょっと嫌な感じではある。

VimFx の issue に何か参考になるものがないのかなと見てみたら、丁度よく wasavi が動かない、正確には Pterosaur や CodeMirror や wasavi 等々、自前で色々キー入力を消費するタイプの拡張と競合する、というが上がっていたので乗っかってみよう。

Compatibility to VimFx

Firefox の拡張には、キーボード周りを含めたブラウザのインターフェースを「強力に」書き換える能力があるわけなのだが、それらと wasavi の相性は一般的によくない。wasavi もまたすべてのキーボード入力を自分で消費するからだ。すると、あるキー入力が他の拡張に横取りされてしまうとか、wasavi と同時に処理されてしまうと言った不具合が起きる。

それを避けるために、VimperatorKeysnail 用のプラグインスクリプトを用意してある。これらは主に 2 つの仕事を受け持つ:

  1. タブの切り替えを監視し、アクティブなタブのドキュメント上に実行中の wasavi が存在するかどうかで対象の拡張を一時的にサスペンドさせるかを制御する(ちなみにこの辺の処理は e10s が有効だと完璧に破綻する: プラグインは Chrome 権限上で動いている、そして content を直接触るためだ)
  2. ドキュメントのカスタムイベント [cci]WasaviStarted[/cci] [cci]WasaviTerminated[/cci] を監視し、各イベントに応じて適宜対象の拡張を一時的にサスペンドさせるかを制御する

ところで、こういったインターフェースを再定義する拡張はもちろんこれだけではない。issue では VimFx との相性がよくないというものが上がっているので、とりあえずそれに対応してみることにしよう。

* * *

VimFx というのはなんぞやというと、実はあまり知らないのだが、本質的には Vimperator と同様に Firefox のインターフェースを vim っぽくする系の拡張である。ただし、Vimperator がインターフェースを「ドラスティックに」変えるのに対して VimFx はもうちょっと大人しいようだ。

プラグインの機構はない。ただし、Chrome 権限のスクリプトから使用できる API が提供されている。

さて、対象の拡張が変わったとしても、やるべきことは上記の 2 点なのは変わらない。ただし 1 は上記のとおり、e10s を見越して content 内で処理する必要がある。普通の DOM のイベントでタブがアクティブ・非アクティブになったというのを判定するには Page Visibility という割と新し目の API で定義される visibilitychange イベントを使えば良いだろう。一方で、wasavi の状態によって他の拡張にアクセスするのは content ではなくバックエンドの chrome スクリプトでなければならないので、つまり visibilitychange イベントでやることはアクティブなタブだったらバックエンドにメッセージを投げる、ということになる。そんなわけで:


window.addEventListener('visibilitychange', function () {
if (document.hidden) return;
extension.postMessage({
type: 'visibility-change',
wasaviRunning: !!wasaviFrame
})
}, false);

というような感じ。

このメッセージをバックエンドで受け取り、wasavi が起動しているようなら VimFx をサスペンドさせればいいのである。

* * *

というわけで色々作って、VimFx の API を使ってどうこうするところまで来たのだが。

ドキュメントによれば、API はそれを提供するオブジェクトのインスタンスを通して使用する。インスタンスの取得は

let {classes: Cc, interfaces: Ci, utils: Cu} = Components
Cu.import('resource://gre/modules/Services.jsm')
let apiPref = 'extensions.VimFx.api_url'
let apiUrl = Services.prefs.getComplexValue(apiPref, Ci.nsISupportsString).data
Cu.import(apiUrl, {}).getAPI(vimfx => {

// Do things with the `vimfx` object here.

})

こんな感じで、ここで [cci]getAPI[/cci] に渡しているコールバックに渡される仮引数 vimfx がそのインスタンスなのだが。困ったことにその中に VimFx をサスペンドさせるようなそのものズバリの機能はないのである。

というより VimFx 自体にサスペンドという概念がない。その代わり、すべての入力を素通りさせて VimFx は関与しない ignore モードというのがあって、normal モードから ignore モードへ遷移することで結果的にサスペンドが行える。で、このモードを遷移するための [cci]enterMode()[/cci] メソッドはどこに属しているのかというと VimFx のコマンド群のそのハンドラに渡される vim パラメータが持っているらしい。しかしこれを API から直接的に得る手段はない。うーんどうすれば……。

しかし、間接的に得る手段はある。[cci]vimfx.on()[/cci] でいくつかのイベントハンドラを登録することができて、その時も vim パラメータが渡される。これを取っておいて、必要なときに使えばいけるかもしれない。

* * *

というわけで、期待した動きをするようになった。

The Traditional vi

issue #113

これはいわゆる vim の compatible mode を実装してくれという要望なのだが、あれを実装するのは大変そうだ。具体的にはテストが大変だ。様々なオプションのオン・オフの組み合わせを網羅するとテスト項目が半端ないものになってしまう。

件の issue の方は最低でも traditional な vi の uu だけでもいいということなのでそういう方向で行ってみたい。

vi の uu というのは、u で一旦 undo した後、続けて u を押すと今度は redo 動作になる、という不思議な動作のことだ。この動作のことを一般に何と言うのかわからないが、とりあえず flipping undo と呼んでおこう。

これをどうやって実装するかは、素直に boolean なフラグを保持しておいて(以下 flipped)、u が押された時にそれを元に動作を振り分けということになるだろう。

いくつか気になる点:

  • flipping undo とふつーの undo をどう切り替えるか? vim では

    :set compatible
    :set cpoptions+=u

    とすることで flipping undo モードになる。しかし前述の通り [cci]compatible[/cci] や [cci]cpoptions[/cci] の類はあんまり実装したくない。ちなみに vim では、flipping undo モードにするための方法はもう一つあり、それはつまり [cci]undolevels[/cci] を 0 にするというものだ。じゃあこれでいいかな。
  • wasavi では vim と同様、[cci]^R[/cci] に redo を割り当てている。flipping undo の場合これは何をするべきなのか? vim の場合は、どうも flipped 変数を変更しない undo として振る舞うようだ。つまり flipping undo 時の u コマンドは

    if (flipped)
    redo();
    else
    undo();
    flipped = !flipped;

    という動作になる。[cci]^R[/cci] もほぼ同じなのだけど、最後の flipped 変数の変更だけは行わない。

Two of three

issue をちまちま片付けていたのだがなぜが次々と新しい issue が上がってくる。なにこれ…と思ったら、3 日に Hacker News で wasavi が取り上げられていたようだ。

前にも書いたが、書いたプログラムが Hacker News と Reddit と Slashdot で取り上げられたらまあある程度浸透したのかもしれないと勝手に思っているのである。Reddit にはすでに取り上げられていて、今回は Hacker News なので 2/3 のスコアになった。

残りは Slashdot なのだが。しかし最近の Slashdot はなんか知らないがとても gdgd な状況になっているようなので、まあいいかなという気分になりつつある。