PhiaUi.Editor.OtEngine (phia_ui v0.1.17)

Copy Markdown View Source

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 forward n characters (keep them unchanged)
  • %{insert: text} — insert text at the current position
  • %{delete: n} — delete n characters at the current position

Core Functions

  • apply/2 — apply a delta to a document string
  • transform/3 — transform concurrent deltas (OT core)
  • compose/2 — merge two sequential deltas into one
  • invert/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(doc, delta)

@spec apply(String.t(), list()) :: {:ok, String.t()} | {:error, String.t()}

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(delta_a, delta_b)

@spec compose(list(), list()) :: {:ok, list()}

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"}]

delete(n)

@spec delete(pos_integer()) :: %{delete: pos_integer()}

Create a delete operation.

insert(text)

@spec insert(String.t()) :: %{insert: String.t()}

Create an insert operation.

invert(delta, doc)

@spec invert(list(), String.t()) :: {:ok, list()}

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"}]

length(delta)

@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

retain(n)

@spec retain(pos_integer()) :: %{retain: pos_integer()}

Create a retain operation.

transform(op_a, op_b, priority)

@spec transform(list(), list(), :left | :right) :: {:ok, list()}

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"}]