Position mapping between flat offsets and node paths.
Converts between:
- Flat offset: single integer counting characters from start
- Node position:
{node_index, offset_within_node}tuple
Summary
Types
Any node tuple, including a custom type a consumer has introduced
List of inline children, which need not all be text nodes
Position as {node_index, offset_within_node}
A text node tuple
Functions
Get the text length of a single node.
Get nodes fully contained within a given offset range.
Get all nodes that overlap with a given offset range.
Convert a flat offset to a node position {node_index, offset_within_node}.
Whether a node holds its own text flow rather than joining its parent's.
Convert a node position to a flat offset.
Calculate the total text length of a list of nodes.
Types
Any node tuple, including a custom type a consumer has introduced
@type children() :: [any_node()]
List of inline children, which need not all be text nodes
@type position() :: {non_neg_integer(), non_neg_integer()}
Position as {node_index, offset_within_node}
A text node tuple
Functions
@spec node_length(any_node()) :: non_neg_integer()
Get the text length of a single node.
A node whose children are inline is transparent to offset math - it contributes the combined length of those children, so the text inside it stays addressable. A node whose children are blocks holds its own flow and occupies exactly one position in its parent, so the text inside it is addressed separately rather than as part of the surrounding sentence.
A node with no children measures 0 unless it declares atomic: true, which
gives it one position and makes it selectable.
See own_flow?/1 for how these are told apart.
Examples
iex> Quillon.Transform.Position.node_length({:text, %{text: "Hello", marks: []}, []})
5
iex> Quillon.Transform.Position.node_length({:text, %{text: "", marks: []}, []})
0
iex> Quillon.Transform.Position.node_length({:line, %{page: 2}, [Quillon.text("Hi")]})
2
iex> footnote = {:footnote, %{}, [Quillon.paragraph("See Rabuya, p. 412.")]}
iex> Quillon.Transform.Position.node_length(footnote)
1
@spec nodes_fully_in_range(children(), non_neg_integer(), non_neg_integer()) :: children()
Get nodes fully contained within a given offset range.
A node is fully contained if its entire text falls within [start_offset, end_offset].
Examples
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " ", marks: []}, []},
...> {:text, %{text: "world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.nodes_fully_in_range(children, 5, 6)
[{:text, %{text: " ", marks: []}, []}]
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " ", marks: []}, []},
...> {:text, %{text: "world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.nodes_fully_in_range(children, 0, 11)
[
{:text, %{text: "Hello", marks: []}, []},
{:text, %{text: " ", marks: []}, []},
{:text, %{text: "world", marks: []}, []}
]
@spec nodes_in_range(children(), non_neg_integer(), non_neg_integer()) :: children()
Get all nodes that overlap with a given offset range.
A node overlaps if any part of it falls within [start_offset, end_offset).
Examples
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " ", marks: []}, []},
...> {:text, %{text: "world", marks: [:bold]}, []}
...> ]
iex> Quillon.Transform.Position.nodes_in_range(children, 5, 6)
[{:text, %{text: " ", marks: []}, []}]
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.nodes_in_range(children, 3, 8)
[
{:text, %{text: "Hello", marks: []}, []},
{:text, %{text: " world", marks: []}, []}
]
@spec offset_to_position(children(), non_neg_integer()) :: position()
Convert a flat offset to a node position {node_index, offset_within_node}.
If offset is at a node boundary, returns the end of the previous node. If offset is beyond content, returns the end of the last node.
Examples
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.offset_to_position(children, 0)
{0, 0}
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.offset_to_position(children, 3)
{0, 3}
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.offset_to_position(children, 7)
{1, 2}
Whether a node holds its own text flow rather than joining its parent's.
A block cannot sit inside an inline text run - that is what makes it a block - so a node with any block-level child holds a flow of its own. A node whose children are inline is part of the surrounding sentence. This is derived from the document, so a consumer introducing a custom node type gets the right behaviour without registering anything.
A node with no children has nothing to infer from, and a node holding custom
blocks reads as transparent because an unregistered type is not a known block
type. Both are settled by an atomic: true attribute, which declares the
answer and takes precedence over the inference. An atomic node occupies
exactly one position in its parent, so a marker can be selected rather than
being zero-width and unaddressable.
Examples
iex> Quillon.Transform.Position.own_flow?({:footnote, %{}, [Quillon.paragraph("x")]})
true
iex> Quillon.Transform.Position.own_flow?({:line, %{page: 2}, [Quillon.text("Hi")]})
false
iex> Quillon.Transform.Position.own_flow?({:pagebreak, %{}, []})
falseA childless node declares itself addressable:
iex> Quillon.Transform.Position.own_flow?({:footnote_ref, %{atomic: true}, []})
trueSo does a node whose blocks the library cannot recognise:
iex> card = {:card, %{atomic: true}, [{:my_block, %{}, []}]}
iex> Quillon.Transform.Position.own_flow?(card)
true
@spec position_to_offset(children(), position()) :: non_neg_integer()
Convert a node position to a flat offset.
Examples
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.position_to_offset(children, {0, 3})
3
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.position_to_offset(children, {1, 2})
7
@spec total_length(children()) :: non_neg_integer()
Calculate the total text length of a list of nodes.
Examples
iex> children = [
...> {:text, %{text: "Hello", marks: []}, []},
...> {:text, %{text: " world", marks: []}, []}
...> ]
iex> Quillon.Transform.Position.total_length(children)
11
iex> Quillon.Transform.Position.total_length([])
0