Skip to main content

granit_parser/input/
str.rs

1use crate::{
2    char_traits::is_breakz,
3    error::ErrorKind,
4    input::{BorrowedInput, Input, SkipTabs},
5};
6use alloc::string::String;
7
8/// A parser input backed by a `&str`.
9#[allow(clippy::module_name_repetitions)]
10pub struct StrInput<'a> {
11    /// The full, original input string.
12    ///
13    /// This is kept to support O(1) byte-offset capture and zero-copy slicing via the optional
14    /// [`Input::byte_offset`] / [`Input::slice_bytes`] APIs.
15    original: &'a str,
16    /// The remaining input slice.
17    ///
18    /// This is a moving window into [`Self::original`]. All consuming operations advance this
19    /// slice.
20    buffer: &'a str,
21    /// The number of characters we have looked ahead.
22    ///
23    /// This tracks how many characters the parser asked us to look ahead for so we can return the
24    /// correct value in [`Self::buflen`].
25    lookahead: usize,
26}
27
28impl<'a> StrInput<'a> {
29    /// Create a new [`StrInput`] over the given string slice.
30    #[must_use]
31    pub fn new(input: &'a str) -> Self {
32        Self {
33            original: input,
34            buffer: input,
35            lookahead: 0,
36        }
37    }
38
39    /// Return the number of bytes consumed from the original input.
40    ///
41    /// This is an O(1) operation derived from the invariant that [`Self::buffer`] is always a
42    /// suffix of [`Self::original`].
43    #[inline]
44    #[must_use]
45    fn consumed_bytes(&self) -> usize {
46        self.original.len() - self.buffer.len()
47    }
48
49    /// Find the end of the next plain-scalar chunk in the current input buffer.
50    ///
51    /// Returns its byte length and character count. This keeps the existing batched scanner in one
52    /// place so callers can either materialize the chunk or retain it as a borrowed source slice.
53    fn plain_scalar_chunk_len(&self, flow_level_gt_0: bool) -> (usize, usize) {
54        let bytes = self.buffer.as_bytes();
55        let mut byte_pos = 0;
56        let mut chars_consumed = 0;
57
58        while byte_pos < bytes.len() {
59            let byte = bytes[byte_pos];
60            if byte < 0x80 {
61                let character = byte as char;
62                if crate::char_traits::is_blank_or_breakz(character)
63                    || flow_level_gt_0 && crate::char_traits::is_flow(character)
64                {
65                    break;
66                }
67                if character == ':' {
68                    let next_byte = bytes.get(byte_pos + 1).copied().unwrap_or(0);
69                    // A non-ASCII character cannot be blank, breakz, or a flow indicator.
70                    let stops_scalar = next_byte < 0x80
71                        && (crate::char_traits::is_blank_or_breakz(next_byte as char)
72                            || flow_level_gt_0 && crate::char_traits::is_flow(next_byte as char));
73                    if stops_scalar {
74                        break;
75                    }
76                }
77                byte_pos += 1;
78            } else {
79                let character = self.buffer[byte_pos..].chars().next().unwrap();
80                byte_pos += character.len_utf8();
81            }
82            chars_consumed += 1;
83        }
84
85        (byte_pos, chars_consumed)
86    }
87}
88
89impl Input for StrInput<'_> {
90    #[inline]
91    fn lookahead(&mut self, x: usize) {
92        // We already have all characters that we need.
93        // We cannot add '\0's to the buffer when we reach EOF.
94        // Character-retrieving functions return '\0' when they read past EOF.
95        self.lookahead = self.lookahead.max(x);
96    }
97
98    #[inline]
99    fn buflen(&self) -> usize {
100        self.lookahead
101    }
102
103    #[inline]
104    fn bufmaxlen(&self) -> usize {
105        BUFFER_LEN
106    }
107
108    #[inline]
109    fn raw_read_ch(&mut self) -> char {
110        let mut chars = self.buffer.chars();
111        if let Some(c) = chars.next() {
112            self.buffer = chars.as_str();
113            c
114        } else {
115            '\0'
116        }
117    }
118
119    #[inline]
120    fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
121        if let Some((c, sub_str)) = split_first_char(self.buffer) {
122            if is_breakz(c) {
123                None
124            } else {
125                self.buffer = sub_str;
126                Some(c)
127            }
128        } else {
129            None
130        }
131    }
132
133    #[inline]
134    fn skip(&mut self) {
135        if !self.buffer.is_empty() {
136            let b = self.buffer.as_bytes()[0];
137            if b < 0x80 {
138                self.buffer = &self.buffer[1..];
139            } else {
140                let mut chars = self.buffer.chars();
141                chars.next();
142                self.buffer = chars.as_str();
143            }
144        }
145    }
146
147    #[inline]
148    fn skip_n(&mut self, count: usize) {
149        let mut chars = self.buffer.chars();
150        for _ in 0..count {
151            if chars.next().is_none() {
152                break;
153            }
154        }
155        self.buffer = chars.as_str();
156    }
157
158    #[inline]
159    fn peek(&self) -> char {
160        if self.buffer.is_empty() {
161            return '\0';
162        }
163        let b = self.buffer.as_bytes()[0];
164        if b < 0x80 {
165            b as char
166        } else {
167            self.buffer.chars().next().unwrap()
168        }
169    }
170
171    #[inline]
172    fn peek_nth(&self, n: usize) -> char {
173        if n == 0 {
174            return self.peek();
175        }
176        let bytes = self.buffer.as_bytes();
177        if n == 1 && bytes.len() >= 2 && bytes[0] < 0x80 && bytes[1] < 0x80 {
178            return bytes[1] as char;
179        }
180        let mut chars = self.buffer.chars();
181        for _ in 0..n {
182            if chars.next().is_none() {
183                return '\0';
184            }
185        }
186        chars.next().unwrap_or('\0')
187    }
188
189    #[inline]
190    fn byte_offset(&self) -> Option<usize> {
191        Some(self.consumed_bytes())
192    }
193
194    #[inline]
195    fn slice_bytes(&self, start: usize, end: usize) -> Option<&str> {
196        debug_assert!(start <= end);
197        debug_assert!(end <= self.original.len());
198        self.original.get(start..end)
199    }
200
201    #[inline]
202    fn may_contain_comments(&self) -> bool {
203        self.original.as_bytes().contains(&b'#')
204    }
205
206    #[inline]
207    fn next_2_are(&self, c1: char, c2: char) -> bool {
208        let mut chars = self.buffer.chars();
209        chars.next() == Some(c1) && chars.next() == Some(c2)
210    }
211
212    #[inline]
213    fn next_3_are(&self, c1: char, c2: char, c3: char) -> bool {
214        let mut chars = self.buffer.chars();
215        chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3)
216    }
217
218    #[inline]
219    fn next_is_document_indicator(&self) -> bool {
220        if self.buffer.len() < 3 {
221            false
222        } else {
223            // Since all characters we look for are ASCII, we can directly use the byte API of str.
224            let bytes = self.buffer.as_bytes();
225            (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
226                && (bytes[0] == b'.' || bytes[0] == b'-')
227                && bytes[0] == bytes[1]
228                && bytes[1] == bytes[2]
229        }
230    }
231
232    #[inline]
233    fn next_is_document_start(&self) -> bool {
234        if self.buffer.len() < 3 {
235            false
236        } else {
237            // Since all characters we look for are ASCII, we can directly use the byte API of str.
238            let bytes = self.buffer.as_bytes();
239            (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
240                && bytes[0] == b'-'
241                && bytes[1] == b'-'
242                && bytes[2] == b'-'
243        }
244    }
245
246    #[inline]
247    fn next_is_document_end(&self) -> bool {
248        if self.buffer.len() < 3 {
249            false
250        } else {
251            // Since all characters we look for are ASCII, we can directly use the byte API of str.
252            let bytes = self.buffer.as_bytes();
253            (bytes.len() == 3 || matches!(bytes[3], b' ' | b'\t' | 0 | b'\n' | b'\r'))
254                && bytes[0] == b'.'
255                && bytes[1] == b'.'
256                && bytes[2] == b'.'
257        }
258    }
259
260    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, ErrorKind>) {
261        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
262
263        let mut new_str = self.buffer;
264        let mut has_yaml_ws = false;
265        let mut encountered_tab = false;
266
267        // Separate loops keep the fast space-only path while still tracking whether tabs were seen.
268        if skip_tabs == SkipTabs::Yes {
269            loop {
270                if let Some(sub_str) = new_str.strip_prefix(' ') {
271                    has_yaml_ws = true;
272                    new_str = sub_str;
273                } else if let Some(sub_str) = new_str.strip_prefix('\t') {
274                    encountered_tab = true;
275                    new_str = sub_str;
276                } else {
277                    break;
278                }
279            }
280        } else {
281            while let Some(sub_str) = new_str.strip_prefix(' ') {
282                has_yaml_ws = true;
283                new_str = sub_str;
284            }
285        }
286
287        // All characters consumed were ASCII. We can use the byte length difference to count the
288        // number of whitespace ignored.
289        let mut chars_consumed = self.buffer.len() - new_str.len();
290
291        if !new_str.is_empty() && new_str.as_bytes()[0] == b'#' {
292            if !encountered_tab && !has_yaml_ws {
293                return (chars_consumed, Err(ErrorKind::CommentNotSeparated));
294            }
295
296            // Skip remaining characters until we hit a breakz.
297            while let Some((c, sub_str)) = split_first_char(new_str) {
298                if is_breakz(c) {
299                    break;
300                }
301                new_str = sub_str;
302                chars_consumed += 1;
303            }
304        }
305
306        self.buffer = new_str;
307
308        (
309            chars_consumed,
310            Ok(SkipTabs::Result(encountered_tab, has_yaml_ws)),
311        )
312    }
313
314    fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, SkipTabs) {
315        assert!(!matches!(skip_tabs, SkipTabs::Result(..)));
316
317        let bytes = self.buffer.as_bytes();
318        let mut i = 0;
319        let mut encountered_tab = false;
320        let mut has_yaml_ws = false;
321
322        if skip_tabs == SkipTabs::Yes {
323            while i < bytes.len() {
324                match bytes[i] {
325                    b' ' => {
326                        has_yaml_ws = true;
327                        i += 1;
328                    }
329                    b'\t' => {
330                        encountered_tab = true;
331                        i += 1;
332                    }
333                    _ => break,
334                }
335            }
336        } else {
337            while i < bytes.len() && bytes[i] == b' ' {
338                has_yaml_ws = true;
339                i += 1;
340            }
341        }
342
343        self.buffer = &self.buffer[i..];
344
345        (i, SkipTabs::Result(encountered_tab, has_yaml_ws))
346    }
347
348    #[inline]
349    fn next_is_blank_or_break(&self) -> bool {
350        !self.buffer.is_empty() && matches!(self.buffer.as_bytes()[0], b' ' | b'\t' | b'\n' | b'\r')
351    }
352
353    #[inline]
354    fn next_is_blank_or_breakz(&self) -> bool {
355        self.buffer.is_empty()
356            || matches!(self.buffer.as_bytes()[0], b' ' | b'\t' | 0 | b'\n' | b'\r')
357    }
358
359    #[inline]
360    fn next_is_blank(&self) -> bool {
361        !self.buffer.is_empty() && matches!(self.buffer.as_bytes()[0], b' ' | b'\t')
362    }
363
364    #[inline]
365    fn next_is_break(&self) -> bool {
366        !self.buffer.is_empty() && matches!(self.buffer.as_bytes()[0], b'\n' | b'\r')
367    }
368
369    #[inline]
370    fn next_is_breakz(&self) -> bool {
371        self.buffer.is_empty() || matches!(self.buffer.as_bytes()[0], 0 | b'\n' | b'\r')
372    }
373
374    #[inline]
375    fn next_is_z(&self) -> bool {
376        self.buffer.is_empty()
377    }
378
379    #[inline]
380    fn next_is_flow(&self) -> bool {
381        !self.buffer.is_empty()
382            && matches!(self.buffer.as_bytes()[0], b',' | b'[' | b']' | b'{' | b'}')
383    }
384
385    #[inline]
386    fn next_is_digit(&self) -> bool {
387        !self.buffer.is_empty() && self.buffer.as_bytes()[0].is_ascii_digit()
388    }
389
390    /// Check if the next character is an ASCII alphanumeric, `_`, or `-`.
391    ///
392    /// This is used as a heuristic for error detection (e.g., when `:` is followed
393    /// by tab and then a potential value character). The ASCII-only check is intentional:
394    /// it catches common cases like `key:\tvalue` while avoiding false positives for
395    /// valid YAML constructs. Unicode value starters (e.g., `äöü`) are not detected,
396    /// but such cases will still fail to parse (with a less specific error message).
397    #[inline]
398    fn next_is_alpha(&self) -> bool {
399        !self.buffer.is_empty()
400            && matches!(self.buffer.as_bytes()[0], b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-')
401    }
402
403    fn skip_while_non_breakz(&mut self) -> usize {
404        let mut byte_pos = 0;
405        let mut chars_consumed = 0;
406
407        for (i, c) in self.buffer.char_indices() {
408            if is_breakz(c) || !crate::char_traits::is_printable(c) {
409                break;
410            }
411            byte_pos = i + c.len_utf8();
412            chars_consumed += 1;
413        }
414
415        self.buffer = &self.buffer[byte_pos..];
416        chars_consumed
417    }
418
419    #[inline]
420    fn skip_while_blank(&mut self) -> usize {
421        let bytes = self.buffer.as_bytes();
422
423        let mut i = 0;
424        while i < bytes.len() {
425            match bytes[i] {
426                b' ' | b'\t' => i += 1,
427                _ => break,
428            }
429        }
430
431        self.buffer = &self.buffer[i..];
432        i
433    }
434
435    /// Fetch characters matching `is_alpha` (ASCII alphanumeric, `_`, `-`).
436    ///
437    /// This is used for scanning tag handles (e.g., `!foo!`). Per YAML 1.2 spec,
438    /// tag handles use `ns-word-char` which is `[0-9a-zA-Z-]`. Our implementation
439    /// is slightly more permissive by also accepting `_`, but this is harmless
440    /// and matches common practice. Unicode characters like `ä` or `π` are NOT
441    /// valid in tag handles per spec, so the ASCII-only byte-based scanning here
442    /// is both correct and efficient.
443    fn fetch_while_is_alpha(&mut self, out: &mut String) -> usize {
444        let bytes = self.buffer.as_bytes();
445        let mut i = 0;
446
447        // All target characters are ASCII, so we can scan bytes directly.
448        while i < bytes.len() {
449            match bytes[i] {
450                b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-' => i += 1,
451                _ => break,
452            }
453        }
454
455        // All matched characters are ASCII, so we can safely slice and convert.
456        out.push_str(&self.buffer[..i]);
457        self.buffer = &self.buffer[i..];
458
459        i
460    }
461
462    fn fetch_while_is_yaml_non_space(&mut self, out: &mut String) -> usize {
463        let mut byte_pos = 0;
464        let mut chars_consumed = 0;
465
466        for (i, c) in self.buffer.char_indices() {
467            if !crate::char_traits::is_yaml_non_space(c) || crate::char_traits::is_z(c) {
468                break;
469            }
470
471            byte_pos = i + c.len_utf8();
472            chars_consumed += 1;
473        }
474
475        out.push_str(&self.buffer[..byte_pos]);
476        self.buffer = &self.buffer[byte_pos..];
477
478        chars_consumed
479    }
480
481    #[inline]
482    fn fetch_block_scalar_line(&mut self, out: &mut String) -> usize {
483        let end = self
484            .buffer
485            .as_bytes()
486            .iter()
487            .position(|&byte| matches!(byte, b'\r' | b'\n' | 0))
488            .unwrap_or(self.buffer.len());
489
490        // All terminators are ASCII, so the line ends on a UTF-8 boundary.
491        let (line, remaining) = self.buffer.split_at(end);
492        out.push_str(line);
493        self.buffer = remaining;
494        line.chars().count()
495    }
496
497    #[inline]
498    fn take_quoted_scalar_ascii_chunk(&mut self, single: bool) -> &str {
499        let quote = if single { b'\'' } else { b'"' };
500        let end = self
501            .buffer
502            .as_bytes()
503            .iter()
504            .position(|&byte| {
505                !(b'!'..=b'~').contains(&byte) || byte == quote || (!single && byte == b'\\')
506            })
507            .unwrap_or(self.buffer.len());
508
509        // The run is entirely ASCII, so its end is a UTF-8 boundary.
510        let (chunk, remaining) = self.buffer.split_at(end);
511        self.buffer = remaining;
512        chunk
513    }
514
515    fn fetch_plain_scalar_chunk(
516        &mut self,
517        out: &mut String,
518        _count: usize,
519        flow_level_gt_0: bool,
520    ) -> (bool, usize) {
521        let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0);
522        out.push_str(&self.buffer[..byte_pos]);
523        self.buffer = &self.buffer[byte_pos..];
524        (true, chars_consumed)
525    }
526
527    fn skip_plain_scalar_chunk(&mut self, _count: usize, flow_level_gt_0: bool) -> (bool, usize) {
528        let (byte_pos, chars_consumed) = self.plain_scalar_chunk_len(flow_level_gt_0);
529        self.buffer = &self.buffer[byte_pos..];
530        (true, chars_consumed)
531    }
532}
533
534impl<'a> BorrowedInput<'a> for StrInput<'a> {
535    #[inline]
536    fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'a str> {
537        debug_assert!(start <= end);
538        debug_assert!(end <= self.original.len());
539        self.original.get(start..end)
540    }
541}
542
543/// The buffer size we return to the scanner.
544///
545/// This does not correspond to any allocated buffer size. In practice, the scanner may request any
546/// character in the virtual buffer: characters inside the input are returned as-is, and positions
547/// past EOF return `\0`.
548///
549/// The number of characters we are asked to retrieve in [`lookahead`] depends on the buffer size
550/// of the input. Our buffer here is virtually unlimited, but the scanner cannot work with that. It
551/// may allocate buffers of its own of the size we return in [`bufmaxlen`] (so we can't return
552/// [`usize::MAX`]). We can't always return the number of characters left either, as the scanner
553/// expects [`buflen`] to return the same value that was given to [`lookahead`] right after its
554/// call.
555///
556/// This creates a complex situation where [`bufmaxlen`] influences what value [`lookahead`] is
557/// called with, which in turn dictates what [`buflen`] returns. In order to avoid breaking any
558/// function, we return this constant in [`bufmaxlen`] which, since the input is processed one line
559/// at a time, should fit what we expect to be a good balance between memory consumption and what
560/// we expect the maximum line length to be.
561///
562/// [`lookahead`]: `StrInput::lookahead`
563/// [`bufmaxlen`]: `StrInput::bufmaxlen`
564/// [`buflen`]: `StrInput::buflen`
565const BUFFER_LEN: usize = 128;
566
567/// Splits the first character of the given string and returns it along with the rest of the
568/// string.
569#[inline]
570fn split_first_char(s: &str) -> Option<(char, &str)> {
571    let mut chars = s.chars();
572    let c = chars.next()?;
573    Some((c, chars.as_str()))
574}
575
576#[cfg(test)]
577mod test {
578    use alloc::string::String;
579
580    use crate::{
581        error::ErrorKind,
582        input::{BorrowedInput, Input, SkipTabs},
583    };
584
585    use super::StrInput;
586
587    #[test]
588    pub fn is_document_start() {
589        let input = StrInput::new("---\n");
590        assert!(input.next_is_document_start());
591        assert!(input.next_is_document_indicator());
592        let input = StrInput::new("---");
593        assert!(input.next_is_document_start());
594        assert!(input.next_is_document_indicator());
595        let input = StrInput::new("...\n");
596        assert!(!input.next_is_document_start());
597        assert!(input.next_is_document_indicator());
598        let input = StrInput::new("--- ");
599        assert!(input.next_is_document_start());
600        assert!(input.next_is_document_indicator());
601    }
602
603    #[test]
604    pub fn is_document_end() {
605        let input = StrInput::new("...\n");
606        assert!(input.next_is_document_end());
607        assert!(input.next_is_document_indicator());
608        let input = StrInput::new("...");
609        assert!(input.next_is_document_end());
610        assert!(input.next_is_document_indicator());
611        let input = StrInput::new("---\n");
612        assert!(!input.next_is_document_end());
613        assert!(input.next_is_document_indicator());
614        let input = StrInput::new("... ");
615        assert!(input.next_is_document_end());
616        assert!(input.next_is_document_indicator());
617    }
618
619    #[test]
620    fn raw_reads_track_byte_offsets_and_eof() {
621        let mut input = StrInput::new("aé");
622
623        assert_eq!(input.raw_read_ch(), 'a');
624        assert_eq!(input.byte_offset(), Some(1));
625        assert_eq!(input.raw_read_ch(), 'é');
626        assert_eq!(input.byte_offset(), Some(3));
627        assert_eq!(input.raw_read_ch(), '\0');
628        assert_eq!(input.byte_offset(), Some(3));
629    }
630
631    #[test]
632    fn raw_read_non_breakz_stops_before_breakz() {
633        let mut input = StrInput::new("a\n");
634
635        assert_eq!(input.raw_read_non_breakz_ch(), Some('a'));
636        assert_eq!(input.raw_read_non_breakz_ch(), None);
637        assert_eq!(input.peek(), '\n');
638
639        let mut empty = StrInput::new("");
640        assert_eq!(empty.raw_read_non_breakz_ch(), None);
641    }
642
643    #[test]
644    fn skip_handles_ascii_unicode_and_eof() {
645        let mut input = StrInput::new("éab");
646
647        input.skip();
648        assert_eq!(input.peek(), 'a');
649
650        input.skip_n(8);
651        assert_eq!(input.peek(), '\0');
652
653        input.skip();
654        assert_eq!(input.peek(), '\0');
655    }
656
657    #[test]
658    fn peeking_past_end_returns_nul() {
659        let ascii = StrInput::new("ab");
660        assert_eq!(ascii.peek_nth(1), 'b');
661        assert_eq!(ascii.peek_nth(3), '\0');
662
663        let unicode = StrInput::new("éab");
664        assert!(unicode.next_3_are('é', 'a', 'b'));
665        assert!(!unicode.next_3_are('é', 'a', 'c'));
666    }
667
668    #[test]
669    fn skip_ws_to_eol_without_tabs_stops_before_tab() {
670        let mut input = StrInput::new("  \t# comment\n");
671
672        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::No);
673
674        assert_eq!(consumed, 2);
675        let result = result.unwrap();
676        assert!(!result.found_tabs());
677        assert!(result.has_valid_yaml_ws());
678        assert_eq!(input.peek(), '\t');
679    }
680
681    #[test]
682    fn skip_ws_to_eol_skips_comments_after_whitespace() {
683        let mut input = StrInput::new("  # comment\nnext");
684
685        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
686
687        assert_eq!(consumed, 11);
688        let result = result.unwrap();
689        assert!(!result.found_tabs());
690        assert!(result.has_valid_yaml_ws());
691        assert_eq!(input.peek(), '\n');
692    }
693
694    #[test]
695    fn skip_ws_to_eol_rejects_unseparated_comment() {
696        let mut input = StrInput::new("# comment\n");
697
698        let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
699
700        assert_eq!(consumed, 0);
701        assert_eq!(result.err(), Some(ErrorKind::CommentNotSeparated));
702        assert_eq!(input.peek(), '#');
703    }
704
705    #[test]
706    fn fetch_while_is_alpha_is_ascii_only() {
707        let mut input = StrInput::new("abc_123-é");
708        let mut out = String::new();
709
710        assert_eq!(input.fetch_while_is_alpha(&mut out), 8);
711        assert_eq!(out, "abc_123-");
712        assert_eq!(input.peek(), 'é');
713    }
714
715    #[test]
716    fn fetch_plain_scalar_chunk_handles_non_ascii_after_colon() {
717        let mut input = StrInput::new("a:é ");
718        let mut out = String::new();
719
720        assert_eq!(
721            input.fetch_plain_scalar_chunk(&mut out, 16, false),
722            (true, 3)
723        );
724        assert_eq!(out, "a:é");
725        assert_eq!(input.peek(), ' ');
726    }
727
728    #[test]
729    fn fetch_plain_scalar_chunk_stops_at_flow_indicator() {
730        let mut input = StrInput::new("abc,def");
731        let mut out = String::new();
732
733        assert_eq!(
734            input.fetch_plain_scalar_chunk(&mut out, 16, true),
735            (true, 3)
736        );
737        assert_eq!(out, "abc");
738        assert_eq!(input.peek(), ',');
739    }
740
741    #[test]
742    fn borrowed_slices_use_original_input_lifetime() {
743        let input = StrInput::new("aéz");
744
745        assert_eq!(BorrowedInput::slice_borrowed(&input, 1, 3), Some("é"));
746        assert_eq!(input.slice_bytes(3, 4), Some("z"));
747    }
748}