Skip to main content

tiptap_rusty_parser/
source_map.rs

1//! [`SourceMap`]: byte spans of every node object in a document's JSON text.
2//!
3//! Built by a single-pass, allocation-light scanner over the text (no
4//! `Value` tree), so it is exact for whatever produced the text (pretty,
5//! compact, hand-written) and for unknown node types and fields. A node is an
6//! object at the root or an object element of a node's `"content"` array;
7//! paths are child indices into `content`, the same addressing as
8//! [`Node::node_at`](crate::Node::node_at).
9
10use std::fmt;
11use std::ops::Range;
12
13use crate::node::Node;
14
15/// Nesting deeper than this (objects/arrays) is refused instead of recursing.
16const MAX_DEPTH: usize = 1024;
17
18#[derive(Debug, Clone, Copy)]
19struct Entry {
20    start: usize,
21    end: usize,
22    /// Index in `entries` of the parent; `u32::MAX` for the root.
23    parent: u32,
24    /// Child index within the parent's `content`.
25    index: u32,
26    /// This node's children: `kids[kid_start..kid_start + kid_len]`.
27    kid_start: u32,
28    kid_len: u32,
29}
30
31/// Why a [`SourceMap`] could not be built: the text is not a JSON node tree.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SourceMapError {
34    /// Byte offset where scanning stopped.
35    pub offset: usize,
36    /// What was expected there.
37    pub message: &'static str,
38}
39
40impl fmt::Display for SourceMapError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "{} at byte {}", self.message, self.offset)
43    }
44}
45
46impl std::error::Error for SourceMapError {}
47
48/// Byte spans of every node object in a JSON text, addressable by index path
49/// and by offset.
50///
51/// Build cost is O(text); [`span`](Self::span) is O(depth) and
52/// [`path_at`](Self::path_at) is O(log nodes + depth).
53///
54/// ```
55/// use tiptap_rusty_parser::{Document, SourceMap};
56///
57/// let doc = Document::from_json_str(
58///     r#"{"type":"doc","content":[{"type":"callout","x":1,"content":[{"type":"text","text":"hi"}]}]}"#,
59/// ).unwrap();
60/// let (text, map) = SourceMap::pretty(doc.root());
61/// let span = map.span(&[0, 0]).unwrap();
62/// assert!(text[span.clone()].contains("\"hi\""));
63/// // Any byte inside the text node resolves back to it.
64/// assert_eq!(map.path_at(span.start + 3), Some(vec![0, 0]));
65/// ```
66#[derive(Debug, Clone, Default)]
67pub struct SourceMap {
68    /// Pre-order, so `start` is strictly increasing.
69    entries: Vec<Entry>,
70    kids: Vec<u32>,
71}
72
73impl SourceMap {
74    /// Scan `text` (a JSON node object, any formatting).
75    pub fn build(text: &str) -> Result<SourceMap, SourceMapError> {
76        let mut s = Scanner {
77            b: text.as_bytes(),
78            i: 0,
79            map: SourceMap::default(),
80            stack: Vec::new(),
81        };
82        s.ws();
83        if s.peek() != Some(b'{') {
84            return Err(s.err("expected a JSON object"));
85        }
86        s.node(u32::MAX, 0, 0)?;
87        s.ws();
88        if s.i != s.b.len() {
89            return Err(s.err("trailing characters"));
90        }
91        Ok(s.map)
92    }
93
94    /// Serialize `node` with `serde_json` pretty formatting (the same text as
95    /// [`Document::to_string_pretty`](crate::Document::to_string_pretty)) and
96    /// map it.
97    pub fn pretty(node: &Node) -> (String, SourceMap) {
98        let text = serde_json::to_string_pretty(node).expect("Node serializes");
99        let map = SourceMap::build(&text).expect("serde_json output is a node tree");
100        (text, map)
101    }
102
103    /// Number of node objects.
104    #[inline]
105    pub fn len(&self) -> usize {
106        self.entries.len()
107    }
108
109    /// Whether no node was mapped (only for `SourceMap::default()`).
110    #[inline]
111    pub fn is_empty(&self) -> bool {
112        self.entries.is_empty()
113    }
114
115    fn entry_at(&self, path: &[usize]) -> Option<usize> {
116        let mut e = 0usize;
117        self.entries.first()?;
118        for &i in path {
119            let n = &self.entries[e];
120            if i >= n.kid_len as usize {
121                return None;
122            }
123            e = self.kids[n.kid_start as usize + i] as usize;
124        }
125        Some(e)
126    }
127
128    /// Byte range of the node object at `path` (`{` through `}`), or `None`
129    /// when the path does not resolve.
130    pub fn span(&self, path: &[usize]) -> Option<Range<usize>> {
131        self.entry_at(path).map(|e| {
132            let n = &self.entries[e];
133            n.start..n.end
134        })
135    }
136
137    /// Number of children of the node at `path`.
138    pub fn child_count(&self, path: &[usize]) -> Option<usize> {
139        self.entry_at(path)
140            .map(|e| self.entries[e].kid_len as usize)
141    }
142
143    /// Path of the deepest node whose span contains byte `offset`, or `None`
144    /// when `offset` lies outside the root object.
145    pub fn path_at(&self, offset: usize) -> Option<Vec<usize>> {
146        // Last node starting at or before `offset`; then climb to the first
147        // ancestor-or-self that still contains it.
148        let k = self.entries.partition_point(|e| e.start <= offset);
149        let mut e = k.checked_sub(1)?;
150        loop {
151            let n = &self.entries[e];
152            if offset < n.end {
153                break;
154            }
155            if n.parent == u32::MAX {
156                return None;
157            }
158            e = n.parent as usize;
159        }
160        let mut path = Vec::new();
161        while self.entries[e].parent != u32::MAX {
162            path.push(self.entries[e].index as usize);
163            e = self.entries[e].parent as usize;
164        }
165        path.reverse();
166        Some(path)
167    }
168}
169
170struct Scanner<'a> {
171    b: &'a [u8],
172    i: usize,
173    map: SourceMap,
174    /// Child entry ids of the nodes being scanned, flattened.
175    stack: Vec<u32>,
176}
177
178impl Scanner<'_> {
179    #[inline]
180    fn peek(&self) -> Option<u8> {
181        self.b.get(self.i).copied()
182    }
183
184    fn err(&self, message: &'static str) -> SourceMapError {
185        SourceMapError {
186            offset: self.i,
187            message,
188        }
189    }
190
191    #[inline]
192    fn ws(&mut self) {
193        while let Some(b' ' | b'\n' | b'\r' | b'\t') = self.peek() {
194            self.i += 1;
195        }
196    }
197
198    fn expect(&mut self, c: u8, message: &'static str) -> Result<(), SourceMapError> {
199        self.ws();
200        if self.peek() == Some(c) {
201            self.i += 1;
202            Ok(())
203        } else {
204            Err(self.err(message))
205        }
206    }
207
208    /// At `"`: skip the string; returns its raw body and whether it has escapes.
209    fn string(&mut self) -> Result<(usize, usize, bool), SourceMapError> {
210        self.i += 1;
211        let start = self.i;
212        let mut escaped = false;
213        loop {
214            // Fast path: jump to the next quote or backslash.
215            match self.b[self.i..]
216                .iter()
217                .position(|&c| c == b'"' || c == b'\\')
218            {
219                None => {
220                    self.i = self.b.len();
221                    return Err(self.err("unterminated string"));
222                }
223                Some(k) => {
224                    self.i += k;
225                    if self.b[self.i] == b'"' {
226                        let end = self.i;
227                        self.i += 1;
228                        return Ok((start, end, escaped));
229                    }
230                    escaped = true;
231                    self.i += 2;
232                    if self.i > self.b.len() {
233                        self.i = self.b.len();
234                        return Err(self.err("unterminated string"));
235                    }
236                }
237            }
238        }
239    }
240
241    fn is_content_key(&self, (s, e, escaped): (usize, usize, bool)) -> bool {
242        let raw = &self.b[s..e];
243        if !escaped {
244            return raw == b"content";
245        }
246        // Rare: an escaped key; decode it exactly as serde would.
247        let quoted = &self.b[s - 1..e + 1];
248        serde_json::from_slice::<String>(quoted).is_ok_and(|k| k == "content")
249    }
250
251    /// Any JSON value (not a node).
252    fn value(&mut self, depth: usize) -> Result<(), SourceMapError> {
253        if depth > MAX_DEPTH {
254            return Err(self.err("nesting too deep"));
255        }
256        self.ws();
257        match self.peek() {
258            Some(b'"') => self.string().map(|_| ()),
259            Some(b'{') => {
260                self.i += 1;
261                self.ws();
262                if self.peek() == Some(b'}') {
263                    self.i += 1;
264                    return Ok(());
265                }
266                loop {
267                    self.ws();
268                    if self.peek() != Some(b'"') {
269                        return Err(self.err("expected a key"));
270                    }
271                    self.string()?;
272                    self.expect(b':', "expected `:`")?;
273                    self.value(depth + 1)?;
274                    self.ws();
275                    match self.peek() {
276                        Some(b',') => self.i += 1,
277                        Some(b'}') => {
278                            self.i += 1;
279                            return Ok(());
280                        }
281                        _ => return Err(self.err("expected `,` or `}`")),
282                    }
283                }
284            }
285            Some(b'[') => {
286                self.i += 1;
287                self.ws();
288                if self.peek() == Some(b']') {
289                    self.i += 1;
290                    return Ok(());
291                }
292                loop {
293                    self.value(depth + 1)?;
294                    self.ws();
295                    match self.peek() {
296                        Some(b',') => self.i += 1,
297                        Some(b']') => {
298                            self.i += 1;
299                            return Ok(());
300                        }
301                        _ => return Err(self.err("expected `,` or `]`")),
302                    }
303                }
304            }
305            Some(b'-' | b'0'..=b'9' | b't' | b'f' | b'n') => {
306                let start = self.i;
307                while let Some(c) = self.peek() {
308                    if matches!(c, b',' | b'}' | b']' | b' ' | b'\n' | b'\r' | b'\t') {
309                        break;
310                    }
311                    self.i += 1;
312                }
313                (self.i > start)
314                    .then_some(())
315                    .ok_or_else(|| self.err("expected a value"))
316            }
317            _ => Err(self.err("expected a value")),
318        }
319    }
320
321    /// At `{` of a node: record it and its `content` children.
322    fn node(&mut self, parent: u32, index: u32, depth: usize) -> Result<(), SourceMapError> {
323        if depth > MAX_DEPTH {
324            return Err(self.err("nesting too deep"));
325        }
326        let id = self.map.entries.len() as u32;
327        self.map.entries.push(Entry {
328            start: self.i,
329            end: 0,
330            parent,
331            index,
332            kid_start: 0,
333            kid_len: 0,
334        });
335        let kids_from = self.stack.len();
336        self.i += 1;
337        self.ws();
338        if self.peek() == Some(b'}') {
339            self.i += 1;
340        } else {
341            loop {
342                self.ws();
343                if self.peek() != Some(b'"') {
344                    return Err(self.err("expected a key"));
345                }
346                let key = self.string()?;
347                self.expect(b':', "expected `:`")?;
348                self.ws();
349                if self.peek() == Some(b'[') && self.is_content_key(key) {
350                    self.content(id, depth)?;
351                } else {
352                    self.value(depth + 1)?;
353                }
354                self.ws();
355                match self.peek() {
356                    Some(b',') => self.i += 1,
357                    Some(b'}') => {
358                        self.i += 1;
359                        break;
360                    }
361                    _ => return Err(self.err("expected `,` or `}`")),
362                }
363            }
364        }
365        let kid_start = self.map.kids.len() as u32;
366        let kid_len = (self.stack.len() - kids_from) as u32;
367        self.map.kids.extend(self.stack.drain(kids_from..));
368        let e = &mut self.map.entries[id as usize];
369        e.end = self.i;
370        e.kid_start = kid_start;
371        e.kid_len = kid_len;
372        Ok(())
373    }
374
375    /// At `[` of a node's `content`: every element must be a node object.
376    fn content(&mut self, parent: u32, depth: usize) -> Result<(), SourceMapError> {
377        self.i += 1;
378        self.ws();
379        if self.peek() == Some(b']') {
380            self.i += 1;
381            return Ok(());
382        }
383        let mut index = 0u32;
384        loop {
385            self.ws();
386            if self.peek() != Some(b'{') {
387                return Err(self.err("expected a node object in `content`"));
388            }
389            self.stack.push(self.map.entries.len() as u32);
390            self.node(parent, index, depth + 1)?;
391            index += 1;
392            self.ws();
393            match self.peek() {
394                Some(b',') => self.i += 1,
395                Some(b']') => {
396                    self.i += 1;
397                    return Ok(());
398                }
399                _ => return Err(self.err("expected `,` or `]`")),
400            }
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use crate::Document;
409
410    const SRC: &str = r#"{"type":"doc","content":[
411        {"type":"heading","attrs":{"level":1,"content":[1,2]},"content":[{"type":"text","text":"T \"q\" }"}]},
412        {"type":"callout","weird":{"content":[{"a":1}]},"content":[]},
413        {"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold"}],"text":"a"},{"type":"hardBreak"}]}
414    ],"extra":[true,null,-1.5e3]}"#;
415
416    #[test]
417    fn spans_match_every_node_exactly() {
418        let doc = Document::from_json_str(SRC).unwrap();
419        let map = SourceMap::build(SRC).unwrap();
420        let mut count = 0;
421        fn go(n: &Node, path: &mut Vec<usize>, map: &SourceMap, src: &str, count: &mut usize) {
422            *count += 1;
423            let span = map.span(path).unwrap();
424            let parsed: Node = serde_json::from_str(&src[span]).unwrap();
425            assert_eq!(&parsed, n, "path {path:?}");
426            assert_eq!(map.child_count(path), Some(n.child_count()));
427            for (i, c) in n.children().iter().enumerate() {
428                path.push(i);
429                go(c, path, map, src, count);
430                path.pop();
431            }
432        }
433        go(doc.root(), &mut Vec::new(), &map, SRC, &mut count);
434        assert_eq!(count, map.len());
435        assert_eq!(map.span(&[0, 1]), None);
436        assert_eq!(map.span(&[9]), None);
437    }
438
439    #[test]
440    fn content_inside_attrs_or_unknown_fields_is_not_a_node() {
441        let map = SourceMap::build(SRC).unwrap();
442        assert_eq!(map.child_count(&[0]), Some(1));
443        assert_eq!(map.child_count(&[1]), Some(0));
444        assert_eq!(map.len(), 7);
445    }
446
447    #[test]
448    fn path_at_finds_deepest() {
449        let map = SourceMap::build(SRC).unwrap();
450        let text = SRC.find("\"a\"}").unwrap();
451        assert_eq!(map.path_at(text), Some(vec![2, 0]));
452        let hb = SRC.find("hardBreak").unwrap();
453        assert_eq!(map.path_at(hb), Some(vec![2, 1]));
454        // Between children: the parent.
455        let comma = map.span(&[2, 0]).unwrap().end;
456        assert_eq!(map.path_at(comma), Some(vec![2]));
457        assert_eq!(map.path_at(SRC.find("extra").unwrap()), Some(vec![]));
458        assert_eq!(map.path_at(0), Some(vec![]));
459        assert_eq!(map.path_at(SRC.len()), None);
460        let s = map.span(&[1]).unwrap();
461        assert_eq!(map.path_at(s.start), Some(vec![1]));
462        assert_eq!(map.path_at(s.end - 1), Some(vec![1]));
463    }
464
465    #[test]
466    fn pretty_matches_document_and_roundtrips_paths() {
467        let doc = Document::from_json_str(SRC).unwrap();
468        let (text, map) = SourceMap::pretty(doc.root());
469        assert_eq!(text, doc.to_string_pretty().unwrap());
470        for path in [vec![], vec![0], vec![0, 0], vec![1], vec![2, 1]] {
471            let s = map.span(&path).unwrap();
472            assert_eq!(map.path_at(s.start), Some(path.clone()));
473            assert!(text[s].starts_with('{'));
474        }
475    }
476
477    #[test]
478    fn escaped_content_key() {
479        let src = r#"{"type":"doc","content":[{"type":"p"}]}"#;
480        assert_eq!(
481            Document::from_json_str(src).unwrap().child_count(),
482            SourceMap::build(src).unwrap().child_count(&[]).unwrap()
483        );
484    }
485
486    #[test]
487    fn errors() {
488        for bad in [
489            "",
490            "[]",
491            "{",
492            r#"{"a":}"#,
493            r#"{"a":"x}"#,
494            r#"{"content":[1]}"#,
495            r#"{"a":1} x"#,
496        ] {
497            assert!(SourceMap::build(bad).is_err(), "{bad}");
498        }
499        let deep = format!("{}{}", "[".repeat(5000), "]".repeat(5000));
500        assert!(SourceMap::build(&format!(r#"{{"a":{deep}}}"#)).is_err());
501    }
502}