Skip to main content

saphyr_parser/
input.rs

1//! Utilities to create a source of input to the parser.
2//!
3//! [`Input`] must be implemented for the parser to fetch input. Make sure your needs aren't
4//! covered by the [`BufferedInput`].
5
6use alloc::string::String;
7
8use crate::char_traits::is_yaml_non_break;
9
10pub(crate) mod buffered;
11pub(crate) mod str;
12
13#[allow(clippy::module_name_repetitions)]
14pub use buffered::BufferedInput;
15
16pub use crate::char_traits::{
17    is_alpha, is_blank, is_blank_or_breakz, is_break, is_breakz, is_digit, is_flow, is_z,
18};
19
20/// Interface for a source of characters.
21///
22/// Hiding the input's implementation behind this trait allows mostly:
23///  * For input-specific optimizations (for instance, using `str` methods instead of manually
24///    transferring one `char` at a time to a buffer).
25///  * To return `&str`s referencing the input string, thus avoiding potentially costly
26///    allocations. Should users need an owned version of the data, they can always `.to_owned()`
27///    their YAML object.
28pub trait Input {
29    /// A hint to the input source that we will need to read `count` characters.
30    ///
31    /// If the input is exhausted, `\0` can be used to pad the last characters and later returned.
32    /// The characters must not be consumed, but may be placed in an internal buffer.
33    ///
34    /// This method may be a no-op if buffering yields no performance improvement.
35    ///
36    /// Implementers of [`Input`] must _not_ load more than `count` characters into the buffer. The
37    /// parser tracks how many characters are loaded in the buffer and acts accordingly.
38    fn lookahead(&mut self, count: usize);
39
40    /// Return the number of buffered characters in `self`.
41    #[must_use]
42    fn buflen(&self) -> usize;
43
44    /// Return the capacity of the buffer in `self`.
45    #[must_use]
46    fn bufmaxlen(&self) -> usize;
47
48    /// Return whether the buffer (!= stream) is empty.
49    #[inline]
50    #[must_use]
51    fn buf_is_empty(&self) -> bool {
52        self.buflen() == 0
53    }
54
55    /// Read a character from the input stream and return it directly.
56    ///
57    /// The internal buffer (if any) is bypassed.
58    #[must_use]
59    fn raw_read_ch(&mut self) -> char;
60
61    /// Read a non-break character from the input stream and return it directly. A non-break
62    /// character is neither a [breakz] nor a byte order mark, see 5.4. Line Break Characters.
63    ///
64    /// The internal buffer (if any) is bypassed.
65    ///
66    /// If the next character is a breakz or a byte order mark, it is either not consumed or placed
67    /// into the buffer (if any).
68    ///
69    /// [breakz]: is_breakz
70    #[must_use]
71    fn raw_read_non_breakz_ch(&mut self) -> Option<char>;
72
73    /// Consume the next character.
74    fn skip(&mut self);
75
76    /// Consume the next `count` character.
77    fn skip_n(&mut self, count: usize);
78
79    /// Return the next character, without consuming it.
80    ///
81    /// Users of the [`Input`] must make sure that the character has been loaded through a prior
82    /// call to [`Input::lookahead`]. Implementors of [`Input`] may assume that a valid call to
83    /// [`Input::lookahead`] has been made beforehand.
84    ///
85    /// # Return
86    /// If the input source is not exhausted, returns the next character to be fed into the
87    /// scanner. Otherwise, returns `\0`.
88    #[must_use]
89    fn peek(&self) -> char;
90
91    /// Return the `n`-th character in the buffer, without consuming it.
92    ///
93    /// This function assumes that the n-th character in the input has already been fetched through
94    /// [`Input::lookahead`].
95    #[must_use]
96    fn peek_nth(&self, n: usize) -> char;
97
98    /// Return the next character in the input source, without consuming it, unless the input
99    /// source is exhausted.
100    ///
101    /// Unlike [`Input::peek`], this function does not conflate a NUL character with the
102    /// end-of-stream sentinel: a raw NUL character in the stream is returned as `Some('\0')`,
103    /// while an exhausted input source returns `None`. Padding characters introduced by
104    /// [`Input::lookahead`] are never returned.
105    ///
106    /// This function assumes that the next character in the input has already been fetched through
107    /// [`Input::lookahead`].
108    #[must_use]
109    fn peek_opt(&self) -> Option<char>;
110
111    /// Look for the next character and return it.
112    ///
113    /// The character is not consumed.
114    /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
115    #[inline]
116    #[must_use]
117    fn look_ch(&mut self) -> char {
118        self.lookahead(1);
119        self.peek()
120    }
121
122    /// Return whether the next character in the input source is equal to `c`.
123    ///
124    /// This function assumes that the next character in the input has already been fetched through
125    /// [`Input::lookahead`].
126    #[inline]
127    #[must_use]
128    fn next_char_is(&self, c: char) -> bool {
129        self.peek() == c
130    }
131
132    /// Return whether the `n`-th character in the input source is equal to `c`.
133    ///
134    /// This function assumes that the n-th character in the input has already been fetched through
135    /// [`Input::lookahead`].
136    #[inline]
137    #[must_use]
138    fn nth_char_is(&self, n: usize, c: char) -> bool {
139        self.peek_nth(n) == c
140    }
141
142    /// Return whether the next 2 characters in the input source match the given characters.
143    ///
144    /// This function assumes that the next 2 characters in the input have already been fetched
145    /// through [`Input::lookahead`].
146    #[inline]
147    #[must_use]
148    fn next_2_are(&self, c1: char, c2: char) -> bool {
149        assert!(self.buflen() >= 2);
150        self.peek() == c1 && self.peek_nth(1) == c2
151    }
152
153    /// Return whether the next 3 characters in the input source match the given characters.
154    ///
155    /// This function assumes that the next 3 characters in the input have already been fetched
156    /// through [`Input::lookahead`].
157    #[inline]
158    #[must_use]
159    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
160        assert!(self.buflen() >= 3);
161        self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
162    }
163
164    /// Check whether the next characters correspond to a document indicator.
165    ///
166    /// This function assumes that the next 4 characters in the input has already been fetched
167    /// through [`Input::lookahead`].
168    #[inline]
169    #[must_use]
170    fn next_is_document_indicator(&self) -> bool {
171        assert!(self.buflen() >= 4);
172        is_blank_or_breakz(self.peek_nth(3))
173            && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
174    }
175
176    /// Check whether the next characters correspond to a start of document.
177    ///
178    /// This function assumes that the next 4 characters in the input has already been fetched
179    /// through [`Input::lookahead`].
180    #[inline]
181    #[must_use]
182    fn next_is_document_start(&self) -> bool {
183        assert!(self.buflen() >= 4);
184        self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
185    }
186
187    /// Check whether the next characters correspond to an end of document.
188    ///
189    /// This function assumes that the next 4 characters in the input has already been fetched
190    /// through [`Input::lookahead`].
191    #[inline]
192    #[must_use]
193    fn next_is_document_end(&self) -> bool {
194        assert!(self.buflen() >= 4);
195        self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
196    }
197
198    /// Skip yaml whitespace at most up to eol. Also skips comments. Advances the input.
199    ///
200    /// # Return
201    /// Return a tuple with the number of characters that were consumed and the result of skipping
202    /// whitespace. The number of characters returned can be used to advance the index and column,
203    /// since no end-of-line character will be consumed.
204    /// See [`SkipTabs`] For more details on the success variant.
205    ///
206    /// # Errors
207    /// Errors if a comment is encountered but it was not preceded by a whitespace. In that event,
208    /// the first tuple element will contain the number of characters consumed prior to reaching
209    /// the `#`.
210    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
211        let mut encountered_tab = false;
212        let mut has_yaml_ws = false;
213        let mut chars_consumed = 0;
214        loop {
215            match self.look_ch() {
216                ' ' => {
217                    has_yaml_ws = true;
218                    self.skip();
219                }
220                '\t' if skip_tabs != SkipTabs::No => {
221                    encountered_tab = true;
222                    self.skip();
223                }
224                // YAML comments must be preceded by whitespace.
225                '#' if !encountered_tab && !has_yaml_ws => {
226                    return (
227                        chars_consumed,
228                        Err("comments must be separated from other tokens by whitespace"),
229                    );
230                }
231                '#' => {
232                    self.skip(); // Skip over '#'
233                    while is_yaml_non_break(self.look_ch()) {
234                        self.skip();
235                        chars_consumed += 1;
236                    }
237                }
238                _ => break,
239            }
240            chars_consumed += 1;
241        }
242
243        (
244            chars_consumed,
245            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
246        )
247    }
248
249    /// Check whether the next characters may be part of a plain scalar.
250    ///
251    /// This function assumes we are not given a blankz character.
252    #[allow(clippy::inline_always)]
253    #[inline(always)]
254    fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
255        let nc = self.peek_nth(1);
256        match self.peek() {
257            // indicators can end a plain scalar, see 7.3.3. Plain Style
258            ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
259            // A byte order mark isn't a plain scalar character, see 5.2. Character Encodings
260            '\u{FEFF}' => false,
261            c if in_flow && is_flow(c) => false,
262            _ => true,
263        }
264    }
265
266    /// Check whether the next character is [a blank] or [a break].
267    ///
268    /// The character must have previously been fetched through [`lookahead`]
269    ///
270    /// # Return
271    /// Returns true if the character is [a blank] or [a break], false otherwise.
272    ///
273    /// [`lookahead`]: Input::lookahead
274    /// [a blank]: is_blank
275    /// [a break]: is_break
276    #[inline]
277    fn next_is_blank_or_break(&self) -> bool {
278        is_blank(self.peek()) || is_break(self.peek())
279    }
280
281    /// Check whether the next character is [a blank] or [a breakz].
282    ///
283    /// The character must have previously been fetched through [`lookahead`]
284    ///
285    /// # Return
286    /// Returns true if the character is [a blank] or [a break], false otherwise.
287    ///
288    /// [`lookahead`]: Input::lookahead
289    /// [a blank]: is_blank
290    /// [a breakz]: is_breakz
291    #[inline]
292    fn next_is_blank_or_breakz(&self) -> bool {
293        is_blank(self.peek()) || is_breakz(self.peek())
294    }
295
296    /// Check whether the next character is [a blank].
297    ///
298    /// The character must have previously been fetched through [`lookahead`]
299    ///
300    /// # Return
301    /// Returns true if the character is [a blank], false otherwise.
302    ///
303    /// [`lookahead`]: Input::lookahead
304    /// [a blank]: is_blank
305    #[inline]
306    fn next_is_blank(&self) -> bool {
307        is_blank(self.peek())
308    }
309
310    /// Check whether the next character is [a break].
311    ///
312    /// The character must have previously been fetched through [`lookahead`]
313    ///
314    /// # Return
315    /// Returns true if the character is [a break], false otherwise.
316    ///
317    /// [`lookahead`]: Input::lookahead
318    /// [a break]: is_break
319    #[inline]
320    fn next_is_break(&self) -> bool {
321        is_break(self.peek())
322    }
323
324    /// Check whether the next character is [a breakz].
325    ///
326    /// The character must have previously been fetched through [`lookahead`]
327    ///
328    /// # Return
329    /// Returns true if the character is [a breakz], false otherwise.
330    ///
331    /// [`lookahead`]: Input::lookahead
332    /// [a breakz]: is_breakz
333    #[inline]
334    fn next_is_breakz(&self) -> bool {
335        is_breakz(self.peek())
336    }
337
338    /// Check whether the next character is [a z].
339    ///
340    /// The character must have previously been fetched through [`lookahead`]
341    ///
342    /// # Return
343    /// Returns true if the character is [a z], false otherwise.
344    ///
345    /// [`lookahead`]: Input::lookahead
346    /// [a z]: is_z
347    #[inline]
348    fn next_is_z(&self) -> bool {
349        is_z(self.peek())
350    }
351
352    /// Check whether the next character is [a flow].
353    ///
354    /// The character must have previously been fetched through [`lookahead`]
355    ///
356    /// # Return
357    /// Returns true if the character is [a flow], false otherwise.
358    ///
359    /// [`lookahead`]: Input::lookahead
360    /// [a flow]: is_flow
361    #[inline]
362    fn next_is_flow(&self) -> bool {
363        is_flow(self.peek())
364    }
365
366    /// Check whether the next character is [a digit].
367    ///
368    /// The character must have previously been fetched through [`lookahead`]
369    ///
370    /// # Return
371    /// Returns true if the character is [a digit], false otherwise.
372    ///
373    /// [`lookahead`]: Input::lookahead
374    /// [a digit]: is_digit
375    #[inline]
376    fn next_is_digit(&self) -> bool {
377        is_digit(self.peek())
378    }
379
380    /// Check whether the next character is [a letter].
381    ///
382    /// The character must have previously been fetched through [`lookahead`]
383    ///
384    /// # Return
385    /// Returns true if the character is [a letter], false otherwise.
386    ///
387    /// [`lookahead`]: Input::lookahead
388    /// [a letter]: is_alpha
389    #[inline]
390    fn next_is_alpha(&self) -> bool {
391        is_alpha(self.peek())
392    }
393
394    /// Skip characters from the input until a [breakz] or a byte order mark is found.
395    ///
396    /// The characters are consumed from the input.
397    ///
398    /// # Return
399    /// Return the number of characters that were consumed. The number of characters returned can
400    /// be used to advance the index and column, since no end-of-line character will be consumed.
401    ///
402    /// [breakz]: is_breakz
403    #[inline]
404    fn skip_while_non_breakz(&mut self) -> usize {
405        let mut count = 0;
406        while is_yaml_non_break(self.look_ch()) {
407            count += 1;
408            self.skip();
409        }
410        count
411    }
412
413    /// Skip characters from the input while [blanks] are found.
414    ///
415    /// The characters are consumed from the input.
416    ///
417    /// # Return
418    /// Return the number of characters that were consumed. The number of characters returned can
419    /// be used to advance the index and column, since no end-of-line character will be consumed.
420    ///
421    /// [blanks]: is_blank
422    fn skip_while_blank(&mut self) -> usize {
423        let mut n_chars = 0;
424        while is_blank(self.look_ch()) {
425            n_chars += 1;
426            self.skip();
427        }
428        n_chars
429    }
430
431    /// Fetch characters from the input while we encounter letters and store them in `out`.
432    ///
433    /// The characters are consumed from the input.
434    ///
435    /// # Return
436    /// Return the number of characters that were consumed. The number of characters returned can
437    /// be used to advance the index and column, since no end-of-line character will be consumed.
438    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
439        let mut n_chars = 0;
440        while is_alpha(self.look_ch()) {
441            n_chars += 1;
442            out.push(self.peek());
443            self.skip();
444        }
445        n_chars
446    }
447
448    /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
449    ///
450    /// The characters are consumed from the input.
451    ///
452    /// # Return
453    /// Return the number of characters that were consumed. The number of characters returned can
454    /// be used to advance the index and column, since no end-of-line character will be consumed.
455    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
456        let mut n_chars = 0;
457        while crate::char_traits::is_yaml_non_space(self.look_ch()) {
458            n_chars += 1;
459            out.push(self.peek());
460            self.skip();
461        }
462        n_chars
463    }
464}
465
466/// Behavior to adopt regarding treating tabs as whitespace.
467///
468/// Although tab is a valid yaml whitespace, it doesn't always behave the same as a space.
469#[derive(Copy, Clone, Eq, PartialEq)]
470pub enum SkipTabs {
471    /// Skip all tabs as whitespace.
472    Yes,
473    /// Don't skip any tab. Return from the function when encountering one.
474    No,
475    /// Return value from the function.
476    Result(
477        /// Whether tabs were encountered.
478        bool,
479        /// Whether at least 1 valid yaml whitespace has been encountered.
480        bool,
481    ),
482}
483
484impl SkipTabs {
485    /// Whether tabs were found while skipping whitespace.
486    ///
487    /// This function must be called after a call to `skip_ws_to_eol`.
488    #[must_use]
489    pub fn found_tabs(self) -> bool {
490        matches!(self, SkipTabs::Result(true, _))
491    }
492
493    /// Whether a valid YAML whitespace has been found in skipped-over content.
494    ///
495    /// This function must be called after a call to `skip_ws_to_eol`.
496    #[must_use]
497    pub fn has_valid_yaml_ws(self) -> bool {
498        matches!(self, SkipTabs::Result(_, true))
499    }
500}