この日記はMozillaのプロダクトへの貢献者としての私の成果を中心に、気になったバグやWeb界隈の話題について書いていますが、 断り書きがある場合を除き、いかなる団体のオフィシャルな見解ではありません。あくまでも個人的なものです。 Mozilla Foundation、Mozilla Corporation、及び関連企業の公式情報ではないことに注意してください。

現在、XHTML 1.0 (もどき)から、HTML5なコンテンツに修正中です。古い日記は修正が完了していませんので表示が崩れます。 順次、修正していく予定ですのでしばらくお待ちください。

もずはっく日記

2021年9月23日

最後発でbeforeinputイベントを実装しているはずなのに…… 初回投稿日時: 2021年09月23日08時48分53秒
カテゴリ: Editor Events Firefox Google Chrome Safari
固定リンク: id=2021092300
SNS: (list)

Geckoのエディタのリファクタリングからの続きです。

beforeinputイベントは各ユーザの操作に応じて、DOMツリーの変更前に一度だけ発火するというイベントです。例えば、テキストをキーボードから入力するとinputTypeinsertTextbeforeinputイベントが発火され、これをpreventDefault()するとブラウザのエディタが何も変更しなくなるので、例えば入力文字の限定等に使えます。Geckoのエディタのpublicなメソッドを整理する必要があったのは、この「一度だけ」発火する、という点にありました。私の当初の目論見では、この巨大な変更さえ行えばなんとか形にはなるんじゃないかと思っていました。ところが、現実は、Geckoは唯一beforeinputイベントを実装できていないブラウザエンジンで、つまり、最後に実装するブラウザエンジンであるにも関わらず、仕様がかつ、ブラウザ間の互換性維持のためのWPTに自動テストがほぼゼロという信じられない状況でした。

Document.execCommandでは発火するべきではないイベントで、なおかつユーザ入力をWPT上でエミュレートするTestDriverがようやく使えるようになってきている時期だったので、まあ、後者が無いのはまだ分かりますが、とにかく前者がひどい。Googleの開発者もAppleの開発者も全然ブラウザ間の互換性に興味がなく、かつ、丁寧な実装をするつもりが無かったことが明白です。

Google Chrome/Chromiumのバグで例を挙げると、特定の入力パスではbeforeinputイベントが発火されなかったり(これは私のpublicメソッドの整理等を行わず、発火用のコードを差し込んでいった実装であることが推測されます)、InputEvent.dataが適切にセットされていなかったり(これは、WPTだけではなく、Chromiumのツリーにも自動テストが無いことを示唆します)。さらには、beforeinputイベントが存在するのかどうなのかを判定するために必要なGlobalEventHandler.onbeforeinputが実装されていないためにGeckoが安全にbeforeinputイベントをshipしにくいことが当時からMozilla内での懸念事項となったり、またはshipしてもUA名で判定されているWebアプリではいつまでもGeckoだけ(下手すればバグ持ちだったり、機能制限があったりする)専用のパスで処理されてしまうというひどい状況を生んでくれていました。

また、beforeinputイベントのgetTargetRanges()の戻り値にも苦しめられます。仕様書の定義では、

InputEvent.getTargetRanges(): returns an arrays of StaticRanges which will be affected by the change to the DOM if it is not canceled.

とあり、驚くことにこれが全文です。実装者から見ればこれは何も定義されていないのと同義です。でも何故かBlinkやWebKitの開発者の人達はこれだけで実装してしまったのです。

そこで先行するBlinkとWebKitの動作を確認してみると、やはり互換性が無かったり、実際に変更がある範囲とは異なる場合がかなり多く見受けられました。とりあえず色々と割りきって、これは削除する時の範囲がSelectionと異なる場合にだけ有用だろうという判断で、そこだけを中心にテストを書いてみました(これだけでも結構な日数がかかりました)。そのテスト結果は現在でも芳しくありません(期待される結果を概ねBlinkの動作としており、また、あるべきDOMツリーの形もBlinkの動作にあわせているものが多く、Geckoのスコアはまだ低いままです。順次このスコアを上げていくことでBlinkとの互換性が改善していくという準備のためにあえこうしています)。

これらの問題をある程度のところまで解決していくのにとにかく時間がかかるので、またしてもbeforeinputイベントの実装完了が遠のいたのです(いったい、一部のWeb開発者の方の早くGeckoでもbeforeinputを実装してくれという要望の、そのゴール、つまりどんな実装を要求しているのか疑問だらけになってきました)。

ちなみに、Input Eventsの仕様はLevel 1Level 2に別れており、Web開発者が欲しい機能はおそらくLevel 2(Level 1ではIMEからの入力は一切preventDefault()でキャンセルできない)であるにもかかわらず、Level 2のinputイベントの発火タイミングは後方互換性がなく、とても受け入れられるものではありません。この問題は残念ながら現在でも解決していません。

続く……

2021年9月9日

Geckoのエディタのリファクタリング 初回投稿日時: 2021年09月09日21時46分39秒
最終更新日時: 2021年09月23日08時50分13秒
カテゴリ: Editor Firefox
固定リンク: id=2021090900
SNS: (list)

なんとなく気まぐれで久しぶりに書いてみようかなと。

今はGeckoのeditor moduleのオーナーとしてゴリゴリとやっていますが、振り返るとエディタをどうこうしないと、みたいな感じでオーナーになったわけではなく、当時のマネージャに「IME周りのハンドリングをもっと向上するには最終的なアウトプットであるエディタの理解も必要」とか、「エディタはこの10年ぐらい(当時)、誰も積極的にメンテナンスしてないので他の場所のバグに重点を置くけど面倒見る人が必要」みたいにだまして説得して就任した記憶があります。

最初に着手したのはたぶん、主要クラスのリネーム。それまでのグローバルなnamespaceにnsというprefixでクラスを置いておくのが、当時、既に他のコンポーネント(例えばDOM)が既にmozilla::domというnamespaceに移行していたりで、放置しておくとヘッダファイルの記述が面倒だな、という後ろ向きなモチベーションから入っていった気がします。まあ、実際に修正してスッキリはしたんですが。

次に取り組んだのは脱XPCOM。Geckoのエディタは当時、mozilla::EditorBase(旧名:nsEditor)、mozilla::TextEditor(旧名:nsTextEditor<input><textarea>用のエディタ)、mozilla::HTMLEditor(旧名:nsHTMLEditordesignModecontenteditable用のエディタ)を、最初にnsIEditorインターフェースを実装するエディタとしてEditorBaseを得、その上でさらに目的に合致するものとしてnsIPlaintextEditornsIHTMLEditorのインターフェースを持つ、TextEditorHTMLEditorを得てメソッドを呼び出したりしていました。COM周りをやったことある人なら分かると思いますが、デザイン的には綺麗なものの、とにかくQIと、その仮想クラスで定義されているvirtualメソッドの呼び出しが遅いというのがMozilla内でも当時問題になっていました。前者は64bitを超える容量の比較が連続するので当然ですし、後者はもうhot pathでは無視できないコストだというのがC++を書かれている方にはうなずいて頂けるかと。なのでこれらを解消するために各エディタユーザにはconcreate classに直接アクセスするようにしたり、各エディタが複数のルールで動作できるように作られていたediting rulesという概念を削除して、とにかく基本的に安全かつシンプルな構成にしていきました。

そんな感じでリファクタリングを楽しんでいる時に舞い込んできたのがbeforeinputイベントの案件でした。それ以前から懸案としては上がっていたのですが、どうにかしないといけない、ということで実装を始めると予想通りその道のりは長く、想像以上のものになりました(少なくとも最初に問いかけられてから実際にshipするまで5年弱かかっています)。

まずbeforeinputイベントの実装にあたって問題だったのは、エディタの各クラスが内部からpublicなメソッドを呼び出しているため、一回のユーザのオペレーションや、JSからのAPIの呼び出しがエディタモジュール内ではハッキリしないということでした。そこで、publicなメソッドを制限しこれらをActionと(勝手に)命名し、そこから呼び出されていたpublicメソッドで行われていたことをSubActionと(やはり勝手に)命名し、どこから始まって、どこで終わるのかを明確にする作業を複数のbugで行い、さらに、publicメソッドでスタック上に一時情報を保存するようにし、管理の簡略化や省メモリ化を実現しました

これらにより、beforeinputイベントの実装への下準備が全て整うと思っていました。でも、そうではなかったのが現実の厳しさでした。

最後発でbeforeinputイベントを実装しているはずなのに……に続く