Editing a document through a selection.
Quillon.Commands works on one block and two integers - the caller has to know
which block and where. This module takes a whole document and a
Quillon.Selection instead, resolves the selection, and applies the existing
command to the right block.
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> selection = Quillon.Selection.text(
...> Quillon.Selection.point([0], 0),
...> Quillon.Selection.point([0], 5)
...> )
iex> {:ok, doc, _selection} = Quillon.Edit.apply_mark(doc, selection, :bold)
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p><strong>Hello</strong></p></div>)Every function returns the new selection
An edit moves the text under the caret, so each function answers with
{:ok, document, selection} and the caret follows what it just did - after
split_block/2 it sits at the start of the new block, after insert_block/3
inside the inserted one.
This is not general position mapping. A selection held by another user is not rebased by these functions; that arrives with the collaboration layer, which is what decides how positions are tracked across concurrent edits.
Errors
:invalid_path- the selection points at a node that is not there:out_of_range- an offset falls outside its block:cross_block- a text selection spanning two blocks. Representable, but not yet acted on:unsupported_block- the block is not one the transform layer edits, which today means anything but a paragraph or heading
Summary
Functions
Apply a mark across the selection.
Delete what the selection covers, leaving the caret where it started.
Whether every text node in the selection carries a mark.
Insert a block after the one holding the caret, and put the caret in it.
Remove a mark across the selection.
Split the block at a collapsed caret into two, and put the caret at the start of the second.
Toggle a mark across the selection.
Types
@type error() :: :invalid_path | :out_of_range | :cross_block | :unsupported_block
Why an edit could not be applied
@type result() :: {:ok, tuple(), Quillon.Selection.t()} | {:error, error()}
The result of an edit
Functions
Apply a mark across the selection.
The selection is returned unchanged - marking text does not move it.
Examples
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> selection = Quillon.Selection.text(
...> Quillon.Selection.point([0], 0),
...> Quillon.Selection.point([0], 2)
...> )
iex> {:ok, doc, _selection} = Quillon.Edit.apply_mark(doc, selection, :bold)
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p><strong>He</strong>llo</p></div>)
@spec delete_selection(tuple(), Quillon.Selection.t()) :: result()
Delete what the selection covers, leaving the caret where it started.
A node selection removes the node outright.
Examples
iex> doc = Quillon.document([Quillon.paragraph("Hello world")])
iex> selection = Quillon.Selection.text(
...> Quillon.Selection.point([0], 5),
...> Quillon.Selection.point([0], 11)
...> )
iex> {:ok, doc, selection} = Quillon.Edit.delete_selection(doc, selection)
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p>Hello</p></div>)
iex> Quillon.Selection.Point.offset(selection.head)
5
iex> doc = Quillon.document([Quillon.paragraph("One"), Quillon.divider()])
iex> {:ok, doc, _selection} = Quillon.Edit.delete_selection(doc, Quillon.Selection.node([1]))
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p>One</p></div>)
@spec has_mark?(tuple(), Quillon.Selection.t(), atom()) :: boolean()
Whether every text node in the selection carries a mark.
Examples
iex> doc = Quillon.document([{:paragraph, %{}, [Quillon.text("Hello", [:bold])]}])
iex> selection = Quillon.Selection.text(
...> Quillon.Selection.point([0], 0),
...> Quillon.Selection.point([0], 5)
...> )
iex> Quillon.Edit.has_mark?(doc, selection, :bold)
true
@spec insert_block(tuple(), Quillon.Selection.t(), tuple()) :: result()
Insert a block after the one holding the caret, and put the caret in it.
The containing block is not split, so inserting mid-paragraph places the new
block after that whole paragraph. Use split_block/2 first to break the
paragraph at the caret.
Examples
iex> doc = Quillon.document([Quillon.paragraph("One")])
iex> {:ok, doc, selection} =
...> Quillon.Edit.insert_block(doc, Quillon.Selection.cursor([0], 3), Quillon.paragraph("Two"))
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p>One</p><p>Two</p></div>)
iex> Quillon.Selection.Point.path(selection.head)
[1]
@spec remove_mark(tuple(), Quillon.Selection.t(), atom()) :: result()
Remove a mark across the selection.
Examples
iex> doc = Quillon.document([{:paragraph, %{}, [Quillon.text("Hello", [:bold])]}])
iex> selection = Quillon.Selection.text(
...> Quillon.Selection.point([0], 0),
...> Quillon.Selection.point([0], 5)
...> )
iex> {:ok, doc, _selection} = Quillon.Edit.remove_mark(doc, selection, :bold)
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p>Hello</p></div>)
@spec split_block(tuple(), Quillon.Selection.t()) :: result()
Split the block at a collapsed caret into two, and put the caret at the start of the second.
Both halves keep the original block's type and attributes.
Examples
iex> doc = Quillon.document([Quillon.paragraph("HelloWorld")])
iex> {:ok, doc, selection} = Quillon.Edit.split_block(doc, Quillon.Selection.cursor([0], 5))
iex> Quillon.to_html(doc)
~s(<div class="quillon"><p>Hello</p><p>World</p></div>)
iex> Quillon.Selection.Point.path(selection.head)
[1]
Toggle a mark across the selection.
Examples
iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> selection = Quillon.Selection.text(
...> Quillon.Selection.point([0], 0),
...> Quillon.Selection.point([0], 5)
...> )
iex> {:ok, bolded, selection} = Quillon.Edit.toggle_mark(doc, selection, :bold)
iex> {:ok, plain, _selection} = Quillon.Edit.toggle_mark(bolded, selection, :bold)
iex> Quillon.to_html(plain)
~s(<div class="quillon"><p>Hello</p></div>)