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