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}