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}