Skip to main content

granit_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::error::ErrorKind;
9
10pub(crate) mod buffered;
11pub(crate) mod str;
12
13#[allow(clippy::module_name_repetitions)]
14pub use buffered::{BufferedInput, FallibleBufferedInput};
15
16/// A trait for inputs that can provide borrowed slices with a specific lifetime.
17///
18/// This trait enables zero-copy (`Cow::Borrowed`) token values for inputs that keep a stable
19/// backing string. The key difference from [`Input::slice_bytes`] is that this method returns
20/// a slice with the input's original lifetime `'a`, not tied to `&self`.
21///
22/// For inputs that support zero-copy (like [`str::StrInput`]), this returns `Some(&'a str)`.
23/// For streaming inputs that don't have stable backing storage, this returns `None`.
24pub trait BorrowedInput<'a>: Input {
25    /// Return a borrowed slice of the underlying source between two byte offsets.
26    ///
27    /// Unlike [`Input::slice_bytes`], this returns a slice with the input's lifetime `'a`,
28    /// allowing the slice to outlive the borrow of `&self`.
29    ///
30    /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
31    /// half-open: `[start, end)`.
32    ///
33    /// Returns `None` if the input does not support zero-copy slicing.
34    ///
35    /// # Panics
36    /// Implementations may panic in debug builds if `start` is greater than `end` or `end` is past
37    /// the end of the underlying source.
38    #[track_caller]
39    #[must_use]
40    fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'a str>;
41}
42
43pub use crate::char_traits::{
44    is_alpha, is_blank, is_blank_or_breakz, is_break, is_breakz, is_digit, is_flow, is_z,
45};
46
47/// Interface for a source of characters.
48///
49/// Hiding the input's implementation behind this trait allows input-specific optimizations, such
50/// as using `str` methods instead of manually transferring one `char` at a time to a buffer.
51/// Implementations with stable backing storage can also return borrowed `&str` slices and avoid
52/// allocating token values.
53pub trait Input {
54    /// A hint to the input source that we will need to read `count` characters.
55    ///
56    /// If the input is exhausted, `\0` can be used to pad the last characters and later returned.
57    /// The characters must not be consumed, but may be placed in an internal buffer.
58    ///
59    /// This method may be a no-op if buffering yields no performance improvement.
60    ///
61    /// Implementers of [`Input`] must _not_ expose a lookahead window larger than
62    /// [`Input::bufmaxlen`]. They may retain a larger window requested by an earlier call; callers
63    /// should use [`Input::buflen`] to observe the currently available window.
64    fn lookahead(&mut self, count: usize);
65
66    /// Return the number of characters in the active lookahead window.
67    ///
68    /// This is the number of characters that the input promises can be read through [`peek`] and
69    /// [`peek_nth`] after prior [`lookahead`] calls. It is not necessarily the number of source
70    /// characters remaining: inputs may keep the window available after consuming characters and
71    /// may pad positions past EOF with `\0`.
72    ///
73    /// [`lookahead`]: Input::lookahead
74    /// [`peek`]: Input::peek
75    /// [`peek_nth`]: Input::peek_nth
76    #[must_use]
77    fn buflen(&self) -> usize;
78
79    /// Return the maximum number of characters this input can buffer for lookahead.
80    #[must_use]
81    fn bufmaxlen(&self) -> usize;
82
83    /// Return whether the active lookahead window is empty.
84    ///
85    /// This is equivalent to `self.buflen() == 0`. It does not mean the underlying source is
86    /// exhausted: after a previous [`lookahead`] call, an input may keep a non-empty lookahead
87    /// window available even after all source characters have been consumed, with positions past
88    /// EOF observed as `\0`.
89    ///
90    /// [`lookahead`]: Input::lookahead
91    #[inline]
92    #[must_use]
93    fn buf_is_empty(&self) -> bool {
94        self.buflen() == 0
95    }
96
97    /// Read the next character from the logical input stream and return it directly.
98    ///
99    /// If an implementation has already fetched characters for lookahead, this consumes the
100    /// buffered stream front before reading farther from the underlying source.
101    #[must_use]
102    fn raw_read_ch(&mut self) -> char;
103
104    /// Read a non-breakz character from the input stream and return it directly.
105    ///
106    /// If an implementation has already fetched characters for lookahead, this consumes from the
107    /// buffered stream front before reading farther from the underlying source.
108    ///
109    /// If the next character is a breakz, it is either not consumed or placed into the buffer (if
110    /// any).
111    #[must_use]
112    fn raw_read_non_breakz_ch(&mut self) -> Option<char>;
113
114    /// Consume the next character.
115    fn skip(&mut self);
116
117    /// Consume the next `count` characters.
118    fn skip_n(&mut self, count: usize);
119
120    /// Return the next character, without consuming it.
121    ///
122    /// Users of the [`Input`] must make sure that the character has been loaded through a prior
123    /// call to [`Input::lookahead`]. Implementors of [`Input`] may assume that a valid call to
124    /// [`Input::lookahead`] has been made beforehand.
125    ///
126    /// # Return
127    /// If the input source is not exhausted, returns the next character to be fed into the
128    /// scanner. Otherwise, returns `\0`.
129    #[must_use]
130    fn peek(&self) -> char;
131
132    /// Return the `n`-th character in the buffer, without consuming it.
133    ///
134    /// This function assumes that the `n`-th character in the input has already been fetched through
135    /// [`Input::lookahead`].
136    #[must_use]
137    fn peek_nth(&self, n: usize) -> char;
138
139    /// Return the current byte offset in the underlying source, if available.
140    ///
141    /// This is an *optional* capability that enables zero-copy (`Cow::Borrowed`) token values
142    /// for inputs that keep a stable backing string (notably [`str::StrInput`]).
143    ///
144    /// The returned value (when `Some`) is the number of bytes that have been consumed so far,
145    /// i.e. an offset into the original source string.
146    ///
147    /// # Correctness contract
148    /// Implementations returning `Some(_)` must satisfy all of the following:
149    ///
150    /// - The offset is a valid UTF-8 boundary in the underlying source.
151    /// - The offset is monotonically non-decreasing as characters are consumed.
152    /// - The underlying source is stable for the duration of parsing (no reallocation/mutation)
153    ///   so that slices returned by [`Input::slice_bytes`] remain valid.
154    ///
155    /// Inputs that cannot provide stable slicing (e.g. stream/iterator inputs) must return
156    /// `None`.
157    #[inline]
158    #[must_use]
159    fn byte_offset(&self) -> Option<usize> {
160        None
161    }
162
163    /// Return a borrowed slice of the underlying source between two byte offsets.
164    ///
165    /// This is an *optional* capability used to produce `Cow::Borrowed` values without
166    /// allocating.
167    ///
168    /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
169    /// half-open: `[start, end)`.
170    ///
171    /// # Correctness contract
172    /// Implementations returning `Some(&str)` must ensure:
173    ///
174    /// - `start <= end`.
175    /// - Both offsets are valid UTF-8 boundaries.
176    /// - The returned `&str` is a view into the stable underlying source associated with this
177    ///   input.
178    ///
179    /// Implementations that return `None` from [`Input::byte_offset`] must also return `None`
180    /// here.
181    ///
182    /// # Panics
183    /// Implementations may panic in debug builds if `start` is greater than `end` or `end` is past
184    /// the end of the underlying source.
185    #[track_caller]
186    #[inline]
187    #[must_use]
188    fn slice_bytes(&self, _start: usize, _end: usize) -> Option<&str> {
189        None
190    }
191
192    /// Return whether this input may contain a `#` character.
193    ///
194    /// This is a conservative performance hint. Inputs that cannot answer cheaply should return
195    /// `true`, which keeps full comment handling enabled.
196    #[inline]
197    #[must_use]
198    fn may_contain_comments(&self) -> bool {
199        true
200    }
201
202    /// Take a terminal error reported by the underlying source.
203    ///
204    /// Infallible inputs use the default implementation. Fallible streaming inputs latch their
205    /// first source error and return it here so the scanner can distinguish the failure from clean
206    /// end-of-input. Once an implementation reports an error, it must not read from its source
207    /// again.
208    ///
209    /// Source adapters should use an input-related [`ErrorKind`] such as
210    /// [`ErrorKind::InputIo`], [`ErrorKind::InputDecoding`], or
211    /// [`ErrorKind::InputByteLimitExceeded`].
212    #[inline]
213    fn take_source_error(&mut self) -> Option<ErrorKind> {
214        None
215    }
216
217    /// Look for the next character and return it.
218    ///
219    /// The character is not consumed.
220    /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
221    #[inline]
222    #[must_use]
223    fn look_ch(&mut self) -> char {
224        self.lookahead(1);
225        self.peek()
226    }
227
228    /// Return whether the next character in the input source is equal to `c`.
229    ///
230    /// This function assumes that the next character in the input has already been fetched through
231    /// [`Input::lookahead`].
232    #[inline]
233    #[must_use]
234    fn next_char_is(&self, c: char) -> bool {
235        self.peek() == c
236    }
237
238    /// Return whether the `n`-th character in the input source is equal to `c`.
239    ///
240    /// This function assumes that the `n`-th character in the input has already been fetched through
241    /// [`Input::lookahead`].
242    #[inline]
243    #[must_use]
244    fn nth_char_is(&self, n: usize, c: char) -> bool {
245        self.peek_nth(n) == c
246    }
247
248    /// Return whether the next 2 characters in the input source match the given characters.
249    ///
250    /// This function assumes that the next 2 characters in the input have already been fetched
251    /// through [`Input::lookahead`].
252    ///
253    /// # Panics
254    /// Panics if the active lookahead window contains fewer than 2 characters.
255    #[track_caller]
256    #[inline]
257    #[must_use]
258    fn next_2_are(&self, c1: char, c2: char) -> bool {
259        assert!(self.buflen() >= 2);
260        self.peek() == c1 && self.peek_nth(1) == c2
261    }
262
263    /// Return whether the next 3 characters in the input source match the given characters.
264    ///
265    /// This function assumes that the next 3 characters in the input have already been fetched
266    /// through [`Input::lookahead`].
267    ///
268    /// # Panics
269    /// Panics if the active lookahead window contains fewer than 3 characters.
270    #[track_caller]
271    #[inline]
272    #[must_use]
273    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
274        assert!(self.buflen() >= 3);
275        self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
276    }
277
278    /// Check whether the next characters correspond to a document indicator.
279    ///
280    /// This function assumes that the next 4 characters in the input have already been fetched
281    /// through [`Input::lookahead`].
282    ///
283    /// # Panics
284    /// Panics if the active lookahead window contains fewer than 4 characters.
285    #[track_caller]
286    #[inline]
287    #[must_use]
288    fn next_is_document_indicator(&self) -> bool {
289        assert!(self.buflen() >= 4);
290        is_blank_or_breakz(self.peek_nth(3))
291            && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
292    }
293
294    /// Check whether the next characters correspond to a start of document.
295    ///
296    /// This function assumes that the next 4 characters in the input have already been fetched
297    /// through [`Input::lookahead`].
298    ///
299    /// # Panics
300    /// Panics if the active lookahead window contains fewer than 4 characters.
301    #[track_caller]
302    #[inline]
303    #[must_use]
304    fn next_is_document_start(&self) -> bool {
305        assert!(self.buflen() >= 4);
306        self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
307    }
308
309    /// Check whether the next characters correspond to an end of document.
310    ///
311    /// This function assumes that the next 4 characters in the input have already been fetched
312    /// through [`Input::lookahead`].
313    ///
314    /// # Panics
315    /// Panics if the active lookahead window contains fewer than 4 characters.
316    #[track_caller]
317    #[inline]
318    #[must_use]
319    fn next_is_document_end(&self) -> bool {
320        assert!(self.buflen() >= 4);
321        self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
322    }
323
324    /// Skip YAML whitespace up to the end of the current line.
325    ///
326    /// Inline comments are consumed only after at least one preceding YAML whitespace character.
327    ///
328    /// # Return
329    /// Return a tuple with the number of characters that were consumed and the result of skipping
330    /// whitespace. The number of characters returned can be used to advance the index and column,
331    /// since no end-of-line character will be consumed.
332    /// See [`SkipTabs`] for more details on the success variant.
333    ///
334    /// # Errors
335    /// Returns [`ErrorKind::CommentNotSeparated`] if a comment is encountered without preceding
336    /// whitespace. In that event, the first tuple element contains the number of characters
337    /// consumed prior to reaching the `#`.
338    ///
339    /// # Panics
340    /// Panics if `skip_tabs` is [`SkipTabs::Result`], which is an output-only variant.
341    #[track_caller]
342    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, ErrorKind>) {
343        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
344
345        let mut encountered_tab = false;
346        let mut has_yaml_ws = false;
347        let mut chars_consumed = 0;
348        loop {
349            match self.look_ch() {
350                ' ' => {
351                    has_yaml_ws = true;
352                    self.skip();
353                }
354                '\t' if skip_tabs != SkipTabs::No => {
355                    encountered_tab = true;
356                    self.skip();
357                }
358                // YAML comments must be preceded by whitespace.
359                '#' if !encountered_tab && !has_yaml_ws => {
360                    return (chars_consumed, Err(ErrorKind::CommentNotSeparated));
361                }
362                '#' => {
363                    self.skip(); // Skip over '#'
364                    while !is_breakz(self.look_ch()) {
365                        self.skip();
366                        chars_consumed += 1;
367                    }
368                }
369                _ => break,
370            }
371            chars_consumed += 1;
372        }
373
374        (
375            chars_consumed,
376            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
377        )
378    }
379
380    /// Skip YAML blank characters, stopping before comments, line breaks, or other content.
381    ///
382    /// This is the comment-aware counterpart to [`Input::skip_ws_to_eol`]: it preserves a
383    /// following `#` for the scanner to tokenize while still letting input implementations batch
384    /// the common run of spaces and tabs.
385    ///
386    /// # Return
387    /// Returns the number of consumed characters and a [`SkipTabs::Result`] describing whether
388    /// tabs and valid YAML whitespace (` `) were encountered.
389    ///
390    /// # Panics
391    /// Panics if `skip_tabs` is [`SkipTabs::Result`], which is an output-only variant.
392    #[track_caller]
393    fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, SkipTabs) {
394        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
395
396        let mut encountered_tab = false;
397        let mut has_yaml_ws = false;
398        let mut chars_consumed = 0;
399
400        loop {
401            match self.look_ch() {
402                ' ' => {
403                    has_yaml_ws = true;
404                    chars_consumed += 1;
405                    self.skip();
406                }
407                '\t' if skip_tabs != SkipTabs::No => {
408                    encountered_tab = true;
409                    chars_consumed += 1;
410                    self.skip();
411                }
412                _ => break,
413            }
414        }
415
416        (
417            chars_consumed,
418            SkipTabs::Result(encountered_tab, has_yaml_ws),
419        )
420    }
421
422    /// Check whether the next characters may be part of a plain scalar.
423    ///
424    /// This function assumes we are not given a blankz character.
425    #[allow(clippy::inline_always)]
426    #[inline(always)]
427    #[must_use]
428    fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
429        let nc = self.peek_nth(1);
430        match self.peek() {
431            // indicators can end a plain scalar, see 7.3.3. Plain Style
432            ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
433            c if in_flow && is_flow(c) => false,
434            _ => true,
435        }
436    }
437
438    /// Check whether the next character is [a blank] or [a break].
439    ///
440    /// The character must have previously been fetched through [`lookahead`]
441    ///
442    /// # Return
443    /// Returns true if the character is [a blank] or [a break], false otherwise.
444    ///
445    /// [`lookahead`]: Input::lookahead
446    /// [a blank]: is_blank
447    /// [a break]: is_break
448    #[inline]
449    #[must_use]
450    fn next_is_blank_or_break(&self) -> bool {
451        is_blank(self.peek()) || is_break(self.peek())
452    }
453
454    /// Check whether the next character is [a blank] or [a breakz].
455    ///
456    /// The character must have previously been fetched through [`lookahead`]
457    ///
458    /// # Return
459    /// Returns true if the character is [a blank] or [a break], false otherwise.
460    ///
461    /// [`lookahead`]: Input::lookahead
462    /// [a blank]: is_blank
463    /// [a breakz]: is_breakz
464    #[inline]
465    #[must_use]
466    fn next_is_blank_or_breakz(&self) -> bool {
467        is_blank(self.peek()) || is_breakz(self.peek())
468    }
469
470    /// Check whether the next character is [a blank].
471    ///
472    /// The character must have previously been fetched through [`lookahead`]
473    ///
474    /// # Return
475    /// Returns true if the character is [a blank], false otherwise.
476    ///
477    /// [`lookahead`]: Input::lookahead
478    /// [a blank]: is_blank
479    #[inline]
480    #[must_use]
481    fn next_is_blank(&self) -> bool {
482        is_blank(self.peek())
483    }
484
485    /// Check whether the next character is [a break].
486    ///
487    /// The character must have previously been fetched through [`lookahead`]
488    ///
489    /// # Return
490    /// Returns true if the character is [a break], false otherwise.
491    ///
492    /// [`lookahead`]: Input::lookahead
493    /// [a break]: is_break
494    #[inline]
495    #[must_use]
496    fn next_is_break(&self) -> bool {
497        is_break(self.peek())
498    }
499
500    /// Check whether the next character is [a breakz].
501    ///
502    /// The character must have previously been fetched through [`lookahead`]
503    ///
504    /// # Return
505    /// Returns true if the character is [a breakz], false otherwise.
506    ///
507    /// [`lookahead`]: Input::lookahead
508    /// [a breakz]: is_breakz
509    #[inline]
510    #[must_use]
511    fn next_is_breakz(&self) -> bool {
512        is_breakz(self.peek())
513    }
514
515    /// Check whether the input is at its physical end.
516    ///
517    /// The default implementation infers end-of-input from the `\0` sentinel returned by
518    /// [`Self::peek`]. Inputs that can distinguish a literal NUL from end-of-input should override
519    /// this method.
520    /// The next position must have previously been fetched through [`Self::lookahead`].
521    ///
522    /// # Return
523    /// Returns true if the input is exhausted, false otherwise.
524    #[inline]
525    #[must_use]
526    fn next_is_z(&self) -> bool {
527        is_z(self.peek())
528    }
529
530    /// Check whether the next character is [a flow].
531    ///
532    /// The character must have previously been fetched through [`lookahead`]
533    ///
534    /// # Return
535    /// Returns true if the character is [a flow], false otherwise.
536    ///
537    /// [`lookahead`]: Input::lookahead
538    /// [a flow]: is_flow
539    #[inline]
540    #[must_use]
541    fn next_is_flow(&self) -> bool {
542        is_flow(self.peek())
543    }
544
545    /// Check whether the next character is [a digit].
546    ///
547    /// The character must have previously been fetched through [`lookahead`]
548    ///
549    /// # Return
550    /// Returns true if the character is [a digit], false otherwise.
551    ///
552    /// [`lookahead`]: Input::lookahead
553    /// [a digit]: is_digit
554    #[inline]
555    #[must_use]
556    fn next_is_digit(&self) -> bool {
557        is_digit(self.peek())
558    }
559
560    /// Check whether the next character is [a letter].
561    ///
562    /// The character must have previously been fetched through [`lookahead`]
563    ///
564    /// # Return
565    /// Returns true if the character is [a letter], false otherwise.
566    ///
567    /// [`lookahead`]: Input::lookahead
568    /// [a letter]: is_alpha
569    #[inline]
570    #[must_use]
571    fn next_is_alpha(&self) -> bool {
572        is_alpha(self.peek())
573    }
574
575    /// Skip printable characters until a [breakz] or non-printable character is found.
576    ///
577    /// The stopping character is not consumed.
578    ///
579    /// # Return
580    /// Return the number of characters that were consumed. The number of characters returned can
581    /// be used to advance the index and column, since no end-of-line character will be consumed.
582    ///
583    /// [breakz]: is_breakz
584    #[inline]
585    fn skip_while_non_breakz(&mut self) -> usize {
586        let mut count = 0;
587        while {
588            let c = self.look_ch();
589            !is_breakz(c) && crate::char_traits::is_printable(c)
590        } {
591            count += 1;
592            self.skip();
593        }
594        count
595    }
596
597    /// Skip characters from the input while [blanks] are found.
598    ///
599    /// The characters are consumed from the input.
600    ///
601    /// # Return
602    /// Return the number of characters that were consumed. The number of characters returned can
603    /// be used to advance the index and column, since no end-of-line character will be consumed.
604    ///
605    /// [blanks]: is_blank
606    fn skip_while_blank(&mut self) -> usize {
607        let mut n_bytes = 0;
608        while is_blank(self.look_ch()) {
609            n_bytes += self.peek().len_utf8();
610            self.skip();
611        }
612        n_bytes
613    }
614
615    /// Fetch characters from the input while we encounter letters and store them in `out`.
616    ///
617    /// The characters are consumed from the input.
618    ///
619    /// # Return
620    /// Return the number of characters that were consumed. The number of characters returned can
621    /// be used to advance the index and column, since no end-of-line character will be consumed.
622    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
623        let mut n_bytes = 0;
624        while is_alpha(self.look_ch()) {
625            let c = self.peek();
626            n_bytes += c.len_utf8();
627            out.push(c);
628            self.skip();
629        }
630        n_bytes
631    }
632
633    /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
634    ///
635    /// The characters are consumed from the input.
636    ///
637    /// # Return
638    /// Return the number of characters that were consumed. The number of characters returned can
639    /// be used to advance the index and column, since no end-of-line character will be consumed.
640    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
641        let mut chars_consumed = 0;
642        loop {
643            let c = self.look_ch();
644            if !crate::char_traits::is_yaml_non_space(c) || is_z(c) {
645                break;
646            }
647            let c = self.peek();
648            out.push(c);
649            self.skip();
650            chars_consumed += 1;
651        }
652        chars_consumed
653    }
654
655    /// Fetch a chunk of plain scalar characters.
656    ///
657    /// This optimization method allows the input to batch process characters.
658    /// Returns (stopped, `chars_consumed`).
659    /// stopped is true if the chunk ended because of a non-plain-scalar character.
660    fn fetch_plain_scalar_chunk(
661        &mut self,
662        out: &mut String,
663        count: usize,
664        flow_level_gt_0: bool,
665    ) -> (bool, usize) {
666        let mut chars_consumed = 0;
667        for _ in 0..count {
668            self.lookahead(1);
669            if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) {
670                return (true, chars_consumed);
671            }
672            out.push(self.peek());
673            self.skip();
674            chars_consumed += 1;
675        }
676        (false, chars_consumed)
677    }
678
679    /// Consume a chunk of plain-scalar characters without materializing them.
680    ///
681    /// Stable inputs use this while the scanner retains the source as a borrowed slice and only
682    /// promotes it to an owned buffer if YAML folding changes the scalar contents.
683    fn skip_plain_scalar_chunk(&mut self, count: usize, flow_level_gt_0: bool) -> (bool, usize) {
684        let mut chars_consumed = 0;
685        for _ in 0..count {
686            self.lookahead(1);
687            if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) {
688                return (true, chars_consumed);
689            }
690            self.skip();
691            chars_consumed += 1;
692        }
693        (false, chars_consumed)
694    }
695}
696
697/// Behavior to adopt regarding treating tabs as whitespace.
698///
699/// Although tab is valid YAML whitespace, it does not always behave the same as a space.
700#[derive(Copy, Clone, Eq, PartialEq)]
701pub enum SkipTabs {
702    /// Skip all tabs as whitespace.
703    Yes,
704    /// Don't skip any tab. Return from the function when encountering one.
705    No,
706    /// Return value from the function.
707    Result(
708        /// Whether tabs were encountered.
709        bool,
710        /// Whether at least one valid YAML whitespace character has been encountered.
711        bool,
712    ),
713}
714
715impl SkipTabs {
716    /// Whether tabs were found while skipping whitespace.
717    ///
718    /// This function must be called after a call to `skip_ws_to_eol`.
719    #[must_use]
720    pub fn found_tabs(self) -> bool {
721        matches!(self, SkipTabs::Result(true, _))
722    }
723
724    /// Whether a valid YAML whitespace has been found in skipped-over content.
725    ///
726    /// This function must be called after a call to `skip_ws_to_eol`.
727    #[must_use]
728    pub fn has_valid_yaml_ws(self) -> bool {
729        matches!(self, SkipTabs::Result(_, true))
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use super::{Input, SkipTabs};
736    use crate::error::ErrorKind;
737
738    struct MinimalInput;
739
740    impl Input for MinimalInput {
741        fn lookahead(&mut self, _count: usize) {}
742
743        fn buflen(&self) -> usize {
744            0
745        }
746
747        fn bufmaxlen(&self) -> usize {
748            0
749        }
750
751        fn raw_read_ch(&mut self) -> char {
752            '\0'
753        }
754
755        fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
756            None
757        }
758
759        fn skip(&mut self) {}
760
761        fn skip_n(&mut self, _count: usize) {}
762
763        fn peek(&self) -> char {
764            '\0'
765        }
766
767        fn peek_nth(&self, _n: usize) -> char {
768            '\0'
769        }
770    }
771
772    #[test]
773    fn default_slice_bytes_returns_none() {
774        let mut input = MinimalInput;
775
776        input.lookahead(4);
777        assert_eq!(input.buflen(), 0);
778        assert_eq!(input.bufmaxlen(), 0);
779        assert_eq!(input.raw_read_ch(), '\0');
780        assert_eq!(input.raw_read_non_breakz_ch(), None);
781        input.skip();
782        input.skip_n(2);
783        assert_eq!(input.peek(), '\0');
784        assert_eq!(input.peek_nth(1), '\0');
785        assert_eq!(input.byte_offset(), None);
786        assert_eq!(input.slice_bytes(0, 0), None);
787    }
788
789    #[test]
790    fn default_skip_ws_to_eol_rejects_unseparated_comment() {
791        let mut input = super::buffered::BufferedInput::new("#comment\n".chars());
792
793        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
794
795        assert_eq!(consumed, 0);
796        assert_eq!(result.err(), Some(ErrorKind::CommentNotSeparated));
797        assert_eq!(input.peek(), '#');
798    }
799}