Skip to main content

tiptap_rusty_parser/
range.rs

1//! Editor-style range editing over a block's inline content.
2//!
3//! These methods treat `self` as a **single block** (e.g. a `paragraph`) and
4//! edit its inline children — text nodes and inline leaves — addressed by a
5//! [`Position`] (`child` index + Unicode-scalar `offset` into that child's
6//! text). A [`Range`] spans two positions in the *same* block. To edit a nested
7//! block, resolve it first: `doc.node_at_mut(&path)?.delete_range(range)`.
8//!
9//! Text nodes are split at range boundaries as needed, and adjacent text nodes
10//! with equal marks are merged again afterwards (sharing the [`normalize`]
11//! primitive), so edits leave the inline content canonical.
12//!
13//! Offsets count **Unicode scalar values** (`char`s), consistent with
14//! [`Node::char_count`]. Splits always land on a scalar boundary (never mid-code
15//! point); they may split a grapheme cluster. Out-of-range positions return a
16//! [`RangeError`] rather than clamping.
17//!
18//! ```
19//! use tiptap_rusty_parser::{Mark, Node, Position, Range};
20//!
21//! // "Hello world" as one text node inside a paragraph.
22//! let mut p = Node::element("paragraph").with_child(Node::text("Hello world"));
23//! // Bold "world".
24//! p.add_mark_range(Range::new(Position::new(0, 6), Position::new(0, 11)), Mark::new("bold")).unwrap();
25//! assert_eq!(p.child_count(), 2); // "Hello " | "world"(bold)
26//! assert!(p.child(1).unwrap().has_mark("bold"));
27//! ```
28//!
29//! [`normalize`]: Node::normalize
30
31use crate::node::{Mark, Node};
32use crate::normalize::{normalize_children, NormalizeOptions};
33use serde::{Deserialize, Serialize};
34use std::fmt;
35
36/// A position in a block's inline content: a `child` index plus a Unicode-scalar
37/// `offset` into that child's text. For a non-text child, `offset` must be `0`
38/// (the boundary before it); the boundary after it is the next child's offset 0.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Position {
41    /// Index of the child within the block's content.
42    pub child: usize,
43    /// Unicode-scalar offset into that child's text.
44    pub offset: usize,
45}
46
47impl Position {
48    /// Construct a position.
49    #[inline]
50    pub fn new(child: usize, offset: usize) -> Self {
51        Self { child, offset }
52    }
53}
54
55/// A range between two [`Position`]s in the same block (`start <= end` in
56/// document order).
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Range {
59    /// Start position (inclusive).
60    pub start: Position,
61    /// End position (exclusive).
62    pub end: Position,
63}
64
65impl Range {
66    /// Construct a range.
67    #[inline]
68    pub fn new(start: Position, end: Position) -> Self {
69        Self { start, end }
70    }
71
72    /// A collapsed (empty) range at `pos`.
73    #[inline]
74    pub fn collapsed(pos: Position) -> Self {
75        Self {
76            start: pos,
77            end: pos,
78        }
79    }
80}
81
82/// Why a range operation could not be applied.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum RangeError {
85    /// `child` index is past the end of the content.
86    ChildOutOfRange {
87        /// The offending child index.
88        child: usize,
89    },
90    /// `offset` is past the end of that child's text.
91    OffsetOutOfRange {
92        /// The child index.
93        child: usize,
94        /// The offending offset.
95        offset: usize,
96    },
97    /// A non-zero `offset` was given for a non-text child.
98    NotTextNode {
99        /// The child index.
100        child: usize,
101    },
102    /// `end` precedes `start` in document order.
103    InvertedRange,
104}
105
106impl fmt::Display for RangeError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            RangeError::ChildOutOfRange { child } => write!(f, "range: child {child} out of range"),
110            RangeError::OffsetOutOfRange { child, offset } => {
111                write!(f, "range: offset {offset} out of range in child {child}")
112            }
113            RangeError::NotTextNode { child } => {
114                write!(f, "range: child {child} is not a text node")
115            }
116            RangeError::InvertedRange => write!(f, "range: end precedes start"),
117        }
118    }
119}
120
121impl std::error::Error for RangeError {}
122
123impl Node {
124    /// Insert `text` (with optional `marks`) at `pos` in this block's inline
125    /// content, splitting the target text node if needed and merging with
126    /// adjacent equal-mark text afterwards.
127    pub fn insert_text(
128        &mut self,
129        pos: Position,
130        text: &str,
131        marks: Option<&[Mark]>,
132    ) -> Result<(), RangeError> {
133        let children = self.children_mut();
134        let i = ensure_boundary(children, pos)?;
135        children.insert(i, make_text(text, marks));
136        normalize_children(children, &NormalizeOptions::default());
137        Ok(())
138    }
139
140    /// Delete everything in `range`, splitting text nodes at the boundaries and
141    /// removing fully-covered inline nodes. A collapsed range is a no-op.
142    pub fn delete_range(&mut self, range: Range) -> Result<(), RangeError> {
143        let children = self.children_mut();
144        let (s, e) = resolve_range(children, range)?;
145        children.drain(s..e);
146        normalize_children(children, &NormalizeOptions::default());
147        Ok(())
148    }
149
150    /// Replace `range` with `text` (carrying optional `marks`).
151    pub fn replace_range(
152        &mut self,
153        range: Range,
154        text: &str,
155        marks: Option<&[Mark]>,
156    ) -> Result<(), RangeError> {
157        let children = self.children_mut();
158        let (s, e) = resolve_range(children, range)?;
159        children.drain(s..e);
160        if !text.is_empty() {
161            children.insert(s, make_text(text, marks));
162        }
163        normalize_children(children, &NormalizeOptions::default());
164        Ok(())
165    }
166
167    /// Add `mark` to every text node covered by `range` (splitting at the
168    /// boundaries first), then re-merge equal-mark neighbours.
169    pub fn add_mark_range(&mut self, range: Range, mark: Mark) -> Result<(), RangeError> {
170        let children = self.children_mut();
171        let (s, e) = resolve_range(children, range)?;
172        for node in &mut children[s..e] {
173            if is_text(node) {
174                node.add_mark(mark.clone());
175            }
176        }
177        normalize_children(children, &NormalizeOptions::default());
178        Ok(())
179    }
180
181    /// Remove every mark of `mark_type` from text nodes covered by `range`.
182    pub fn remove_mark_range(&mut self, range: Range, mark_type: &str) -> Result<(), RangeError> {
183        let children = self.children_mut();
184        let (s, e) = resolve_range(children, range)?;
185        for node in &mut children[s..e] {
186            if is_text(node) {
187                node.remove_mark(mark_type);
188            }
189        }
190        normalize_children(children, &NormalizeOptions::default());
191        Ok(())
192    }
193
194    /// Toggle `mark` over `range`: if **every** covered text node already has a
195    /// mark of that type, remove it from all; otherwise add it to all. Mirrors
196    /// the ProseMirror toggle semantics, range-scoped.
197    pub fn toggle_mark_range(&mut self, range: Range, mark: Mark) -> Result<(), RangeError> {
198        let children = self.children_mut();
199        let (s, e) = resolve_range(children, range)?;
200        let all_have = children[s..e]
201            .iter()
202            .filter(|n| is_text(n))
203            .all(|n| n.has_mark(&mark.mark_type));
204        for node in &mut children[s..e] {
205            if is_text(node) {
206                if all_have {
207                    node.remove_mark(&mark.mark_type);
208                } else {
209                    node.add_mark(mark.clone());
210                }
211            }
212        }
213        normalize_children(children, &NormalizeOptions::default());
214        Ok(())
215    }
216}
217
218#[inline]
219fn is_text(n: &Node) -> bool {
220    n.node_type.as_deref() == Some("text")
221}
222
223#[inline]
224fn char_len(n: &Node) -> usize {
225    n.text.as_deref().unwrap_or("").chars().count()
226}
227
228/// Build a text node with optional marks (empty/absent marks -> a plain text node).
229fn make_text(text: &str, marks: Option<&[Mark]>) -> Node {
230    match marks {
231        Some(m) if !m.is_empty() => Node::text_with_marks(text, m.iter().cloned()),
232        _ => Node::text(text),
233    }
234}
235
236/// Split a text node at scalar offset `k`, preserving marks/attrs/extra on both
237/// halves. `k` is assumed `<= char_len(node)`.
238fn split_text_at(node: &Node, k: usize) -> (Node, Node) {
239    let s = node.text.as_deref().unwrap_or("");
240    let byte = s.char_indices().nth(k).map_or(s.len(), |(b, _)| b);
241    let (l, r) = s.split_at(byte);
242    let mut left = node.clone();
243    left.text = Some(l.to_owned());
244    let mut right = node.clone();
245    right.text = Some(r.to_owned());
246    (left, right)
247}
248
249/// Ensure a child boundary exists exactly at `pos`, splitting a text node if it
250/// falls mid-text. Returns the index of the first child at or after `pos`.
251fn ensure_boundary(children: &mut Vec<Node>, pos: Position) -> Result<usize, RangeError> {
252    if pos.child > children.len() {
253        return Err(RangeError::ChildOutOfRange { child: pos.child });
254    }
255    if pos.child == children.len() {
256        // End-of-content position; only offset 0 is meaningful.
257        return if pos.offset == 0 {
258            Ok(children.len())
259        } else {
260            Err(RangeError::OffsetOutOfRange {
261                child: pos.child,
262                offset: pos.offset,
263            })
264        };
265    }
266    if is_text(&children[pos.child]) {
267        let cl = char_len(&children[pos.child]);
268        if pos.offset == 0 {
269            return Ok(pos.child);
270        }
271        if pos.offset == cl {
272            return Ok(pos.child + 1);
273        }
274        if pos.offset > cl {
275            return Err(RangeError::OffsetOutOfRange {
276                child: pos.child,
277                offset: pos.offset,
278            });
279        }
280        let (l, r) = split_text_at(&children[pos.child], pos.offset);
281        children[pos.child] = l;
282        children.insert(pos.child + 1, r);
283        Ok(pos.child + 1)
284    } else if pos.offset == 0 {
285        Ok(pos.child)
286    } else {
287        Err(RangeError::NotTextNode { child: pos.child })
288    }
289}
290
291/// Resolve a range to a `[s, e)` child-index span, splitting text nodes at both
292/// boundaries. Splits the end boundary first, then the start, adjusting the end
293/// index if the start split inserted a node before it.
294fn resolve_range(children: &mut Vec<Node>, range: Range) -> Result<(usize, usize), RangeError> {
295    let (sp, ep) = (range.start, range.end);
296    if (ep.child, ep.offset) < (sp.child, sp.offset) {
297        return Err(RangeError::InvertedRange);
298    }
299    let e = ensure_boundary(children, ep)?;
300    let len_after_end = children.len();
301    let s = ensure_boundary(children, sp)?;
302    let start_split_inserted = children.len() > len_after_end;
303    let e = if start_split_inserted && s <= e {
304        e + 1
305    } else {
306        e
307    };
308    Ok((s, e))
309}