エディタ状態を文書として保存しない
Notion のようにブロック単位で編集する文書モデルだ。ただし React や ProseMirror をコアに入れず、文書構造と編集ルールだけを TypeScript で実装した。UI は sdui-document-react、SDUI 描画は sdui-template が担当する。
エディタの内部状態がそのまま保存データになると、サーバー検証や UI の交換が難しくなる。そこで「文書はエディタより長く残るデータである」という基準を最初に置いた。
Markdown ↔ block document → patch 適用 → SDUI または React で描画

同じブロック文書を読み取り専用 Notion テーマで描画した Storybook catalog。
配列 index ではなく block anchor を使う
挿入、更新、移動、分割、結合を一つの patch 形式で表す。配列 index は前の編集一つでずれるため、ブロック ID と before・after anchor で位置を決める。無効な anchor を見つけた場合は、勝手な位置に置かずエラーにする。
patch を一つ適用するコード
import {
applyDocumentPatch,
createDocumentBlock,
} from "@lodado/sdui-document";
const next = applyDocumentPatch(content, {
type: "block.insert",
parentId: "root",
after: "problem",
block: createDocumentBlock({
id: "decision",
type: "document.callout",
state: { text: "文書はエディタより長く残る。" },
}),
});
// content は変更せず、next に新しい文書を返す。
applyDocumentPatch は原本を変更せず、新しい文書を返す。同じルールをブラウザとサーバーで使え、autosave と undo/redo も同じ patch 契約の上で動く。
設計思想
文書はエディタの内部状態ではない
公開モデルには ProseMirror transaction、DOM selection、React state を含めない。編集 UI を変えても文書構造と保存データは残るべきだからだ。このパッケージは「文書とは何か」だけを定義する。
すべての編集を patch で表す
挿入・更新・削除・移動を直接 mutation せず、patch として残す。同じ patch を optimistic UI、autosave、undo/redo、競合処理で再利用できる。変更をデータとして扱うことで、保存前後の流れもテストしやすくなる。
まずブロック単位の問題を解く
文書構造、ブロック移動、権限、保存状態を先に安定させた。文字単位 CRDT と複雑な inline 編集は、実際の要件が出たときに追加する。最初からすべての rich text 問題を抱えないことも、このパッケージの範囲だ。
副作用は adapter の外に置く
patch 適用、権限判定、autosave 状態は pure function と reducer で作った。DB、ファイル保存、検索、ネットワークは interface だけを提供し、実装しない。そのためブラウザ・サーバー・worker で同じルールを使える。
クライアント権限はセキュリティではない
クライアント側の権限判定は、操作ボタンを隠したり読み取り専用 UI を作ったりするためのものだ。実際の保存リクエストでは、サーバー adapter が同じ権限をもう一度確認しなければならない。
まだ入れていないもの
DB と検索の実装は adapter の外に置き、リアルタイム共同編集用 CRDT もまだ入れていない。要件が具体化するまでは、ブロック単位 patch を単純に保つ。
/about の履歴書は、この文書モデルと React エディタを実際に使って作成・描画している。