Pure Elixir Operational Transform (OT) engine for collaborative text editing.
Implements the core OT algorithms for plain-text documents using a delta-based format compatible with Quill Delta / ShareDB. Each delta is a list of operations that describe a document transformation:
%{retain: n}— skip forwardncharacters (keep them unchanged)%{insert: text}— inserttextat the current position%{delete: n}— deletencharacters at the current position
Core Functions
apply/2— apply a delta to a document stringtransform/3— transform concurrent deltas (OT core)compose/2— merge two sequential deltas into oneinvert/2— produce an undo delta from the original document
Example
iex> doc = "Hello World"
iex> delta = [%{retain: 5}, %{insert: ","}, %{retain: 6}]
iex> {:ok, result} = PhiaUi.Editor.OtEngine.apply(doc, delta)
iex> result
"Hello, World"Transform Example
iex> # User A inserts "X" at position 0, User B inserts "Y" at position 0
iex> op_a = [%{insert: "X"}]
iex> op_b = [%{insert: "Y"}]
iex> {:ok, b_prime} = PhiaUi.Editor.OtEngine.transform(op_a, op_b, :left)
iex> b_prime
[%{retain: 1}, %{insert: "Y"}]
Summary
Functions
Apply a delta to a document string, producing the transformed document.
Compose two sequential deltas into a single equivalent delta.
Create a delete operation.
Create an insert operation.
Produce an inverted delta that undoes the given delta when applied to the document that results from applying the original delta.
Compute the input length a delta operates on (sum of retain + delete).
Create a retain operation.
Transform delta B against delta A so that B can be applied after A.
Functions
Apply a delta to a document string, producing the transformed document.
Returns {:ok, new_string} on success, or {:error, reason} if the delta
does not match the document length.
Example
iex> PhiaUi.Editor.OtEngine.apply("abcdef", [%{retain: 3}, %{delete: 2}, %{insert: "XY"}, %{retain: 1}])
{:ok, "abcXYf"}
Compose two sequential deltas into a single equivalent delta.
If delta A transforms document D into D', and delta B transforms D' into D'',
then compose(A, B) produces a delta C that transforms D directly into D''.
Returns {:ok, composed}.
Example
iex> a = [%{insert: "abc"}]
iex> b = [%{retain: 1}, %{delete: 1}, %{retain: 1}]
iex> {:ok, composed} = PhiaUi.Editor.OtEngine.compose(a, b)
iex> composed
[%{insert: "ac"}]
@spec delete(pos_integer()) :: %{delete: pos_integer()}
Create a delete operation.
Create an insert operation.
Produce an inverted delta that undoes the given delta when applied to the document that results from applying the original delta.
Given {:ok, new_doc} = apply(doc, delta), then
{:ok, inverted} = invert(delta, doc) satisfies
apply(new_doc, inverted) == {:ok, doc}.
Returns {:ok, inverted_delta}.
Example
iex> doc = "Hello"
iex> delta = [%{delete: 5}, %{insert: "Bye"}]
iex> {:ok, inverted} = PhiaUi.Editor.OtEngine.invert(delta, doc)
iex> inverted
[%{delete: 3}, %{insert: "Hello"}]
@spec length(list()) :: non_neg_integer()
Compute the input length a delta operates on (sum of retain + delete).
Insert operations do not consume input characters, so they are excluded.
Example
iex> PhiaUi.Editor.OtEngine.length([%{retain: 5}, %{insert: "hi"}, %{delete: 3}])
8
@spec retain(pos_integer()) :: %{retain: pos_integer()}
Create a retain operation.
Transform delta B against delta A so that B can be applied after A.
Given two concurrent operations A and B (both based on the same document),
transform(A, B, priority) produces B' such that:
apply(apply(doc, A), B') == apply(apply(doc, B), A')The priority parameter (:left or :right) resolves ties when both
operations insert at the same position:
:left— A's insert goes first (B' gets a retain to skip over A's insert):right— B's insert goes first
Returns {:ok, transformed_b}.
Example
iex> op_a = [%{retain: 3}, %{insert: "X"}]
iex> op_b = [%{retain: 3}, %{insert: "Y"}]
iex> {:ok, b_prime} = PhiaUi.Editor.OtEngine.transform(op_a, op_b, :left)
iex> b_prime
[%{retain: 4}, %{insert: "Y"}]