Opening an options page

wasavi のオプションページというものを開くために、manifest.json に options_page キーを含めているが、実はこれは旧式である。現行は options_ui キーになっている。options_ui でどのようにオプションページを開くかどうかについてさらに新旧があり、古い方はオプションページをタブで開き、新しい方は chrome://extensions ページにオーバーレイする形で開く。これは TabQueue などの他の Chrome エクステンションでも出てきた話題だ。

さて wasavi を WebExtensions で動かした場合、options_page キーを認識しないようだ。そこで options_ui の、旧式の方に移行した。旧式の方は公式に obsolete であり、今にも削除されそうな勢いである…ということは、ちょっと気にかけておかないといけない。

WebExtensions で動かした場合、options_ui 旧式の場合は、拡張の詳細ページにオプションページを開くボタンが追加される。新式の場合は、オーバーレイではなく、拡張の詳細ページにオプションページが埋め込まれた形で表示される。へー。

Chrome では個々のエクステンションの設定をエクステンション一覧の特別な、モーダルな状態として捉えているが、Firefox ではエクステンション一覧から遷移する独立したページとして捉えている。どちらかというと Firefox の設計のほうが優れてるように思える。ブラウザの設定も昨今の多くのブラウザでは Web ページの1つとして扱われている。であればやはり様々なページを遷移する UI であることを強く意識して設計すべきであり、オーバーレイというモーダルな状態に安易に頼るべきでない。

mozc is…

xubuntu デスクトップで日本語を入力する際、fcitx+mozc という環境にしている。mozc というのは Google 日本語入力のオープンソース版で、違いは Google 日本語入力ほど賢くないということだ。

どれくらい賢くないかというと、「よくみるとかわいい」と打ち込んで変換すると第1に候補として上がるのが「よく覧ると革いい」になったりするのだ。まあ「覧る」でも間違いじゃないだろうけど、いやーそれ第1候補かな? と思うし、それにどんだけ革が好きなのかとも思う。

意味が分からないのは、常に賢くないわけではなく、時々この通りパーになるのである。夏バテでもしてるのかな。

Reloading extensions

ブラウザのエクステンションの開発における基本的なサイクルは、ソースを書き換えて、エクステンションをリロードし、実行結果を確認する…というものだ。このサイクルを延々と繰り返す。したがってサイクルを構成する基本的な要素をちょっと最適化するだけでも、その分だけ開発は快適になる。この内、エクステンションのリロードについて考えてみたい。

かつて、Presto Opera が wasavi のファーストクラスブラウザだった頃は、リロードプロセスは実は最も洗練されていた。というのは、ページを普通にリロードすれば、ページにアタッチされている拡張の injected script 群も自動的にリロードされたからだ。賢すぎる。もちろんバックグラウンド側に修正を入れた場合はエクステンション管理ページから wasavi 全体をリロードする必要はあったものの、そういうケースはそんなにあるわけではないので、フロントエンド側がスマートにリロードされるだけで十分快適なのだった。

しかし、それはもう過去の話。現在はどうかというと、wasavi はまず Chrome で動かしてそれから Firefox や Blink Opera で動作確認という流れになっているのだが、しかしとても残念なことにいずれのブラウザも Presto Opera ほど賢くない。content script を書き換えたとしても必ずエクステンション管理ページから手動でリロードしないといけない。とても面倒くさい。とてつもなく面倒くさい。

そういうわけで、wasavi 自身に reload コマンドを実装してある。これを投入すると、メッセージがバックグラウンドとエージェントの両方に送信される。バックグラウンド側では自身を [cci]chrome.runtime.reload()[/cci] によりリロードする。エージェント側ではメッセージの着信後1秒待ってページをリロードする。これでリロードの面倒臭さがかなり解消される。

ところで、Firefox の WebExtensions ベースのエクステンションを開発するための web-ext というツールがあるのだが、これを経由して Firefox を起動するとエクステンションのソースディレクトリを監視して、いずれかのファイルが更新されたら自動的にエクステンションをリロードするという機能を持っている。これはこれで便利なのだが、最新バージョンだとバグってて動かないという…。[cci]–no-reload[/cci] オプションをつけないと起動しない。安心と信頼の Mozilla クオリティ。

Move by wrapped line

Chrome と Gecko が持っている document.getSelection().modify() というメソッドがある。これは、編集可能な要素上のカーソルを任意の位置へ移動させる機能を持っている。上下左右のカーソルキー、HOME キー、END キー、およびそれらを SHIFT 併用で押下した場合の動作をコードから行わせることができる。カーソル移動の単位は文字、ワード、折り返し行の先頭及び末尾…などがある。

さて、wasavi は [cci]gj[/cci]、[cci]gk[/cci] で物理的な行単位ではなく、表示上の折り返された行ごとにカーソルを上下させる機能があるのだが、従来この機能はかなりめんどくさい複雑なコードで実現していた。あまりに複雑すぎてバグがあってもおいそれと手を入れられないレベルである。実際 issue がいくつか来ているのだが、そういうわけでどうしたものか困っていた。

そこで、上記の modify() を使ってシンプルに書き直した。なんで最初からそうしなかったのかといえば、modify() は Presto Opera には実装されていないからだ(たぶん)。しかしもう Presto Opera 対応は終了したので、これを期に使うようにしたということだ。このほか、let や template literal、arrow function、そしてもちろん Promise や Generator なんかもぼちぼち使い始めている。

WebExtensions and clipboard operation

Chrome の拡張でクリップボードを読み書きするには、manifest.json の permissions に clipboardRead/clipboardWrite を追加した上で

  • 読み出す場合: バックグラウンドで適当な textarea に対して focus() し、document.execCommand(‘paste’) する。適当な textarea のその内容がクリップボードの内容で埋められる
  • 書き出す場合: バックグラウンドで適当な textarea に対して focus() し、内容を全選択した後 document.execCommand(‘copy’) する
  • つまり操作の実体はバックグラウンドになる。コンテントスクリプト側で読み書きの必要が発生したとしたら、バックグラウンドにメッセージを投げ、戻ってくるのを待つことになる。

    これが、WebExtension だとどうなるのかというと、ドキュメント自体は提供されているのだが、それによるとなんかどういう意図なのか知らないが、かなり Chrome の流儀と違う。

    まず short-lived event handler なる独自の概念が出てくる。これはつまり、ユーザーにより発生したイベント、のことらしい。しかしそれなら interactive event handler とか、あるいはそのまんま user generated event handler などと呼ぶべきものではないのか? 微妙によくわからない。何がどう short-lived なの? 意味がわからない。またもうひとつ独自の仕様があり、WebExtensions ではバックグラウンドスクリプト側でのクリップボードへの書き込みはできない。従って、コンテントスクリプト側で適当な textarea 要素に対して execCommand() しないといけない。つまり、Chrome と正反対なのである。

    で、この short-lived event handler 内であれば、clipboardWrite 権限なしにクリップボードへの書き込みは可能である。一方 short-lived ではないもの、つまりタイマによって起動された処理など、あるいはユーザーにより発生したイベント内で生成された Promise の一連の連鎖内も当てはまると思うが、そういう処理からはクリップボードへ書き込みを行うには clipboardWrite 権限を manifest.json に記述することが必要。

    ここまではドキュメントにそう書いてある通りで、その通りに wasavi を修正したならば、その通りに動作した。困るのはクリップボードからの読み出しなのであった。どうもまず、ドキュメントが言葉足らずで微妙に言いたいことがよくわからない。ドキュメントから読み取れるのは:

    • short-lived event handler 内で実行されるか否かに関係なく、常に clipboardRead 権限が必要
    • 読み込みも基本的にコンテントスクリプト側で処理?(サンプルが、適当な要素のクリックイベントを使用しているので)
    • 対象の要素は contentEditable モードでないといけない
    • コンテントスクリプト側で実行する場合、現状では textarea 要素のみに対応
    • バックグラウンド側ではいずれの要素も contentEditable モードにすることができる(できるから、何?)

    そもそもバックグラウンド側で実行できるのかどうかが明示されておらずよくわからない。クリップボードへの書き込みがバックグラウンド側では不可というのは、つまりバックグラウンドでは要素にフォーカスを当てるということができないからだそうだが、それなら読み込みも不可なんじゃないのか? しかし不可と明示されてはいないのである。わからん、ぜんぜんわからん。

    いろいろ試行錯誤してみたところ、以下のような手順でクリップボードの内容を取得することができた。

    • コンテントスクリプト側で実行する
    • ドキュメントに適当な textarea 要素を追加する。この要素のスタイルを [cci]display:none[/cci] とかにはできない。フォーカスを当てる必要があるので。その代わりにスクリーン外の適当な場所に追いやる必要がある
    • textarea 要素は contentEditable 属性を true にしておく必要がある。そもそも最初から編集可能なのに、なんでこの属性が必要なのか意味不明
    • textarea にフォーカスし、document.execCommand(‘paste’) を実行する
    • しかし、Chrome と違い、textarea の内容がクリップボードの内容で埋められたりは「しない」。なんと、なんと、その代わりに document に対して paste イベントが発生する。従って、execCommand 呼び出しの前に paste イベントハンドラを追加し、その中でクリップボードの内容を取り出しておく必要がある
    • なお paste イベントは、同期的に発生する

    どうも execCommand() により paste イベントが発生するという点がミソのようだ。その点を以ってバックグラウンドでも動作可能と匂わせているのかもしれない。ということは、コンテントスクリプト側で動かす際も textarea 要素に focus() する必要はないのか? そこまでは試してない。

    とりあえず、こんな感じのコードになる。

    function getClipboard () {
    let s = '';
    function handlePaste (e) {
    s = e.clipboardData.getData('text/plain');
    }
    let buffer = $('id_of_any_textarea');
    buffer.contentEditable = true;
    buffer.value = '';
    buffer.focus();
    document.addEventListener('paste', handlePaste, false);
    document.execCommand('paste');
    document.removeEventListener('paste', handlePaste, false);
    return s;
    }

    もちろん、すでに paste イベントを他の場所でハンドリングしている場合は、そのハンドラは一旦取り外し、事が済んだ後に再度追加する…的な小細工は必要。また、これはあくまで現状での動作なので、時が過ぎればころっと変えられる恐れは十二分にある。

    あの、なんで、こんなにも Chrome の流儀と全然違うデザインにしたんですか? WebExtensions 自体が Chrome の拡張に恐ろしく馴れ馴れしく擦り寄った代物であるのに、部分的に見るとまるで作法が異なるというのはどういう意図があるのかいまいち…というか、さっぱりわからない。技術的に何か制限があってこうなっているのならわからなくもないが、そうではないとしたら一言「アホじゃないの?」としか言いようがない。

Replacing remote address

ここのサイトは CloudFlare を通しているのだが、そうするとアクセス解析的なことをする際に [cci]$_SERVER[“REMOTE_ADDR”][/cci] の値が CloudFlare 内のサーバの IP アドレスになってしまい、本来のビジターのそれを得られない。

そういう場合は代わりに [cci]$_SERVER[“HTTP_CF_CONNECTING_IP”][/cci] を参照する。自前で作っている PHP アプリケーションの場合はそれだけの話なのだが、このブログの場合はどうだろうか。このブログは WordPress で運用している。

must-use plugin というものを使う。これは WordPress のメインの処理に先駆けた早い段階で読み込まれる。[cci]/wp-content/mu-plugins/ip-override.php[/cci] 的なファイルを編集し

if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
$_SERVER["REMOTE_ADDR"] = $_SERVER["HTTP_CF_CONNECTING_IP"];
}

てな感じのコードを書いておく。

visual in vi

エラーではないが、ユーザに何か通知すべき事柄が発生する場合がある。例えばカーソルが行頭にいる時に [cci]h[/cci] を押したとか。

そういう状況では、vi はビープ音を鳴らす。これが「ビービー鳴らすモード」などと揶揄されているわけだが、この動作は errorbells オプションで制御することができる。これをオフにするとビープ音は鳴らない。

ただこれだと、「ビービー鳴らすのはうっとうしいが通知があったことは知りたい」という要件に応えられない。そういうわけで、visualbell オプションを新設した。[cci]:set errorbells visualbell[/cci] の状態ではビープ音の代わりに wasavi の画面がフラッシュする。

Promise and Generator #7

さて、wasavi の根幹の動作ロジックを Promise ベースに大改造した大元の動機のひとつである、migemo 対応について考えてみたい。

とりあえず Chrome extension としての migemo についてまとめてみたい。migemo 自体のホームはここ。そして Chrome 版の migemo というのは、検索文字列に対応する migemo 的な正規表現を返す API を提供することに特化した拡張で、migemo server ということになっている。

まず edvakf さんが制作した ChromeMigemo Extension というものがかつてあった。これ自体はすでにサポートを終了しているが、ソースは公開されている。ただ困ったことに API リファレンスといったドキュメントはないので、このリポジトリ自体はエンドユーザにとってはそれほど有用ではない。API リファレンス的なものは辛うじて彼のはてなダイアリーの古い記事から得られる。

さて、これを現在サポートしているのは mono さんで、blog の記事でfork 版である Migemo Server for Google Chrome™の告知がなされている。wasavi もこれを参照する。

これを wasavi の [cci]/[/cci]、[cci]?[/cci] コマンドに組み込むわけだが。もともとこれらのコマンドは正規表現を入力して検索するものだ。これが migemo に対応すると、入力した文字列が内部で複雑な正規表現に変換された後に検索が行われるという流れになる。つまり何か入力してそれを検索するという手順は同じなのだが、入力するものの性格は全く違うということだ。migemo の場合は入力するのは文字列リテラルなのだ。

そうなると、コマンド自体を別に起こす(例えば、[cci]g/[/cci]、[cci]g?[/cci] とか)ほうが筋がいいのかもしれない。しかし、ここはやはり [cci]/[/cci]、[cci]?[/cci] コマンドにまとめたい。

ところで vim では、正規表現中に [cci]\C[/cci] というものを含めると ignorecase オプションの値にかかわらず、必ず大文字・小文字を区別して検索するようになる。[cci]\C[/cci] には正規表現のメタキャラクタとしての効果は何もなく、それが正規表現中に存在するか否かに意味があるというわけだ。この考え方を流用してみよう。メタキャラクタ [cci]\M[/cci] を導入し、これが検索文字列に含まれている場合は migemo で検索、そうでない場合は従来の検索というように振り分ける。

このやりかたにすると、検索文字列の入力中に「やっぱ migemo で検索しよーっと」とかその逆が簡単に切り替えられるし、また [cci]map g/ /\\M[/cci] などとすることも可能なので、migemo 検索を独立したコマンドにしたいという向きにも対応できる。

というわけで、できた。

Marquee!

wasavi が起動する際、最小の横幅・縦幅というものがあり、横の最小幅は 320px ということになっている。

このくらい幅が狭いと、ステータスラインに何かメッセージを表示する際に、その長さによっては後ろが隠れてしまうことがある。

これを解決するために、いわゆる marquee 的な動作をさせるようにしてみた。的な、というか、実際に marquee 要素で囲むのである。21 世紀も17年過ぎて marquee 要素を使うとは思わなかった…。

この marquee 要素、いつ廃止されてもおかしくない状況なのだが、とりあえず今のところは Chrome でも Firefox でも使える。ただし Chrome においてはかなりスムーズに動くのに対して Firefox ではかなりガックガクだ。

まああんまり気にしないことにしよう。

Promise and Generator #6

一通り修正が終わって、Selenium による Chrome 上の機能テストも一応クリアした。

一応というのは、800 超の機能テストはカテゴリごとに editing.js とか、insert.js とか分かれているのだが。それらを個別に動かすと 100% パスするものが、全てまとめて通すとぽろぽろと失敗するものがランダムに出てくる。これはなんだろうか。

もうひとつ。以前 Firefox でテストしようとしたところ、geckodriver の機能や、Selenium のサポートなどがまだ熟成されていない感じだった。やたらめったら CPU パワーを消費するし、既存のプロファイルを使用できないし、WebExtensions ベースの拡張を認識してくれないし、要素に対して sendKeys() する機能が未実装だし…など、プロダクトレベルの品質に達しているとはとても言えない状態だった。あれから数ヶ月経ったがどうだろうか。

ということで試してみたところ、
Could not convert 'text' to string
なるエラーが sendKeys() で発生する。うーんまだ実装していないのかな? と調べてみたところ、issue が上がっていた。つまるところ、sendKeys() 自体は実装済みなのだが、Selenium は入力されるべきキー情報を文字の配列の形で送出するのに対し、geckodriver 側は文字列の形で受けることを想定しているという型のミスマッチのようだ。issue 後半に掲載されているクイックパッチの通り直してみたらとりあえず動いた。javascript バインディングの Selenium の場合、スクリプト言語で実装されているおかげでこういう小回りがきくのは嬉しい。

その他、CPU パワーを浪費することもないようで、さすがにいろいろと進歩しているようだ。