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