12k
All articles

新しいテキストエディタ Wordgard を概観する

WordgardはMarijn Haverbekeによる新しいリッチテキストエディタライブラリで、単一変更のトランザクション、corrections、facets、独自選択を備えます。

OpenReplay Team
OpenReplay Team
新しいテキストエディタ Wordgard を概観する

Wordgard は、ProseMirror と CodeMirror の作者である Marijn Haverbeke による JavaScript ライブラリで、ドキュメントがスキーマに準拠するリッチテキストエディタを構築するためのものです。エディタ UI コンポーネントも同梱されていますが、汎用的で自由形式の WYSIWYG エディタや HTML エディタではありません。

ProseMirror の統合をメンテナンスしていると、ステップのリストを通して位置をマッピングしたり、あらゆる場面でコンテンツ式をチェックしなければならない「汎用」コマンドを書いたりすることになりがちです。Wordgard は、そうした不満に対する同じ作者からの回答であり、ProseMirror の上に接ぎ木するのではなくゼロから構築されています。

本記事では、このライブラリが何を変えるのかを取り上げます。変更モデル、コンテンツ制約の廃止、facet ベースの拡張システムとライブラリ内での選択範囲処理、そしてこの作者による最初のリリースが ProseMirror、TipTap、Lexical と並べたときにどこに位置づけられるのかを見ていきます。

要点

  • Wordgard は 2026 年 7 月 2 日に MIT ライセンスのもとで 0.1.0 として初めてリリースされ、npm から wordgard としてインストールできます。作者はリリース時点で、プロジェクトはおそらく少なくとも 1 年は 0.x バージョンにとどまると述べています。
  • Wordgard のトランザクションはちょうど 1 つの変更を持ちます。その変更は、トークン範囲を保持する、置き換える、あるいはマークを追加・削除するといったセクションから構成されるため、影響を受ける範囲をステップのリストから再構築するのではなく直接読み取れます。
  • Wordgard のスキーマは、親が含みうるノード型を制限できますが、その順序は制限できません。矩形テーブルのような不変条件は、修正用の変更スペックを返すオブザーバ関数である corrections が引き受けます。
  • 設定は、値ごとの優先度とユーザー定義可能な facet を備えた拡張のツリーであり、これは CodeMirror 6 から引き継がれたものです。
  • Wordgard はキーボードとポインタによる選択をライブラリ内で処理し、独自のカーソルを描画します。タッチによる選択はブラウザに委ねられています。

Wordgard とは何か

Wordgard は、特定のスキーマに適合するコンテンツのためのリッチテキストエディタシステムであり、ドロップインの WYSIWYG コンポーネントでもアプリケーションでもありません。System Guide によれば、編集面は WYSIWYG のように感じられることを意図していますが、コンテンツと編集操作は見た目(フォントファミリ、段落のインデント、太字)ではなく意味(見出し、リスト、強調)によって名付けられています。このライブラリの看板となるエクスポートは Wordgard UI クラスです。その下には、ドキュメント、エディタ状態、編集アクションのための型があり、それらの大半はブラウザがまったくない環境でも動作します。

2026 年 7 月 2 日付の 0.1 リリース告知 には、MIT ライセンス、npm パッケージ名 wordgard、そしてソースが作者の Forgejo インスタンス上にあることが記されています。プロジェクトのホームページ はライセンスを確認したうえで、バグ報告は歓迎するがプルリクエストは受け付けないと付け加えています。ホームページはさらに、スキーマベースのドキュメント、モジュール式の拡張、双方向テキスト、テーブルやネストされたリストといった構造化コンテンツ、共同編集を機能として挙げています。これらの箇条書きは、プロジェクト自身の主張として受け取ってください。

Wordgard エディタのセットアップ方法

最小構成の Wordgard エディタは、ドキュメント、設定、親要素を渡した Wordgard.create の呼び出し 1 回です。以下はガイドのセットアップ例です。

import {Wordgard, menuBar} from "wordgard/editor"
import {fullSchema} from "wordgard/schema"
import {history} from "wordgard/history"

let editor = Wordgard.create({
  doc: `<p>Starting content</p>`,
  config: [
    fullSchema(), // A predefined document schema
    history(),    // Enable the undo history
    menuBar()     // Show a menu
  ],
  parent: document.body
})

config 配列は拡張ツリーであり、3 つのエントリはそれぞれスキーマオブジェクト、プラグイン、ウィジェットというものではなく、拡張のバンドルです。fullSchema()wordgard/schema からスキーマ要素一式を取り込みますが、そのドキュメント自身が、ライブラリに機能が追加されるにつれてこの集合がさらに要素を取り込む可能性があると警告しています。ガイドの後半の例では basicSchema() が使われており、これはブロックドキュメント、段落、見出し、改行、および strong・emphasis・link マークをバンドルしたものです。doc 文字列は、そのスキーマに照らして HTML としてパースされます。パッケージは wordgard/docwordgard/statewordgard/editorwordgard/commandwordgard/historywordgard/schemawordgard/types といったモジュールに分割されており、各部分の結び付きが密であることから、ガイドは TypeScript の使用を推奨しています。

Wordgard の変更モデルは ProseMirror とどう違うのか

Wordgard では、トランザクションはちょうど 1 つの change オブジェクトを持ちます。この change は、ドキュメントの一区間を保持する、置き換える、あるいはマークを追加・削除するというセクションから構成されるため、編集が影響を及ぼす範囲を、ステップのリストから再構築するのではなく直接読み取れます。ProseMirror では、トランザクションはアトミックなステップの順序付きリストであり、各ステップは 1 つ前のステップが生成したドキュメントに作用するため、位置計算や範囲の検査はそのチェーンをたどる必要があります。

告知が挙げる根拠は、ShareJS 由来の CodeMirror のデルタ形式のほうが、よりシンプルでありながら表現力も高いというものです。change は旧ドキュメント上のフラットなシーケンスです。トークン 10 個分のドキュメントを考えてみましょう。位置 4 にトークンを 1 つ追加する操作は「4 個保持、0 個をそのトークンで置換、6 個保持」となり、位置 3 から 6 を太字にする操作は「3 個保持、3 個を更新してマークを追加、4 個保持」となります。このマーク更新セクションが、CodeMirror のモデルに対する Wordgard の拡張部分です。

これがツリー上でも機能するのは、位置がトークン単位で数えられるためです。ガイドの index system では、各 plot の開始、plot の終了、テキスト以外のリーフ、そして UTF-16 の各文字が位置を 1 ずつ進め、位置 0 は最初の子の直前にあり、ドキュメントノード自身の開始・終了トークンは数えられません。これにより、change はまるでフラットな構造であるかのように新しいトークン列をドキュメントに差し込むことができ、結果が依然として整形式のツリーであることを確認する役割は change 生成コードが担います。

複数の change をまとめて ChangeSet.create に渡した場合、すべての位置は元のドキュメントに対して解釈され、ライブラリが自動的にオフセットを調整します。マークの変更はコンテンツにまったく触れません。

let makeStrong = ChangeSet.create(doc, {
  from: 1, to: 5,
  add: Strong
})

同じオブジェクトは change 同士の変換もサポートしており、これが undo 履歴と共同編集の土台となっています。

ProseMirror のコンテンツ式に代わるものは何か

Wordgard のスキーマは、親がどのノード型を含みうるか、およびブロック plot が空であってよいかを制限できますが、子が現れる順序は制限できません。ProseMirror の正規表現的なコンテンツ式に相当するものはありません。告知は 2 つの理由を挙げています。1 つは、任意の順序制約を前提にすると、すべての操作をチェックせずに汎用のドキュメント操作コードを書けないこと。もう 1 つは、厳格な制約が、実際の編集が通過する中間的な乱れた状態を妨げてしまうことです。

スキーマでは表現できないルールは corrections が扱います。correction はノードクエリに紐づいたウォッチャで、一致するノードが変更されたり出現したりするたびに実行され、ライブラリがトランザクションに追加する change spec を返すことができます。correction はコードであるため、形状を機械的に拒絶するのではなく、ユーザーが途中まで行っている操作を尊重できます。ガイドの例では、ドキュメントがレベル 1 の見出しで始まっていない場合にそれを挿入するために Correction.onChildList(Doc, ...) を使っています。告知は、ProseMirror のコンテンツ式では決して表現できなかったケースとして矩形テーブルを挙げています。

Wordgard はなぜプラグインではなく facet を使うのか

Wordgard は、設定と優先度の単位としての ProseMirror のプラグインを、それぞれが独自の優先度を持てる細粒度の拡張値のツリーへと置き換えています。告知の指摘は的確です。ProseMirror のプラグインは複数のフックを 1 つの優先度位置の下に束ねるため、あるフックでは高優先度、別のフックでは低優先度である必要があるプラグインは、その両方を得られません。

ガイドの configuration セクション では、拡張は次の 3 つのいずれかです。ライブラリ組み込みの拡張型の値、extension フィールドに拡張を保持する任意のオブジェクト、あるいはそれらをさらに含む配列です。明示的な優先度は GardState.prec の関数によって与えられ、同一レベル内ではツリー上の順序が決定します。facet は任意のコードが定義できる型付きの拡張ポイントで、入力を 1 つの出力にまとめるためのオプションの combine 関数を備えます。また compartment により、状態を捨てることなく設定の一部を差し替えられます。「プラグイン」という言葉が消えたわけではありません。Wordgard.Plugin.define は依然として存在し、独自の状態を保持し DOM の近くに位置する必要のあるオブジェクトのために使われます。ライブラリに同梱されるツールチップやパネルはこの方式で構築されています。

ライブラリが描画する選択範囲

Wordgard はキーボードとポインタによる選択をライブラリ内で処理し、ネイティブのキャレットを隠して独自のカーソルを描画します。一方、ネイティブの選択ハイライト自体は表示されたままです。告知はこれをブラウザ挙動の信頼性の低さに帰しています。特定のコンテンツを越えて動かないカーソル、誤った位置に着地する、あるいはまったく描画されないカーソル、そして誤作動するマウスドラッグ選択などです。そのためライブラリは、コンテンツのレイアウトについて独自の把握を構築し、双方向テキストの処理を自前で行い、カーソルを自ら配置します。ガイドの DOM サンプルには、コンテンツに重なる専用のカーソルレイヤ要素が示されており、マイグレーションドキュメント は、ネイティブのハイライトはそのままにしておくほうが問題が少ないため残していると述べています。

0.1 の告知時点では、タッチ選択が唯一の例外でネイティブのままでした。再実装するとプラットフォームのコンテキストメニューが壊れるためです。この方針はその後変化しています。changelog には、0.5.0 でネイティブ選択が到達できない位置に対するタッチ選択が、0.5.1 でインライン plot の端に追加のカーソル位置が記録されており、後者によりタッチのドラッグ選択に停止先が与えられます。告知は入力処理についても暫定的なものと位置づけています。Wordgard は composition を除くすべてで beforeinput を処理し、ProseMirror の DOM ミューテーション解析を廃止していますが、これは実環境でのテストを待っている状態です。ブラウザのサポートマトリクスは公開されていません。

ProseMirror、TipTap、Lexical と並べた Wordgard

ProseMirrorTipTapLexicalWordgard
変更モデル順序付きステップProseMirror を継承独自モデルセクションベースの単一 change
コンテンツの形状正規表現的なコンテンツ式ProseMirror を継承独自モデル子型の集合 + corrections
設定プラグインProseMirror プラグイン上の拡張独自モデル値ごとの優先度を持つ facet 拡張
選択範囲ブラウザネイティブブラウザネイティブ独自モデルライブラリ描画のカーソル、タッチはネイティブ

TipTap は ProseMirror の上に載るフレームワーク層でありそのコアモデルを継承しています。Lexical は Meta による独立したエディタフレームワークです。いずれも Wordgard とインターフェースを共有していません。

待つべきなのは誰か。現時点では大半のチームです。Wordgard は 0.1.0 として初めてリリースされ、npm 上のパッケージ はそれ以降いくつかのリリースを重ねています。changelog の最新エントリは 2026 年 9 月 6 日付の 0.5.2 で、changelog には 0.2.0、0.3.0、0.4.0、0.5.0 での破壊的変更が記録されています。作者は公開インターフェースの一部を再考する見込みで、おそらく 1 年以上 0.x にとどまるとしています。ProseMirror からのアップグレードパスはありません。プロジェクトの Migrating from ProseMirror ドキュメントは、各 ProseMirror パッケージを Wordgard のモジュールに対応付けたうえで、インターフェースの互換性は一切試みていないと明記しています。

総評

Wordgard は、ProseMirror の系譜に連なるエディタとして初めて、ステップ、順序付きコンテンツ式、ブラウザ任せの選択範囲を 1 つの設計の中で捨て去りました。注目に値するのは作者の名前ではなく、この 3 つの決断です。ProseMirror ベースのプロダクトを運用しているなら、マイグレーションドキュメントとガイドの Changes および Corrections のセクションを読み、扱いに困っているスキーマ不変条件を 1 つ correction としてプロトタイプしてみてください。その実験は、どんな機能一覧よりも適合性について多くを教えてくれるはずです。

FAQ

Wordgard には共同編集が含まれていますか。それともサーバーを自前で構築する必要がありますか。

Wordgard は wordgard/collab にクライアントサイドの共同編集拡張を同梱していますが、サーバーは含みません。collab() 拡張は未確定のローカル変更を追跡し、collab.sendableUpdate と collab.receive が、あなたが実装する中央権威との間で更新をやり取りします。また collab.transformUpdate(0.2.0 で追加)により、そのサーバーは古くなった更新をリベースできます。corrections はリモートのトランザクションをスキップするため、クライアント設定とサーバー側の変換には、同じ順序で並べた同じ corrections を与えてください。

Wordgard における plot と leaf の違いは何ですか。

plot は段落、リスト、テーブル、ドキュメントのようにコンテンツを持つノードです。leaf はテキスト、画像、改行のようにコンテンツを持たないノードです。両者は Plot と Leaf という別々のクラスであり、isPlot と isLeaf プロパティによって TypeScript 上で型を絞り込めます。leaf はそれ自体がタグ(型、パラメータ、マーク)である一方、plot はタグに加えてコンテンツ配列を保持します。

ブラウザ外、たとえば Node で Wordgard のドキュメントを作成・変更できますか。

ドキュメントモデルについては可能ですが、エディタについては不可能です。wordgard/doc、wordgard/state、wordgard/types の各モジュールは DOM なしで動作するよう設計されているため、サーバーサイドでドキュメントの構築、change set の適用、corrections の実行、JSON へのシリアライズが行えます。wordgard/types は wordgard/doc にのみ依存します。wordgard/editor はブラウザ外でも読み込めますが有用な動作はせず、HTML 文字列のドキュメントにはブラウザのパーサーが必要なので、JSON を渡すか jsdom を使ってください。

Wordgard はテーブルをサポートしていますか。また、どのように矩形を保っていますか。

サポートしています。wordgard/table モジュールは tables() 拡張バンドルをエクスポートし、テーブルのスキーマ要素、セルの矩形領域を選択するための CellSelection 型、貼り付けおよびドロップのハンドラ、テーブルメニュー、そしてセルがきれいな矩形に揃わないテーブルを修復する組み込みの correction である tables.correction を追加します。オプションは headerCells、cellSpanning、cellContent(inline または block)です。結合セルは RowSpan と ColSpan マークを使います。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.