Skip to main content

oxc_markdown_parser/
pos.rs

1use std::{borrow::Cow, ops::Range};
2
3/// Span represents a range of a piece of source code.
4/// It counts by byte offset, so it's 0-based.
5///
6/// Offsets are `u32` (matching oxc convention);
7/// sources larger than 4 GiB are rejected by the parser up front.
8///
9/// Every AST node carries spans into the original source,
10/// including inline nodes, whose offsets are never derived from a rebuilt buffer.
11#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
12pub struct Span {
13    /// Start offset. (Inclusive)
14    pub start: u32,
15    /// End offset. (Exclusive)
16    pub end: u32,
17}
18
19impl Span {
20    pub fn new(start: u32, end: u32) -> Self {
21        Self { start, end }
22    }
23
24    /// A zero-width span at the given offset. Covers no characters, but its
25    /// position may still be meaningful (e.g. a marker between two tokens).
26    pub fn empty(at: u32) -> Self {
27        Self { start: at, end: at }
28    }
29
30    /// Whether `offset` lies inside the span (`start <= offset < end`).
31    pub fn contains(self, offset: u32) -> bool {
32        self.start <= offset && offset < self.end
33    }
34
35    /// A span for an in-line byte range relative to `base`.
36    #[expect(clippy::cast_possible_truncation)] // in-line ranges
37    pub fn at(base: u32, range: Range<usize>) -> Self {
38        Self::new(base + range.start as u32, base + range.end as u32)
39    }
40
41    /// The source text this span covers.
42    pub fn slice(self, source: &str) -> &str {
43        &source[self.start as usize..self.end as usize]
44    }
45
46    /// Whether the span covers no characters.
47    pub fn is_empty(self) -> bool {
48        self.start == self.end
49    }
50}
51
52/// One logical line of a leaf block:
53/// the source line with container prefixes (`>`, list indents) already stripped.
54///
55/// `padding` is the number of virtual spaces in front of `span` left over
56/// when prefix consumption stopped inside a tab
57/// (tabs advance to 4-column stops, so a prefix can end mid-tab).
58/// Consumers render `padding` spaces, then the slice.
59#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
60pub struct Segment {
61    pub span: Span,
62    pub padding: u8,
63}
64
65impl Segment {
66    pub fn new(span: Span, padding: u8) -> Self {
67        Self { span, padding }
68    }
69
70    /// The logical text of a multi-line construct:
71    /// pieces joined with `\n`, padding rendered as spaces.
72    /// Borrows for the common single-line case.
73    /// This is what labels are normalized from and what a multi-line title decodes from;
74    /// a raw source slice across the pieces would include the container prefixes between them.
75    pub fn join<'s>(source: &'s str, pieces: &[Segment]) -> Cow<'s, str> {
76        match pieces {
77            [single] if single.padding == 0 => Cow::Borrowed(single.span.slice(source)),
78            _ => {
79                let mut out = String::new();
80                for (i, piece) in pieces.iter().enumerate() {
81                    if i > 0 {
82                        out.push('\n');
83                    }
84                    out.extend(std::iter::repeat_n(' ', usize::from(piece.padding)));
85                    out.push_str(piece.span.slice(source));
86                }
87                Cow::Owned(out)
88            }
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn span_basics() {
99        let span = Span::at(10, 2..5);
100        assert_eq!(span, Span::new(12, 15));
101        assert!(!span.is_empty());
102        assert!(Span::empty(3).is_empty());
103        assert_eq!(Span::new(2, 5).slice("abcdefg"), "cde");
104    }
105
106    /// A single unpadded piece borrows; anything else renders padding and joins with `\n`.
107    #[test]
108    fn segment_join() {
109        let source = "> a\n>   b\n";
110        let a = Segment::new(Span::new(2, 3), 0);
111        let b = Segment::new(Span::new(6, 9), 2);
112        assert!(matches!(Segment::join(source, &[a]), Cow::Borrowed("a")));
113        assert_eq!(Segment::join(source, &[b]), "    b");
114        assert_eq!(Segment::join(source, &[a, b]), "a\n    b");
115    }
116}