rupertlssmith / rte-toolkit-patch / RichText.Node

This module contains convenience functions for working with Block and Inline nodes.

Insert / Replace

insertAfter : RichText.Model.Node.Path -> Fragment -> RichText.Model.Node.Block -> Result String RichText.Model.Node.Block

Inserts the fragments after the node at the given path and returns the result. Returns an error if the path is invalid or the fragment cannot be inserted.

insertAfter [ 0, 0 ] fragment root
--> Inserts the fragment after the node at path [0, 0]

insertBefore : RichText.Model.Node.Path -> Fragment -> RichText.Model.Node.Block -> Result String RichText.Model.Node.Block

Inserts the fragments before the node at the given path and returns the result. Returns an error if the path is invalid or the fragment cannot be inserted.

insertBefore [ 0, 0 ] fragment root
--> Inserts the fragment before the node at path [0, 0]

replace : RichText.Model.Node.Path -> Node -> RichText.Model.Node.Block -> Result String RichText.Model.Node.Block

Replaces the node at the path with the given editor node.

-- replaces the node at [0, 0] with the inline text
replace [ 0, 0 ] (Inline textNode) rootNode

replaceWithFragment : RichText.Model.Node.Path -> Fragment -> RichText.Model.Node.Block -> Result String RichText.Model.Node.Block

Returns a Ok Block that replaces the node at the node path with the given fragment. If it is unable to replace it do to an invalid path or the wrong type of node, a Err string describing the error is returned.

-- replaces the node at [0, 0] with the given inline fragment
replaceWithFragment [ 0, 0 ] (InlineFragment <| Array.fromList [ textNode ]) rootNode

Remove

removeInRange : RichText.Model.Node.Path -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> RichText.Model.Node.Block

This method removes all the nodes inclusive to both the start and end path. Note that an ancestor is not removed if the start path or end path is a child node.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])


emptyRoot : Block
emptyRoot =
    block
        (Element.element doc [])
        (blockChildren <| Array.empty)

removeInRange [0] [0] root == emptyRoot
--> True

removeNodeAndEmptyParents : RichText.Model.Node.Path -> RichText.Model.Node.Block -> RichText.Model.Node.Block

Removes the node at the given path, and recursively removes parent blocks that have no remaining child nodes, excluding the root.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <|
            Array.fromList [ removedPHtmlNode ]
        )

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode ]
        )

textNode : Inline
textNode =
    plainText "sample1"

removedRoot : Block
removedRoot =
    block
        (Element.element doc [])
        (blockChildren Array.empty)

removeNodeAndEmptyParents [0, 0] root == removedRoot
--> True

Predicates

allRange : (Node -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Basics.Bool

Determine if all elements in range satisfy some test.

-- Query to determine if all the elements in range are selectable
allRange isSelectable [ 0, 0 ] [ 0, 2 ] root

anyRange : (Node -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Basics.Bool

Determine if any elements in range satisfy some test.

-- Query to determine if any elements in range are selectable
allRange isSelectable [ 0, 0 ] [ 0, 2 ] root

isEmptyTextBlock : Node -> Basics.Bool

True if this block has inline content with no children or a single empty text node, false otherwise

pNode : Block
    pNode =
        block
            (Element.element paragraph [])
            (inlineChildren <|
                Array.fromList [ emptyText ]
            )

isEmptyTextBlock pNode
--> True

selectionIsBeginningOfTextBlock : RichText.Model.Selection.Selection -> RichText.Model.Node.Block -> Basics.Bool

True if the selection is collapsed at the beginning of a text block, false otherwise.

-- selectionIsBeginningOfTextBlock is used for things like lift and join backward
if selectionIsBeginningOfTextBlock selection (State.root editorState) then
    -- Do join backward logic
else
    -- Do something else

selectionIsEndOfTextBlock : RichText.Model.Selection.Selection -> RichText.Model.Node.Block -> Basics.Bool

True if the selection is collapsed at the end of a text block, false otherwise.

-- selectionIsEndOfTextBlock is used for things like join forward
if selectionIsEndOfTextBlock selection (State.root editorState) then
    -- Do join forward logic
else
    -- Do something else

Transform

concatMap : (Node -> List Node) -> RichText.Model.Node.Block -> RichText.Model.Node.Block

Map a given function onto a block's children recursively and flatten the resulting list.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

textNode1 : Inline
textNode1 =
    plainText "sample1"

textNode2 : Inline
textNode2 =
    plainText "sample2"

doubleRoot : Block
doubleRoot =
    block
        (Element.element doc [])
        (blockChildren <|
            Array.fromList [ doublePNode, doublePNode ]
        )

doublePNode : Block
doublePNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode1, textNode2, textNode2 ]
        )

concatMap (\node -> [ node, node ]) rootNode == doubleRoot
--> True

indexedMap : (RichText.Model.Node.Path -> Node -> Node) -> Node -> Node

Same as map but the function is also applied with the path of each element (starting at []).

indexedMap
    (\path node ->
        if path == [ 0, 0 ] then
            text2

        else
            node
    )
    (Block rootNode)
--> replaces the node at [0, 0] with the text2 node

joinBlocks : RichText.Model.Node.Block -> RichText.Model.Node.Block -> Maybe RichText.Model.Node.Block

If the two blocks have the same type of children, returns the joined block. Otherwise, if the blocks have different children or one or more is a leaf node, then Nothing is return.

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

pNodeReverse : Block
pNodeReverse =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode2, textNode1 ]
        )

pNodeExpectedJoin : Block
pNodeExpectedJoin =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2, textNode2, textNode1 ]
        )

pNodeExpectedJoin == joinBlocks pNode pNodeReverse

map : (Node -> Node) -> Node -> Node

Apply a function to this node and all child nodes.

setAnnotations : String -> Node -> Node
setAnnotations mark node =
    let
        annotations =
            Set.fromList [ mark ]
    in
    case node of
        Block bn ->
            let
                params =
                    Node.element bn
            in
            Block (bn |> withElement (params |> Element.withAnnotations annotations))

        Inline il ->
            case il of
                Text tl ->
                    Inline (Text (tl |> Text.withAnnotations annotations))

                InlineElement l ->
                    let
                        params =
                            InlineElement.element l
                    in
                    Inline (InlineElement (l |> InlineElement.withElement (params |> Element.withAnnotations annotations)))

setDummyAnnotation : Node -> Node
setDummyAnnotation node =
    setAnnotations dummyAnnotation node

map setDummyAnnotation (Block rootNode)
--> Recursively adds a dummy annotation to rootNode and all its children

Searching


type alias Iterator =
RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path
, Node 
}

Type alias for a function that takes a path and a root block and returns a path and node. Useful for generalizing functions like previous and next that can iterate through a Block.

last : RichText.Model.Node.Block -> ( RichText.Model.Node.Path, Node )

Returns the last path and node in the block.

( lastPath, lastNode ) =
    last node

next : RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, Node )

Returns the next path and node, if one exists, relative to the given path.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

textNode1 : Inline
textNode1 =
    plainText "sample1"

textNode2 : Inline
textNode2 =
    plainText "sample2"

next [0, 0] rootNode == Just ([0, 1], Inline textNode2)

nodeAt : RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe Node

Returns the node at the specified path if it exists.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

nodeAt [0] rootNode == Just (Block pNode)

previous : RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, Node )

Returns the previous path and node, if one exists, relative to the given path.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

textNode1 : Inline
textNode1 =
    plainText "sample1"

textNode2 : Inline
textNode2 =
    plainText "sample2"

previous [0, 1] rootNode == Just ([0, 0], Inline textNode1)

findAncestor : (RichText.Model.Node.Block -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, RichText.Model.Node.Block )

Find ancestor from path finds the closest ancestor from the given NodePath that matches the predicate.

-- Finds the closest list item ancestor if it exists
findAncestor (\n -> Element.name (Node.element n) == "list_item")

findBackwardFrom : (RichText.Model.Node.Path -> Node -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, Node )

Starting from the given path, scans the node backward until the predicate has been met or it reaches the last node.

findBackwardFromExclusive : (RichText.Model.Node.Path -> Node -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, Node )

Starting from but excluding the given path, scans the node backward until the predicate has been met or it reaches the last node.

findClosestBlockPath : RichText.Model.Node.Path -> RichText.Model.Node.Block -> RichText.Model.Node.Path

If the node specified by the path is an inline node, returns the parent. If the node at the path is a block, then returns the same path. Otherwise if the path is invalid, returns the root path.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

textNode1 : Inline
textNode1 =
    plainText "sample1"

textNode2 : Inline
textNode2 =
    plainText "sample2"

findClosestBlockPath [0, 0] rootNode
--> [0]
findClosestBlockPath [0] rootNode
--> [0]

findForwardFrom : (RichText.Model.Node.Path -> Node -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, Node )

Starting from the given path, scans the node forward until the predicate has been met or it reaches the last node.

findForwardFromExclusive : (RichText.Model.Node.Path -> Node -> Basics.Bool) -> RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, Node )

Starting from but excluding the given path, scans the node forward until the predicate has been met or it reaches the last node.

findTextBlockNodeAncestor : RichText.Model.Node.Path -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Path, RichText.Model.Node.Block )

Returns Just the parent of the given path if the path refers to an inline node, otherwise inline content, otherwisereturn Nothing.

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

textNode1 : Inline
textNode1 =
    plainText "sample1"

textNode2 : Inline
textNode2 =
    plainText "sample2"

findTextBlockNodeAncestor [ 0, 0 ] rootNode
--> Just ( [ 0 ], pNode )

findTextBlockNodeAncestor [ 0 ] rootNode
--> Nothing ==

Folds

foldl : (Node -> b -> b) -> b -> Node -> b

Reduce a node from the top left (e.g. from first to last).

rootNode : Block
rootNode =
    block
        (Element.element doc [])
        (blockChildren <| Array.fromList [ pNode ])

pNode : Block
pNode =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1, textNode2 ]
        )

textNode1 : Inline
textNode1 =
    plainText "sample1"

textNode2 : Inline
textNode2 =
    plainText "sample2"

nodeNameOrTextValue : Node -> List String -> List String
nodeNameOrTextValue node list =
    (case node of
        Block bn ->
            Element.name (Node.element bn)

        Inline il ->
            case il of
                Text tl ->
                    text tl

                InlineElement p ->
                    Element.name (InlineElement.element p)
    )
        :: list

foldl nodeNameOrTextValue [] (Block rootNode)
-->  [ "sample2", "sample1", "paragraph", "doc" ]

foldlRange : RichText.Model.Node.Path -> RichText.Model.Node.Path -> (Node -> b -> b) -> b -> RichText.Model.Node.Block -> b

Same as foldl but only applied the nodes between the given paths, inclusive.

foldlRange [] [ 1 ] nodeNameOrTextValue [] (Block rootNode)
-->  [ "sample2", "sample1", "paragraph" ]

foldr : (Node -> b -> b) -> b -> Node -> b

Reduce a node from the bottom right (e.g. from last to first).

nodeNameOrTextValue : Node -> List String -> List String
nodeNameOrTextValue node list =
    (case node of
        Block bn ->
            Element.name (Node.element bn)

        Inline il ->
            case il of
                Text tl ->
                    text tl

                InlineElement p ->
                    Element.name (InlineElement.element p)
    )
        :: list

foldr nodeNameOrTextValue [] (Block rootNode)
--> [ "doc", "paragraph", "sample1", "sample2" ]

foldrRange : RichText.Model.Node.Path -> RichText.Model.Node.Path -> (Node -> b -> b) -> b -> RichText.Model.Node.Block -> b

Same as foldr but only applied the nodes between the given paths, inclusive.

foldlRange [ 0 ] [ 0, 1 ] nodeNameOrTextValue [] (Block rootNode)
--> [ "paragraph", "sample1", "sample2" ]

indexedFoldl : (RichText.Model.Node.Path -> Node -> b -> b) -> b -> Node -> b

Same as foldl but the reduce function also has the current node's path.

pathList : Path -> Node -> List Path -> List Path
pathList path _ list =
    path :: list

(indexedFoldl pathList [] (Block rootNode)) == [ [ 0, 1 ], [ 0, 0 ], [ 0 ], [] ]

indexedFoldr : (RichText.Model.Node.Path -> Node -> b -> b) -> b -> Node -> b

Same as foldr but the reduce function also has the current node's path.

pathList : Path -> Node -> List Path -> List Path
pathList path _ list =
    path :: list

(indexedFoldr pathList [] (Block rootNode)) == [ [], [ 0 ], [ 0, 0 ], [ 0, 1 ] ]

Split

splitBlockAtPathAndOffset : RichText.Model.Node.Path -> Basics.Int -> RichText.Model.Node.Block -> Maybe ( RichText.Model.Node.Block, RichText.Model.Node.Block )

Splits a block at the given path and offset and returns Just the split nodes. If the path is invalid or the node cannot be split, Nothing is returned.

textNode1 : Inline
textNode1 =
    plainText "sample1"

nodeWithTextLeafToSplit : Block
nodeWithTextLeafToSplit =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ textNode1 ]
        )

nodeBeforeTextLeafSplit : Block
nodeBeforeTextLeafSplit =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ plainText "sam" ]
        )

nodeAfterTextLeafSplit : Block
nodeAfterTextLeafSplit =
    block
        (Element.element paragraph [])
        (inlineChildren <|
            Array.fromList [ plainText "ple1" ]
        )

Just (nodeBeforeTextLeafSplit, nodeAfterTextLeafSplit) ==
    splitBlockAtPathAndOffset [ 0 ] 3 nodeWithTextLeafToSplit

splitTextLeaf : Basics.Int -> RichText.Model.Text.Text -> ( RichText.Model.Text.Text, RichText.Model.Text.Text )

Splits a text leaf into two based on the given offset.

splitTextLeaf 1 (emptyText <| withText "test")
--> (Text "t", Text "est")

Marks

toggleMark : RichText.Model.Mark.ToggleAction -> RichText.Model.Mark.MarkOrder -> RichText.Model.Mark.Mark -> Node -> Node

Runs the toggle action on the node for the given mark.

toggleMark Add markOrder bold node
--> Adds bold to the given node

Types

These are convenience types to wrap around inline and block node and arrays


type Fragment
    = BlockFragment (Array RichText.Model.Node.Block)
    | InlineFragment (Array RichText.Model.Node.Inline)

A Fragment represents an array of Block or Inline nodes. It's a convenience type used for things like insertion or deserialization.


type Node
    = Block RichText.Model.Node.Block
    | Inline RichText.Model.Node.Inline

Node represents either a Block or Inline. It's a convenience type that wraps an argument or return value of a function that can use either block or inline, like nodeAt or replace.