Mysterious bound keyword

Firefox 46a だったか 47a だったか忘れたが、wasavi と赤福プラスが両方動かなくなったことがあって調べてみた所、

  1. 両方が使っている extension_wrapper.js 内で、それが Firefox + Add on SDK で動いているかどうかを判断している箇所があり
  2. Firefox のその該当バージョンで判断のもとになっている箇所が変更されたため

動かなくなっているということだった。

extension_wrapper.js は Chrome でも Opera でも Firefox でも共通して動くので、それぞれのブラウザのうちどれで動いているのかを判断しないといけない。Chrome なら window.chrome が存在しており、Opera なら window.opera が存在しているのでそれが判断の助けになる。。

一方 Add on SDK の PageMod における content script かどうか、というのを判断するためのあんしんあんぜんな方法はないように思える。とりあえずのところ、self.on が存在し、それが function であり、かつその toString() の値が “function on (.*?) { [native code] }” とかそんな感じになっていればまあたぶんきっと SDK ベースの content script だろう…というような判断をしている。”[native code]” がミソなのだが、これが最適解というわけでは全然ない。

これが、今回動かなくなってしまったのである。それは、self.on の toString() の値が “function bound on (” というように、謎のキーワード bound が入るようになっていたからだ。bound……って何?

Bringing a tab to the front

Chrome や Firefox は標準状態だと新規タブを開いたときそれをバックグラウンドにするのだが、個人的にはその仕様は使いにくく、フォアグラウンド(アクティブ)にしたい。Firefox の場合はそういうふうにするオプションが本体にあるので単にそれを使えばいい。しかし Chrome にはそういった標準的オプションはない。代わりにエクステンションを入れるしかない。いくらなんでもこの程度の機能は本体が持ってていいと思うんですけど。

というわけで、今までは Tabs to the front! というエクステンションを入れていた。

しかし、どうもこれが cVim と相性が悪い。Chrome 上で target=”_blank” なリンクをクリックして作成したタブは確かに常にアクティブになるのだが、cVim がエクステンションの chrome.tabs.* API を用いて生成したタブについては、アクティブになったりならなかったりとても不安定なのだ。

そこで、Tabs to the front! を使うのはやめ、cVim 側で対応することにした。cVim 側で新しいタブを開くのは単に t キーを押す。これは [cci]:TabNew[/cci] というキーストロークに仮想的にマップされているのだが、これを [cci]:TabNew![/cci] にマップし直すことで([cci]![/cci] を付ける)新しいタブがフォアグラウンドで開くことを指定する。これで解決!

と思ったら全然解決せず、不安定なままなのである。困ったことに cVim のバックグラウンドをデバッグし、実際に新規タブを生成するところで止めてみると確かに active パラメータは正しく渡されている。というかデバッガを起動している状態では思った通りの動作をするのだが閉じると不安定な状態に戻ってしまうのだ。ひっどい。つまりこれは、chrome.tabs.create() がなんかバグってんじゃないのかしらん?

この不具合は、しかし前述の通り Tabs to the front! を有効にしても直らない。このエクステンションは何をやっているのかといえば非常にシンプルで:

chrome.tabs.onCreated.addListener(function (tab) {
chrome.tabs.update(tab.id, {active: true});
});

とまあ実にこれだけなのである。要するに新しいタブが作成されたことを察知したら、すかさずそれをアクティブに更新している。

これが動いたり動かなかったりするというのは、おそらくは [cci]chrome.tabs.update()[/cci] と、chrome 内でタブを deactive にしている「何か」との、実行される順番がその時々で不定ということなんだろう。ということで、

chrome.tabs.onCreated.addListener(function (tab) {
setTimeout(function () {
chrome.tabs.update(tab.id, {active: true});
}, 100);
});

なんて感じに遅延を挟んでやると思った通りの動きになる。しかし Tabs to the front! は人様のエクステンションなのでいじることはできない。どうしたものかな。

タブの動作の変更といえば拙作の Tabqueue があったので、そっちに機能を持たせることにした。Tabqueue とは、あるタブを閉じた後にどのタブをアクティブにするかについて、Opera12 のようにタブをアクティブ化した順のキューに応じて決定するという Presto Opera への未練タラタラなエクステンションである。従来はその機能に絞った単機能のエクステンションだったので、複数の機能を持たせるにあたり新しくオプションページを作成し
tabqueue-options
それを通して有効・無効を指定するようにした。ちなみに設定は chrome.storage.sync に保存されるようにしたので、複数のデバイス間で同期する。

ところでいつのまにかエクステンションのオプションはこのようにオーバーレイ形式になっていた。従来の、タブを1枚消費するタイプは obsolete なのだという。知らなかったそんなの…。

そういうわけで更新した

wasavi and content type of a page

issue #128 と関連して。

discuz という PHP アプリケーションがある。これは要するに中国版 phpBB であって、phpBB 同様にいわゆるフォーラム機能を提供する。中国語圏内ではとても有名なのだそうだ。ざっと見た感じそれほど phpBB との機能差は見受けられなく、なんで似たようなものを一から作りなおす必要があるのかよく分からないが、まあそれは特に問題ではない。ちなみに disqus とは何の関連もない。

で、wasavi を有効にしていると discuz 上での post が上手くいかないというクレームが来たのである。それに対応してみたい。

まずダウンロードし、ローカルにインストールした。エンコーディングによって何種類かあるが、とりあえず繁体字・UTF-8版を持ってきて、適当なディレクトリに展開する。discuz はバックエンドとして MySQL を使用するので、あらかじめ適当なデータベース、それにアクセスするためのユーザアカウントとパスワードを作っておく。ブラウザから [cci]展開ディレクトリ/upload/install/[/cci] にアクセスし、適当に必要な項目(含データベース情報)を埋めつつウィザードを進めるとインストールが完了する。それにしても繁体字ならなんとなく雰囲気で読めるんじゃないかと期待したが、さっぱりわからないものです。登録、がログインのことなのだそうだ。そうなんだ…。

そんなわけでその環境で試してみると、たしかに上手くいかない。discuz はサーバへの post は ajax ベースであり、隠れた iframe を経由している。post の応答である XML ドキュメントを iframe に受けて、その結果が動作を左右する。ここで wasavi のエージェントが XML ドキュメントに対して余計な script 要素を追加してしまうと、Chrome がそれを XML とみなさなくなってしまい、XMLDocument プロパティが無効になってしまう(このへんは深くは追ってないけど)。その後 discuz 側が無効な XMLDocument プロパティを参照して例外で中断してしまう…という筋書きだ。つまりつきつめると XML 文書に対して wasavi のエージェントを動かしてしまうことがバグの原因だ。

これは正に issue #128 と関連している。前の記事では text/html だけではなく application/xml(あるいは、+xml を含むもの)に対しても実行したほうがいいのかなと思ったわけだが、今回のバグは実行してはダメなパターンなのである。

エージェントは wasavi を起動するために textarea などの HTML 要素を含む文書を対象としなければいけない。なので、単純に考えれば対象となる文書は text/html か、application/xhtml のいずれかだ。ただしもう一つ可能性があって、サーバからは xml が送られてくるが、ブラウザ上でそれを XSLT で html に変換するという構造のアプリケーションだ。この手のアプリケーションを、そういう構造だと判断する処理はかなりめんどくさい。

しかしそんなへんてこな構造のアプリケーション存在するんだろうかと考えると、うーんまあ、ないか。ないな。うんうん。ないない。

というわけでエージェントを実行すべき contentType は上記の通り html と xhtml だけにしよう。xhtml にしても生きてるのか死んでるのかよく分からない状態ではあるが…。

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