「訳」ProseMirrorの起源

✍🏼 作成日 2021年11月05日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください
🖥  説明:原文の著者は2015年7月7日に、リッチテキストフレームワークを作成しようと決心した際に書いた記事です。2年後にProseMirror 1.0がリリースされました。本記事は意訳を中心としています。

時々、夜ベッドに横たわりながら、わずかな収入しか得られない責任をさらに引き受けるための新しい方法を熱狂的に探し求めています。そして思いつくのです、もう一つオープンソースプロジェクトを始めよう!と。

まあ、実際にそんなことが起こったわけではありませんが、結果は同じです。私は常に複雑で高度なコードを構築し、その後それを放棄しています。実際、このプロセスのメカニズムは通常、まず何らかの技術的な概念を思いつき、調査した結果それがまだ実現されていないことを発見し、最後に好奇心と自己実現欲を満たすために、自分がそれを_できるかどうか_確かめようと決心するというものです。

このメカニズムが生み出した最新の「災い」(とはいえ放棄するつもりはありません)が、ProseMirror、ブラウザベースのリッチテキストエディターです。私はクラウドファンディングを通じてこれをオープンソース化し、リリース後のメンテナンスを継続する方法について検討しました。

エディター?

「まだ誰もやっていない」ことを実現すると言ったばかりではありませんか?ブラウザベースのリッチテキストエディターは少なくとも数百はあるのでは?

はい、はい、その通りです。しかし、既存のプロジェクトのどれも、私が理想的と考えるアプローチを取っていません。それらの多くは、contentEditable要素に依存し、その結果生じる混乱を整理しようとする古いパターンにしっかりと根ざしています。これでは、ユーザーやブラウザが私たちのドキュメントに対して行うことに対してほとんど制御ができません。

何を制御する必要があるのでしょうか?まず、リッチテキストエディターは、ドキュメントを合理的な状態に保つのを容易にするべきです。ドキュメントがあなたのコードによってのみ変更される場合、それらの変更を定義して、保持したい不変性を維持させることができ、また異なるブラウザで同じことが起こるようにできます。

しかし、より重要なのは、これらの変更を単なる状態の変更ではなく、より抽象的な方法で表現できることです。「ここで何かが変更され、新しいドキュメントができた」というだけでなく。変更を抽象的に表現することは、共同編集時に非常に役立ちます----複数のユーザーからの競合する変更を効果的にマージし、変更の_意図_を正確に表現するのに役立ちます。

基本的な実装方法

ProseMirrorは実際にcontentEditable要素を作成し、その中にドキュメントを表示します。これにより、フォーカスやカーソル移動に関連するすべてのロジックを自由に操作でき、スクリーンリーダーや双方向テキストのサポートも容易になります。

ドキュメントに行われる実際の変更は、適切なブラウザイベントを処理することでキャプチャされ、それらの変更に対する独自の表現に変換されます。比較的最近のブラウザでは、ほとんどのタイプの変更を抽象的に記述するのは簡単です。キー押下イベントを処理して入力テキストやバックスペース、エンターなどをキャプチャできます。クリップボードイベントを処理してコピー、切り取り、貼り付けを正常に動作させられます。ドラッグ&ドロップもイベントを通じて実現されます。IME入力も比較的使いやすい合成イベントをトリガーできます。

残念ながら、ブラウザがユーザーの意図を記述するイベントをトリガーせず、事後の输入イベントの結果しか得られない場合もあります。例えば、スペルチェックメニューから修正を選択した時や、特殊文字を入力するためにコンビネーションキーを使用した時(例:Linuxで「Multi + e + =」を使って「€」を入力する場合)にこれが起こります。幸い、これまでに私が遭遇したすべてのケースは、単純な文字レベルの入力に関わるものだけです。DOMを検査し、ドキュメントの表現と比較して、そこから意図された変更を導き出すことができます。

変更が行われると、エディターのドキュメント表現が変更され、表示(画面上のDOM要素)が新しいドキュメントを反映するように更新されます。ドキュメントに永続的なデータ構造を使用することで----変更が新しいドキュメントオブジェクトを作成し、古いオブジェクトを変更しないようにする----非常に高速なドキュメント差分アルゴリズムを使用し、実際に必要なDOM更新のみを行うことができます。これはReactとその各種派生品が行っていることに少し似ていますが、ProseMirrorは汎用的なDOM風のデータ構造ではなく、独自のドキュメント表現を使用しています。

エディタードキュメント

このドキュメント表現はもちろんHTMLではありません。しかし、それはドキュメントの「意味的」表現でもあります。段落、見出し、リスト、強調、リンクなどでテキストの構造を記述するツリーデータ構造です。これはDOMツリーとしてレンダリングすることも、Markdownテキストとして表示することも、その他、それがエンコードする概念を表現できる形式であれば何でも可能です。

この表現の外側の層、つまり段落、見出し、リストなどを扱う部分は、構造的にDOMに非常に似ています—子ノードを持つノードで構成されています。段落ノード(および見出しなどの他のブロックレベル要素)の内容は、それぞれに関連するスタイルのセットを持つインライン要素のフラットなシーケンスとして表現されます。これはDOMのようなツリー構造を全面的に使用するよりも優れています。これにより、強調タグでテキストを二重にラップすることを禁止するなどの不変部分を追跡しやすくなり、また段落内の位置を単純な文字オフセットとして表現できるため、ツリー内の位置よりも推論が容易になります。

段落の外側では、ツリー構造を使用せざるを得ません。したがって、ドキュメント内の位置は、ツリーの各レベルの子インデックスを示す整数シーケンスと、このパスの末端にあるノードのオフセットで表されるパスによって表現されます。これがカーソル位置の表現方法であり、変更が発生した位置を記録する方法でもあります。

ProseMirrorの現在のドキュメントモデルはMarkdownのモデルを反映しており、その形式で表現できるものを完全にサポートしています。将来的には、特定のエディターインスタンスで使用したいドキュメントモデルを拡張およびカスタマイズできるようになる予定です。

ユーザーインターフェース

現在市場にあるエディタには2つのスタイルのユーザーインターフェースがあります。1つは上部に配置されたクラシックなツールバーで、もう1つは選択範囲の上にインラインスタイルを設定するためのツールチップを表示し、現在選択中の段落の右側にブロック操作用のメニューボタンが配置されるタイプです。私は後者を好みます。なぜなら、使用していないときには完全に消えてしまわず(ドキュメントに一切影響を与えない)、しかし多くの人は慣れ親しんだツールバーを好むだろうと思います。

これらのユーザーインターフェースはすべて、エディタのコア機能とは別のモジュールとして実装されています。同じAPI上で他のスタイルのインターフェースも実装可能です。

キーバインディングも設定可能で、CodeMirrorのパターンに従っています。キーにバインドされた機能は「コマンド」として利用できるほか、execCommandメソッドを通じてスクリプトからも実行できます。

最後に、inputrulesというモジュールがあり、特定のパターンに一致するテキストが入力されたときに何をすべきかを指定するのに使用できます。これは「スマート引用符」のような機能や、「1.」と入力してスペースを押したときにリストを作成するような場合に利用できます。

共同編集

先ほど共同編集について触れました。このプロジェクトで多くの作業が行われたのは、リアルタイムの共同編集をサポートするためです。技術的な詳細については別のブログ記事([中国語訳](/tech/Collaborative-Editing-in-ProseMirror.html)もあります)を書きましたが、基本的な概念は以下の通りです:

ドキュメントに変更が加えられると、新しいドキュメントと、旧ドキュメントの位置を新ドキュメントにマッピングする位置マップが作成されます。例えば、変更に応じてカーソルを移動する場合などです。

位置をマッピングできることで、適用すべき位置をマッピングすることにより、他の変更の上に「rebase」することが可能になります。他にも様々な要素があり、このシステムを完成させるために何度も書き直しましたが、最終的には期待通りに動作するコードを得られたと確信しています。

共同編集のシナリオでは、クライアントが変更を行うと、まずローカルでバッファリングされ、その後サーバーに送信されます。もし他のクライアントが私たちの変更が到着する前に変更を送信した場合、サーバーは「いいえ、まずこれらの変更を適用してください」と応答し、他のクライアントはそれらの変更を受け入れ、その上で自分の変更をリベースして再度試みます。変更が通過すると、他のすべてのクライアントにブロードキャストされ、全員が同期された状態を保ちます。

ターゲットユーザー

ProseMirrorは誰に適しているのでしょうか?

  • まず、Markdownや類似のフォーマットで入力を受け付けているウェブサイトで、技術的に詳しくないユーザー向けに学習しやすいインターフェースを提供し、結果をMarkdownに変換したい場合。

  • 次に、従来のリッチテキスト入力を提供しているが、出力内容を制御したいウェブサイトでは、ProseMirrorに移行する価値があります。編集体験を直接反映し制約を実行できる方が、乱雑なHTMLをクリーンアップして最良の結果を期待するよりもはるかに優れているからです。

  • 最後に、共同編集をサポートしたい企業で、ユーザーがGoogleドキュメントで行っていることを自社製品上でドキュメント編集できるようにしたい場合。

興味を持たれましたか?このオープンソースプロジェクトのクラウドファンディングキャンペーンがどのように進んでいるかご覧ください。

- EOF -
この記事の初出: 「訳」ProseMirrorの起源 - Xheldon Blog