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