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}