lib/philtre/block/content_editable/clean_empty_cells.ex

defmodule Philtre.Block.ContentEditable.CleanEmptyCells do
  @moduledoc """
  Cleans up empty cells from a block.

  Various block actions will result in empty cells being left over. These are
  redundant and can be removed without any consequences, resulting in a simpler
  html.
  """
  alias Philtre.Block.ContentEditable
  alias Philtre.Block.ContentEditable.Cell
  alias Philtre.Block.ContentEditable.Selection

  @doc """
  Removes empty cells from a block.

  Since a new block must contain an empty cell, that one is not cleaned up in
  this scenario.

  If block selection starts and/or ends within a cleaned up cell, that edge of
  selection is moved into the next appropriate cell.
  """
  @spec call(ContentEditable.t()) :: ContentEditable.t()
  def call(%ContentEditable{cells: []} = block), do: block
  def call(%ContentEditable{cells: [_]} = block), do: block

  def call(%ContentEditable{} = block) do
    {new_cells, new_selection} = clean([], block.cells, block.selection)
    %{block | cells: new_cells, selection: new_selection}
  end

  @spec clean(
          list(Cell.t()),
          list(Cell.t()),
          Selection.t() | nil
        ) :: {list(Cell.t()), Selection.t() | nil}
  defp clean(cleaned, [%Cell{} = current, %Cell{} = next | rest], selection_or_nil) do
    if current.text === "" do
      selection = move_selection(selection_or_nil, current, next)
      clean(cleaned, [next] ++ rest, selection)
    else
      clean(cleaned ++ [current], [next] ++ rest, selection_or_nil)
    end
  end

  defp clean(cleaned, uncleaned, selection_or_nil), do: {cleaned ++ uncleaned, selection_or_nil}

  @spec move_selection(Selection.t() | nil, Cell.t(), Cell.t()) :: Selection.t()
  defp move_selection(nil, %Cell{}, %Cell{}), do: nil
  defp move_selection(%Selection{start_id: nil} = selection, %Cell{}, %Cell{}), do: selection

  defp move_selection(%Selection{} = selection, %Cell{} = cleaned_cell, %Cell{} = new_cell) do
    {new_start_id, new_start_offset} =
      if selection.start_id == cleaned_cell.id do
        {new_cell.id, 0}
      else
        {selection.start_id, selection.start_offset}
      end

    {new_end_id, new_end_offset} =
      if selection.end_id == cleaned_cell.id do
        {new_cell.id, 0}
      else
        {selection.end_id, selection.end_offset}
      end

    %Selection{
      start_id: new_start_id,
      start_offset: new_start_offset,
      end_id: new_end_id,
      end_offset: new_end_offset
    }
  end
end