Skip to main content

saphyr_parser/input/
str.rs

1use crate::{
2    char_traits::{
3        is_alpha, is_blank, is_blank_or_breakz, is_break, is_breakz, is_digit, is_flow, is_z,
4    },
5    input::{Input, SkipTabs},
6};
7use alloc::string::String;
8
9/// A parser input that uses a `&str` as source.
10#[allow(clippy::module_name_repetitions)]
11#[derive(Clone, Copy)]
12pub struct StrInput<'a> {
13    /// The input str buffer.
14    buffer: &'a str,
15    /// The number of characters we have looked ahead.
16    ///
17    /// We must however keep track of how many characters the parser asked us to look ahead for so
18    /// that we can return the correct value in [`Self::buflen`].
19    lookahead: usize,
20}
21
22impl<'a> StrInput<'a> {
23    /// Create a new [`StrInput`] with the given str.
24    #[must_use]
25    pub fn new(input: &'a str) -> Self {
26        Self {
27            buffer: input,
28            lookahead: 0,
29        }
30    }
31}
32
33impl Input for StrInput<'_> {
34    #[inline]
35    fn lookahead(&mut self, x: usize) {
36        // We already have all characters that we need.
37        // We cannot add '\0's to the buffer should we prematurely reach EOF.
38        // Returning '\0's befalls the character-retrieving functions.
39        self.lookahead = self.lookahead.max(x);
40    }
41
42    #[inline]
43    fn buflen(&self) -> usize {
44        self.lookahead
45    }
46
47    #[inline]
48    fn bufmaxlen(&self) -> usize {
49        BUFFER_LEN
50    }
51
52    fn buf_is_empty(&self) -> bool {
53        self.buflen() == 0
54    }
55
56    #[inline]
57    fn raw_read_ch(&mut self) -> char {
58        let mut chars = self.buffer.chars();
59        if let Some(c) = chars.next() {
60            self.buffer = chars.as_str();
61            c
62        } else {
63            '\0'
64        }
65    }
66
67    #[inline]
68    fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
69        if let Some((c, sub_str)) = split_first_char(self.buffer) {
70            if is_breakz(c) {
71                None
72            } else {
73                self.buffer = sub_str;
74                Some(c)
75            }
76        } else {
77            None
78        }
79    }
80
81    #[inline]
82    fn skip(&mut self) {
83        let mut chars = self.buffer.chars();
84        if chars.next().is_some() {
85            self.buffer = chars.as_str();
86        }
87    }
88
89    #[inline]
90    fn skip_n(&mut self, count: usize) {
91        let mut chars = self.buffer.chars();
92        for _ in 0..count {
93            if chars.next().is_none() {
94                break;
95            }
96        }
97        self.buffer = chars.as_str();
98    }
99
100    #[inline]
101    fn peek(&self) -> char {
102        self.buffer.chars().next().unwrap_or('\0')
103    }
104
105    #[inline]
106    fn peek_nth(&self, n: usize) -> char {
107        let mut chars = self.buffer.chars();
108        for _ in 0..n {
109            if chars.next().is_none() {
110                return '\0';
111            }
112        }
113        chars.next().unwrap_or('\0')
114    }
115
116    #[inline]
117    fn peek_opt(&self) -> Option<char> {
118        self.buffer.chars().next()
119    }
120
121    #[inline]
122    fn look_ch(&mut self) -> char {
123        self.lookahead(1);
124        self.peek()
125    }
126
127    #[inline]
128    fn next_char_is(&self, c: char) -> bool {
129        self.peek() == c
130    }
131
132    #[inline]
133    fn nth_char_is(&self, n: usize, c: char) -> bool {
134        self.peek_nth(n) == c
135    }
136
137    #[inline]
138    fn next_2_are(&self, c1: char, c2: char) -> bool {
139        let mut chars = self.buffer.chars();
140        chars.next() == Some(c1) && chars.next() == Some(c2)
141    }
142
143    #[inline]
144    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
145        let mut chars = self.buffer.chars();
146        chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3)
147    }
148
149    #[inline]
150    fn next_is_document_indicator(&self) -> bool {
151        if self.buffer.len() < 3 {
152            false
153        } else {
154            // Since all characters we look for are ascii, we can directly use the byte API of str.
155            let bytes = self.buffer.as_bytes();
156            (bytes.len() == 3 || is_blank_or_breakz(bytes[3] as char))
157                && (bytes[0] == b'.' || bytes[0] == b'-')
158                && bytes[0] == bytes[1]
159                && bytes[1] == bytes[2]
160        }
161    }
162
163    #[inline]
164    fn next_is_document_start(&self) -> bool {
165        if self.buffer.len() < 3 {
166            false
167        } else {
168            // Since all characters we look for are ascii, we can directly use the byte API of str.
169            let bytes = self.buffer.as_bytes();
170            (bytes.len() == 3 || is_blank_or_breakz(bytes[3] as char))
171                && bytes[0] == b'-'
172                && bytes[1] == b'-'
173                && bytes[2] == b'-'
174        }
175    }
176
177    #[inline]
178    fn next_is_document_end(&self) -> bool {
179        if self.buffer.len() < 3 {
180            false
181        } else {
182            // Since all characters we look for are ascii, we can directly use the byte API of str.
183            let bytes = self.buffer.as_bytes();
184            (bytes.len() == 3 || is_blank_or_breakz(bytes[3] as char))
185                && bytes[0] == b'.'
186                && bytes[1] == b'.'
187                && bytes[2] == b'.'
188        }
189    }
190
191    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
192        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
193
194        let mut new_str = self.buffer;
195        let mut has_yaml_ws = false;
196        let mut encountered_tab = false;
197
198        // This ugly pair of loops is the fastest way of trimming spaces (and maybe tabs) I found
199        // while keeping track of whether we encountered spaces and/or tabs.
200        if skip_tabs == SkipTabs::Yes {
201            loop {
202                if let Some(sub_str) = new_str.strip_prefix(' ') {
203                    has_yaml_ws = true;
204                    new_str = sub_str;
205                } else if let Some(sub_str) = new_str.strip_prefix('\t') {
206                    encountered_tab = true;
207                    new_str = sub_str;
208                } else {
209                    break;
210                }
211            }
212        } else {
213            while let Some(sub_str) = new_str.strip_prefix(' ') {
214                has_yaml_ws = true;
215                new_str = sub_str;
216            }
217        }
218
219        // All characters consumed were ascii. We can use the byte length difference to count the
220        // number of whitespace ignored.
221        let mut chars_consumed = self.buffer.len() - new_str.len();
222
223        if !new_str.is_empty() && new_str.as_bytes()[0] == b'#' {
224            if !encountered_tab && !has_yaml_ws {
225                return (
226                    chars_consumed,
227                    Err("comments must be separated from other tokens by whitespace"),
228                );
229            }
230
231            // Skip remaining characters until we hit a breakz.
232            while let Some((c, sub_str)) = split_first_char(new_str) {
233                if is_breakz(c) {
234                    break;
235                }
236                new_str = sub_str;
237                chars_consumed += 1;
238            }
239        }
240
241        self.buffer = new_str;
242
243        (
244            chars_consumed,
245            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
246        )
247    }
248
249    #[allow(clippy::inline_always)]
250    #[inline(always)]
251    fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
252        let nc = self.peek_nth(1);
253        match self.peek() {
254            // indicators can end a plain scalar, see 7.3.3. Plain Style
255            ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
256            c if in_flow && is_flow(c) => false,
257            _ => true,
258        }
259    }
260
261    #[inline]
262    fn next_is_blank_or_break(&self) -> bool {
263        !self.buffer.is_empty()
264            && (is_blank(self.buffer.as_bytes()[0] as char)
265                || is_break(self.buffer.as_bytes()[0] as char))
266    }
267
268    #[inline]
269    fn next_is_blank_or_breakz(&self) -> bool {
270        self.buffer.is_empty()
271            || (is_blank(self.buffer.as_bytes()[0] as char)
272                || is_breakz(self.buffer.as_bytes()[0] as char))
273    }
274
275    #[inline]
276    fn next_is_blank(&self) -> bool {
277        !self.buffer.is_empty() && is_blank(self.buffer.as_bytes()[0] as char)
278    }
279
280    #[inline]
281    fn next_is_break(&self) -> bool {
282        !self.buffer.is_empty() && is_break(self.buffer.as_bytes()[0] as char)
283    }
284
285    #[inline]
286    fn next_is_breakz(&self) -> bool {
287        self.buffer.is_empty() || is_breakz(self.buffer.as_bytes()[0] as char)
288    }
289
290    #[inline]
291    fn next_is_z(&self) -> bool {
292        self.buffer.is_empty() || is_z(self.buffer.as_bytes()[0] as char)
293    }
294
295    #[inline]
296    fn next_is_flow(&self) -> bool {
297        !self.buffer.is_empty() && is_flow(self.buffer.as_bytes()[0] as char)
298    }
299
300    #[inline]
301    fn next_is_digit(&self) -> bool {
302        !self.buffer.is_empty() && is_digit(self.buffer.as_bytes()[0] as char)
303    }
304
305    #[inline]
306    fn next_is_alpha(&self) -> bool {
307        !self.buffer.is_empty() && is_alpha(self.buffer.as_bytes()[0] as char)
308    }
309
310    fn skip_while_non_breakz(&mut self) -> usize {
311        let mut new_str = self.buffer;
312        let mut count = 0;
313
314        // Skip over all non-breaks.
315        while let Some((c, sub_str)) = split_first_char(new_str) {
316            if is_breakz(c) {
317                break;
318            }
319            new_str = sub_str;
320            count += 1;
321        }
322
323        self.buffer = new_str;
324
325        count
326    }
327
328    fn skip_while_blank(&mut self) -> usize {
329        // Since all characters we look for are ascii, we can directly use the byte API of str.
330        let mut i = 0;
331        while i < self.buffer.len() {
332            if !is_blank(self.buffer.as_bytes()[i] as char) {
333                break;
334            }
335            i += 1;
336        }
337        self.buffer = &self.buffer[i..];
338        i
339    }
340
341    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
342        let mut not_alpha = None;
343
344        // Skip while we have alpha characters.
345        let mut chars = self.buffer.chars();
346        for c in chars.by_ref() {
347            if !is_alpha(c) {
348                not_alpha = Some(c);
349                break;
350            }
351        }
352
353        let remaining_string = if let Some(c) = not_alpha {
354            let n_bytes_read = chars.as_str().as_ptr() as usize - self.buffer.as_ptr() as usize;
355            let last_char_bytes = c.len_utf8();
356            &self.buffer[n_bytes_read - last_char_bytes..]
357        } else {
358            chars.as_str()
359        };
360
361        let n_bytes_to_append = remaining_string.as_ptr() as usize - self.buffer.as_ptr() as usize;
362        out.reserve(n_bytes_to_append);
363        out.push_str(&self.buffer[..n_bytes_to_append]);
364        self.buffer = remaining_string;
365
366        n_bytes_to_append
367    }
368
369    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
370        let mut not_non_space = None;
371
372        // Skip while we have non-space characters.
373        let mut chars = self.buffer.chars();
374        for c in chars.by_ref() {
375            if !crate::char_traits::is_yaml_non_space(c) {
376                not_non_space = Some(c);
377                break;
378            }
379        }
380
381        let remaining_string = if let Some(c) = not_non_space {
382            let n_bytes_read = chars.as_str().as_ptr() as usize - self.buffer.as_ptr() as usize;
383            let last_char_bytes = c.len_utf8();
384            &self.buffer[n_bytes_read - last_char_bytes..]
385        } else {
386            chars.as_str()
387        };
388
389        let n_bytes_to_append = remaining_string.as_ptr() as usize - self.buffer.as_ptr() as usize;
390        out.reserve(n_bytes_to_append);
391        out.push_str(&self.buffer[..n_bytes_to_append]);
392        self.buffer = remaining_string;
393
394        n_bytes_to_append
395    }
396}
397
398/// The buffer size we return to the scanner.
399///
400/// This does not correspond to any allocated buffer size. In practice, the scanner can withdraw
401/// any character they want. If it's within the input buffer, the given character is returned,
402/// otherwise `\0` is returned.
403///
404/// The number of characters we are asked to retrieve in [`lookahead`] depends on the buffer size
405/// of the input. Our buffer here is virtually unlimited, but the scanner cannot work with that. It
406/// may allocate buffers of its own of the size we return in [`bufmaxlen`] (so we can't return
407/// [`usize::MAX`]). We can't always return the number of characters left either, as the scanner
408/// expects [`buflen`] to return the same value that was given to [`lookahead`] right after its
409/// call.
410///
411/// This create a complex situation where [`bufmaxlen`] influences what value [`lookahead`] is
412/// called with, which in turns dictates what [`buflen`] returns. In order to avoid breaking any
413/// function, we return this constant in [`bufmaxlen`] which, since the input is processed one line
414/// at a time, should fit what we expect to be a good balance between memory consumption and what
415/// we expect the maximum line length to be.
416///
417/// [`lookahead`]: `StrInput::lookahead`
418/// [`bufmaxlen`]: `StrInput::bufmaxlen`
419/// [`buflen`]: `StrInput::buflen`
420const BUFFER_LEN: usize = 128;
421
422/// Splits the first character of the given string and returns it along with the rest of the
423/// string.
424#[inline]
425fn split_first_char(s: &str) -> Option<(char, &str)> {
426    let mut chars = s.chars();
427    let c = chars.next()?;
428    Some((c, chars.as_str()))
429}
430
431#[cfg(test)]
432mod test {
433    use crate::input::Input;
434
435    use super::StrInput;
436
437    #[test]
438    pub fn is_document_start() {
439        let input = StrInput::new("---\n");
440        assert!(input.next_is_document_start());
441        assert!(input.next_is_document_indicator());
442        let input = StrInput::new("---");
443        assert!(input.next_is_document_start());
444        assert!(input.next_is_document_indicator());
445        let input = StrInput::new("...\n");
446        assert!(!input.next_is_document_start());
447        assert!(input.next_is_document_indicator());
448        let input = StrInput::new("--- ");
449        assert!(input.next_is_document_start());
450        assert!(input.next_is_document_indicator());
451    }
452
453    #[test]
454    pub fn is_document_end() {
455        let input = StrInput::new("...\n");
456        assert!(input.next_is_document_end());
457        assert!(input.next_is_document_indicator());
458        let input = StrInput::new("...");
459        assert!(input.next_is_document_end());
460        assert!(input.next_is_document_indicator());
461        let input = StrInput::new("---\n");
462        assert!(!input.next_is_document_end());
463        assert!(input.next_is_document_indicator());
464        let input = StrInput::new("... ");
465        assert!(input.next_is_document_end());
466        assert!(input.next_is_document_indicator());
467    }
468}