BotanJS-vim: Yet another vi editor in your browser

Demo: http://tgckpg.github.io/BotanJS-vim/
Source: https://github.com/tgckpg/BotanJS-vim
Reddit comments: https://www.reddit.com/r/vim/comments/4d42r3/hi_ive_implemented_vim_in_a_textarea/

Looks good.

two issues

issue #124

Shadow DOM というとてもナウいテクノロジーがあるのだが、Shadow DOM に含まれる textarea に対して wasavi を起動できないという issue。

なぜ起動できないのか。textarea が Shadow DOM に含まれているとき、そこで発生したイベントのターゲットがその textarea ではなく、Shadow DOM のルート要素になってしまうからだ。wasavi を起動させる直前にエージェントはターゲットが textarea とか、input とか、それ系の要素であることを確認しているので、そこで弾かれる。

で、実際にイベントが発生した要素を得る方法はわからなかった。その代わり、ルート要素の activeElement を参照すれば、それが高い確率でキーボードイベントを実際に発生させた要素であるはずなので、それを利用するようにした。このとき Shadow DOM 内の要素が更に入れ子の Shadow DOM ルートである可能性もあるので、無限ループにする工夫が必要、らしい。このへんは issue で挙げられた POC コードを参考にした。

* * *

issue #128

Chrome で wasavi をインストールした状態だと、PDF ファイルを開けないという不思議なバグ。ファイルを開けず、ページは白紙のままになる。

これは wasavi 本体というよりはエージェントの問題なのだが、白紙のままになる原因自体は不明。Chrome で PDF を開くというのはつまり PDF Viewer という PPAPI プラグインが差し込まれて実行されるということなので、その内部でやっていることと何か競合しているように思える。とりあえずエージェント側では DOMContentLoaded が発生しない。

そもそも PDF ファイル上でエージェントを実行する必要自体がないので、agent.js の先頭で document.contentType を見て [cci]^text/[/cci] の時だけ実行するよう修正。これだと普通の html ページに見えて、実は content-type が text/* ではない不思議なページだと動かなくなるわけだが、まあそれはそのページのほうがおかしいですよね…? と思ったけど [cci]application/xml[/cci] なんかの場合はありえるのか。はてどうするか。

Match about about:blank

issue #123

それが何なのかまださっぱり理解していないのだけど、https://www.visualstudio.com/ というものがある。たぶん一言で言い表わせば、Microsoft 版の github ということなんだろうと思う。そして件の issue は、ここで作る issue に含まれる textarea(実際には iframe の content editable な div)で wasavi が起動しないというもの。

起動しない理由は明快だ。その iframe に src 属性が含まれておらず、要するに内容がすべて動的に構築されているタイプだからだ。特に Chrome のエクステンションの場合、コンテントスクリプトは manifest.json で表明した URL に対してのみアタッチされる。src 属性が与えられていない iframe 要素は、実質的にはその内容は about:blank のはずであるが、あくまでも src 属性は空なので、どのコンテントスクリプトも結び付けられない。

ちなみにググってみると、こういうやりとりがあって、manifest.json の content_scripts.match_about_blank を true にすると [cci][/cci] やそれに類する URL パターンが about:blank も含むようになるそうなので、そうした。

ところで件の Microsoft 版 github のページを見てみると、content editable div には editarea なんちゃらというクラス名が入っている。EditArea というのはそういうライブラリがあるのだけど、それなのだろうか。未確認。また、サンプルページ は上記 match_about_blank の修正を施した上でも wasavi が動作しない。

Editing rich text with wasavi #4

ぼちぼち wasavi に組み込み始めたいのだが、1つ考えることがある。

contenteditable な要素にどのようにテキストの各行を格納するかは、サイトによってまちまちでありおおよそ以下の種類がある:

  1. 段落を div で区切る
  2. 段落を p で区切る
  3. 段落自体はテキストノードであり、br で区切る
  4. テキスト全体がテキストノードであり、\n そのもので区切る

この他、もちろん今回対応している gmail のように、完全なリッチエディットコントロールとして扱うか、それとも Twitter のように多少書式付けられる textarea 要素の亜種として扱うかの別もある。

面倒なのは、どのサイトがどのタイプかを機械的に判断することはまったくできないということだ。リストを保持して、泥臭く判断するしかないのである。例えばすでに issue として挙げられた件では、workflowy で使用されている contentEditable な div 要素ではテキストを 3. のパターンで格納しているが、このサイトがこれをどこかで表明しているわけではまったくない。wasavi 側で勝手にうまく辻褄を合わせるしかない。

とりあえずそのリストは agent.js 内に定数の形で持っている。もしかしたら将来的には、オプションページでユーザーが編集できるようにするかもしれない。

* * *

というわけで組み込んだ。何がどうなったのか再度まとめてみよう。

1. contentEditable な要素を wasavi で編集する際は、要素の内容を markdown に変換するようになった

2. markdown ではあるが例外があり、wasavi 独自のタグが含まれることがある
img、a、object、embed については markdown ではなく、元の要素への ID を持つリンク要素として表現される。例えば以下のような:


3. wasavi で編集した内容を contentEditable な要素に書き戻す際、いくつかの方法がある
方法は以下の通り:

  • html – 内容を markdown とみなした上で html を構築する
  • div – 内容の各行を div 要素に変換する
  • textAndBreak – 内容の各行をテキストノードに変換し、br 要素で区切る
  • plaintext – 内容全体を単体のテキストノードに変換する

これらのいずれかがサイトに応じて自動的に決定される。

4. wasavi のオプション writeas が新設された
3. で選択された値が writeas オプションに設定され、set コマンドによりユーザーが別の値を上書きできる。

Editing rich text with wasavi #3

markdown-test

  • DOM ツリーから markdown への変換は、自前で書くようにした。この処理は agent.js に内包させる必要があるので、できるだけコンパクトではないと困るのである
  • 逆に markdown からマークアップされた文字列を得るのは、これはなかなか大仕事であるため、とりあえず marked を使ってみることにした。このライブラリはおそらくバックエンド側に保持させることになると思う。

    そんなわけで、一度 markdown に落としたものを再びマークアップしてみたのが上記の図。左から元の DOM ツリー、生成された markdown、再構築された DOM ツリー。だいたいいい感じなのだが、いくつか marked についていくつか気になることがある:

    • [cci][/cci] のような、内容を持たない空要素のつもりで書いたタグをそのように扱ってくれない。開始タグとみなしてしまう。これは[cci][/cci] と冗長に書けば解決する
    • 画像のとおり、連続してはいるが空行で区切っていることで別個のものとして書いたつもりのリストをまとめてしまう。これは隣り合うリストについては 2 つの空行で区切ることでまとめられないようなので、そうする
    • markdown 自身の仕様の問題なのだけど、u 要素に対応する記法がない

    その他、marked はパラグラフを p 要素に置換する。これ自体は正しいのだが gmail はパラグラフを div で表現するため、ひと工夫する必要がある。README.md で記述されている通り、marked ではレンダラー、つまり markdown を走査して得た各要素の情報を実際に HTML でマークアップされた文字列に変換する部分を独立させてあり、かつ自由に上書きすることができる。その仕組みによって、デフォルトで p 要素が出力されるケースを div で置き換えることは可能だ。また同じやり方で [cci]**…**[/cci] が strong 要素になるのを b 要素に、[cci]_…_[/cci] が em 要素になるのを i 要素に上書きすることも必要。

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 をマークアップしたとき最初の状態に戻らない。

WebExtensions, or Standardizing a browser extension

Edge で WebExtensions ベースのエクステンションが動き始めたとか、Firefox の WebExtension が未だにバグだらけとか、そういう話題の WebExtensions 界隈なのだがひとつ気になることがある。

WebExtensions は Chrome のエクステンションの仕様が元になっているというかまるごとパクっているわけだが、これについて当の Google が何か公式にアナウンスしたのを見た記憶がない。もしも Google が気まぐれでエクステンションの仕様をごっそり変えたらどうするの?

と思ってたらいつのまにやら W3C Browser Extension Community Group なるものができていて、Microsoft、Opera、Mozilla、そして Google といったところから開発者が参加している。ふーんじゃあどこか1社が勝手にちゃぶ台ひっくり返したりすることはないのかな。

ところで Apple からは誰も参加していないのだね。

Multiple Instances #2

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

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

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

Multiple Instances

issue #118

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

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

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