Skip to main content

svelte_syntax/
source.rs

1use camino::Utf8Path;
2
3use crate::error::LineColumn;
4use crate::primitives::{SourceId, Span};
5
6/// Borrowed source text with an identifier and optional filename.
7///
8/// `SourceText` pairs a string slice with metadata needed by the parser and
9/// diagnostic system: a [`SourceId`] for distinguishing multiple inputs, and
10/// an optional file path for error messages.
11///
12/// # Example
13///
14/// ```
15/// use svelte_syntax::{SourceId, SourceText};
16///
17/// let source = SourceText::new(SourceId::new(0), "<div>hi</div>", None);
18/// assert_eq!(source.len(), 13);
19///
20/// let (line, col) = source.line_column_at_offset(5);
21/// assert_eq!((line, col), (1, 5));
22/// ```
23#[derive(Debug, Clone, Copy)]
24pub struct SourceText<'src> {
25    /// Identifier for this source (useful when processing multiple files).
26    pub id: SourceId,
27    /// The raw source text.
28    pub text: &'src str,
29    /// Optional file path for diagnostics.
30    pub filename: Option<&'src Utf8Path>,
31}
32
33impl<'src> SourceText<'src> {
34    /// Create a source view over a Svelte or CSS input string.
35    pub fn new(id: SourceId, text: &'src str, filename: Option<&'src Utf8Path>) -> Self {
36        Self { id, text, filename }
37    }
38
39    /// Return the source length in bytes.
40    pub fn len(self) -> usize {
41        self.text.len()
42    }
43
44    /// Return `true` when the source contains no bytes.
45    pub fn is_empty(self) -> bool {
46        self.text.is_empty()
47    }
48
49    /// Return a span that covers the full source.
50    pub fn span_all(self) -> Span {
51        Span::from_offsets(0, self.text.len()).unwrap_or(Span::EMPTY)
52    }
53
54    /// Borrow the substring covered by `span`.
55    pub fn slice(self, span: Span) -> Option<&'src str> {
56        self.text.get(span.start.as_usize()..span.end.as_usize())
57    }
58
59    /// Convert a byte offset into a UTF-16 code-unit offset.
60    ///
61    /// Carriage returns are ignored so CRLF input reports the same coordinates
62    /// as Svelte's JavaScript compiler.
63    pub fn utf16_offset(self, offset: usize) -> usize {
64        let bounded = offset.min(self.text.len());
65        self.text[..bounded]
66            .chars()
67            .filter(|&ch| ch != '\r')
68            .map(char::len_utf16)
69            .sum()
70    }
71
72    /// Convert a byte offset into a one-based line number and zero-based UTF-16 column.
73    pub fn line_column_at_offset(self, offset: usize) -> (usize, usize) {
74        let mut line = 1usize;
75        let mut column = 0usize;
76        let limit = offset.min(self.text.len());
77        for ch in self.text[..limit].chars() {
78            match ch {
79                '\n' => {
80                    line += 1;
81                    column = 0;
82                }
83                '\r' => {}
84                _ => {
85                    column += ch.len_utf16();
86                }
87            }
88        }
89        (line, column)
90    }
91
92    /// Build a full source location for a byte offset.
93    pub fn location_at_offset(self, offset: usize) -> LineColumn {
94        let (line, column) = self.line_column_at_offset(offset);
95        LineColumn {
96            line,
97            column,
98            character: self.utf16_offset(offset),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use camino::Utf8Path;
106
107    use super::{SourceId, SourceText};
108
109    #[test]
110    fn source_text_reports_utf16_locations() {
111        let source = SourceText::new(
112            SourceId::new(1),
113            "a\nšŸ˜€b",
114            Some(Utf8Path::new("input.svelte")),
115        );
116
117        assert_eq!(source.utf16_offset(0), 0);
118        assert_eq!(source.utf16_offset(2), 2);
119        assert_eq!(source.utf16_offset("a\nšŸ˜€".len()), 4);
120
121        let location = source.location_at_offset("a\nšŸ˜€".len());
122        assert_eq!(location.line, 2);
123        assert_eq!(location.column, 2);
124        assert_eq!(location.character, 4);
125    }
126
127    #[test]
128    fn source_text_normalizes_crlf_offsets() {
129        let source = SourceText::new(
130            SourceId::new(2),
131            "a\r\nb\r\nšŸ˜€c",
132            Some(Utf8Path::new("input.svelte")),
133        );
134
135        let offset = "a\r\nb\r\nšŸ˜€".len();
136        assert_eq!(source.utf16_offset(offset), 6);
137
138        let location = source.location_at_offset(offset);
139        assert_eq!(location.line, 3);
140        assert_eq!(location.column, 2);
141        assert_eq!(location.character, 6);
142    }
143}