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.
53///
54/// # Scalar scanning hooks
55///
56/// [`Self::fetch_block_scalar_line`] and [`Self::take_quoted_scalar_ascii_chunk`] allow bulk
57/// processing of block and quoted scalar content. Both provide defaults: custom inputs need
58/// only override them when they can offer a more efficient implementation.
59///
60/// Overrides must consume from the logical stream front, including any characters already
61/// buffered for lookahead, and keep [`Self::byte_offset`] accurate if it is supported. They
62/// must work without a preceding [`Self::lookahead`] call; callers refresh lookahead before
63/// inspecting subsequent characters. Source errors must remain available through
64/// [`Self::take_source_error`], and a terminal source error must prevent further source reads.
65/// The scanner, not the input hook, updates line/column markers and interprets YAML syntax.
66pub trait Input {
67    /// A hint to the input source that we will need to read `count` characters.
68    ///
69    /// If the input is exhausted, `\0` can be used to pad the last characters and later returned.
70    /// The characters must not be consumed, but may be placed in an internal buffer.
71    ///
72    /// This method may be a no-op if buffering yields no performance improvement.
73    ///
74    /// Implementers of [`Input`] must _not_ expose a lookahead window larger than
75    /// [`Input::bufmaxlen`]. They may retain a larger window requested by an earlier call; callers
76    /// should use [`Input::buflen`] to observe the currently available window.
77    fn lookahead(&mut self, count: usize);
78
79    /// Return the number of characters in the active lookahead window.
80    ///
81    /// This is the number of characters that the input promises can be read through [`peek`] and
82    /// [`peek_nth`] after prior [`lookahead`] calls. It is not necessarily the number of source
83    /// characters remaining: inputs may keep the window available after consuming characters and
84    /// may pad positions past EOF with `\0`.
85    ///
86    /// [`lookahead`]: Input::lookahead
87    /// [`peek`]: Input::peek
88    /// [`peek_nth`]: Input::peek_nth
89    #[must_use]
90    fn buflen(&self) -> usize;
91
92    /// Return the maximum number of characters this input can buffer for lookahead.
93    #[must_use]
94    fn bufmaxlen(&self) -> usize;
95
96    /// Return whether the active lookahead window is empty.
97    ///
98    /// This is equivalent to `self.buflen() == 0`. It does not mean the underlying source is
99    /// exhausted: after a previous [`lookahead`] call, an input may keep a non-empty lookahead
100    /// window available even after all source characters have been consumed, with positions past
101    /// EOF observed as `\0`.
102    ///
103    /// [`lookahead`]: Input::lookahead
104    #[inline]
105    #[must_use]
106    fn buf_is_empty(&self) -> bool {
107        self.buflen() == 0
108    }
109
110    /// Read the next character from the logical input stream and return it directly.
111    ///
112    /// If an implementation has already fetched characters for lookahead, this consumes the
113    /// buffered stream front before reading farther from the underlying source.
114    #[must_use]
115    fn raw_read_ch(&mut self) -> char;
116
117    /// Read a non-breakz character from the input stream and return it directly.
118    ///
119    /// If an implementation has already fetched characters for lookahead, this consumes from the
120    /// buffered stream front before reading farther from the underlying source.
121    ///
122    /// If the next character is a breakz, it is either not consumed or placed into the buffer (if
123    /// any).
124    #[must_use]
125    fn raw_read_non_breakz_ch(&mut self) -> Option<char>;
126
127    /// Consume the next character.
128    fn skip(&mut self);
129
130    /// Consume the next `count` characters.
131    fn skip_n(&mut self, count: usize);
132
133    /// Return the next character, without consuming it.
134    ///
135    /// Users of the [`Input`] must make sure that the character has been loaded through a prior
136    /// call to [`Input::lookahead`]. Implementors of [`Input`] may assume that a valid call to
137    /// [`Input::lookahead`] has been made beforehand.
138    ///
139    /// # Return
140    /// If the input source is not exhausted, returns the next character to be fed into the
141    /// scanner. Otherwise, returns `\0`.
142    #[must_use]
143    fn peek(&self) -> char;
144
145    /// Return the `n`-th character in the buffer, without consuming it.
146    ///
147    /// This function assumes that the `n`-th character in the input has already been fetched through
148    /// [`Input::lookahead`].
149    #[must_use]
150    fn peek_nth(&self, n: usize) -> char;
151
152    /// Return the current byte offset in the underlying source, if available.
153    ///
154    /// This is an *optional* capability that enables zero-copy (`Cow::Borrowed`) token values
155    /// for inputs that keep a stable backing string (notably [`str::StrInput`]).
156    ///
157    /// The returned value (when `Some`) is the number of bytes that have been consumed so far,
158    /// i.e. an offset into the original source string.
159    ///
160    /// # Correctness contract
161    /// Implementations returning `Some(_)` must satisfy all of the following:
162    ///
163    /// - The offset is a valid UTF-8 boundary in the underlying source.
164    /// - The offset is monotonically non-decreasing as characters are consumed.
165    /// - The underlying source is stable for the duration of parsing (no reallocation/mutation)
166    ///   so that slices returned by [`Input::slice_bytes`] remain valid.
167    ///
168    /// Inputs that cannot provide stable slicing (e.g. stream/iterator inputs) must return
169    /// `None`.
170    #[inline]
171    #[must_use]
172    fn byte_offset(&self) -> Option<usize> {
173        None
174    }
175
176    /// Return a borrowed slice of the underlying source between two byte offsets.
177    ///
178    /// This is an *optional* capability used to produce `Cow::Borrowed` values without
179    /// allocating.
180    ///
181    /// `start` and `end` are byte offsets as returned by [`Input::byte_offset`]. The interval is
182    /// half-open: `[start, end)`.
183    ///
184    /// # Correctness contract
185    /// Implementations returning `Some(&str)` must ensure:
186    ///
187    /// - `start <= end`.
188    /// - Both offsets are valid UTF-8 boundaries.
189    /// - The returned `&str` is a view into the stable underlying source associated with this
190    ///   input.
191    ///
192    /// Implementations that return `None` from [`Input::byte_offset`] must also return `None`
193    /// here.
194    ///
195    /// # Panics
196    /// Implementations may panic in debug builds if `start` is greater than `end` or `end` is past
197    /// the end of the underlying source.
198    #[track_caller]
199    #[inline]
200    #[must_use]
201    fn slice_bytes(&self, _start: usize, _end: usize) -> Option<&str> {
202        None
203    }
204
205    /// Return whether this input may contain a `#` character.
206    ///
207    /// This is a conservative performance hint. Inputs that cannot answer cheaply should return
208    /// `true`, which keeps full comment handling enabled.
209    #[cfg(feature = "comments")]
210    #[inline]
211    #[must_use]
212    fn may_contain_comments(&self) -> bool {
213        true
214    }
215
216    /// Take a terminal error reported by the underlying source.
217    ///
218    /// Infallible inputs use the default implementation. Fallible streaming inputs latch their
219    /// first source error and return it here so the scanner can distinguish the failure from clean
220    /// end-of-input. Once an implementation reports an error, it must not read from its source
221    /// again.
222    ///
223    /// Source adapters should use an input-related [`ErrorKind`] such as
224    /// [`ErrorKind::InputIo`], [`ErrorKind::InputDecoding`], or
225    /// [`ErrorKind::InputByteLimitExceeded`].
226    #[inline]
227    fn take_source_error(&mut self) -> Option<ErrorKind> {
228        None
229    }
230
231    /// Look for the next character and return it.
232    ///
233    /// The character is not consumed.
234    /// Equivalent to calling [`Input::lookahead`] and [`Input::peek`].
235    #[inline]
236    #[must_use]
237    fn look_ch(&mut self) -> char {
238        self.lookahead(1);
239        self.peek()
240    }
241
242    /// Return whether the next character in the input source is equal to `c`.
243    ///
244    /// This function assumes that the next character in the input has already been fetched through
245    /// [`Input::lookahead`].
246    #[inline]
247    #[must_use]
248    fn next_char_is(&self, c: char) -> bool {
249        self.peek() == c
250    }
251
252    /// Return whether the `n`-th character in the input source is equal to `c`.
253    ///
254    /// This function assumes that the `n`-th character in the input has already been fetched through
255    /// [`Input::lookahead`].
256    #[inline]
257    #[must_use]
258    fn nth_char_is(&self, n: usize, c: char) -> bool {
259        self.peek_nth(n) == c
260    }
261
262    /// Return whether the next 2 characters in the input source match the given characters.
263    ///
264    /// This function assumes that the next 2 characters in the input have already been fetched
265    /// through [`Input::lookahead`].
266    ///
267    /// # Panics
268    /// Panics if the active lookahead window contains fewer than 2 characters.
269    #[track_caller]
270    #[inline]
271    #[must_use]
272    fn next_2_are(&self, c1: char, c2: char) -> bool {
273        assert!(self.buflen() >= 2);
274        self.peek() == c1 && self.peek_nth(1) == c2
275    }
276
277    /// Return whether the next 3 characters in the input source match the given characters.
278    ///
279    /// This function assumes that the next 3 characters in the input have already been fetched
280    /// through [`Input::lookahead`].
281    ///
282    /// # Panics
283    /// Panics if the active lookahead window contains fewer than 3 characters.
284    #[track_caller]
285    #[inline]
286    #[must_use]
287    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
288        assert!(self.buflen() >= 3);
289        self.peek() == c1 && self.peek_nth(1) == c2 && self.peek_nth(2) == c3
290    }
291
292    /// Check whether the next characters correspond to a document indicator.
293    ///
294    /// This function assumes that the next 4 characters in the input have already been fetched
295    /// through [`Input::lookahead`].
296    ///
297    /// # Panics
298    /// Panics if the active lookahead window contains fewer than 4 characters.
299    #[track_caller]
300    #[inline]
301    #[must_use]
302    fn next_is_document_indicator(&self) -> bool {
303        assert!(self.buflen() >= 4);
304        is_blank_or_breakz(self.peek_nth(3))
305            && (self.next_3_are('.', '.', '.') || self.next_3_are('-', '-', '-'))
306    }
307
308    /// Check whether the next characters correspond to a start of document.
309    ///
310    /// This function assumes that the next 4 characters in the input have already been fetched
311    /// through [`Input::lookahead`].
312    ///
313    /// # Panics
314    /// Panics if the active lookahead window contains fewer than 4 characters.
315    #[track_caller]
316    #[inline]
317    #[must_use]
318    fn next_is_document_start(&self) -> bool {
319        assert!(self.buflen() >= 4);
320        self.next_3_are('-', '-', '-') && is_blank_or_breakz(self.peek_nth(3))
321    }
322
323    /// Check whether the next characters correspond to an end of document.
324    ///
325    /// This function assumes that the next 4 characters in the input have already been fetched
326    /// through [`Input::lookahead`].
327    ///
328    /// # Panics
329    /// Panics if the active lookahead window contains fewer than 4 characters.
330    #[track_caller]
331    #[inline]
332    #[must_use]
333    fn next_is_document_end(&self) -> bool {
334        assert!(self.buflen() >= 4);
335        self.next_3_are('.', '.', '.') && is_blank_or_breakz(self.peek_nth(3))
336    }
337
338    /// Skip YAML whitespace up to the end of the current line.
339    ///
340    /// Inline comments are consumed only after at least one preceding YAML whitespace character.
341    ///
342    /// # Return
343    /// Return a tuple with the number of characters that were consumed and the result of skipping
344    /// whitespace. The number of characters returned can be used to advance the index and column,
345    /// since no end-of-line character will be consumed.
346    /// The [`WhitespaceResult`] reports whether tabs and spaces were consumed.
347    ///
348    /// # Errors
349    /// Returns [`ErrorKind::CommentNotSeparated`] if a comment is encountered without preceding
350    /// whitespace. In that event, the first tuple element contains the number of characters
351    /// consumed prior to reaching the `#`.
352    fn skip_ws_to_eol(
353        &mut self,
354        skip_tabs: SkipTabs,
355    ) -> (usize, Result<WhitespaceResult, ErrorKind>) {
356        let mut encountered_tab = false;
357        let mut has_yaml_ws = false;
358        let mut chars_consumed = 0;
359        loop {
360            match self.look_ch() {
361                ' ' => {
362                    has_yaml_ws = true;
363                    self.skip();
364                }
365                '\t' if skip_tabs != SkipTabs::No => {
366                    encountered_tab = true;
367                    self.skip();
368                }
369                // YAML comments must be preceded by whitespace.
370                '#' if !encountered_tab && !has_yaml_ws => {
371                    return (chars_consumed, Err(ErrorKind::CommentNotSeparated));
372                }
373                '#' => {
374                    self.skip(); // Skip over '#'
375                    while !is_breakz(self.look_ch()) {
376                        self.skip();
377                        chars_consumed += 1;
378                    }
379                }
380                _ => break,
381            }
382            chars_consumed += 1;
383        }
384
385        (
386            chars_consumed,
387            Ok(WhitespaceResult::new(encountered_tab, has_yaml_ws)),
388        )
389    }
390
391    /// Skip YAML blank characters, stopping before comments, line breaks, or other content.
392    ///
393    /// This is the comment-aware counterpart to [`Input::skip_ws_to_eol`]: it preserves a
394    /// following `#` for the scanner to tokenize while still letting input implementations batch
395    /// the common run of spaces and tabs.
396    ///
397    /// # Return
398    /// Returns the number of consumed characters and a [`WhitespaceResult`] describing whether
399    /// tabs and spaces were consumed.
400    fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, WhitespaceResult) {
401        let mut encountered_tab = false;
402        let mut has_yaml_ws = false;
403        let mut chars_consumed = 0;
404
405        loop {
406            match self.look_ch() {
407                ' ' => {
408                    has_yaml_ws = true;
409                    chars_consumed += 1;
410                    self.skip();
411                }
412                '\t' if skip_tabs != SkipTabs::No => {
413                    encountered_tab = true;
414                    chars_consumed += 1;
415                    self.skip();
416                }
417                _ => break,
418            }
419        }
420
421        (
422            chars_consumed,
423            WhitespaceResult::new(encountered_tab, has_yaml_ws),
424        )
425    }
426
427    /// Check whether the next characters may be part of a plain scalar.
428    ///
429    /// This function assumes we are not given a blankz character.
430    #[allow(clippy::inline_always)]
431    #[inline(always)]
432    #[must_use]
433    fn next_can_be_plain_scalar(&self, in_flow: bool) -> bool {
434        let nc = self.peek_nth(1);
435        match self.peek() {
436            // indicators can end a plain scalar, see 7.3.3. Plain Style
437            ':' if is_blank_or_breakz(nc) || (in_flow && is_flow(nc)) => false,
438            c if in_flow && is_flow(c) => false,
439            _ => true,
440        }
441    }
442
443    /// Check whether the next character is [a blank] or [a break].
444    ///
445    /// The character must have previously been fetched through [`lookahead`]
446    ///
447    /// # Return
448    /// Returns true if the character is [a blank] or [a break], false otherwise.
449    ///
450    /// [`lookahead`]: Input::lookahead
451    /// [a blank]: is_blank
452    /// [a break]: is_break
453    #[inline]
454    #[must_use]
455    fn next_is_blank_or_break(&self) -> bool {
456        is_blank(self.peek()) || is_break(self.peek())
457    }
458
459    /// Check whether the next character is [a blank] or [a breakz].
460    ///
461    /// The character must have previously been fetched through [`lookahead`]
462    ///
463    /// # Return
464    /// Returns true if the character is [a blank] or [a break], false otherwise.
465    ///
466    /// [`lookahead`]: Input::lookahead
467    /// [a blank]: is_blank
468    /// [a breakz]: is_breakz
469    #[inline]
470    #[must_use]
471    fn next_is_blank_or_breakz(&self) -> bool {
472        is_blank(self.peek()) || is_breakz(self.peek())
473    }
474
475    /// Check whether the next character is [a blank].
476    ///
477    /// The character must have previously been fetched through [`lookahead`]
478    ///
479    /// # Return
480    /// Returns true if the character is [a blank], false otherwise.
481    ///
482    /// [`lookahead`]: Input::lookahead
483    /// [a blank]: is_blank
484    #[inline]
485    #[must_use]
486    fn next_is_blank(&self) -> bool {
487        is_blank(self.peek())
488    }
489
490    /// Check whether the next character is [a break].
491    ///
492    /// The character must have previously been fetched through [`lookahead`]
493    ///
494    /// # Return
495    /// Returns true if the character is [a break], false otherwise.
496    ///
497    /// [`lookahead`]: Input::lookahead
498    /// [a break]: is_break
499    #[inline]
500    #[must_use]
501    fn next_is_break(&self) -> bool {
502        is_break(self.peek())
503    }
504
505    /// Check whether the next character is [a breakz].
506    ///
507    /// The character must have previously been fetched through [`lookahead`]
508    ///
509    /// # Return
510    /// Returns true if the character is [a breakz], false otherwise.
511    ///
512    /// [`lookahead`]: Input::lookahead
513    /// [a breakz]: is_breakz
514    #[inline]
515    #[must_use]
516    fn next_is_breakz(&self) -> bool {
517        is_breakz(self.peek())
518    }
519
520    /// Check whether the input is at its physical end.
521    ///
522    /// The default implementation infers end-of-input from the `\0` sentinel returned by
523    /// [`Self::peek`]. Inputs that can distinguish a literal NUL from end-of-input should override
524    /// this method.
525    /// The next position must have previously been fetched through [`Self::lookahead`].
526    ///
527    /// # Return
528    /// Returns true if the input is exhausted, false otherwise.
529    #[inline]
530    #[must_use]
531    fn next_is_z(&self) -> bool {
532        is_z(self.peek())
533    }
534
535    /// Check whether the next character is [a flow].
536    ///
537    /// The character must have previously been fetched through [`lookahead`]
538    ///
539    /// # Return
540    /// Returns true if the character is [a flow], false otherwise.
541    ///
542    /// [`lookahead`]: Input::lookahead
543    /// [a flow]: is_flow
544    #[inline]
545    #[must_use]
546    fn next_is_flow(&self) -> bool {
547        is_flow(self.peek())
548    }
549
550    /// Check whether the next character is [a digit].
551    ///
552    /// The character must have previously been fetched through [`lookahead`]
553    ///
554    /// # Return
555    /// Returns true if the character is [a digit], false otherwise.
556    ///
557    /// [`lookahead`]: Input::lookahead
558    /// [a digit]: is_digit
559    #[inline]
560    #[must_use]
561    fn next_is_digit(&self) -> bool {
562        is_digit(self.peek())
563    }
564
565    /// Check whether the next character is [a letter].
566    ///
567    /// The character must have previously been fetched through [`lookahead`]
568    ///
569    /// # Return
570    /// Returns true if the character is [a letter], false otherwise.
571    ///
572    /// [`lookahead`]: Input::lookahead
573    /// [a letter]: is_alpha
574    #[inline]
575    #[must_use]
576    fn next_is_alpha(&self) -> bool {
577        is_alpha(self.peek())
578    }
579
580    /// Skip printable characters until a [breakz] or non-printable character is found.
581    ///
582    /// The stopping character is not consumed.
583    ///
584    /// # Return
585    /// Return the number of characters that were consumed. The number of characters returned can
586    /// be used to advance the index and column, since no end-of-line character will be consumed.
587    ///
588    /// [breakz]: is_breakz
589    #[inline]
590    fn skip_while_non_breakz(&mut self) -> usize {
591        let mut count = 0;
592        while {
593            let c = self.look_ch();
594            !is_breakz(c) && crate::char_traits::is_printable(c)
595        } {
596            count += 1;
597            self.skip();
598        }
599        count
600    }
601
602    /// Skip characters from the input while [blanks] are found.
603    ///
604    /// The characters are consumed from the input.
605    ///
606    /// # Return
607    /// Return the number of characters that were consumed. The number of characters returned can
608    /// be used to advance the index and column, since no end-of-line character will be consumed.
609    ///
610    /// [blanks]: is_blank
611    fn skip_while_blank(&mut self) -> usize {
612        let mut n_bytes = 0;
613        while is_blank(self.look_ch()) {
614            n_bytes += self.peek().len_utf8();
615            self.skip();
616        }
617        n_bytes
618    }
619
620    /// Fetch characters from the input while we encounter letters and store them in `out`.
621    ///
622    /// The characters are consumed from the input.
623    ///
624    /// # Return
625    /// Return the number of characters that were consumed. The number of characters returned can
626    /// be used to advance the index and column, since no end-of-line character will be consumed.
627    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
628        let mut n_bytes = 0;
629        while is_alpha(self.look_ch()) {
630            let c = self.peek();
631            n_bytes += c.len_utf8();
632            out.push(c);
633            self.skip();
634        }
635        n_bytes
636    }
637
638    /// Fetch characters as long as they satisfy `is_yaml_non_space(c)`.
639    ///
640    /// The characters are consumed from the input.
641    ///
642    /// # Return
643    /// Return the number of characters that were consumed. The number of characters returned can
644    /// be used to advance the index and column, since no end-of-line character will be consumed.
645    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
646        let mut chars_consumed = 0;
647        loop {
648            let c = self.look_ch();
649            if !crate::char_traits::is_yaml_non_space(c) || is_z(c) {
650                break;
651            }
652            let c = self.peek();
653            out.push(c);
654            self.skip();
655            chars_consumed += 1;
656        }
657        chars_consumed
658    }
659
660    /// Append a block scalar's content line to `out`, stopping before CR, LF, NUL, or EOF.
661    ///
662    /// The caller positions the input after the line's indentation. This method consumes the
663    /// entire remaining content line and appends it without clearing existing contents of `out`.
664    /// The stopping character is not consumed; in particular, both characters of CRLF remain
665    /// unconsumed, even if already buffered for lookahead.
666    ///
667    /// This copies content verbatim, including tabs and any non-printable characters other than
668    /// NUL. Unicode characters such as NEL (`U+0085`), line separator (`U+2028`), and paragraph
669    /// separator (`U+2029`) are content, not line terminators here. The scanner remains responsible
670    /// for validation, indentation, folding, and chomping; overrides must not discard or replace
671    /// invalid content.
672    ///
673    /// The default uses [`Self::raw_read_non_breakz_ch`], including any buffered lookahead.
674    /// Inputs with contiguous storage, such as [`str::StrInput`], can override this to append a
675    /// source slice in one operation. No prior lookahead is required; callers refresh lookahead
676    /// before inspecting the next character. If a source error interrupts the line, append and
677    /// count only the characters consumed before it. Keep the error available through
678    /// [`Self::take_source_error`]; the return value alone does not distinguish a source failure
679    /// from a normal line ending.
680    ///
681    /// # Returns
682    ///
683    /// The number of consumed Unicode scalar values (`char`s), **not UTF-8 bytes**, for advancing
684    /// the character index and column. A return value of zero means no content was appended or
685    /// consumed, for example when already at a line terminator or EOF.
686    ///
687    /// # Examples
688    ///
689    /// ```
690    /// use granit_parser::{Input, StrInput};
691    ///
692    /// let mut input = StrInput::new("é🦀\r\nnext");
693    /// let mut output = String::from("prefix:");
694    /// assert_eq!(input.fetch_block_scalar_line(&mut output), 2);
695    /// assert_eq!(output, "prefix:é🦀");
696    /// assert_eq!(input.byte_offset(), Some("é🦀".len()));
697    /// input.lookahead(2);
698    /// assert_eq!(input.peek(), '\r');
699    /// assert_eq!(input.peek_nth(1), '\n');
700    /// ```
701    fn fetch_block_scalar_line(&mut self, out: &mut String) -> usize {
702        // Raw reads consume the logical stream front even when lookahead is still buffered.
703        let mut chars_consumed = 0;
704        while let Some(character) = self.raw_read_non_breakz_ch() {
705            out.push(character);
706            chars_consumed += 1;
707        }
708        chars_consumed
709    }
710
711    /// Consume and return an ordinary ASCII run inside a quoted scalar, if supported.
712    ///
713    /// The caller has already consumed the opening quote. The boolean selects the quote style:
714    /// `true` for single quotes, `false` for double quotes. This optional optimization batches
715    /// characters needing no YAML escape or folding handling. The default returns an empty slice
716    /// without consuming input, leaving character-by-character scanning in place.
717    ///
718    /// # Override contract
719    ///
720    /// A non-empty result must contain exactly the consumed source prefix, with no decoding or
721    /// substitution. Only bytes in `0x21..=0x7e` may be consumed, excluding the matching quote
722    /// (`'` for single quotes, otherwise `"`) and, in double-quoted scalars, backslashes.
723    /// Backslashes and double quotes are ordinary content in single-quoted scalars; single quotes
724    /// are ordinary content in double-quoted scalars. Whitespace, non-ASCII text, control
725    /// characters, escapes, and closing or doubled matching quotes are left for the scanner.
726    /// The returned byte length is also the number of consumed characters, and any supported
727    /// [`Self::byte_offset`] must advance by that length.
728    ///
729    /// The run need not be maximal. Returning an empty slice must consume nothing, even if an
730    /// eligible run is present; it does **not** indicate EOF or the end of the scalar. No prior
731    /// lookahead is required, and already-buffered characters must not be skipped. Callers refresh
732    /// lookahead before inspecting the next character. The returned slice is tied to the borrow
733    /// of `self`, not to the original source lifetime used by [`BorrowedInput::slice_borrowed`].
734    ///
735    /// # Examples
736    ///
737    /// ```
738    /// use granit_parser::{BufferedInput, Input, StrInput};
739    ///
740    /// // Remaining content after a double-quoted scalar's opening quote.
741    /// let mut input = StrInput::new(r#"name\n""#);
742    /// assert_eq!(input.take_quoted_scalar_ascii_chunk(false), "name");
743    /// input.lookahead(1);
744    /// assert_eq!(input.peek(), '\\'); // The escape is left for the scanner.
745    ///
746    /// // Streaming inputs may retain the default and consume nothing.
747    /// let mut stream = BufferedInput::new("name".chars());
748    /// assert_eq!(stream.take_quoted_scalar_ascii_chunk(false), "");
749    /// stream.lookahead(1);
750    /// assert_eq!(stream.peek(), 'n');
751    /// ```
752    #[inline]
753    // Overrides return slices borrowed from the input, unlike this no-op default.
754    #[allow(clippy::unnecessary_literal_bound)]
755    fn take_quoted_scalar_ascii_chunk(&mut self, _single: bool) -> &str {
756        // Inputs with contiguous storage, such as StrInput, override this to return a whole run.
757        ""
758    }
759
760    /// Consume an ordinary printable run inside a quoted scalar and return its character count.
761    ///
762    /// Like [`Self::take_quoted_scalar_ascii_chunk`], this optional hook leaves matching quotes,
763    /// double-quoted escapes, ASCII whitespace, and invalid control characters for the scanner.
764    /// It may additionally consume printable non-ASCII characters. The returned count must be
765    /// the number of Unicode scalar values, not bytes. A non-empty slice must exactly match the
766    /// consumed source prefix; an empty slice must consume nothing and have a count of zero.
767    /// Source byte offsets advance by the slice's byte length. No prior lookahead is required,
768    /// buffered characters must not be skipped, and callers refresh lookahead before peeking.
769    ///
770    /// YAML printable characters are tab, LF, CR, U+0020–U+007E, U+0085, U+00A0–U+D7FF,
771    /// U+E000–U+FFFD, and U+10000–U+10FFFF. Tab, LF, CR, and space still end this run so the
772    /// scanner can fold whitespace. U+0085, U+2028, and U+2029 remain ordinary content.
773    /// The default preserves existing ASCII hook overrides and otherwise consumes nothing.
774    #[inline]
775    fn take_quoted_scalar_chunk(&mut self, single: bool) -> (&str, usize) {
776        let chunk = self.take_quoted_scalar_ascii_chunk(single);
777        (chunk, chunk.len())
778    }
779
780    /// Fetch a chunk of plain scalar characters.
781    ///
782    /// This optimization method allows the input to batch process characters.
783    /// Returns (stopped, `chars_consumed`).
784    /// stopped is true if the chunk ended because of a non-plain-scalar character.
785    fn fetch_plain_scalar_chunk(
786        &mut self,
787        out: &mut String,
788        count: usize,
789        flow_level_gt_0: bool,
790    ) -> (bool, usize) {
791        let mut chars_consumed = 0;
792        for _ in 0..count {
793            self.lookahead(1);
794            if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) {
795                return (true, chars_consumed);
796            }
797            out.push(self.peek());
798            self.skip();
799            chars_consumed += 1;
800        }
801        (false, chars_consumed)
802    }
803
804    /// Consume a chunk of plain-scalar characters without materializing them.
805    ///
806    /// Stable inputs use this while the scanner retains the source as a borrowed slice and only
807    /// promotes it to an owned buffer if YAML folding changes the scalar contents.
808    fn skip_plain_scalar_chunk(&mut self, count: usize, flow_level_gt_0: bool) -> (bool, usize) {
809        let mut chars_consumed = 0;
810        for _ in 0..count {
811            self.lookahead(1);
812            if self.next_is_blank_or_breakz() || !self.next_can_be_plain_scalar(flow_level_gt_0) {
813                return (true, chars_consumed);
814            }
815            self.skip();
816            chars_consumed += 1;
817        }
818        (false, chars_consumed)
819    }
820}
821
822/// Behavior to adopt regarding treating tabs as whitespace.
823///
824/// Although tab is valid YAML whitespace, it does not always behave the same as a space.
825/// Whitespace results cannot be used as input policies:
826///
827/// ```compile_fail,E0308
828/// use granit_parser::{input::WhitespaceResult, BufferedInput, Input};
829///
830/// let mut input = BufferedInput::new(" \tvalue".chars());
831/// let previous_result = WhitespaceResult::new(true, true);
832/// input.skip_ws_to_eol(previous_result); // Expected SkipTabs, not WhitespaceResult.
833/// ```
834#[derive(Copy, Clone, Eq, PartialEq)]
835pub enum SkipTabs {
836    /// Skip all tabs as whitespace.
837    Yes,
838    /// Don't skip any tab. Return from the function when encountering one.
839    No,
840}
841
842/// Information about whitespace consumed by an input scanning hook.
843///
844/// Tabs and spaces are tracked independently. When [`SkipTabs::No`] leaves a tab in the input,
845/// that tab is not included in this result. Characters inside a skipped comment do not affect
846/// either flag.
847///
848/// # Examples
849///
850/// ```
851/// use granit_parser::{input::{SkipTabs, WhitespaceResult}, BufferedInput, Input};
852///
853/// let mut input = BufferedInput::new(" \t#comment".chars());
854/// let (consumed, result) = input.skip_ws_to_eol_blanks(SkipTabs::Yes);
855/// assert_eq!(consumed, 2);
856/// assert_eq!(result, WhitespaceResult::new(true, true));
857/// assert!(result.found_tabs());
858/// assert!(result.has_valid_yaml_ws());
859/// ```
860#[derive(Copy, Clone, Debug, Eq, PartialEq)]
861pub struct WhitespaceResult {
862    found_tabs: bool,
863    has_valid_yaml_ws: bool,
864}
865
866impl WhitespaceResult {
867    /// Create whitespace information from independently tracked tabs and spaces.
868    ///
869    /// `has_valid_yaml_ws` indicates whether at least one space was consumed outside a comment.
870    #[must_use]
871    pub const fn new(found_tabs: bool, has_valid_yaml_ws: bool) -> Self {
872        Self {
873            found_tabs,
874            has_valid_yaml_ws,
875        }
876    }
877
878    /// Return whether at least one tab was consumed outside a comment.
879    #[must_use]
880    pub const fn found_tabs(self) -> bool {
881        self.found_tabs
882    }
883
884    /// Return whether at least one space was consumed outside a comment.
885    #[must_use]
886    pub const fn has_valid_yaml_ws(self) -> bool {
887        self.has_valid_yaml_ws
888    }
889}
890
891#[cfg(test)]
892mod tests {
893    use super::{Input, SkipTabs, WhitespaceResult};
894    use crate::error::ErrorKind;
895
896    struct MinimalInput;
897
898    impl Input for MinimalInput {
899        fn lookahead(&mut self, _count: usize) {}
900
901        fn buflen(&self) -> usize {
902            0
903        }
904
905        fn bufmaxlen(&self) -> usize {
906            0
907        }
908
909        fn raw_read_ch(&mut self) -> char {
910            '\0'
911        }
912
913        fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
914            None
915        }
916
917        fn skip(&mut self) {}
918
919        fn skip_n(&mut self, _count: usize) {}
920
921        fn peek(&self) -> char {
922            '\0'
923        }
924
925        fn peek_nth(&self, _n: usize) -> char {
926            '\0'
927        }
928    }
929
930    #[test]
931    fn default_slice_bytes_returns_none() {
932        let mut input = MinimalInput;
933
934        input.lookahead(4);
935        assert_eq!(input.buflen(), 0);
936        assert_eq!(input.bufmaxlen(), 0);
937        assert_eq!(input.raw_read_ch(), '\0');
938        assert_eq!(input.raw_read_non_breakz_ch(), None);
939        input.skip();
940        input.skip_n(2);
941        assert_eq!(input.peek(), '\0');
942        assert_eq!(input.peek_nth(1), '\0');
943        assert_eq!(input.byte_offset(), None);
944        assert_eq!(input.slice_bytes(0, 0), None);
945    }
946
947    #[test]
948    fn default_skip_ws_to_eol_rejects_unseparated_comment() {
949        for policy in [SkipTabs::Yes, SkipTabs::No] {
950            let mut input = super::buffered::BufferedInput::new("#é中\n".chars());
951
952            let (consumed, result) = input.skip_ws_to_eol(policy);
953
954            assert_eq!(consumed, 0);
955            assert_eq!(result, Err(ErrorKind::CommentNotSeparated));
956            assert_eq!(input.peek(), '#');
957        }
958    }
959
960    #[test]
961    fn whitespace_result_flags_are_independent() {
962        const TAB_ONLY: WhitespaceResult = WhitespaceResult::new(true, false);
963        const TAB_FLAGS: (bool, bool) = (TAB_ONLY.found_tabs(), TAB_ONLY.has_valid_yaml_ws());
964        assert_eq!(TAB_FLAGS, (true, false));
965
966        for found_tabs in [false, true] {
967            for found_spaces in [false, true] {
968                let result = WhitespaceResult::new(found_tabs, found_spaces);
969                assert_eq!(result.found_tabs(), found_tabs);
970                assert_eq!(result.has_valid_yaml_ws(), found_spaces);
971            }
972        }
973    }
974
975    #[test]
976    fn default_skip_ws_to_eol_preserves_policy_flags_and_character_counts() {
977        for (source, policy, consumed, found_tabs, found_spaces, next) in [
978            ("", SkipTabs::Yes, 0, false, false, '\0'),
979            ("  value", SkipTabs::Yes, 2, false, true, 'v'),
980            ("\tvalue", SkipTabs::Yes, 1, true, false, 'v'),
981            (" \t#é中\r\n", SkipTabs::Yes, 5, true, true, '\r'),
982            ("\t# é 中\n", SkipTabs::Yes, 6, true, false, '\n'),
983            (" #é中", SkipTabs::Yes, 4, false, true, '\0'),
984            (" \t#é中\n", SkipTabs::No, 1, false, true, '\t'),
985            ("\t#é中\n", SkipTabs::No, 0, false, false, '\t'),
986        ] {
987            let mut input = super::buffered::BufferedInput::new(source.chars());
988            assert_eq!(
989                input.skip_ws_to_eol(policy),
990                (
991                    consumed,
992                    Ok(WhitespaceResult::new(found_tabs, found_spaces))
993                ),
994                "source: {source:?}",
995            );
996            assert_eq!(input.peek(), next, "source: {source:?}");
997        }
998    }
999
1000    #[test]
1001    fn default_skip_ws_to_eol_blanks_preserves_comments_and_unskipped_tabs() {
1002        for (source, policy, consumed, found_tabs, found_spaces, next) in [
1003            ("", SkipTabs::Yes, 0, false, false, '\0'),
1004            ("  #é中\n", SkipTabs::Yes, 2, false, true, '#'),
1005            ("\t#é中\n", SkipTabs::Yes, 1, true, false, '#'),
1006            (" \t#é中\n", SkipTabs::Yes, 2, true, true, '#'),
1007            (" \t#é中\n", SkipTabs::No, 1, false, true, '\t'),
1008            ("\t#é中\n", SkipTabs::No, 0, false, false, '\t'),
1009        ] {
1010            let mut input = super::buffered::BufferedInput::new(source.chars());
1011            assert_eq!(
1012                input.skip_ws_to_eol_blanks(policy),
1013                (consumed, WhitespaceResult::new(found_tabs, found_spaces)),
1014                "source: {source:?}",
1015            );
1016            assert_eq!(input.peek(), next, "source: {source:?}");
1017        }
1018    }
1019}