Packages

Simple, yet expressive format to describe contents and changes

Current section

Files

Jump to
delta lib delta.ex
Raw

lib/delta.ex

defmodule Delta do
alias Delta.{Attr, Op}
def get_handler!(embed_type) do
:delta
|> Application.get_env(:custom_embeds, [])
|> Enum.find(&(&1.name == embed_type))
|> case do
nil -> raise("no embed handler configured for #{inspect(embed_type)}")
handler -> handler
end
end
def compose(left, right) do
[] |> do_compose(left, right) |> chop() |> Enum.reverse()
end
def compose_all(deltas) do
Enum.reduce(deltas, [], &compose(&2, &1))
end
defp do_compose(result, [], []), do: result
defp do_compose(result, [], [op | delta]) do
Enum.reverse(delta, push(result, op))
end
defp do_compose(result, [op | delta], []) do
Enum.reverse(delta, push(result, op))
end
defp do_compose(result, [op1 | delta1], [op2 | delta2]) do
{op, delta1, delta2} =
cond do
Op.insert?(op2) ->
{op2, [op1 | delta1], delta2}
Op.delete?(op1) ->
{op1, delta1, [op2 | delta2]}
true ->
{composed, op1, op2} = Op.compose(op1, op2)
delta1 = push(delta1, op1)
delta2 = push(delta2, op2)
{composed, delta1, delta2}
end
result
|> push(op)
|> do_compose(delta1, delta2)
end
defp chop([%{"retain" => n} = op | delta]) when is_number(n) and map_size(op) == 1, do: delta
defp chop(delta), do: delta
def compact(delta) do
delta
|> Enum.reduce([], &push(&2, &1))
|> Enum.reverse()
end
def concat(left, []), do: left
def concat([], right), do: right
def concat(left, [first | right]) do
left =
left
|> Enum.reverse()
|> push(first)
|> Enum.reverse()
left ++ right
end
def push(delta, false), do: delta
def push([], op) do
if Op.size(op) > 0, do: [op], else: []
end
# Adds op to the beginning of delta (we expect a reverse)
# TODO: Handle inserts after delete (should move insert before delete)
def push(delta, op) do
[last_op | partial_delta] = delta
merged_op = do_push(last_op, op)
if is_nil(merged_op) do
[op | delta]
else
[merged_op | partial_delta]
end
end
defp do_push(op, %{"delete" => 0}), do: op
defp do_push(op, %{"insert" => ""}), do: op
defp do_push(op, %{"retain" => 0}), do: op
defp do_push(%{"delete" => left}, %{"delete" => right}) do
Op.delete(left + right)
end
defp do_push(%{"retain" => left, "attributes" => attr}, %{
"retain" => right,
"attributes" => attr
})
when is_integer(left) and is_integer(right) do
Op.retain(left + right, attr)
end
defp do_push(%{"retain" => left} = last_op, %{"retain" => right} = op)
when map_size(last_op) == 1 and map_size(op) == 1 and
is_integer(left) and is_integer(right) do
Op.retain(left + right)
end
defp do_push(%{"insert" => left, "attributes" => attr}, %{
"insert" => right,
"attributes" => attr
})
when is_bitstring(left) and is_bitstring(right) do
Op.insert(left <> right, attr)
end
defp do_push(%{"insert" => left} = last_op, %{"insert" => right} = op)
when is_bitstring(left) and is_bitstring(right) and map_size(last_op) == 1 and
map_size(op) == 1 do
Op.insert(left <> right)
end
defp do_push(_, _), do: nil
def size(delta) do
Enum.reduce(delta, 0, fn op, sum ->
sum + Op.size(op)
end)
end
def slice(delta, index, len) do
{_left, right} = split(delta, index)
{middle, _rest} = split(right, len)
middle
end
def slice_max(delta, index, len) do
{_left, right} = split(delta, index, align: true)
{middle, _rest} = split(right, len, align: true)
middle
end
def split(delta, index, opts \\ [])
def split(delta, 0, _), do: {[], delta}
def split(delta, index, opts) when is_integer(index) do
do_split(
[],
delta,
fn op, index ->
op_size = Op.size(op)
if index <= op_size do
index
else
{:cont, index - op_size}
end
end,
index,
opts
)
end
def split(delta, func, opts) when is_function(func) do
do_split([], delta, func, nil, opts)
end
defp do_split(passed, [], _, _, _), do: {passed, []}
defp do_split(passed, remaining, func, context, opts) when is_function(func, 1) do
do_split(passed, remaining, fn op, _ -> func.(op) end, context, opts)
end
defp do_split(passed, remaining, func, context, opts) when is_function(func, 2) do
[first | remaining] = remaining
case func.(first, context) do
:cont ->
do_split([first | passed], remaining, func, context, opts)
{:cont, context} ->
do_split([first | passed], remaining, func, context, opts)
index ->
case Op.take(first, index, opts) do
{false, right} ->
{Enum.reverse(passed), [right | remaining]}
{left, false} ->
{Enum.reverse([left | passed]), remaining}
{left, right} ->
{Enum.reverse([left | passed]), [right | remaining]}
end
end
end
def transform(_, _, priority \\ false)
def transform(index, delta, priority) when is_integer(index) do
do_transform(0, index, delta, priority)
end
def transform(left, right, priority) do
delta = do_transform([], left, right, priority)
delta |> chop() |> Enum.reverse()
end
defp do_transform(offset, index, _, _) when is_integer(index) and offset > index, do: index
defp do_transform(_, index, [], _) when is_integer(index), do: index
defp do_transform(offset, index, [%{"delete" => length} | delta], priority)
when is_integer(index) do
do_transform(offset, index - min(length, index - offset), delta, priority)
end
defp do_transform(offset, index, [op | delta], priority) when is_integer(index) do
{offset, index} = Op.transform(offset, index, op, priority)
do_transform(offset, index, delta, priority)
end
defp do_transform(result, [], [], _), do: result
defp do_transform(result, [], [op | delta], priority) do
do_transform(result, [Op.retain(Op.size(op))], [op | delta], priority)
end
defp do_transform(result, [op | delta], [], priority) do
do_transform(result, [op | delta], [Op.retain(Op.size(op))], priority)
end
defp do_transform(result, [op1 | delta1], [op2 | delta2], priority) do
{op, delta1, delta2} =
cond do
Op.insert?(op1) and (priority or not Op.insert?(op2)) ->
{Op.retain(Op.size(op1)), delta1, [op2 | delta2]}
Op.insert?(op2) ->
{op2, [op1 | delta1], delta2}
true ->
{transformed, op1, op2} = Op.transform(op1, op2, priority)
delta1 = push(delta1, op1)
delta2 = push(delta2, op2)
{transformed, delta1, delta2}
end
result
|> push(op)
|> do_transform(delta1, delta2, priority)
end
def invert(change, base) do
change
|> Enum.reduce({[], 0}, fn op, {inverted, base_index} ->
length = Op.size(op)
cond do
Op.insert?(op) ->
inverted = push(inverted, Op.delete(length))
{inverted, base_index}
Op.retain?(op, :number) && !Op.has_attributes?(op) ->
inverted = push(inverted, Op.retain(length))
{inverted, base_index + length}
Op.retain?(op, :number) || Op.delete?(op) ->
inverted =
base
|> slice(base_index, length)
|> Enum.reduce(inverted, &do_invert_slice(op, &1, &2))
{inverted, base_index + length}
# Delegate to the embed handler when change op is an embed
Op.retain?(op, :map) ->
base_op =
base
|> slice(base_index, length)
|> hd
{embed_type, embed1, embed2} = Op.get_embed_data!(op["retain"], base_op["insert"])
handler = get_handler!(embed_type)
embed = %{embed_type => handler.invert(embed1, embed2)}
attrs = Attr.invert(op["attributes"], base_op["attributes"])
inverted = push(inverted, Op.retain(embed, attrs))
{inverted, base_index + 1}
true ->
{inverted, base_index}
end
end)
|> elem(0)
|> chop()
|> Enum.reverse()
end
defp do_invert_slice(op, base_op, inverted) do
cond do
Op.delete?(op) ->
push(inverted, base_op)
Op.retain?(op) && Op.has_attributes?(op) ->
attrs = Attr.invert(op["attributes"], base_op["attributes"])
retain_op = base_op |> Op.size() |> Op.retain(attrs)
push(inverted, retain_op)
true ->
inverted
end
end
end