Quillon.Transform.Position (Quillon v0.3.0)

Copy Markdown View Source

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()

@type any_node() :: {atom(), map(), list()}

Any node tuple, including a custom type a consumer has introduced

children()

@type children() :: [any_node()]

List of inline children, which need not all be text nodes

position()

@type position() :: {non_neg_integer(), non_neg_integer()}

Position as {node_index, offset_within_node}

text_node()

@type text_node() :: {:text, %{text: String.t(), marks: list()}, []}

A text node tuple

Functions

node_length(node)

@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

nodes_fully_in_range(children, start_offset, end_offset)

@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: []}, []}
]

nodes_in_range(children, start_offset, end_offset)

@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: []}, []}
]

offset_to_position(children, offset)

@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}

own_flow?(arg1)

@spec own_flow?(any_node()) :: boolean()

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, %{}, []})
false

A childless node declares itself addressable:

iex> Quillon.Transform.Position.own_flow?({:footnote_ref, %{atomic: true}, []})
true

So does a node whose blocks the library cannot recognise:

iex> card = {:card, %{atomic: true}, [{:my_block, %{}, []}]}
iex> Quillon.Transform.Position.own_flow?(card)
true

position_to_offset(children, arg)

@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

total_length(children)

@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