Skip to main content

rlsp_yaml_parser/
block.rs

1// SPDX-License-Identifier: MIT
2
3//! YAML 1.2 §8 block style productions [162]–[201].
4//!
5//! Covers block scalar headers, chomping, literal and folded block scalars,
6//! block sequences, block mappings, and block nodes.  Each function is named
7//! after the spec production and cross-referenced by its production number in
8//! a `// [N]` comment.
9
10use crate::chars::{b_break, nb_char, s_white};
11use crate::combinator::{
12    Context, Parser, Reply, State, alt, char_parser, many0, many1, neg_lookahead, opt, seq, token,
13    wrap_tokens,
14};
15use crate::flow::{e_node, ns_flow_node};
16use crate::structure::{
17    b_comment, c_forbidden, c_ns_properties, l_empty, s_b_comment, s_indent, s_indent_content,
18    s_indent_le, s_indent_lt, s_l_comments, s_separate, s_separate_ge, s_separate_in_line,
19};
20use crate::token::Code;
21
22// ---------------------------------------------------------------------------
23// Chomping indicator — Strip / Clip / Keep
24// ---------------------------------------------------------------------------
25
26/// The three YAML chomping modes.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Chomping {
29    /// `-` — strip all trailing line breaks.
30    Strip,
31    /// (default) — keep exactly one trailing line break.
32    Clip,
33    /// `+` — keep all trailing line breaks.
34    Keep,
35}
36
37// ---------------------------------------------------------------------------
38// §8.1.1 – Block scalar headers [162]–[165]
39// ---------------------------------------------------------------------------
40
41/// [164] c-chomping-indicator — `-` for Strip, `+` for Keep, absent for Clip.
42///
43/// Always succeeds: returns Strip/Keep when the indicator is present, Clip
44/// (zero consumption) otherwise.
45fn c_chomping_indicator(state: State<'_>) -> (Chomping, State<'_>) {
46    match state.peek() {
47        Some('-') => (Chomping::Strip, state.advance('-')),
48        Some('+') => (Chomping::Keep, state.advance('+')),
49        _ => (Chomping::Clip, state),
50    }
51}
52
53/// [163] c-indentation-indicator — explicit digit 1–9, or absent (auto).
54///
55/// Returns `Some(n)` when an explicit digit was consumed, `None` for
56/// auto-detect.  Fails when the character is `0` (forbidden by spec).
57fn c_indentation_indicator(state: State<'_>) -> Reply<'_> {
58    match state.peek() {
59        Some('0') => Reply::Failure,
60        Some(ch @ '1'..='9') => {
61            let after = state.advance(ch);
62            let n = i32::from(ch as u8 - b'0');
63            Reply::Success {
64                tokens: vec![crate::token::Token {
65                    code: Code::Meta,
66                    pos: after.pos,
67                    text: "",
68                }],
69                state: State {
70                    input: after.input,
71                    pos: after.pos,
72                    n,
73                    c: after.c,
74                },
75            }
76        }
77        _ => Reply::Success {
78            tokens: Vec::new(),
79            state: State {
80                input: state.input,
81                pos: state.pos,
82                n: 0, // 0 signals auto-detect
83                c: state.c,
84            },
85        },
86    }
87}
88
89/// Internal helper: try indent-then-chomp ordering.
90fn try_indent_chomp(s: State<'_>) -> Option<(i32, Chomping, State<'_>)> {
91    match c_indentation_indicator(s) {
92        Reply::Success { state: s1, .. } => {
93            let m = s1.n;
94            let (chomp, s2) = c_chomping_indicator(s1);
95            Some((m, chomp, s2))
96        }
97        Reply::Failure | Reply::Error(_) => None,
98    }
99}
100
101/// Internal helper: try chomp-then-indent ordering.
102fn try_chomp_indent(s: State<'_>) -> Option<(i32, Chomping, State<'_>)> {
103    let (chomp, s1) = c_chomping_indicator(s);
104    match c_indentation_indicator(s1) {
105        Reply::Success { state: s2, .. } => {
106            let m = s2.n;
107            Some((m, chomp, s2))
108        }
109        Reply::Failure | Reply::Error(_) => None,
110    }
111}
112
113/// Internal: parse a block scalar header and return `(m, t, chomp_char, remaining_state)`.
114/// `m` is the explicit indentation (0 = auto-detect), `t` is `Chomping`,
115/// `chomp_char` is `Some("-")` or `Some("+")` when explicit, `None` for `Clip`.
116fn parse_block_header(
117    state: State<'_>,
118) -> Option<(i32, Chomping, Option<&'static str>, State<'_>)> {
119    // Pick the ordering that advanced furthest.
120    let r1 = try_indent_chomp(state.clone());
121    let r2 = try_chomp_indent(state);
122
123    let (m, chomp, after_indicators) = match (r1, r2) {
124        (Some((_, _, s1)), Some((m2, c2, s2))) if s2.pos.byte_offset > s1.pos.byte_offset => {
125            (m2, c2, s2)
126        }
127        (Some((m, c, s)), _) | (None, Some((m, c, s))) => (m, c, s),
128        (None, None) => return None,
129    };
130
131    let chomp_char = match chomp {
132        Chomping::Strip => Some("-"),
133        Chomping::Keep => Some("+"),
134        Chomping::Clip => None,
135    };
136
137    // Consume optional comment + required line break.
138    match s_b_comment()(after_indicators) {
139        Reply::Success { state: s_after, .. } => Some((m, chomp, chomp_char, s_after)),
140        Reply::Failure | Reply::Error(_) => None,
141    }
142}
143
144// ---------------------------------------------------------------------------
145// Auto-detect indentation
146// ---------------------------------------------------------------------------
147
148/// Scan ahead to find the indentation of the first non-empty content line.
149///
150/// An "empty" line is one that consists entirely of spaces/tabs and then a
151/// line break (or a line containing only whitespace).  The scan starts from
152/// `input` (already past the block header line break).
153///
154/// Returns `Some(indent)` where indent is the column of the first non-space
155/// character, or `None` if all remaining lines are empty.
156fn detect_scalar_indentation(input: &str, min_indent: i32) -> i32 {
157    let mut remaining = input;
158    loop {
159        // Count leading spaces on this line.
160        let spaces = remaining.chars().take_while(|&ch| ch == ' ').count();
161        let after_spaces = &remaining[spaces..];
162        // Check what follows the spaces.
163        match after_spaces.chars().next() {
164            None => {
165                // EOF — no content found, use min_indent.
166                return min_indent;
167            }
168            Some('\n' | '\r') => {
169                // Empty line (spaces then break) — skip.
170                let break_len = if after_spaces.starts_with("\r\n") {
171                    2
172                } else {
173                    1
174                };
175                remaining = &after_spaces[break_len..];
176            }
177            Some('\t') => {
178                // Tab — skip this line (tabs in indentation are not counted).
179                // Find end of line.
180                let line_end = after_spaces.find('\n').unwrap_or(after_spaces.len());
181                remaining = &after_spaces[line_end..];
182                if remaining.starts_with('\n') {
183                    remaining = &remaining[1..];
184                }
185            }
186            Some(_) => {
187                // Non-empty line: the indentation is `spaces`.
188                let indent = i32::try_from(spaces).unwrap_or(i32::MAX);
189                return indent;
190            }
191        }
192    }
193}
194
195// ---------------------------------------------------------------------------
196// §8.1.1.2 – Chomping [165]–[169]
197// ---------------------------------------------------------------------------
198
199/// [165] b-chomped-last(t) — the final line break, emitted based on chomping.
200///
201/// Strip: consume but don't emit.
202/// Clip/Keep: consume and emit `LineFeed`.
203fn b_chomped_last(t: Chomping) -> Parser<'static> {
204    Box::new(move |state| {
205        // Need a line break here.
206        match b_break()(state.clone()) {
207            Reply::Failure => {
208                // EOF is valid for b-chomped-last when the scalar ends at EOF.
209                if state.input.is_empty() {
210                    return Reply::Success {
211                        tokens: Vec::new(),
212                        state,
213                    };
214                }
215                Reply::Failure
216            }
217            Reply::Error(e) => Reply::Error(e),
218            Reply::Success {
219                state: after_break, ..
220            } => match t {
221                Chomping::Strip => Reply::Success {
222                    tokens: Vec::new(),
223                    state: after_break,
224                },
225                Chomping::Clip | Chomping::Keep => Reply::Success {
226                    tokens: vec![crate::token::Token {
227                        code: Code::LineFeed,
228                        pos: state.pos,
229                        text: "",
230                    }],
231                    state: after_break,
232                },
233            },
234        }
235    })
236}
237
238/// Blank line for chomping: at most n indentation spaces, optional trailing
239/// whitespace (tabs), then a line break. This is `s-indent(≤n) b-non-content`
240/// per spec, extended to allow trailing tabs on blank lines.
241fn l_chomped_blank(n: i32) -> Parser<'static> {
242    seq(s_indent_le(n), seq(many0(s_white()), b_break()))
243}
244
245/// [167] l-strip-empty(n) — blank lines (for strip/clip chomping tail).
246///
247/// Per spec: `( s-indent(≤n) b-non-content )* l-trail-comments(n)?`.
248fn l_strip_empty(n: i32) -> Parser<'static> {
249    seq(many0(l_chomped_blank(n)), opt(l_trail_comments(n)))
250}
251
252/// [168] l-keep-empty(n) — blank lines emitting Break tokens (for keep chomping).
253///
254/// Per spec: `l-empty(n,BLOCK-IN)* l-trail-comments(n)?`.
255fn l_keep_empty(n: i32) -> Parser<'static> {
256    seq(
257        many0(token(Code::Break, l_chomped_blank(n))),
258        opt(l_trail_comments(n)),
259    )
260}
261
262/// [169] l-trail-comments(n) — trailing comment lines after a block scalar.
263///
264/// Per spec: `s-indent(<n) c-nb-comment-text b-comment l-comment*`.
265/// The first line must have fewer than n indentation spaces and start with `#`.
266fn l_trail_comments(n: i32) -> Parser<'static> {
267    use crate::structure::l_comment;
268    seq(
269        wrap_tokens(
270            Code::BeginComment,
271            Code::EndComment,
272            seq(
273                s_indent_lt(n),
274                seq(
275                    token(Code::Indicator, char_parser('#')),
276                    seq(token(Code::Text, many0(nb_char())), b_comment()),
277                ),
278            ),
279        ),
280        many0(l_comment()),
281    )
282}
283
284/// [166] l-chomped-empty(n,t) — trailing blank lines per chomping mode.
285fn l_chomped_empty(n: i32, t: Chomping) -> Parser<'static> {
286    match t {
287        Chomping::Strip | Chomping::Clip => l_strip_empty(n),
288        Chomping::Keep => l_keep_empty(n),
289    }
290}
291
292// ---------------------------------------------------------------------------
293// §8.1.2 – Literal block scalar [170]–[174]
294// ---------------------------------------------------------------------------
295
296/// [171] l-nb-literal-text(n) — one line of literal scalar content.
297///
298/// Emits the content as a Text token (without the indentation spaces).
299///
300/// Uses `s_indent_content(n)` which requires at least n leading spaces and
301/// consumes exactly n, leaving any extras for `nb-char+`.  This matches the
302/// spec: a line with more than n spaces contributes the extra spaces as scalar
303/// content.
304fn l_nb_literal_text(n: i32) -> Parser<'static> {
305    seq(
306        many0(l_empty(n, Context::BlockIn)),
307        seq(s_indent_content(n), token(Code::Text, many1(nb_char()))),
308    )
309}
310
311/// [172] b-nb-literal-next(n) — line break then another literal line.
312fn b_nb_literal_next(n: i32) -> Parser<'static> {
313    seq(token(Code::LineFeed, b_break()), l_nb_literal_text(n))
314}
315
316/// [174] l-literal-content(n,t) — full literal scalar body with chomping.
317fn l_literal_content(n: i32, t: Chomping) -> Parser<'static> {
318    Box::new(move |state| {
319        // Try to parse the first content line.
320        match l_nb_literal_text(n)(state.clone()) {
321            Reply::Failure => {
322                // Empty body — just chomped tail.
323                l_chomped_empty(n, t)(state)
324            }
325            Reply::Error(e) => Reply::Error(e),
326            Reply::Success {
327                tokens: first_tokens,
328                state: after_first,
329            } => {
330                // Parse continuation lines.
331                let cont_result = many0(b_nb_literal_next(n))(after_first.clone());
332                let (cont_tokens, after_cont) = match cont_result {
333                    Reply::Success { tokens, state } => (tokens, state),
334                    Reply::Failure | Reply::Error(_) => (Vec::new(), after_first),
335                };
336
337                // b-chomped-last.
338                let last_result = b_chomped_last(t)(after_cont.clone());
339                let (last_tokens, after_last) = match last_result {
340                    Reply::Success { tokens, state } => (tokens, state),
341                    Reply::Failure | Reply::Error(_) => (Vec::new(), after_cont),
342                };
343
344                // l-chomped-empty.
345                let tail_result = l_chomped_empty(n, t)(after_last.clone());
346                let (tail_tokens, final_state) = match tail_result {
347                    Reply::Success { tokens, state } => (tokens, state),
348                    Reply::Failure | Reply::Error(_) => (Vec::new(), after_last),
349                };
350
351                let mut all = first_tokens;
352                all.extend(cont_tokens);
353                all.extend(last_tokens);
354                all.extend(tail_tokens);
355                Reply::Success {
356                    tokens: all,
357                    state: final_state,
358                }
359            }
360        }
361    })
362}
363
364/// [170] c-l+literal(n) — `|` header then literal content.
365#[must_use]
366pub fn c_l_literal(n: i32) -> Parser<'static> {
367    Box::new(move |state| {
368        // Must start with `|`.
369        let Some('|') = state.peek() else {
370            return Reply::Failure;
371        };
372        let start_pos = state.pos;
373        let after_pipe = state.advance('|');
374
375        // Parse header: indentation indicator + chomping indicator + comment + break.
376        let Some((m_raw, chomp, chomp_char, after_header)) = parse_block_header(after_pipe.clone())
377        else {
378            return Reply::Failure;
379        };
380
381        // Determine indentation: explicit or auto-detect.
382        let m = if m_raw == 0 {
383            detect_scalar_indentation(after_header.input, n + 1)
384        } else {
385            n + m_raw
386        };
387
388        let header_tokens: Vec<crate::token::Token<'static>> = {
389            let mut v = vec![crate::token::Token {
390                code: Code::Indicator,
391                pos: start_pos,
392                text: "|",
393            }];
394            if let Some(ch) = chomp_char {
395                v.push(crate::token::Token {
396                    code: Code::Indicator,
397                    pos: start_pos,
398                    text: ch,
399                });
400            }
401            v
402        };
403
404        if m <= n {
405            // No valid content indentation found — empty scalar.
406            let content_result = l_chomped_empty(m, chomp)(after_header.clone());
407            let (content_tokens, final_state) = match content_result {
408                Reply::Success { tokens, state } => (tokens, state),
409                Reply::Failure | Reply::Error(_) => (Vec::new(), after_header),
410            };
411            let mut all = vec![crate::token::Token {
412                code: Code::BeginScalar,
413                pos: start_pos,
414                text: "",
415            }];
416            all.extend(header_tokens);
417            all.extend(content_tokens);
418            all.push(crate::token::Token {
419                code: Code::EndScalar,
420                pos: final_state.pos,
421                text: "",
422            });
423            return Reply::Success {
424                tokens: all,
425                state: final_state,
426            };
427        }
428
429        // Parse the literal content at indentation m.
430        let content_result = l_literal_content(m, chomp)(after_header);
431        let (content_tokens, final_state) = match content_result {
432            Reply::Success { tokens, state } => (tokens, state),
433            Reply::Failure => return Reply::Failure,
434            Reply::Error(e) => return Reply::Error(e),
435        };
436
437        let mut all = vec![crate::token::Token {
438            code: Code::BeginScalar,
439            pos: start_pos,
440            text: "",
441        }];
442        all.extend(header_tokens);
443        all.extend(content_tokens);
444        all.push(crate::token::Token {
445            code: Code::EndScalar,
446            pos: final_state.pos,
447            text: "",
448        });
449        Reply::Success {
450            tokens: all,
451            state: final_state,
452        }
453    })
454}
455
456// ---------------------------------------------------------------------------
457// §8.1.3 – Folded block scalar [175]–[182]
458// ---------------------------------------------------------------------------
459
460/// [176] s-nb-folded-text(n) — one line of folded scalar content (non-spaced).
461///
462/// Uses `s_indent_content(n)` to consume exactly n spaces (allowing lines
463/// with > n spaces to proceed — the extra spaces become part of the content
464/// after `neg_lookahead(s_white())` rejects them as "spaced" text).
465fn s_nb_folded_text(n: i32) -> Parser<'static> {
466    seq(
467        s_indent_content(n),
468        seq(
469            neg_lookahead(s_white()),
470            token(Code::Text, many1(nb_char())),
471        ),
472    )
473}
474
475/// [178] s-nb-spaced-text(n) — a more-indented or whitespace-starting line.
476///
477/// These lines are not folded — they are kept as-is. Uses `s_indent_content(n)`
478/// to consume exactly n spaces. The remaining content (including the leading
479/// whitespace that makes this "spaced") is emitted as Text.
480/// Requires at least one nb-char after the leading whitespace to avoid matching
481/// blank lines (which should be handled by l-empty/l-chomped-empty instead).
482fn s_nb_spaced_text(n: i32) -> Parser<'static> {
483    Box::new(move |state| {
484        // Consume exactly n spaces of indentation.
485        let after_indent = match s_indent_content(n)(state) {
486            Reply::Success { state, .. } => state,
487            Reply::Failure => return Reply::Failure,
488            Reply::Error(e) => return Reply::Error(e),
489        };
490        // Next char must be s-white (space or tab) — the "more indented" marker.
491        match after_indent.peek() {
492            Some(' ' | '\t') => {}
493            _ => return Reply::Failure,
494        }
495        // Emit the whitespace + remaining content as a single Text token.
496        // Use many1(nb_char()) to require at least one non-break char after the
497        // leading whitespace, preventing blank lines from matching.
498        token(Code::Text, seq(s_white(), many1(nb_char())))(after_indent)
499    })
500}
501
502/// [177] s-nb-folded-lines(n) — folded continuation lines.
503///
504/// Per spec: `s-nb-folded-text(n) ( b-l-folded(n,BLOCK-IN) s-nb-folded-text(n) )*`.
505fn s_nb_folded_lines(n: i32) -> Parser<'static> {
506    seq(
507        s_nb_folded_text(n),
508        many0(seq(
509            token(Code::LineFold, b_break()),
510            seq(many0(l_empty(n, Context::BlockIn)), s_nb_folded_text(n)),
511        )),
512    )
513}
514
515/// [179] s-nb-spaced-lines(n) — spaced (more-indented or whitespace) lines.
516fn s_nb_spaced_lines(n: i32) -> Parser<'static> {
517    seq(
518        s_nb_spaced_text(n),
519        many0(seq(
520            token(Code::LineFeed, b_break()),
521            seq(many0(l_empty(n, Context::BlockIn)), s_nb_spaced_text(n)),
522        )),
523    )
524}
525
526/// [180] l-nb-same-lines(n) — folded or spaced lines at same indentation.
527fn l_nb_same_lines(n: i32) -> Parser<'static> {
528    seq(
529        many0(l_empty(n, Context::BlockIn)),
530        alt(s_nb_folded_lines(n), s_nb_spaced_lines(n)),
531    )
532}
533
534/// [181] l-nb-diff-lines(n) — different-indented groups of folded/spaced lines.
535fn l_nb_diff_lines(n: i32) -> Parser<'static> {
536    seq(
537        l_nb_same_lines(n),
538        many0(seq(token(Code::LineFeed, b_break()), l_nb_same_lines(n))),
539    )
540}
541
542/// [182] l-folded-content(n,t) — full folded scalar body with chomping.
543fn l_folded_content(n: i32, t: Chomping) -> Parser<'static> {
544    Box::new(move |state| {
545        // Try to parse content.
546        match l_nb_diff_lines(n)(state.clone()) {
547            Reply::Failure => {
548                // Empty body.
549                l_chomped_empty(n, t)(state)
550            }
551            Reply::Error(e) => Reply::Error(e),
552            Reply::Success {
553                tokens: content_tokens,
554                state: after_content,
555            } => {
556                let last_result = b_chomped_last(t)(after_content.clone());
557                let (last_tokens, after_last) = match last_result {
558                    Reply::Success { tokens, state } => (tokens, state),
559                    Reply::Failure | Reply::Error(_) => (Vec::new(), after_content),
560                };
561
562                let tail_result = l_chomped_empty(n, t)(after_last.clone());
563                let (tail_tokens, final_state) = match tail_result {
564                    Reply::Success { tokens, state } => (tokens, state),
565                    Reply::Failure | Reply::Error(_) => (Vec::new(), after_last),
566                };
567
568                let mut all = content_tokens;
569                all.extend(last_tokens);
570                all.extend(tail_tokens);
571                Reply::Success {
572                    tokens: all,
573                    state: final_state,
574                }
575            }
576        }
577    })
578}
579
580/// [175] c-l+folded(n) — `>` header then folded content.
581#[must_use]
582pub fn c_l_folded(n: i32) -> Parser<'static> {
583    Box::new(move |state| {
584        // Must start with `>`.
585        let Some('>') = state.peek() else {
586            return Reply::Failure;
587        };
588        let start_pos = state.pos;
589        let after_gt = state.advance('>');
590
591        let Some((m_raw, chomp, chomp_char, after_header)) = parse_block_header(after_gt) else {
592            return Reply::Failure;
593        };
594
595        let m = if m_raw == 0 {
596            detect_scalar_indentation(after_header.input, n + 1)
597        } else {
598            n + m_raw
599        };
600
601        let header_tokens: Vec<crate::token::Token<'static>> = {
602            let mut v = vec![crate::token::Token {
603                code: Code::Indicator,
604                pos: start_pos,
605                text: ">",
606            }];
607            if let Some(ch) = chomp_char {
608                v.push(crate::token::Token {
609                    code: Code::Indicator,
610                    pos: start_pos,
611                    text: ch,
612                });
613            }
614            v
615        };
616
617        if m <= n {
618            let content_result = l_chomped_empty(m, chomp)(after_header.clone());
619            let (content_tokens, final_state) = match content_result {
620                Reply::Success { tokens, state } => (tokens, state),
621                Reply::Failure | Reply::Error(_) => (Vec::new(), after_header),
622            };
623            let mut all = vec![crate::token::Token {
624                code: Code::BeginScalar,
625                pos: start_pos,
626                text: "",
627            }];
628            all.extend(header_tokens);
629            all.extend(content_tokens);
630            all.push(crate::token::Token {
631                code: Code::EndScalar,
632                pos: final_state.pos,
633                text: "",
634            });
635            return Reply::Success {
636                tokens: all,
637                state: final_state,
638            };
639        }
640
641        let content_result = l_folded_content(m, chomp)(after_header);
642        let (content_tokens, final_state) = match content_result {
643            Reply::Success { tokens, state } => (tokens, state),
644            Reply::Failure => return Reply::Failure,
645            Reply::Error(e) => return Reply::Error(e),
646        };
647
648        let mut all = vec![crate::token::Token {
649            code: Code::BeginScalar,
650            pos: start_pos,
651            text: "",
652        }];
653        all.extend(header_tokens);
654        all.extend(content_tokens);
655        all.push(crate::token::Token {
656            code: Code::EndScalar,
657            pos: final_state.pos,
658            text: "",
659        });
660        Reply::Success {
661            tokens: all,
662            state: final_state,
663        }
664    })
665}
666
667// ---------------------------------------------------------------------------
668// §8.2.1 – Block sequences [183]–[186]
669// ---------------------------------------------------------------------------
670
671/// [201] seq-spaces(n,c) — indentation level for sequence entries.
672///
673/// `BlockOut` uses n-1 (entries dedent by 1), `BlockIn` uses n.
674#[must_use]
675pub const fn seq_spaces(n: i32, c: Context) -> i32 {
676    match c {
677        Context::BlockOut => n - 1,
678        Context::BlockIn
679        | Context::FlowOut
680        | Context::FlowIn
681        | Context::BlockKey
682        | Context::FlowKey => n,
683    }
684}
685
686/// [183] l+block-sequence(n) — a block sequence at indentation n.
687///
688/// Auto-detects the entry indentation `n+m` (m ≥ 0) from the first entry's column.
689#[must_use]
690pub fn l_block_sequence(n: i32) -> Parser<'static> {
691    Box::new(move |state| {
692        // Auto-detect entry column from the first non-empty line.
693        let detected = detect_scalar_indentation(state.input, n);
694        if detected < n {
695            return Reply::Failure;
696        }
697        wrap_tokens(
698            Code::BeginSequence,
699            Code::EndSequence,
700            many1(c_l_block_seq_entry(detected)),
701        )(state)
702    })
703}
704
705/// [184] c-l-block-seq-entry(n) — a single sequence entry: `- ` then content.
706fn c_l_block_seq_entry(n: i32) -> Parser<'static> {
707    Box::new(move |state| {
708        // Must start with exact indentation n spaces then `- `.
709        let indent_result = s_indent(n)(state.clone());
710        let after_indent = match indent_result {
711            Reply::Success { state, .. } => state,
712            Reply::Failure => return Reply::Failure,
713            Reply::Error(e) => return Reply::Error(e),
714        };
715
716        // Expect `-`.
717        let Some('-') = after_indent.peek() else {
718            return Reply::Failure;
719        };
720        let dash_pos = after_indent.pos;
721        let after_dash = after_indent.advance('-');
722
723        // The `-` must not be immediately followed by a non-space ns-char (that
724        // would make it part of a plain scalar, not a sequence indicator).
725        if let Some(ch) = after_dash.peek() {
726            if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' {
727                return Reply::Failure;
728            }
729        }
730
731        let dash_token = crate::token::Token {
732            code: Code::Indicator,
733            pos: dash_pos,
734            text: "-",
735        };
736
737        // Parse the value: block-indented content.
738        let value_result = s_b_block_indented(n, Context::BlockIn)(after_dash.clone());
739        let (value_tokens, final_state) = match value_result {
740            Reply::Success { tokens, state } => (tokens, state),
741            Reply::Failure => return Reply::Failure,
742            Reply::Error(e) => return Reply::Error(e),
743        };
744
745        let mut all = vec![dash_token];
746        all.extend(value_tokens);
747        Reply::Success {
748            tokens: all,
749            state: final_state,
750        }
751    })
752}
753
754/// [185] s-b+block-indented(n,c) — content after a sequence `- `.
755///
756/// Per spec: `( s-indent(m) ( ns-l-compact-sequence(n+1+m) |
757///              ns-l-compact-mapping(n+1+m) ) )
758///            | s-l+block-node(n,c) | ( e-node s-l-comments )`.
759///
760/// The `m` is the number of extra spaces after the `-` indicator.
761/// Compact forms use `n+1+m` as the indent level for continuation entries.
762fn s_b_block_indented(n: i32, c: Context) -> Parser<'static> {
763    Box::new(move |state| {
764        // Detect m: count leading spaces (the indent after `-`).
765        let m = i32::try_from(state.input.chars().take_while(|&ch| ch == ' ').count()).unwrap_or(0);
766
767        if m > 0 {
768            // Consume the m spaces.
769            let mut after_indent = state.clone();
770            for _ in 0..m {
771                after_indent = after_indent.advance(' ');
772            }
773
774            // Try compact sequence or compact mapping at n+1+m.
775            let compact_n = n + 1 + m;
776            let compact = alt(
777                ns_l_compact_sequence(compact_n),
778                ns_l_compact_mapping(compact_n),
779            );
780
781            match compact(after_indent.clone()) {
782                Reply::Success { tokens, state } => {
783                    return Reply::Success { tokens, state };
784                }
785                Reply::Failure | Reply::Error(_) => {}
786            }
787        }
788
789        // Per spec [185]: s-l+block-node(n,c) or (e-node s-l-comments).
790        let block_node = alt(s_l_block_node(n, c), seq(e_node(), s_l_comments()));
791        block_node(state)
792    })
793}
794
795/// Sequence entry without leading indent — for the first compact entry.
796fn c_l_block_seq_entry_no_indent(n: i32) -> Parser<'static> {
797    Box::new(move |state| {
798        let Some('-') = state.peek() else {
799            return Reply::Failure;
800        };
801        let dash_pos = state.pos;
802        let after_dash = state.advance('-');
803        if let Some(ch) = after_dash.peek() {
804            if ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' {
805                return Reply::Failure;
806            }
807        }
808        let dash_token = crate::token::Token {
809            code: Code::Indicator,
810            pos: dash_pos,
811            text: "-",
812        };
813        let value_result = s_b_block_indented(n, Context::BlockIn)(after_dash.clone());
814        let (value_tokens, final_state) = match value_result {
815            Reply::Success { tokens, state } => (tokens, state),
816            Reply::Failure => return Reply::Failure,
817            Reply::Error(e) => return Reply::Error(e),
818        };
819        let mut all = vec![dash_token];
820        all.extend(value_tokens);
821        Reply::Success {
822            tokens: all,
823            state: final_state,
824        }
825    })
826}
827
828/// [186] ns-l-compact-sequence(n) — compact nested sequence (no leading indent).
829///
830/// First entry has no leading indent (starts at current position after
831/// `s-b+block-indented` consumed the `m` spaces). Continuation entries
832/// use `s-indent(n)` per spec.
833fn ns_l_compact_sequence(n: i32) -> Parser<'static> {
834    wrap_tokens(
835        Code::BeginSequence,
836        Code::EndSequence,
837        seq(
838            c_l_block_seq_entry_no_indent(n),
839            many0(c_l_block_seq_entry(n)),
840        ),
841    )
842}
843
844// ---------------------------------------------------------------------------
845// §8.2.2 – Block mappings [187]–[195]
846// ---------------------------------------------------------------------------
847
848/// Skip blank lines (whitespace-only lines) before a mapping entry.
849/// These can appear between entries and are not structural.
850fn skip_blank_lines(state: State<'_>) -> State<'_> {
851    let mut s = state;
852    loop {
853        // Check if the current line is whitespace-only.
854        let remaining = s.input;
855        let ws_len = remaining
856            .chars()
857            .take_while(|&ch| ch == ' ' || ch == '\t')
858            .count();
859        let after_ws = &remaining[ws_len..];
860        if after_ws.starts_with('\n') {
861            // Skip this blank line.
862            let mut next = s;
863            for ch in remaining[..ws_len].chars() {
864                next = next.advance(ch);
865            }
866            next = next.advance('\n');
867            s = next;
868        } else if after_ws.starts_with("\r\n") {
869            let mut next = s;
870            for ch in remaining[..ws_len].chars() {
871                next = next.advance(ch);
872            }
873            next = next.advance('\r');
874            next = next.advance('\n');
875            s = next;
876        } else {
877            break;
878        }
879    }
880    s
881}
882
883/// [187] l+block-mapping(n) — a block mapping at indentation n.
884///
885/// Auto-detects the entry indentation `n+m` (m ≥ 0) from the first entry's column.
886#[must_use]
887pub fn l_block_mapping(n: i32) -> Parser<'static> {
888    Box::new(move |state| {
889        // Auto-detect entry column from the first non-empty line.
890        let detected = detect_scalar_indentation(state.input, n);
891        if detected < n {
892            return Reply::Failure;
893        }
894        // Parse entries, skipping blank lines between them.
895        wrap_tokens(
896            Code::BeginMapping,
897            Code::EndMapping,
898            Box::new(move |state| {
899                // First entry (required).
900                let (first_tokens, after_first) = match ns_l_block_map_entry(detected)(state) {
901                    Reply::Success { tokens, state } => (tokens, state),
902                    Reply::Failure => return Reply::Failure,
903                    Reply::Error(e) => return Reply::Error(e),
904                };
905                let mut all_tokens = first_tokens;
906                let mut current = after_first;
907                // Subsequent entries (optional), skipping blank lines.
908                loop {
909                    let skipped = skip_blank_lines(current.clone());
910                    match ns_l_block_map_entry(detected)(skipped.clone()) {
911                        Reply::Success { tokens, state } => {
912                            all_tokens.extend(tokens);
913                            current = state;
914                        }
915                        Reply::Failure | Reply::Error(_) => break,
916                    }
917                }
918                Reply::Success {
919                    tokens: all_tokens,
920                    state: current,
921                }
922            }),
923        )(state)
924    })
925}
926
927/// [188] ns-l-block-map-entry(n) — explicit (`?`) or implicit key entry.
928fn ns_l_block_map_entry(n: i32) -> Parser<'static> {
929    Box::new(move |state| {
930        // Must begin at indentation n.
931        let indent_result = s_indent(n)(state.clone());
932        let after_indent = match indent_result {
933            Reply::Success { state, .. } => state,
934            Reply::Failure => return Reply::Failure,
935            Reply::Error(e) => return Reply::Error(e),
936        };
937
938        alt(
939            c_l_block_map_explicit_entry(n),
940            ns_l_block_map_implicit_entry(n),
941        )(after_indent)
942    })
943}
944
945/// [189] c-l-block-map-explicit-entry(n) — `?` key + optional `:` value.
946fn c_l_block_map_explicit_entry(n: i32) -> Parser<'static> {
947    wrap_tokens(
948        Code::BeginPair,
949        Code::EndPair,
950        seq(
951            c_l_block_map_explicit_key(n),
952            opt(l_block_map_explicit_value(n)),
953        ),
954    )
955}
956
957/// [190] c-l-block-map-explicit-key(n) — `?` then block-indented content.
958///
959/// The `?` indicator must be followed by whitespace, a line break, or EOF
960/// to be recognized as an explicit key. `?foo` is a plain scalar, not an
961/// explicit key.
962fn c_l_block_map_explicit_key(n: i32) -> Parser<'static> {
963    Box::new(move |state| {
964        let Some('?') = state.peek() else {
965            return Reply::Failure;
966        };
967        let q_pos = state.pos;
968        let after_q = state.advance('?');
969
970        // `?` must be followed by whitespace, break, or EOF to be an indicator.
971        match after_q.peek() {
972            None | Some(' ' | '\t' | '\n' | '\r') => {}
973            Some(_) => return Reply::Failure,
974        }
975
976        let q_token = crate::token::Token {
977            code: Code::Indicator,
978            pos: q_pos,
979            text: "?",
980        };
981
982        let value_result = s_b_block_indented(n, Context::BlockOut)(after_q.clone());
983        let (value_tokens, final_state) = match value_result {
984            Reply::Success { tokens, state } => (tokens, state),
985            Reply::Failure | Reply::Error(_) => (Vec::new(), after_q),
986        };
987
988        let mut all = vec![q_token];
989        all.extend(value_tokens);
990        Reply::Success {
991            tokens: all,
992            state: final_state,
993        }
994    })
995}
996
997/// [191] l-block-map-explicit-value(n) — `: ` then block-indented content.
998fn l_block_map_explicit_value(n: i32) -> Parser<'static> {
999    Box::new(move |state| {
1000        // Must start at indent n.
1001        let indent_result = s_indent(n)(state.clone());
1002        let after_indent = match indent_result {
1003            Reply::Success { state, .. } => state,
1004            Reply::Failure => return Reply::Failure,
1005            Reply::Error(e) => return Reply::Error(e),
1006        };
1007
1008        let Some(':') = after_indent.peek() else {
1009            return Reply::Failure;
1010        };
1011        let colon_pos = after_indent.pos;
1012        let after_colon = after_indent.advance(':');
1013
1014        let colon_token = crate::token::Token {
1015            code: Code::Indicator,
1016            pos: colon_pos,
1017            text: ":",
1018        };
1019
1020        let value_result = s_b_block_indented(n, Context::BlockOut)(after_colon.clone());
1021        let (value_tokens, final_state) = match value_result {
1022            Reply::Success { tokens, state } => (tokens, state),
1023            Reply::Failure | Reply::Error(_) => (Vec::new(), after_colon),
1024        };
1025
1026        let mut all = vec![colon_token];
1027        all.extend(value_tokens);
1028        Reply::Success {
1029            tokens: all,
1030            state: final_state,
1031        }
1032    })
1033}
1034
1035/// [192] ns-l-block-map-implicit-entry(n) — key then `:` value.
1036///
1037/// Per spec: `( ns-s-block-map-implicit-key | e-node ) c-l-block-map-implicit-value(n)`.
1038/// Uses `c_l_block_map_implicit_value` which handles optional whitespace before `:`.
1039fn ns_l_block_map_implicit_entry(n: i32) -> Parser<'static> {
1040    wrap_tokens(
1041        Code::BeginPair,
1042        Code::EndPair,
1043        Box::new(move |state| {
1044            let (key_tokens, after_key) = match ns_s_block_map_implicit_key()(state.clone()) {
1045                Reply::Success { tokens, state } => (tokens, state),
1046                Reply::Failure | Reply::Error(_) => {
1047                    // Empty key with optional whitespace before `:`.
1048                    let check = match s_separate_in_line()(state.clone()) {
1049                        Reply::Success { state: s, .. } if is_value_indicator(s.input) => s,
1050                        Reply::Success { .. } | Reply::Failure | Reply::Error(_) => state.clone(),
1051                    };
1052                    if is_value_indicator(check.input) {
1053                        (Vec::new(), check)
1054                    } else {
1055                        return Reply::Failure;
1056                    }
1057                }
1058            };
1059            match c_l_block_map_implicit_value(n)(after_key) {
1060                Reply::Success {
1061                    mut tokens,
1062                    state: final_state,
1063                } => {
1064                    let mut all = key_tokens;
1065                    all.append(&mut tokens);
1066                    Reply::Success {
1067                        tokens: all,
1068                        state: final_state,
1069                    }
1070                }
1071                Reply::Failure => Reply::Failure,
1072                Reply::Error(e) => Reply::Error(e),
1073            }
1074        }),
1075    )
1076}
1077
1078/// Check if `:` at the given position is a value indicator (followed by
1079/// whitespace, break, or EOF). `:` followed by a non-space ns-char is
1080/// part of a plain scalar, not a value separator.
1081fn is_value_indicator(input: &str) -> bool {
1082    if !input.starts_with(':') {
1083        return false;
1084    }
1085    match input[1..].chars().next() {
1086        None | Some(' ' | '\t' | '\n' | '\r') => true,
1087        Some(_) => false,
1088    }
1089}
1090
1091/// [193] ns-s-block-map-implicit-key — optional properties then content as key.
1092///
1093/// Per spec [154]: `ns-flow-yaml-node(0,BLOCK-KEY) s-separate-in-line?`.
1094/// Handles: alias nodes, anchored/tagged scalars, plain scalars, quoted scalars,
1095/// and flow collections as keys. Properties-only keys (anchor/tag without content)
1096/// are allowed when `:` follows immediately or after whitespace.
1097#[must_use]
1098pub fn ns_s_block_map_implicit_key() -> Parser<'static> {
1099    Box::new(|state| {
1100        // Try alias node first (*alias), with optional trailing whitespace.
1101        if let Reply::Success {
1102            tokens,
1103            state: after_alias,
1104        } = crate::flow::c_ns_alias_node()(state.clone())
1105        {
1106            // Per spec [154]: s-separate-in-line?
1107            let final_state = match s_separate_in_line()(after_alias.clone()) {
1108                Reply::Success { state, .. } => state,
1109                Reply::Failure | Reply::Error(_) => after_alias,
1110            };
1111            return Reply::Success {
1112                tokens,
1113                state: final_state,
1114            };
1115        }
1116
1117        // Optional node properties (anchor/tag) before the key content.
1118        let (prop_tokens, after_props) =
1119            match seq(c_ns_properties(0, Context::BlockKey), s_separate_in_line())(state.clone()) {
1120                Reply::Success { tokens, state } => (tokens, state),
1121                Reply::Failure | Reply::Error(_) => (Vec::new(), state.clone()),
1122            };
1123
1124        // Key content: quoted scalar, plain scalar, or flow collection.
1125        let key_result = alt(
1126            crate::flow::c_double_quoted(0, Context::BlockKey),
1127            alt(
1128                crate::flow::c_single_quoted(0, Context::BlockKey),
1129                alt(
1130                    crate::flow::ns_plain(0, Context::BlockKey),
1131                    alt(
1132                        crate::flow::c_flow_sequence(0, Context::BlockKey),
1133                        crate::flow::c_flow_mapping(0, Context::BlockKey),
1134                    ),
1135                ),
1136            ),
1137        )(after_props.clone());
1138
1139        match key_result {
1140            Reply::Success {
1141                tokens: key_tokens,
1142                state: after_key,
1143            } => {
1144                // Per spec [154]: s-separate-in-line?
1145                let final_state = match s_separate_in_line()(after_key.clone()) {
1146                    Reply::Success { state, .. } => state,
1147                    Reply::Failure | Reply::Error(_) => after_key,
1148                };
1149                let mut all = prop_tokens;
1150                all.extend(key_tokens);
1151                Reply::Success {
1152                    tokens: all,
1153                    state: final_state,
1154                }
1155            }
1156            Reply::Failure => {
1157                if prop_tokens.is_empty() {
1158                    Reply::Failure
1159                } else {
1160                    // Properties without content — valid as empty-node key.
1161                    Reply::Success {
1162                        tokens: prop_tokens,
1163                        state: after_props,
1164                    }
1165                }
1166            }
1167            Reply::Error(e) => Reply::Error(e),
1168        }
1169    })
1170}
1171
1172/// [194] c-l-block-map-implicit-value(n) — `:` then block node or empty.
1173///
1174/// Per spec, the implicit key ends with `s-separate-in-line?` [154].
1175/// Consumes optional whitespace before `:` to handle `key : value` patterns.
1176fn c_l_block_map_implicit_value(n: i32) -> Parser<'static> {
1177    Box::new(move |state| {
1178        // Skip optional whitespace before `:`.
1179        let check_state = match s_separate_in_line()(state.clone()) {
1180            Reply::Success { state: s, .. } if s.peek() == Some(':') => s,
1181            Reply::Success { .. } | Reply::Failure | Reply::Error(_) => state.clone(),
1182        };
1183        if !is_value_indicator(check_state.input) {
1184            return Reply::Failure;
1185        }
1186        let colon_pos = check_state.pos;
1187        let after_colon = check_state.advance(':');
1188
1189        let colon_token = crate::token::Token {
1190            code: Code::Indicator,
1191            pos: colon_pos,
1192            text: ":",
1193        };
1194
1195        let value_result = alt(
1196            seq(
1197                s_separate(n, Context::BlockOut),
1198                s_l_block_node(n, Context::BlockOut),
1199            ),
1200            alt(
1201                s_l_block_node(n, Context::BlockIn),
1202                seq(e_node(), s_l_comments()),
1203            ),
1204        )(after_colon.clone());
1205
1206        let (value_tokens, final_state) = match value_result {
1207            Reply::Success { tokens, state } => (tokens, state),
1208            Reply::Failure | Reply::Error(_) => (Vec::new(), after_colon),
1209        };
1210
1211        let mut all = vec![colon_token];
1212        all.extend(value_tokens);
1213        Reply::Success {
1214            tokens: all,
1215            state: final_state,
1216        }
1217    })
1218}
1219
1220/// Compact implicit entry — allows whitespace before `:` per spec [154].
1221fn ns_l_compact_implicit_entry(n: i32) -> Parser<'static> {
1222    wrap_tokens(
1223        Code::BeginPair,
1224        Code::EndPair,
1225        Box::new(move |state| {
1226            let (key_tokens, after_key) = match ns_s_block_map_implicit_key()(state.clone()) {
1227                Reply::Success { tokens, state } => (tokens, state),
1228                Reply::Failure | Reply::Error(_) => {
1229                    if is_value_indicator(state.input) {
1230                        (Vec::new(), state.clone())
1231                    } else {
1232                        return Reply::Failure;
1233                    }
1234                }
1235            };
1236            match c_l_block_map_implicit_value(n)(after_key) {
1237                Reply::Success {
1238                    mut tokens,
1239                    state: final_state,
1240                } => {
1241                    let mut all = key_tokens;
1242                    all.append(&mut tokens);
1243                    Reply::Success {
1244                        tokens: all,
1245                        state: final_state,
1246                    }
1247                }
1248                Reply::Failure => Reply::Failure,
1249                Reply::Error(e) => Reply::Error(e),
1250            }
1251        }),
1252    )
1253}
1254
1255/// Block map entry without leading indent — explicit (`?`) or implicit key.
1256/// Used in compact mappings where the first entry has no indent prefix.
1257fn ns_l_block_map_entry_no_indent(n: i32) -> Parser<'static> {
1258    alt(
1259        c_l_block_map_explicit_entry(n),
1260        ns_l_compact_implicit_entry(n),
1261    )
1262}
1263
1264/// [195] ns-l-compact-mapping(n) — compact nested mapping (no leading indent).
1265///
1266/// Per spec: `ns-l-block-map-entry(n) ( s-indent(n) ns-l-block-map-entry(n) )*`.
1267/// Allows both explicit (`?`) and implicit key entries.
1268fn ns_l_compact_mapping(n: i32) -> Parser<'static> {
1269    wrap_tokens(
1270        Code::BeginMapping,
1271        Code::EndMapping,
1272        seq(
1273            ns_l_block_map_entry_no_indent(n),
1274            many0(seq(s_indent(n), ns_l_block_map_entry_no_indent(n))),
1275        ),
1276    )
1277}
1278
1279// ---------------------------------------------------------------------------
1280// §8.2.3 – Block nodes [196]–[200]
1281// ---------------------------------------------------------------------------
1282
1283/// [196] s-l+block-node(n,c) — a full block node with optional properties.
1284#[must_use]
1285pub fn s_l_block_node(n: i32, c: Context) -> Parser<'static> {
1286    alt(s_l_block_in_block(n, c), s_l_flow_in_block(n))
1287}
1288
1289/// [197] s-l+flow-in-block(n) — a flow node used inside a block context.
1290///
1291/// After the separator the parser must not be at a document boundary
1292/// (`c-forbidden`): a flow node that would start on a `---`/`...` line is
1293/// not valid content.
1294#[must_use]
1295pub fn s_l_flow_in_block(n: i32) -> Parser<'static> {
1296    seq(
1297        s_separate_ge(n + 1, Context::FlowOut),
1298        seq(
1299            neg_lookahead(c_forbidden()),
1300            seq(ns_flow_node(n + 1, Context::FlowOut), s_l_comments()),
1301        ),
1302    )
1303}
1304
1305/// [198] s-l+block-in-block(n,c) — a block scalar or block collection.
1306#[must_use]
1307pub fn s_l_block_in_block(n: i32, c: Context) -> Parser<'static> {
1308    alt(s_l_block_scalar(n, c), s_l_block_collection(n, c))
1309}
1310
1311/// [199] s-l+block-scalar(n,c) — a literal or folded block scalar.
1312#[must_use]
1313pub fn s_l_block_scalar(n: i32, c: Context) -> Parser<'static> {
1314    Box::new(move |state| {
1315        // Optional separator — use _ge to allow content at deeper indentation.
1316        let (sep_tokens, after_sep) = match s_separate_ge(n + 1, c)(state.clone()) {
1317            Reply::Success { tokens, state } => (tokens, state),
1318            Reply::Failure | Reply::Error(_) => (Vec::new(), state.clone()),
1319        };
1320
1321        // Optional properties. Separator after properties uses _ge for
1322        // deeper indentation.
1323        let (prop_tokens, after_props) =
1324            match seq(c_ns_properties(n + 1, c), s_separate_ge(n + 1, c))(after_sep.clone()) {
1325                Reply::Success { tokens, state } => (tokens, state),
1326                Reply::Failure | Reply::Error(_) => (Vec::new(), after_sep.clone()),
1327            };
1328
1329        // Literal or folded scalar.
1330        // Trail comments are consumed inside the scalar via l-chomped-empty
1331        // per spec [167]/[168], using the scalar's content indent n.
1332        let scalar_result = alt(c_l_literal(n), c_l_folded(n))(after_props.clone());
1333        let (scalar_tokens, after_scalar) = match scalar_result {
1334            Reply::Success { tokens, state } => (tokens, state),
1335            Reply::Failure => return Reply::Failure,
1336            Reply::Error(e) => return Reply::Error(e),
1337        };
1338
1339        let mut all = sep_tokens;
1340        all.extend(prop_tokens);
1341        all.extend(scalar_tokens);
1342        Reply::Success {
1343            tokens: all,
1344            state: after_scalar,
1345        }
1346    })
1347}
1348
1349/// [200] s-l+block-collection(n,c) — a block sequence or mapping.
1350#[must_use]
1351pub fn s_l_block_collection(n: i32, c: Context) -> Parser<'static> {
1352    Box::new(move |state| {
1353        // Optional properties + separator. Uses s_separate_ge for the property
1354        // separator because properties on a continuation line may be indented
1355        // deeper than n+1 (e.g., anchor on its own line at indent 2 when n=0).
1356        let (prop_tokens, after_props) =
1357            match seq(s_separate_ge(n + 1, c), c_ns_properties(n + 1, c))(state.clone()) {
1358                Reply::Success { tokens, state } => (tokens, state),
1359                Reply::Failure | Reply::Error(_) => (Vec::new(), state.clone()),
1360            };
1361
1362        // Optional s-l-comments before the collection.
1363        let (comment_tokens, after_comments) = match s_l_comments()(after_props.clone()) {
1364            Reply::Success { tokens, state } => (tokens, state),
1365            Reply::Failure | Reply::Error(_) => (Vec::new(), after_props.clone()),
1366        };
1367
1368        // Block sequence or mapping per spec [200]:
1369        //   l+block-sequence(seq-spaces(n,c)) | l+block-mapping(n+1)
1370        // Fall back to indentation level n if the n+1 attempt fails.
1371        let m = seq_spaces(n, c);
1372        // Try with properties.
1373        let with_props = alt(l_block_sequence(m), l_block_mapping(n + 1))(after_comments);
1374
1375        // If properties were consumed, also try without — the anchor/tag may
1376        // belong to the first entry's key, not the collection. Take whichever
1377        // consumes more input.
1378        if !prop_tokens.is_empty() {
1379            let (retry_comments, retry_after) = match s_l_comments()(state.clone()) {
1380                Reply::Success { tokens, state } => (tokens, state),
1381                Reply::Failure | Reply::Error(_) => (Vec::new(), state.clone()),
1382            };
1383            let without_props = alt(l_block_sequence(m), l_block_mapping(n + 1))(retry_after);
1384
1385            let with_end = match &with_props {
1386                Reply::Success { state, .. } => state.pos.byte_offset,
1387                Reply::Failure | Reply::Error(_) => 0,
1388            };
1389            let without_end = match &without_props {
1390                Reply::Success { state, .. } => state.pos.byte_offset,
1391                Reply::Failure | Reply::Error(_) => 0,
1392            };
1393
1394            if without_end > with_end {
1395                match without_props {
1396                    Reply::Success { tokens, state } => {
1397                        let mut all = retry_comments;
1398                        all.extend(tokens);
1399                        return Reply::Success { tokens: all, state };
1400                    }
1401                    Reply::Failure => return Reply::Failure,
1402                    Reply::Error(e) => return Reply::Error(e),
1403                }
1404            }
1405        }
1406
1407        match with_props {
1408            Reply::Success { tokens, state } => {
1409                let mut all = prop_tokens;
1410                all.extend(comment_tokens);
1411                all.extend(tokens);
1412                Reply::Success { tokens: all, state }
1413            }
1414            Reply::Failure => Reply::Failure,
1415            Reply::Error(e) => Reply::Error(e),
1416        }
1417    })
1418}
1419
1420// ---------------------------------------------------------------------------
1421// Tests
1422// ---------------------------------------------------------------------------
1423
1424#[cfg(test)]
1425#[allow(
1426    clippy::indexing_slicing,
1427    clippy::expect_used,
1428    clippy::unwrap_used,
1429    unused_imports
1430)]
1431mod tests {
1432    use super::*;
1433    use crate::combinator::{Context, Reply, State};
1434    use crate::token::Code;
1435
1436    fn state(input: &str) -> State<'_> {
1437        State::new(input)
1438    }
1439
1440    fn state_with(input: &str, n: i32, c: Context) -> State<'_> {
1441        State::with_context(input, n, c)
1442    }
1443
1444    fn is_success(reply: &Reply<'_>) -> bool {
1445        matches!(reply, Reply::Success { .. })
1446    }
1447
1448    fn is_failure(reply: &Reply<'_>) -> bool {
1449        matches!(reply, Reply::Failure)
1450    }
1451
1452    fn remaining<'a>(reply: &'a Reply<'a>) -> &'a str {
1453        match reply {
1454            Reply::Success { state, .. } => state.input,
1455            Reply::Failure | Reply::Error(_) => panic!("expected success, got failure/error"),
1456        }
1457    }
1458
1459    fn codes(reply: Reply<'_>) -> Vec<Code> {
1460        match reply {
1461            Reply::Success { tokens, .. } => tokens.into_iter().map(|t| t.code).collect(),
1462            Reply::Failure | Reply::Error(_) => vec![],
1463        }
1464    }
1465
1466    // -----------------------------------------------------------------------
1467    // Group 1: Chomping indicator [164] (3 tests)
1468    // -----------------------------------------------------------------------
1469
1470    #[test]
1471    fn c_chomping_indicator_strip_returns_strip() {
1472        let (chomp, after) = c_chomping_indicator(state("-"));
1473        assert_eq!(chomp, Chomping::Strip);
1474        assert_eq!(after.input, "");
1475    }
1476
1477    #[test]
1478    fn c_chomping_indicator_keep_returns_keep() {
1479        let (chomp, after) = c_chomping_indicator(state("+"));
1480        assert_eq!(chomp, Chomping::Keep);
1481        assert_eq!(after.input, "");
1482    }
1483
1484    #[test]
1485    fn c_chomping_indicator_absent_returns_clip() {
1486        let (chomp, after) = c_chomping_indicator(state("something"));
1487        assert_eq!(chomp, Chomping::Clip);
1488        assert_eq!(after.input, "something");
1489    }
1490
1491    // -----------------------------------------------------------------------
1492    // Group 2: Indentation indicator [163] (4 tests)
1493    // -----------------------------------------------------------------------
1494
1495    #[test]
1496    fn c_indentation_indicator_explicit_digit() {
1497        let reply = c_indentation_indicator(state("2\n"));
1498        assert!(is_success(&reply));
1499        assert_eq!(remaining(&reply), "\n");
1500    }
1501
1502    #[test]
1503    fn c_indentation_indicator_rejects_zero() {
1504        let reply = c_indentation_indicator(state("0\n"));
1505        assert!(is_failure(&reply));
1506    }
1507
1508    #[test]
1509    fn c_indentation_indicator_absent_succeeds_with_zero_consumption() {
1510        let reply = c_indentation_indicator(state("\n"));
1511        assert!(is_success(&reply));
1512        assert_eq!(remaining(&reply), "\n");
1513    }
1514
1515    #[test]
1516    fn c_indentation_indicator_digit_nine() {
1517        let reply = c_indentation_indicator(state("9rest"));
1518        assert!(is_success(&reply));
1519        assert_eq!(remaining(&reply), "rest");
1520    }
1521
1522    // -----------------------------------------------------------------------
1523    // Group 3: Block header [162] (6 tests)
1524    // -----------------------------------------------------------------------
1525
1526    #[test]
1527    fn c_b_block_header_indent_then_chomp() {
1528        let result = parse_block_header(state("2-\n"));
1529        assert!(result.is_some());
1530        let (m, chomp, _, after) = result.unwrap();
1531        assert_eq!(m, 2);
1532        assert_eq!(chomp, Chomping::Strip);
1533        assert_eq!(after.input, "");
1534    }
1535
1536    #[test]
1537    fn c_b_block_header_chomp_then_indent() {
1538        let result = parse_block_header(state("-2\n"));
1539        assert!(result.is_some());
1540        let (m, chomp, _, after) = result.unwrap();
1541        assert_eq!(m, 2);
1542        assert_eq!(chomp, Chomping::Strip);
1543        assert_eq!(after.input, "");
1544    }
1545
1546    #[test]
1547    fn c_b_block_header_chomp_only() {
1548        let result = parse_block_header(state("-\n"));
1549        assert!(result.is_some());
1550        let (m, chomp, _, after) = result.unwrap();
1551        assert_eq!(chomp, Chomping::Strip);
1552        assert_eq!(after.input, "");
1553        let _ = m;
1554    }
1555
1556    #[test]
1557    fn c_b_block_header_indent_only() {
1558        let result = parse_block_header(state("2\n"));
1559        assert!(result.is_some());
1560        let (m, _, _, after) = result.unwrap();
1561        assert_eq!(m, 2);
1562        assert_eq!(after.input, "");
1563    }
1564
1565    #[test]
1566    fn c_b_block_header_neither_indicator() {
1567        let result = parse_block_header(state("\n"));
1568        assert!(result.is_some());
1569        let (_, _, _, after) = result.unwrap();
1570        assert_eq!(after.input, "");
1571    }
1572
1573    #[test]
1574    fn c_b_block_header_with_trailing_comment() {
1575        let result = parse_block_header(state("2 # comment\n"));
1576        assert!(result.is_some());
1577        let (m, _, _, after) = result.unwrap();
1578        assert_eq!(m, 2);
1579        assert_eq!(after.input, "");
1580    }
1581
1582    // -----------------------------------------------------------------------
1583    // Group 4: Literal block scalar [170]–[174] (18 tests)
1584    // -----------------------------------------------------------------------
1585
1586    #[test]
1587    fn c_l_literal_accepts_simple_literal_scalar() {
1588        let reply = c_l_literal(0)(state("|\n  hello\n"));
1589        assert!(is_success(&reply));
1590        let c = codes(c_l_literal(0)(state("|\n  hello\n")));
1591        assert!(c.contains(&Code::BeginScalar));
1592        assert!(c.contains(&Code::EndScalar));
1593    }
1594
1595    #[test]
1596    fn c_l_literal_emits_indicator_for_pipe() {
1597        let c = codes(c_l_literal(0)(state("|\n  hello\n")));
1598        assert!(c.contains(&Code::Indicator));
1599    }
1600
1601    #[test]
1602    fn c_l_literal_consumes_entire_block() {
1603        let reply = c_l_literal(0)(state("|\n  hello\n  world\n"));
1604        assert!(is_success(&reply));
1605        assert_eq!(remaining(&reply), "");
1606    }
1607
1608    #[test]
1609    fn c_l_literal_leaves_less_indented_content_unconsumed() {
1610        let reply = c_l_literal(0)(state("|\n  hello\nrest\n"));
1611        assert!(is_success(&reply));
1612        assert!(remaining(&reply).starts_with("rest"));
1613    }
1614
1615    #[test]
1616    fn c_l_literal_clip_mode_strips_final_newlines_but_keeps_one() {
1617        let reply = c_l_literal(0)(state("|\n  hello\n\n\n"));
1618        assert!(is_success(&reply));
1619        let c = codes(c_l_literal(0)(state("|\n  hello\n\n\n")));
1620        assert!(c.contains(&Code::LineFeed));
1621    }
1622
1623    #[test]
1624    fn c_l_literal_strip_mode_removes_all_trailing_newlines() {
1625        let reply = c_l_literal(0)(state("|-\n  hello\n\n"));
1626        assert!(is_success(&reply));
1627        let c = codes(c_l_literal(0)(state("|-\n  hello\n\n")));
1628        // After the Text token there should be no LineFeed.
1629        let text_pos = c.iter().rposition(|&x| x == Code::Text);
1630        if let Some(pos) = text_pos {
1631            assert!(!c[pos..].contains(&Code::LineFeed));
1632        }
1633    }
1634
1635    #[test]
1636    fn c_l_literal_keep_mode_retains_all_trailing_newlines() {
1637        let reply = c_l_literal(0)(state("|+\n  hello\n\n\n"));
1638        assert!(is_success(&reply));
1639        // Should have break codes.
1640        let c = codes(c_l_literal(0)(state("|+\n  hello\n\n\n")));
1641        assert!(c.contains(&Code::LineFeed) || c.contains(&Code::Break));
1642    }
1643
1644    #[test]
1645    fn c_l_literal_explicit_indentation_indicator() {
1646        let reply = c_l_literal(0)(state("|2\n  hello\n  world\n"));
1647        assert!(is_success(&reply));
1648        assert_eq!(remaining(&reply), "");
1649    }
1650
1651    #[test]
1652    fn c_l_literal_explicit_indent_does_not_consume_less_indented() {
1653        let reply = c_l_literal(0)(state("|2\n  hello\n world\n"));
1654        assert!(is_success(&reply));
1655        assert!(remaining(&reply).contains("world"));
1656    }
1657
1658    #[test]
1659    fn c_l_literal_auto_detects_indentation_from_first_content_line() {
1660        let reply = c_l_literal(0)(state("|\n    hello\n    world\n"));
1661        assert!(is_success(&reply));
1662        assert_eq!(remaining(&reply), "");
1663    }
1664
1665    #[test]
1666    fn c_l_literal_empty_body_with_strip() {
1667        let reply = c_l_literal(0)(state("|-\n"));
1668        assert!(is_success(&reply));
1669        let c = codes(c_l_literal(0)(state("|-\n")));
1670        assert!(!c.contains(&Code::Text));
1671        assert!(!c.contains(&Code::LineFeed));
1672    }
1673
1674    #[test]
1675    fn c_l_literal_empty_body_with_clip() {
1676        let reply = c_l_literal(0)(state("|\n"));
1677        assert!(is_success(&reply));
1678        let c = codes(c_l_literal(0)(state("|\n")));
1679        assert!(!c.contains(&Code::Text));
1680    }
1681
1682    #[test]
1683    fn c_l_literal_empty_body_with_keep() {
1684        let reply = c_l_literal(0)(state("|+\n"));
1685        assert!(is_success(&reply));
1686    }
1687
1688    #[test]
1689    fn c_l_literal_preserves_internal_blank_lines() {
1690        let reply = c_l_literal(0)(state("|\n  hello\n\n  world\n"));
1691        assert!(is_success(&reply));
1692        let c = codes(c_l_literal(0)(state("|\n  hello\n\n  world\n")));
1693        assert!(c.contains(&Code::Text));
1694    }
1695
1696    #[test]
1697    fn c_l_literal_strip_chomp_with_explicit_indent() {
1698        let reply = c_l_literal(0)(state("|-2\n  hello\n\n"));
1699        assert!(is_success(&reply));
1700        let c = codes(c_l_literal(0)(state("|-2\n  hello\n\n")));
1701        let text_pos = c.iter().rposition(|&x| x == Code::Text);
1702        if let Some(pos) = text_pos {
1703            assert!(!c[pos..].contains(&Code::LineFeed));
1704        }
1705    }
1706
1707    #[test]
1708    fn c_l_literal_keep_chomp_with_explicit_indent() {
1709        let reply = c_l_literal(0)(state("|+2\n  hello\n\n"));
1710        assert!(is_success(&reply));
1711        let c = codes(c_l_literal(0)(state("|+2\n  hello\n\n")));
1712        assert!(c.contains(&Code::LineFeed) || c.contains(&Code::Break));
1713    }
1714
1715    #[test]
1716    fn c_l_literal_nested_at_n_equals_2() {
1717        let reply = c_l_literal(2)(state_with("|\n    hello\n", 2, Context::BlockIn));
1718        assert!(is_success(&reply));
1719    }
1720
1721    #[test]
1722    fn c_l_literal_fails_at_non_pipe_character() {
1723        let reply = c_l_literal(0)(state(">\n  hello\n"));
1724        assert!(is_failure(&reply));
1725    }
1726
1727    // -----------------------------------------------------------------------
1728    // Group 5: Folded block scalar [175]–[182] (18 tests)
1729    // -----------------------------------------------------------------------
1730
1731    #[test]
1732    fn c_l_folded_accepts_simple_folded_scalar() {
1733        let reply = c_l_folded(0)(state(">\n  hello\n"));
1734        assert!(is_success(&reply));
1735        let c = codes(c_l_folded(0)(state(">\n  hello\n")));
1736        assert!(c.contains(&Code::BeginScalar));
1737        assert!(c.contains(&Code::EndScalar));
1738    }
1739
1740    #[test]
1741    fn c_l_folded_emits_indicator_for_gt() {
1742        let c = codes(c_l_folded(0)(state(">\n  hello\n")));
1743        assert!(c.contains(&Code::Indicator));
1744    }
1745
1746    #[test]
1747    fn c_l_folded_folds_two_content_lines() {
1748        let reply = c_l_folded(0)(state(">\n  hello\n  world\n"));
1749        assert!(is_success(&reply));
1750    }
1751
1752    #[test]
1753    fn c_l_folded_clip_mode_keeps_one_trailing_newline() {
1754        let reply = c_l_folded(0)(state(">\n  hello\n\n"));
1755        assert!(is_success(&reply));
1756    }
1757
1758    #[test]
1759    fn c_l_folded_strip_mode_removes_trailing_newlines() {
1760        let reply = c_l_folded(0)(state(">-\n  hello\n\n"));
1761        assert!(is_success(&reply));
1762        let c = codes(c_l_folded(0)(state(">-\n  hello\n\n")));
1763        let text_pos = c.iter().rposition(|&x| x == Code::Text);
1764        if let Some(pos) = text_pos {
1765            assert!(!c[pos..].contains(&Code::LineFeed));
1766        }
1767    }
1768
1769    #[test]
1770    fn c_l_folded_keep_mode_retains_trailing_newlines() {
1771        let reply = c_l_folded(0)(state(">+\n  hello\n\n\n"));
1772        assert!(is_success(&reply));
1773        let c = codes(c_l_folded(0)(state(">+\n  hello\n\n\n")));
1774        assert!(c.contains(&Code::LineFeed) || c.contains(&Code::Break));
1775    }
1776
1777    #[test]
1778    fn c_l_folded_spaced_lines_not_folded() {
1779        let reply = c_l_folded(0)(state(">\n  hello\n\n  world\n"));
1780        assert!(is_success(&reply));
1781        let c = codes(c_l_folded(0)(state(">\n  hello\n\n  world\n")));
1782        assert!(c.iter().filter(|&&x| x == Code::Text).count() >= 2);
1783    }
1784
1785    #[test]
1786    fn c_l_folded_more_indented_lines_not_folded() {
1787        let reply = c_l_folded(0)(state(">\n  normal\n    indented\n  normal\n"));
1788        assert!(is_success(&reply));
1789    }
1790
1791    #[test]
1792    fn c_l_folded_explicit_indentation_indicator() {
1793        let reply = c_l_folded(0)(state(">2\n  hello\n  world\n"));
1794        assert!(is_success(&reply));
1795        assert_eq!(remaining(&reply), "");
1796    }
1797
1798    #[test]
1799    fn c_l_folded_auto_detects_indentation() {
1800        let reply = c_l_folded(0)(state(">\n    hello\n    world\n"));
1801        assert!(is_success(&reply));
1802        assert_eq!(remaining(&reply), "");
1803    }
1804
1805    #[test]
1806    fn c_l_folded_empty_body_with_strip() {
1807        let reply = c_l_folded(0)(state(">-\n"));
1808        assert!(is_success(&reply));
1809        let c = codes(c_l_folded(0)(state(">-\n")));
1810        assert!(!c.contains(&Code::Text));
1811    }
1812
1813    #[test]
1814    fn c_l_folded_empty_body_with_clip() {
1815        let reply = c_l_folded(0)(state(">\n"));
1816        assert!(is_success(&reply));
1817    }
1818
1819    #[test]
1820    fn c_l_folded_empty_body_with_keep() {
1821        let reply = c_l_folded(0)(state(">+\n"));
1822        assert!(is_success(&reply));
1823    }
1824
1825    #[test]
1826    fn c_l_folded_leaves_less_indented_content_unconsumed() {
1827        let reply = c_l_folded(0)(state(">\n  hello\nrest\n"));
1828        assert!(is_success(&reply));
1829        assert!(remaining(&reply).starts_with("rest"));
1830    }
1831
1832    #[test]
1833    fn c_l_folded_strip_with_explicit_indent() {
1834        let reply = c_l_folded(0)(state(">-2\n  hello\n\n"));
1835        assert!(is_success(&reply));
1836        let c = codes(c_l_folded(0)(state(">-2\n  hello\n\n")));
1837        let text_pos = c.iter().rposition(|&x| x == Code::Text);
1838        if let Some(pos) = text_pos {
1839            assert!(!c[pos..].contains(&Code::LineFeed));
1840        }
1841    }
1842
1843    #[test]
1844    fn c_l_folded_keep_with_explicit_indent() {
1845        let reply = c_l_folded(0)(state(">+2\n  hello\n\n"));
1846        assert!(is_success(&reply));
1847        let c = codes(c_l_folded(0)(state(">+2\n  hello\n\n")));
1848        assert!(c.contains(&Code::LineFeed) || c.contains(&Code::Break));
1849    }
1850
1851    #[test]
1852    fn c_l_folded_fails_at_non_gt_character() {
1853        let reply = c_l_folded(0)(state("|\n  hello\n"));
1854        assert!(is_failure(&reply));
1855    }
1856
1857    #[test]
1858    fn c_l_folded_nested_at_n_equals_2() {
1859        let reply = c_l_folded(2)(state_with(">\n    hello\n", 2, Context::BlockIn));
1860        assert!(is_success(&reply));
1861    }
1862
1863    // -----------------------------------------------------------------------
1864    // Group 6: Chomping helpers [165]–[169] (12 tests)
1865    // -----------------------------------------------------------------------
1866
1867    #[test]
1868    fn l_trail_comments_accepts_comment_at_lower_indent() {
1869        // n=2: trail comment at 0 spaces indent (< 2).
1870        let reply = l_trail_comments(2)(state("# comment\n"));
1871        assert!(is_success(&reply));
1872        let c = codes(l_trail_comments(2)(state("# comment\n")));
1873        assert!(c.contains(&Code::BeginComment));
1874    }
1875
1876    #[test]
1877    fn l_trail_comments_accepts_multiple_comments() {
1878        // n=2: trail comments at indent < 2.
1879        let reply = l_trail_comments(2)(state("# one\n# two\n"));
1880        assert!(is_success(&reply));
1881    }
1882
1883    #[test]
1884    fn l_trail_comments_fails_on_non_comment() {
1885        // n=2: content that isn't a comment should fail.
1886        let reply = l_trail_comments(2)(state("plaintext\n"));
1887        assert!(is_failure(&reply));
1888    }
1889
1890    #[test]
1891    fn l_strip_empty_consumes_blank_lines() {
1892        let reply = l_strip_empty(0)(state("\n\n\n"));
1893        assert!(is_success(&reply));
1894        assert_eq!(remaining(&reply), "");
1895    }
1896
1897    #[test]
1898    fn l_strip_empty_stops_before_non_blank() {
1899        let reply = l_strip_empty(0)(state("\n\ncontent"));
1900        assert!(is_success(&reply));
1901        assert_eq!(remaining(&reply), "content");
1902    }
1903
1904    #[test]
1905    fn l_keep_empty_consumes_blank_indented_lines() {
1906        let reply = l_keep_empty(2)(state("\n\n"));
1907        assert!(is_success(&reply));
1908        let c = codes(l_keep_empty(2)(state("\n\n")));
1909        assert!(c.contains(&Code::Break));
1910    }
1911
1912    #[test]
1913    fn l_chomped_empty_strip_consumes_only_blank_lines() {
1914        let reply = l_chomped_empty(0, Chomping::Strip)(state("\n\nrest"));
1915        assert!(is_success(&reply));
1916        let c = codes(l_chomped_empty(0, Chomping::Strip)(state("\n\nrest")));
1917        assert!(!c.contains(&Code::LineFeed));
1918        assert_eq!(
1919            remaining(&l_chomped_empty(0, Chomping::Strip)(state("\n\nrest"))),
1920            "rest"
1921        );
1922    }
1923
1924    #[test]
1925    fn l_chomped_empty_clip_consumes_nothing() {
1926        // Clip uses l_strip_empty which consumes blank lines without emitting.
1927        let reply = l_chomped_empty(0, Chomping::Clip)(state("\n"));
1928        assert!(is_success(&reply));
1929    }
1930
1931    #[test]
1932    fn l_chomped_empty_keep_emits_breaks() {
1933        let reply = l_chomped_empty(0, Chomping::Keep)(state("\n\n"));
1934        assert!(is_success(&reply));
1935        let c = codes(l_chomped_empty(0, Chomping::Keep)(state("\n\n")));
1936        assert!(c.contains(&Code::Break));
1937    }
1938
1939    #[test]
1940    fn b_chomped_last_strip_consumes_break() {
1941        let reply = b_chomped_last(Chomping::Strip)(state("\nrest"));
1942        assert!(is_success(&reply));
1943        assert_eq!(remaining(&reply), "rest");
1944        let c = codes(b_chomped_last(Chomping::Strip)(state("\nrest")));
1945        assert!(!c.contains(&Code::LineFeed));
1946    }
1947
1948    #[test]
1949    fn b_chomped_last_clip_emits_line_feed() {
1950        let reply = b_chomped_last(Chomping::Clip)(state("\nrest"));
1951        assert!(is_success(&reply));
1952        let c = codes(b_chomped_last(Chomping::Clip)(state("\nrest")));
1953        assert!(c.contains(&Code::LineFeed));
1954        assert_eq!(remaining(&reply), "rest");
1955    }
1956
1957    #[test]
1958    fn b_chomped_last_keep_emits_line_feed() {
1959        let reply = b_chomped_last(Chomping::Keep)(state("\nrest"));
1960        assert!(is_success(&reply));
1961        let c = codes(b_chomped_last(Chomping::Keep)(state("\nrest")));
1962        assert!(c.contains(&Code::LineFeed));
1963        assert_eq!(remaining(&reply), "rest");
1964    }
1965
1966    // -----------------------------------------------------------------------
1967    // Group 7: Block sequence [183]–[186] (16 tests)
1968    // -----------------------------------------------------------------------
1969
1970    #[test]
1971    fn l_block_sequence_accepts_single_entry() {
1972        let reply = l_block_sequence(0)(state("- hello\n"));
1973        assert!(is_success(&reply));
1974        let c = codes(l_block_sequence(0)(state("- hello\n")));
1975        assert!(c.contains(&Code::BeginSequence));
1976        assert!(c.contains(&Code::EndSequence));
1977    }
1978
1979    #[test]
1980    fn l_block_sequence_accepts_multiple_entries() {
1981        let reply = l_block_sequence(0)(state("- hello\n- world\n"));
1982        assert!(is_success(&reply));
1983        assert_eq!(remaining(&reply), "");
1984    }
1985
1986    #[test]
1987    fn l_block_sequence_emits_indicator_for_dash() {
1988        let c = codes(l_block_sequence(0)(state("- hello\n")));
1989        assert!(c.contains(&Code::Indicator));
1990    }
1991
1992    #[test]
1993    fn l_block_sequence_stops_before_less_indented_line() {
1994        // Sequence at n=1 (entries at 1 space); then "rest" at column 0.
1995        let reply = l_block_sequence(1)(state("  - hello\n  - world\nrest\n"));
1996        assert!(is_success(&reply));
1997        assert!(remaining(&reply).starts_with("rest"));
1998    }
1999
2000    #[test]
2001    fn l_block_sequence_fails_when_no_dash_present() {
2002        let reply = l_block_sequence(0)(state("hello\n"));
2003        assert!(is_failure(&reply));
2004    }
2005
2006    #[test]
2007    fn c_l_block_seq_entry_accepts_block_scalar_value() {
2008        let reply = l_block_sequence(0)(state("- |\n  hello\n"));
2009        assert!(is_success(&reply));
2010        let c = codes(l_block_sequence(0)(state("- |\n  hello\n")));
2011        assert!(c.contains(&Code::BeginScalar));
2012    }
2013
2014    #[test]
2015    fn c_l_block_seq_entry_accepts_nested_sequence() {
2016        let reply = l_block_sequence(0)(state("- - hello\n"));
2017        assert!(is_success(&reply));
2018    }
2019
2020    #[test]
2021    fn c_l_block_seq_entry_accepts_empty_value() {
2022        let reply = l_block_sequence(0)(state("-\n"));
2023        assert!(is_success(&reply));
2024    }
2025
2026    #[test]
2027    fn s_b_block_indented_accepts_compact_sequence() {
2028        let reply = s_b_block_indented(0, Context::BlockIn)(state("- a\n- b\n"));
2029        assert!(is_success(&reply));
2030    }
2031
2032    #[test]
2033    fn s_b_block_indented_accepts_compact_mapping() {
2034        let reply = s_b_block_indented(0, Context::BlockIn)(state(" a: 1\n b: 2\n"));
2035        assert!(is_success(&reply));
2036    }
2037
2038    #[test]
2039    fn ns_l_compact_sequence_accepts_nested_entries() {
2040        let reply = ns_l_compact_sequence(0)(state("- a\n- b\n"));
2041        assert!(is_success(&reply));
2042        let c = codes(ns_l_compact_sequence(0)(state("- a\n- b\n")));
2043        assert!(c.contains(&Code::BeginSequence));
2044    }
2045
2046    #[test]
2047    fn l_block_sequence_accepts_block_mapping_entry() {
2048        let reply = l_block_sequence(0)(state("- a: 1\n  b: 2\n"));
2049        assert!(is_success(&reply));
2050        let c = codes(l_block_sequence(0)(state("- a: 1\n  b: 2\n")));
2051        assert!(c.contains(&Code::BeginMapping));
2052    }
2053
2054    #[test]
2055    fn l_block_sequence_multiline_entry_consumed() {
2056        let reply = l_block_sequence(0)(state("- |\n  line1\n  line2\n- next\n"));
2057        assert!(is_success(&reply));
2058    }
2059
2060    #[test]
2061    fn l_block_sequence_entry_with_flow_sequence_value() {
2062        let reply = l_block_sequence(0)(state("- [a, b]\n"));
2063        assert!(is_success(&reply));
2064        let c = codes(l_block_sequence(0)(state("- [a, b]\n")));
2065        assert!(c.contains(&Code::BeginSequence));
2066    }
2067
2068    #[test]
2069    fn l_block_sequence_entry_with_plain_scalar() {
2070        let reply = l_block_sequence(0)(state("- hello world\n"));
2071        assert!(is_success(&reply));
2072        let c = codes(l_block_sequence(0)(state("- hello world\n")));
2073        assert!(c.contains(&Code::Text));
2074    }
2075
2076    #[test]
2077    fn l_block_sequence_entry_with_anchor() {
2078        let reply = l_block_sequence(0)(state("- &anchor hello\n"));
2079        assert!(is_success(&reply));
2080        let c = codes(l_block_sequence(0)(state("- &anchor hello\n")));
2081        assert!(c.contains(&Code::BeginAnchor));
2082    }
2083
2084    // -----------------------------------------------------------------------
2085    // Group 8: Block mapping [187]–[195] (20 tests)
2086    // -----------------------------------------------------------------------
2087
2088    #[test]
2089    fn l_block_mapping_accepts_single_implicit_entry() {
2090        let reply = l_block_mapping(0)(state("key: value\n"));
2091        assert!(is_success(&reply));
2092        let c = codes(l_block_mapping(0)(state("key: value\n")));
2093        assert!(c.contains(&Code::BeginMapping));
2094        assert!(c.contains(&Code::EndMapping));
2095    }
2096
2097    #[test]
2098    fn l_block_mapping_accepts_multiple_entries() {
2099        let reply = l_block_mapping(0)(state("a: 1\nb: 2\n"));
2100        assert!(is_success(&reply));
2101        assert_eq!(remaining(&reply), "");
2102    }
2103
2104    #[test]
2105    fn l_block_mapping_emits_begin_pair_per_entry() {
2106        let c = codes(l_block_mapping(0)(state("a: 1\nb: 2\n")));
2107        assert_eq!(c.iter().filter(|&&x| x == Code::BeginPair).count(), 2);
2108    }
2109
2110    #[test]
2111    fn l_block_mapping_emits_indicator_for_colon() {
2112        let c = codes(l_block_mapping(0)(state("key: value\n")));
2113        assert!(c.contains(&Code::Indicator));
2114    }
2115
2116    #[test]
2117    fn l_block_mapping_stops_before_less_indented_line() {
2118        // Mapping at n=1 (entries at 2 spaces), then "rest" at col 0.
2119        let reply = l_block_mapping(1)(state("  a: 1\n  b: 2\nrest\n"));
2120        assert!(is_success(&reply));
2121        assert!(remaining(&reply).starts_with("rest"));
2122    }
2123
2124    #[test]
2125    fn l_block_mapping_fails_when_no_key_present() {
2126        let reply = l_block_mapping(0)(state("- hello\n"));
2127        assert!(is_failure(&reply));
2128    }
2129
2130    #[test]
2131    fn ns_l_block_map_entry_accepts_explicit_key() {
2132        let reply = l_block_mapping(0)(state("? key\n: value\n"));
2133        assert!(is_success(&reply));
2134        let c = codes(l_block_mapping(0)(state("? key\n: value\n")));
2135        assert!(c.contains(&Code::Indicator));
2136    }
2137
2138    #[test]
2139    fn ns_l_block_map_entry_accepts_implicit_key() {
2140        let reply = l_block_mapping(0)(state("key: value\n"));
2141        assert!(is_success(&reply));
2142    }
2143
2144    #[test]
2145    fn c_l_block_map_explicit_key_emits_indicator() {
2146        let c = codes(l_block_mapping(0)(state("? key\n: value\n")));
2147        assert!(c.contains(&Code::Indicator));
2148    }
2149
2150    #[test]
2151    fn l_block_map_explicit_value_accepts_colon_value() {
2152        let reply = l_block_mapping(0)(state("? key\n: value\n"));
2153        assert!(is_success(&reply));
2154        let c = codes(l_block_mapping(0)(state("? key\n: value\n")));
2155        assert!(c.contains(&Code::Indicator));
2156    }
2157
2158    #[test]
2159    fn l_block_map_explicit_value_accepts_empty_value() {
2160        let reply = l_block_mapping(0)(state("? key\n:\n"));
2161        assert!(is_success(&reply));
2162    }
2163
2164    #[test]
2165    fn ns_s_block_map_implicit_key_accepts_plain_scalar() {
2166        let reply = ns_s_block_map_implicit_key()(state("key"));
2167        assert!(is_success(&reply));
2168        let c = codes(ns_s_block_map_implicit_key()(state("key")));
2169        assert!(c.contains(&Code::Text));
2170    }
2171
2172    #[test]
2173    fn ns_s_block_map_implicit_key_accepts_quoted_scalar() {
2174        let reply = ns_s_block_map_implicit_key()(state("\"key\""));
2175        assert!(is_success(&reply));
2176        let c = codes(ns_s_block_map_implicit_key()(state("\"key\"")));
2177        assert!(c.contains(&Code::BeginScalar));
2178    }
2179
2180    #[test]
2181    fn c_l_block_map_implicit_value_accepts_block_scalar() {
2182        let reply = l_block_mapping(0)(state("key: |\n  content\n"));
2183        assert!(is_success(&reply));
2184        let c = codes(l_block_mapping(0)(state("key: |\n  content\n")));
2185        assert!(c.contains(&Code::BeginScalar));
2186    }
2187
2188    #[test]
2189    fn c_l_block_map_implicit_value_accepts_plain_scalar() {
2190        let reply = l_block_mapping(0)(state("key: value\n"));
2191        assert!(is_success(&reply));
2192        let c = codes(l_block_mapping(0)(state("key: value\n")));
2193        assert!(c.contains(&Code::Text));
2194    }
2195
2196    #[test]
2197    fn c_l_block_map_implicit_value_accepts_empty_value() {
2198        let reply = l_block_mapping(0)(state("key:\n"));
2199        assert!(is_success(&reply));
2200    }
2201
2202    #[test]
2203    fn ns_l_compact_mapping_accepts_multiple_entries() {
2204        let reply = ns_l_compact_mapping(0)(state("a: 1\nb: 2\n"));
2205        assert!(is_success(&reply));
2206        let c = codes(ns_l_compact_mapping(0)(state("a: 1\nb: 2\n")));
2207        assert!(c.contains(&Code::BeginMapping));
2208    }
2209
2210    #[test]
2211    fn l_block_mapping_value_is_block_sequence() {
2212        let reply = l_block_mapping(0)(state("items:\n  - a\n  - b\n"));
2213        assert!(is_success(&reply));
2214        let c = codes(l_block_mapping(0)(state("items:\n  - a\n  - b\n")));
2215        assert!(c.contains(&Code::BeginSequence));
2216    }
2217
2218    #[test]
2219    fn l_block_mapping_value_is_nested_mapping() {
2220        let reply = l_block_mapping(0)(state("outer:\n  inner: val\n"));
2221        assert!(is_success(&reply));
2222        let c = codes(l_block_mapping(0)(state("outer:\n  inner: val\n")));
2223        assert!(c.iter().filter(|&&x| x == Code::BeginPair).count() >= 2);
2224    }
2225
2226    #[test]
2227    fn l_block_mapping_entry_with_anchor_on_key() {
2228        let reply = l_block_mapping(0)(state("&k key: value\n"));
2229        assert!(is_success(&reply));
2230        let c = codes(l_block_mapping(0)(state("&k key: value\n")));
2231        assert!(c.contains(&Code::BeginAnchor));
2232    }
2233
2234    // -----------------------------------------------------------------------
2235    // Group 9: Block nodes [196]–[201] (14 tests)
2236    // -----------------------------------------------------------------------
2237
2238    #[test]
2239    fn s_l_block_node_accepts_literal_scalar_in_block_in() {
2240        let reply = s_l_block_node(0, Context::BlockIn)(state("|\n  hello\n"));
2241        assert!(is_success(&reply));
2242        let c = codes(s_l_block_node(0, Context::BlockIn)(state("|\n  hello\n")));
2243        assert!(c.contains(&Code::BeginScalar));
2244    }
2245
2246    #[test]
2247    fn s_l_block_node_accepts_folded_scalar_in_block_in() {
2248        let reply = s_l_block_node(0, Context::BlockIn)(state(">\n  hello\n"));
2249        assert!(is_success(&reply));
2250        let c = codes(s_l_block_node(0, Context::BlockIn)(state(">\n  hello\n")));
2251        assert!(c.contains(&Code::BeginScalar));
2252    }
2253
2254    #[test]
2255    fn s_l_block_node_accepts_block_sequence_in_block_out() {
2256        let reply = s_l_block_node(0, Context::BlockOut)(state("\n- hello\n"));
2257        assert!(is_success(&reply));
2258        let c = codes(s_l_block_node(0, Context::BlockOut)(state("\n- hello\n")));
2259        assert!(c.contains(&Code::BeginSequence));
2260    }
2261
2262    #[test]
2263    fn s_l_block_node_accepts_block_mapping_in_block_out() {
2264        // Per spec [200], block collection at n=0 requires mapping at indent n+1=1.
2265        let reply = s_l_block_node(0, Context::BlockOut)(state("\n key: value\n"));
2266        assert!(is_success(&reply));
2267        let c = codes(s_l_block_node(0, Context::BlockOut)(state(
2268            "\n key: value\n",
2269        )));
2270        assert!(c.contains(&Code::BeginMapping));
2271    }
2272
2273    #[test]
2274    fn s_l_flow_in_block_accepts_flow_sequence() {
2275        // n=0 means content must be at column ≥ 1 (s-separate(n+1=1,flow-out)).
2276        let reply = s_l_flow_in_block(0)(state("\n [a, b]\n"));
2277        assert!(is_success(&reply));
2278        let c = codes(s_l_flow_in_block(0)(state("\n [a, b]\n")));
2279        assert!(c.contains(&Code::BeginSequence));
2280    }
2281
2282    #[test]
2283    fn s_l_flow_in_block_accepts_flow_mapping() {
2284        let reply = s_l_flow_in_block(0)(state("\n {a: b}\n"));
2285        assert!(is_success(&reply));
2286        let c = codes(s_l_flow_in_block(0)(state("\n {a: b}\n")));
2287        assert!(c.contains(&Code::BeginMapping));
2288    }
2289
2290    #[test]
2291    fn s_l_flow_in_block_accepts_double_quoted_scalar() {
2292        let reply = s_l_flow_in_block(0)(state("\n \"hello\"\n"));
2293        assert!(is_success(&reply));
2294        let c = codes(s_l_flow_in_block(0)(state("\n \"hello\"\n")));
2295        assert!(c.contains(&Code::BeginScalar));
2296    }
2297
2298    #[test]
2299    fn s_l_block_scalar_accepts_literal_scalar() {
2300        let reply = s_l_block_scalar(0, Context::BlockIn)(state("|\n  hello\n"));
2301        assert!(is_success(&reply));
2302        let c = codes(s_l_block_scalar(0, Context::BlockIn)(state("|\n  hello\n")));
2303        assert!(c.contains(&Code::BeginScalar));
2304    }
2305
2306    #[test]
2307    fn s_l_block_scalar_accepts_folded_scalar() {
2308        let reply = s_l_block_scalar(0, Context::BlockIn)(state(">\n  hello\n"));
2309        assert!(is_success(&reply));
2310        let c = codes(s_l_block_scalar(0, Context::BlockIn)(state(">\n  hello\n")));
2311        assert!(c.contains(&Code::BeginScalar));
2312    }
2313
2314    #[test]
2315    fn s_l_block_collection_accepts_block_sequence() {
2316        let reply = s_l_block_collection(0, Context::BlockOut)(state("\n- a\n- b\n"));
2317        assert!(is_success(&reply));
2318        let c = codes(s_l_block_collection(0, Context::BlockOut)(state(
2319            "\n- a\n- b\n",
2320        )));
2321        assert!(c.contains(&Code::BeginSequence));
2322    }
2323
2324    #[test]
2325    fn s_l_block_collection_accepts_block_mapping() {
2326        // Per spec [200], block collection at n=0 requires mapping at indent n+1=1.
2327        let reply = s_l_block_collection(0, Context::BlockOut)(state("\n a: 1\n b: 2\n"));
2328        assert!(is_success(&reply));
2329        let c = codes(s_l_block_collection(0, Context::BlockOut)(state(
2330            "\n a: 1\n b: 2\n",
2331        )));
2332        assert!(c.contains(&Code::BeginMapping));
2333    }
2334
2335    #[test]
2336    fn seq_spaces_block_out_uses_n_minus_1() {
2337        // In BlockOut, seq_spaces(1, BlockOut) = 0, so sequence at col 0 is accepted.
2338        let reply = l_block_sequence(0)(state_with("- hello\n", 0, Context::BlockOut));
2339        assert!(is_success(&reply));
2340    }
2341
2342    #[test]
2343    fn seq_spaces_block_in_uses_n() {
2344        // In BlockIn, seq_spaces(0, BlockIn) = 0.
2345        let reply = l_block_sequence(0)(state_with("- hello\n", 0, Context::BlockIn));
2346        assert!(is_success(&reply));
2347    }
2348
2349    #[test]
2350    fn s_l_block_in_block_accepts_block_scalar_content() {
2351        let reply = s_l_block_in_block(0, Context::BlockOut)(state("|\n  hello\n"));
2352        assert!(is_success(&reply));
2353        let c = codes(s_l_block_in_block(0, Context::BlockOut)(state(
2354            "|\n  hello\n",
2355        )));
2356        assert!(c.contains(&Code::BeginScalar));
2357    }
2358
2359    // -----------------------------------------------------------------------
2360    // Group 10: Properties and tags on block nodes (5 tests)
2361    // -----------------------------------------------------------------------
2362
2363    #[test]
2364    fn s_l_block_scalar_accepts_anchor_before_literal() {
2365        let reply = s_l_block_scalar(0, Context::BlockIn)(state("&a |\n  hello\n"));
2366        assert!(is_success(&reply));
2367        let c = codes(s_l_block_scalar(0, Context::BlockIn)(state(
2368            "&a |\n  hello\n",
2369        )));
2370        assert!(c.contains(&Code::BeginAnchor));
2371        assert!(c.contains(&Code::BeginScalar));
2372    }
2373
2374    #[test]
2375    fn s_l_block_scalar_accepts_tag_before_folded() {
2376        let reply = s_l_block_scalar(0, Context::BlockIn)(state("!!str >\n  hello\n"));
2377        assert!(is_success(&reply));
2378        let c = codes(s_l_block_scalar(0, Context::BlockIn)(state(
2379            "!!str >\n  hello\n",
2380        )));
2381        assert!(c.contains(&Code::BeginTag));
2382        assert!(c.contains(&Code::BeginScalar));
2383    }
2384
2385    #[test]
2386    fn l_block_mapping_accepts_tagged_value() {
2387        let reply = l_block_mapping(0)(state("key: !!str value\n"));
2388        assert!(is_success(&reply));
2389        let c = codes(l_block_mapping(0)(state("key: !!str value\n")));
2390        assert!(c.contains(&Code::BeginTag));
2391    }
2392
2393    #[test]
2394    fn l_block_sequence_accepts_anchored_entry() {
2395        let reply = l_block_sequence(0)(state("- &a hello\n"));
2396        assert!(is_success(&reply));
2397        let c = codes(l_block_sequence(0)(state("- &a hello\n")));
2398        assert!(c.contains(&Code::BeginAnchor));
2399    }
2400
2401    #[test]
2402    fn l_block_mapping_accepts_alias_as_value() {
2403        let reply = l_block_mapping(0)(state("key: *anchor\n"));
2404        assert!(is_success(&reply));
2405        let c = codes(l_block_mapping(0)(state("key: *anchor\n")));
2406        assert!(c.contains(&Code::BeginAlias));
2407    }
2408
2409    // -----------------------------------------------------------------------
2410    // Group 11: Auto-detect indentation edge cases (6 tests)
2411    // -----------------------------------------------------------------------
2412
2413    #[test]
2414    fn detect_indentation_skips_leading_blank_lines() {
2415        let reply = c_l_literal(0)(state("|\n\n  hello\n"));
2416        assert!(is_success(&reply));
2417        let c = codes(c_l_literal(0)(state("|\n\n  hello\n")));
2418        assert!(c.contains(&Code::Text));
2419    }
2420
2421    #[test]
2422    fn detect_indentation_uses_first_non_empty_line() {
2423        let reply = c_l_literal(0)(state("|\n\n\n    hello\n"));
2424        assert!(is_success(&reply));
2425        assert_eq!(remaining(&reply), "");
2426    }
2427
2428    #[test]
2429    fn detect_indentation_minimum_is_n_plus_1() {
2430        // n=2, content at 3 spaces (n+1=3) — valid.
2431        let reply = c_l_literal(2)(state("|\n   hello\n"));
2432        assert!(is_success(&reply));
2433    }
2434
2435    #[test]
2436    fn detect_indentation_rejects_content_at_n_or_less() {
2437        // n=2, content at 2 spaces — not valid (requires >n).
2438        let reply = c_l_literal(2)(state("|\n  hello\n"));
2439        // Either failure or remaining contains "hello" (not consumed as content).
2440        if is_success(&reply) {
2441            assert!(remaining(&reply).contains("hello"));
2442        }
2443    }
2444
2445    #[test]
2446    fn detect_indentation_all_blank_body_succeeds() {
2447        let reply = c_l_literal(0)(state("|\n\n"));
2448        assert!(is_success(&reply));
2449    }
2450
2451    #[test]
2452    fn detect_indentation_with_tab_in_blank_line_ignored() {
2453        // Tab in "blank" line — treated as non-content for auto-detect.
2454        let reply = c_l_literal(0)(state("|\n\t\n  hello\n"));
2455        assert!(is_success(&reply));
2456    }
2457
2458    // -----------------------------------------------------------------------
2459    // Group 12: Chomping × scalar style matrix (8 tests)
2460    // -----------------------------------------------------------------------
2461
2462    #[test]
2463    fn literal_clip_single_trailing_newline() {
2464        let reply = c_l_literal(0)(state("|\n  hello\n\n"));
2465        assert!(is_success(&reply));
2466        let c = codes(c_l_literal(0)(state("|\n  hello\n\n")));
2467        // Clip: exactly one LineFeed after Text.
2468        let text_pos = c.iter().rposition(|&x| x == Code::Text);
2469        let lf_count = text_pos.map_or(0, |pos| {
2470            c[pos..].iter().filter(|&&x| x == Code::LineFeed).count()
2471        });
2472        assert_eq!(lf_count, 1);
2473    }
2474
2475    #[test]
2476    fn literal_strip_no_trailing_newline() {
2477        let reply = c_l_literal(0)(state("|-\n  hello\n"));
2478        assert!(is_success(&reply));
2479        let c = codes(c_l_literal(0)(state("|-\n  hello\n")));
2480        let text_pos = c.iter().rposition(|&x| x == Code::Text);
2481        if let Some(pos) = text_pos {
2482            assert!(!c[pos..].contains(&Code::LineFeed));
2483        }
2484    }
2485
2486    #[test]
2487    fn literal_keep_multiple_trailing_newlines() {
2488        let reply = c_l_literal(0)(state("|+\n  hello\n\n\n"));
2489        assert!(is_success(&reply));
2490        let c = codes(c_l_literal(0)(state("|+\n  hello\n\n\n")));
2491        let break_count = c
2492            .iter()
2493            .filter(|&&x| x == Code::LineFeed || x == Code::Break)
2494            .count();
2495        assert!(break_count >= 2);
2496    }
2497
2498    #[test]
2499    fn folded_clip_single_trailing_newline() {
2500        let reply = c_l_folded(0)(state(">\n  hello\n\n"));
2501        assert!(is_success(&reply));
2502    }
2503
2504    #[test]
2505    fn folded_strip_no_trailing_newline() {
2506        let reply = c_l_folded(0)(state(">-\n  hello\n"));
2507        assert!(is_success(&reply));
2508        let c = codes(c_l_folded(0)(state(">-\n  hello\n")));
2509        let text_pos = c.iter().rposition(|&x| x == Code::Text);
2510        if let Some(pos) = text_pos {
2511            assert!(!c[pos..].contains(&Code::LineFeed));
2512        }
2513    }
2514
2515    #[test]
2516    fn folded_keep_multiple_trailing_newlines() {
2517        let reply = c_l_folded(0)(state(">+\n  hello\n\n\n"));
2518        assert!(is_success(&reply));
2519        let c = codes(c_l_folded(0)(state(">+\n  hello\n\n\n")));
2520        let break_count = c
2521            .iter()
2522            .filter(|&&x| x == Code::LineFeed || x == Code::Break)
2523            .count();
2524        assert!(break_count >= 2);
2525    }
2526
2527    #[test]
2528    fn literal_strip_empty_body_no_tokens_after_scalar_begin() {
2529        let reply = c_l_literal(0)(state("|-\n\n"));
2530        assert!(is_success(&reply));
2531        let c = codes(c_l_literal(0)(state("|-\n\n")));
2532        assert!(!c.contains(&Code::Text));
2533    }
2534
2535    #[test]
2536    fn folded_strip_empty_body_no_tokens_after_scalar_begin() {
2537        let reply = c_l_folded(0)(state(">-\n\n"));
2538        assert!(is_success(&reply));
2539        let c = codes(c_l_folded(0)(state(">-\n\n")));
2540        assert!(!c.contains(&Code::Text));
2541    }
2542}