Rich text / foldkit-richtext
Transforms
Normalization runs as a registry of transforms. Each is a pure function of the document plus what the transaction touched, and returns the new document with the position steps and identity bookkeeping that go with it:
const DropEmptyRuns: RichText.Transform = {
name: 'dropEmptyRuns',
apply: (document, { dirtyNodes }) => ({ document: next, steps, removedNodes, dirtyNodes, textChanged }),
}
RichText.apply(state, transaction, [DropEmptyRuns])
RichText.apply(state, transaction) // defaultTransforms: mergeAdjacentRunsTwo rules make a loop over them safe: a transform is deterministic and
idempotent on normalized state, so the loop knows it has settled when a pass
changes nothing. A pass that keeps changing the document is refused with
UnstableNormalization after MAX_NORMALIZATION_PASSES rather than spinning.
Three constraints follow from replay and from positions:
- A transform cannot mint identities. It may merge, move, or remove, never create — so "ensure an empty Document has a Paragraph" belongs to a command that mints, not to a transform.
- Removing a run that positions address requires a step. Report a
RelocateStep(or a collapse) for it, or the selection can be left dangling. - Only what the transaction touched is in scope.
dirtyNodesis how a transform stays incremental; the shipped merge uses it and leaves every other block alone.
apply folds a transform's report into the same ChangeSet and positionMap
it was already building, and maps the selection through the transform's steps,
so a caller cannot tell whether a change came from an operation or from
normalization.