Skip to main content

tstring_yaml/
lib.rs

1use saphyr::{ScalarOwned, YamlOwned};
2use saphyr_parser::{ScalarStyle, Tag};
3use std::borrow::Cow;
4use std::str::FromStr;
5use tstring_syntax::{
6    BackendError, BackendResult, NormalizedDocument, NormalizedEntry, NormalizedFloat,
7    NormalizedKey, NormalizedKeyEntry, NormalizedStream, NormalizedValue, SourcePosition,
8    SourceSpan, StreamItem, TemplateInput,
9};
10
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub enum YamlProfile {
13    V1_2_2,
14}
15
16impl YamlProfile {
17    #[must_use]
18    pub const fn as_str(self) -> &'static str {
19        match self {
20            Self::V1_2_2 => "1.2.2",
21        }
22    }
23}
24
25impl Default for YamlProfile {
26    fn default() -> Self {
27        Self::V1_2_2
28    }
29}
30
31impl FromStr for YamlProfile {
32    type Err = String;
33
34    fn from_str(value: &str) -> Result<Self, Self::Err> {
35        match value {
36            "1.2.2" => Ok(Self::V1_2_2),
37            other => Err(format!(
38                "Unsupported YAML profile {other:?}. Supported profiles: \"1.2.2\"."
39            )),
40        }
41    }
42}
43
44#[derive(Clone, Debug)]
45pub struct YamlInterpolationNode {
46    pub span: SourceSpan,
47    pub interpolation_index: usize,
48    pub role: String,
49}
50
51#[derive(Clone, Debug)]
52pub struct YamlTextChunkNode {
53    pub span: SourceSpan,
54    pub value: String,
55}
56
57#[derive(Clone, Debug)]
58pub enum YamlChunk {
59    Text(YamlTextChunkNode),
60    Interpolation(YamlInterpolationNode),
61}
62
63#[derive(Clone, Debug)]
64pub struct YamlTagNode {
65    pub span: SourceSpan,
66    pub chunks: Vec<YamlChunk>,
67}
68
69#[derive(Clone, Debug)]
70pub struct YamlAnchorNode {
71    pub span: SourceSpan,
72    pub chunks: Vec<YamlChunk>,
73}
74
75#[derive(Clone, Debug)]
76pub struct YamlPlainScalarNode {
77    pub span: SourceSpan,
78    pub chunks: Vec<YamlChunk>,
79}
80
81#[derive(Clone, Debug)]
82pub struct YamlDoubleQuotedScalarNode {
83    pub span: SourceSpan,
84    pub chunks: Vec<YamlChunk>,
85}
86
87#[derive(Clone, Debug)]
88pub struct YamlSingleQuotedScalarNode {
89    pub span: SourceSpan,
90    pub chunks: Vec<YamlChunk>,
91}
92
93#[derive(Clone, Debug)]
94pub struct YamlBlockScalarNode {
95    pub span: SourceSpan,
96    pub style: String,
97    pub chomping: Option<String>,
98    pub indent_indicator: Option<usize>,
99    pub chunks: Vec<YamlChunk>,
100}
101
102#[derive(Clone, Debug)]
103pub struct YamlAliasNode {
104    pub span: SourceSpan,
105    pub chunks: Vec<YamlChunk>,
106}
107
108#[derive(Clone, Debug)]
109pub enum YamlKeyValue {
110    Scalar(YamlScalarNode),
111    Interpolation(YamlInterpolationNode),
112    Complex(Box<YamlValueNode>),
113}
114
115#[derive(Clone, Debug)]
116pub struct YamlKeyNode {
117    pub span: SourceSpan,
118    pub value: YamlKeyValue,
119}
120
121#[derive(Clone, Debug)]
122pub struct YamlMappingEntryNode {
123    pub span: SourceSpan,
124    pub key: YamlKeyNode,
125    pub value: YamlValueNode,
126}
127
128#[derive(Clone, Debug)]
129pub struct YamlMappingNode {
130    pub span: SourceSpan,
131    pub entries: Vec<YamlMappingEntryNode>,
132    pub flow: bool,
133}
134
135#[derive(Clone, Debug)]
136pub struct YamlSequenceNode {
137    pub span: SourceSpan,
138    pub items: Vec<YamlValueNode>,
139    pub flow: bool,
140}
141
142#[derive(Clone, Debug)]
143pub struct YamlDecoratedNode {
144    pub span: SourceSpan,
145    pub value: Box<YamlValueNode>,
146    pub tag: Option<YamlTagNode>,
147    pub anchor: Option<YamlAnchorNode>,
148}
149
150#[derive(Clone, Debug)]
151pub struct YamlDocumentNode {
152    pub span: SourceSpan,
153    pub directives: Vec<String>,
154    pub explicit_start: bool,
155    pub explicit_end: bool,
156    pub value: YamlValueNode,
157}
158
159#[derive(Clone, Debug)]
160pub struct YamlStreamNode {
161    pub span: SourceSpan,
162    pub documents: Vec<YamlDocumentNode>,
163}
164
165#[derive(Clone, Debug)]
166pub enum YamlScalarNode {
167    Plain(YamlPlainScalarNode),
168    DoubleQuoted(YamlDoubleQuotedScalarNode),
169    SingleQuoted(YamlSingleQuotedScalarNode),
170    Block(YamlBlockScalarNode),
171    Alias(YamlAliasNode),
172}
173
174#[derive(Clone, Debug)]
175pub enum YamlValueNode {
176    Scalar(YamlScalarNode),
177    Interpolation(YamlInterpolationNode),
178    Mapping(YamlMappingNode),
179    Sequence(YamlSequenceNode),
180    Decorated(YamlDecoratedNode),
181}
182
183pub struct YamlParser {
184    items: Vec<StreamItem>,
185    index: usize,
186}
187
188impl YamlParser {
189    #[must_use]
190    pub fn new(template: &TemplateInput) -> Self {
191        Self {
192            items: template.flatten(),
193            index: 0,
194        }
195    }
196
197    #[must_use]
198    pub fn from_items(items: Vec<StreamItem>) -> Self {
199        Self { items, index: 0 }
200    }
201
202    pub fn parse(&mut self) -> BackendResult<YamlDocumentNode> {
203        let start = self.mark();
204        self.skip_blank_lines();
205        self.consume_line_start_tabs()?;
206        let value = self.parse_block_node(self.current_line_indent())?;
207        self.skip_trailing_document_space();
208        self.skip_blank_lines();
209        if self.current_kind() != "eof" {
210            return Err(self.error("Unexpected trailing YAML content."));
211        }
212        Ok(YamlDocumentNode {
213            span: self.span_from(start),
214            directives: Vec::new(),
215            explicit_start: false,
216            explicit_end: false,
217            value,
218        })
219    }
220
221    fn current(&self) -> &StreamItem {
222        &self.items[self.index]
223    }
224
225    fn current_kind(&self) -> &'static str {
226        self.current().kind()
227    }
228
229    fn current_char(&self) -> Option<char> {
230        self.current().char()
231    }
232
233    fn mark(&self) -> SourcePosition {
234        self.current().span().start.clone()
235    }
236
237    fn previous_end(&self) -> SourcePosition {
238        if self.index == 0 {
239            return self.current().span().start.clone();
240        }
241        self.items[self.index - 1].span().end.clone()
242    }
243
244    fn span_from(&self, start: SourcePosition) -> SourceSpan {
245        SourceSpan::between(start, self.previous_end())
246    }
247
248    fn error(&self, message: impl Into<String>) -> BackendError {
249        BackendError::parse_at("yaml.parse", message, Some(self.current().span().clone()))
250    }
251
252    fn advance(&mut self) {
253        if self.current_kind() != "eof" {
254            self.index += 1;
255        }
256    }
257
258    fn is_line_start(&self) -> bool {
259        self.index == 0 || self.items[self.index - 1].char() == Some('\n')
260    }
261
262    fn current_line_indent(&self) -> usize {
263        let mut probe = self.index;
264        while probe > 0 && self.items[probe - 1].char() != Some('\n') {
265            probe -= 1;
266        }
267        let mut indent = 0;
268        while matches!(self.items[probe].char(), Some(' ')) {
269            indent += 1;
270            probe += 1;
271        }
272        indent
273    }
274
275    fn skip_blank_lines(&mut self) {
276        loop {
277            if self.current_kind() == "eof" || !self.is_line_start() {
278                return;
279            }
280            let mut probe = self.index;
281            while matches!(self.items[probe].char(), Some(' ')) {
282                probe += 1;
283            }
284            match &self.items[probe] {
285                StreamItem::Eof { .. } => {
286                    self.index = probe;
287                    return;
288                }
289                StreamItem::Char { ch: '#', .. } => {
290                    self.index = probe;
291                    while !matches!(self.current_char(), None | Some('\n')) {
292                        self.advance();
293                    }
294                    if self.current_char() == Some('\n') {
295                        self.advance();
296                    }
297                }
298                _ if self.items[probe].char() == Some('\n') => {
299                    self.index = probe;
300                    self.advance();
301                }
302                _ => return,
303            }
304        }
305    }
306
307    fn skip_trailing_document_space(&mut self) {
308        loop {
309            while self.current_char() == Some(' ') {
310                self.advance();
311            }
312            if self.current_starts_comment() {
313                while !matches!(self.current_char(), None | Some('\n')) {
314                    self.advance();
315                }
316            }
317            if self.current_char() == Some('\n') {
318                self.advance();
319                continue;
320            }
321            return;
322        }
323    }
324
325    fn consume_indent(&mut self, indent: usize) -> BackendResult<()> {
326        if !self.is_line_start() {
327            return Ok(());
328        }
329        for _ in 0..indent {
330            if self.current_char() != Some(' ') {
331                return Err(self.error("Incorrect YAML indentation."));
332            }
333            self.advance();
334        }
335        Ok(())
336    }
337
338    fn parse_block_node(&mut self, indent: usize) -> BackendResult<YamlValueNode> {
339        self.skip_blank_lines();
340        self.consume_line_start_tabs()?;
341        if self.current_kind() == "eof" || self.current_line_indent() < indent {
342            return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
343                null_plain_scalar(),
344            )));
345        }
346        self.consume_indent(indent)?;
347        if self.starts_sequence_item() {
348            return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
349        }
350        if self.line_has_mapping_key() {
351            return Ok(YamlValueNode::Mapping(self.parse_block_mapping(indent)?));
352        }
353        let decorated = self.parse_decorators()?;
354        if decorated.is_some() && self.starts_sequence_item() {
355            return Err(self.error("Unexpected trailing YAML content."));
356        }
357        let value = if decorated.is_some() && self.decorator_line_break_ahead() {
358            self.consume_inline_comment_and_line_break();
359            self.skip_blank_lines();
360            self.consume_line_start_tabs()?;
361            let next_indent = self.current_line_indent();
362            let has_nested_value = self.current_kind() != "eof"
363                && (next_indent >= indent || (indent > 0 && next_indent + 1 == indent));
364            if has_nested_value {
365                self.parse_block_node(next_indent)?
366            } else {
367                YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
368            }
369        } else {
370            self.parse_inline_value(None, indent, false)?
371        };
372        wrap_decorators(decorated, value)
373    }
374
375    fn value_owns_following_lines(value: &YamlValueNode) -> bool {
376        match value {
377            YamlValueNode::Mapping(node) => !node.flow,
378            YamlValueNode::Sequence(node) => !node.flow,
379            YamlValueNode::Scalar(YamlScalarNode::Block(_)) => true,
380            YamlValueNode::Decorated(node) => Self::value_owns_following_lines(node.value.as_ref()),
381            _ => false,
382        }
383    }
384
385    fn parse_decorators(
386        &mut self,
387    ) -> BackendResult<Option<(Option<YamlTagNode>, Option<YamlAnchorNode>)>> {
388        let mut tag = None;
389        let mut anchor = None;
390        let mut consumed = false;
391        loop {
392            if self.current_char() == Some('!') {
393                tag = Some(self.parse_tag()?);
394                consumed = true;
395                self.skip_inline_spaces();
396                continue;
397            }
398            if self.current_char() == Some('&') {
399                anchor = Some(self.parse_anchor()?);
400                consumed = true;
401                self.skip_inline_spaces();
402                continue;
403            }
404            break;
405        }
406        Ok(consumed.then_some((tag, anchor)))
407    }
408
409    fn parse_tag(&mut self) -> BackendResult<YamlTagNode> {
410        let start = self.mark();
411        self.advance();
412        let chunks = if self.current_char() == Some('<') {
413            self.parse_verbatim_tag_chunks()?
414        } else {
415            self.parse_symbol_chunks(&[' ', '\t', '\n', '[', ']', '{', '}', ',', ':'])?
416        };
417        Ok(YamlTagNode {
418            span: self.span_from(start),
419            chunks,
420        })
421    }
422
423    fn parse_anchor(&mut self) -> BackendResult<YamlAnchorNode> {
424        let start = self.mark();
425        self.advance();
426        let chunks = self.parse_symbol_chunks(&[' ', '\t', '\n', '[', ']', '{', '}', ','])?;
427        Ok(YamlAnchorNode {
428            span: self.span_from(start),
429            chunks,
430        })
431    }
432
433    fn parse_alias(&mut self) -> BackendResult<YamlAliasNode> {
434        let start = self.mark();
435        self.advance();
436        let chunks = self.parse_symbol_chunks(&[' ', '\t', '\n', '[', ']', '{', '}', ','])?;
437        Ok(YamlAliasNode {
438            span: self.span_from(start),
439            chunks,
440        })
441    }
442
443    fn parse_symbol_chunks(&mut self, stop_chars: &[char]) -> BackendResult<Vec<YamlChunk>> {
444        let mut chunks = Vec::new();
445        let mut buffer = String::new();
446        while self.current_kind() != "eof" {
447            if self.current_kind() == "interpolation" {
448                self.flush_buffer(&mut buffer, &mut chunks);
449                chunks.push(YamlChunk::Interpolation(
450                    self.consume_interpolation("metadata_fragment")?,
451                ));
452                continue;
453            }
454            let Some(ch) = self.current_char() else {
455                break;
456            };
457            if stop_chars.contains(&ch) {
458                break;
459            }
460            buffer.push(ch);
461            self.advance();
462        }
463        self.flush_buffer(&mut buffer, &mut chunks);
464        Ok(chunks)
465    }
466
467    fn parse_verbatim_tag_chunks(&mut self) -> BackendResult<Vec<YamlChunk>> {
468        let mut chunks = Vec::new();
469        let mut buffer = String::new();
470        buffer.push('<');
471        self.advance();
472
473        while self.current_kind() != "eof" {
474            if self.current_kind() == "interpolation" {
475                self.flush_buffer(&mut buffer, &mut chunks);
476                chunks.push(YamlChunk::Interpolation(
477                    self.consume_interpolation("metadata_fragment")?,
478                ));
479                continue;
480            }
481
482            let Some(ch) = self.current_char() else {
483                break;
484            };
485            buffer.push(ch);
486            self.advance();
487            if ch == '>' {
488                self.flush_buffer(&mut buffer, &mut chunks);
489                return Ok(chunks);
490            }
491        }
492
493        Err(self.error("Unterminated YAML verbatim tag."))
494    }
495
496    fn skip_inline_spaces(&mut self) {
497        while matches!(self.current_char(), Some(' ' | '\t')) {
498            self.advance();
499        }
500    }
501
502    fn skip_flow_separation(&mut self) {
503        self.skip_flow_separation_with_breaks();
504    }
505
506    fn skip_flow_separation_with_breaks(&mut self) -> bool {
507        let mut saw_line_break = false;
508        loop {
509            while matches!(self.current_char(), Some(' ' | '\t' | '\r')) {
510                self.advance();
511            }
512            if self.current_starts_comment() {
513                while !matches!(self.current_char(), None | Some('\n')) {
514                    self.advance();
515                }
516            }
517            if self.current_char() == Some('\n') {
518                saw_line_break = true;
519                self.advance();
520                continue;
521            }
522            break;
523        }
524        saw_line_break
525    }
526
527    fn starts_sequence_item(&self) -> bool {
528        self.starts_sequence_item_at(self.index)
529    }
530
531    fn starts_sequence_item_at(&self, mut probe: usize) -> bool {
532        while matches!(self.items.get(probe).and_then(StreamItem::char), Some(' ')) {
533            probe += 1;
534        }
535        matches!(self.items.get(probe).and_then(StreamItem::char), Some('-'))
536            && matches!(
537                self.items.get(probe + 1).and_then(StreamItem::char),
538                Some(' ' | '\t' | '\n')
539            )
540    }
541
542    fn peek_char(&self, offset: usize) -> Option<char> {
543        self.items
544            .get(self.index + offset)
545            .and_then(StreamItem::char)
546    }
547
548    fn line_has_mapping_key(&self) -> bool {
549        self.line_has_mapping_key_at(self.index)
550    }
551
552    fn line_has_mapping_key_at(&self, probe: usize) -> bool {
553        if matches!(self.items.get(probe).and_then(StreamItem::char), Some('?'))
554            && matches!(
555                self.items.get(probe + 1).and_then(StreamItem::char),
556                Some(' ' | '\t' | '\n')
557            )
558        {
559            return true;
560        }
561        let mut parser = Self {
562            items: self.items.clone(),
563            index: probe,
564        };
565        if parser.parse_key().is_err() {
566            return false;
567        }
568        parser.skip_key_value_spaces();
569        if parser.current_char() != Some(':') {
570            return false;
571        }
572        matches!(parser.peek_char(1), Some(' ' | '\t' | '\n') | None)
573    }
574
575    fn skip_key_value_spaces(&mut self) {
576        while matches!(self.current_char(), Some(' ' | '\t')) {
577            self.advance();
578        }
579    }
580
581    fn decorator_line_break_ahead(&self) -> bool {
582        let mut probe = self.index;
583        while matches!(
584            self.items.get(probe).and_then(StreamItem::char),
585            Some(' ' | '\t')
586        ) {
587            probe += 1;
588        }
589        if self.items.get(probe).and_then(StreamItem::char) == Some('#') {
590            while !matches!(
591                self.items.get(probe).and_then(StreamItem::char),
592                None | Some('\n')
593            ) {
594                probe += 1;
595            }
596        }
597        matches!(self.items.get(probe).and_then(StreamItem::char), Some('\n'))
598            || matches!(self.items.get(probe), Some(StreamItem::Eof { .. }))
599    }
600
601    fn consume_inline_comment_and_line_break(&mut self) {
602        self.skip_inline_spaces();
603        if self.current_starts_comment() {
604            while !matches!(self.current_char(), None | Some('\n')) {
605                self.advance();
606            }
607        }
608        if self.current_char() == Some('\n') {
609            self.advance();
610        }
611    }
612
613    fn consume_line_end_required(&mut self) -> BackendResult<()> {
614        if self.current_kind() == "eof" || self.is_line_start() {
615            return Ok(());
616        }
617        self.skip_inline_spaces();
618        if self.current_starts_comment() {
619            while !matches!(self.current_char(), None | Some('\n')) {
620                self.advance();
621            }
622        }
623        match self.current_char() {
624            Some('\n') => {
625                self.advance();
626                Ok(())
627            }
628            None => Ok(()),
629            _ if self.current_kind() == "eof" => Ok(()),
630            _ => Err(self.error("Unexpected trailing YAML content.")),
631        }
632    }
633
634    fn consume_line_start_tabs(&mut self) -> BackendResult<()> {
635        if !self.is_line_start() || self.current_char() != Some('\t') {
636            return Ok(());
637        }
638
639        let mut probe = self.index;
640        while self.items.get(probe).and_then(StreamItem::char) == Some('\t') {
641            probe += 1;
642        }
643
644        if self.starts_sequence_item_at(probe) || self.line_has_mapping_key_at(probe) {
645            return Err(self.error("Tabs are not allowed as YAML indentation."));
646        }
647
648        self.index = probe;
649        Ok(())
650    }
651
652    fn current_starts_comment(&self) -> bool {
653        self.current_char() == Some('#')
654            && (self.index == 0
655                || matches!(
656                    self.items
657                        .get(self.index.saturating_sub(1))
658                        .and_then(StreamItem::char),
659                    Some(' ' | '\t' | '\n' | '\r')
660                ))
661    }
662
663    fn parse_key_value_separator(&mut self) -> BackendResult<()> {
664        self.skip_key_value_spaces();
665        self.consume_char(':')?;
666        if self.current_char() == Some('\t') {
667            let mut probe = self.index;
668            while self.items.get(probe).and_then(StreamItem::char) == Some('\t') {
669                probe += 1;
670            }
671            if !matches!(
672                self.items.get(probe).and_then(StreamItem::char),
673                Some(
674                    ' ' | '\n'
675                        | '\r'
676                        | '#'
677                        | '['
678                        | '{'
679                        | '"'
680                        | '\''
681                        | '!'
682                        | '&'
683                        | '*'
684                        | '|'
685                        | '>'
686                        | '?'
687                ) | None
688            ) {
689                return Err(self.error("Tabs are not allowed as YAML indentation."));
690            }
691        }
692        Ok(())
693    }
694
695    fn classify_key_value(&self, start: SourcePosition, value: YamlValueNode) -> YamlKeyNode {
696        let value = match value {
697            YamlValueNode::Interpolation(node) => YamlKeyValue::Interpolation(node),
698            YamlValueNode::Scalar(
699                node @ (YamlScalarNode::Plain(_)
700                | YamlScalarNode::DoubleQuoted(_)
701                | YamlScalarNode::SingleQuoted(_)),
702            ) => YamlKeyValue::Scalar(node),
703            other => YamlKeyValue::Complex(Box::new(other)),
704        };
705        YamlKeyNode {
706            span: self.span_from(start),
707            value,
708        }
709    }
710
711    fn parse_block_sequence(&mut self, indent: usize) -> BackendResult<YamlSequenceNode> {
712        let start = self.mark();
713        let mut items = Vec::new();
714        let mut first_item = true;
715        loop {
716            if !first_item || self.is_line_start() {
717                self.consume_indent(indent)?;
718            }
719            first_item = false;
720            if !self.starts_sequence_item() {
721                break;
722            }
723            self.advance();
724            if matches!(self.current_char(), Some(' ' | '\t')) {
725                self.skip_inline_spaces();
726            }
727            if self.current_starts_comment() {
728                while !matches!(self.current_char(), None | Some('\n')) {
729                    self.advance();
730                }
731                if self.current_char() == Some('\n') {
732                    self.advance();
733                }
734                self.skip_blank_lines();
735                if self.is_line_start() && self.current_char() == Some('\t') {
736                    self.consume_line_start_tabs()?;
737                }
738                let item = if self.current_kind() == "eof" || self.current_line_indent() <= indent {
739                    YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
740                } else {
741                    self.parse_block_node(self.current_line_indent())?
742                };
743                let owns_following_lines = Self::value_owns_following_lines(&item);
744                items.push(item);
745                if !owns_following_lines && self.current_kind() != "eof" {
746                    self.consume_line_end_required()?;
747                }
748            } else if self.current_kind() == "eof" || self.current_char() == Some('\n') {
749                if self.current_char() == Some('\n') {
750                    self.advance();
751                }
752                self.skip_blank_lines();
753                let item = self.parse_block_node(self.current_line_indent())?;
754                let owns_following_lines = Self::value_owns_following_lines(&item);
755                items.push(item);
756                if !owns_following_lines {
757                    self.consume_line_end_required()?;
758                }
759            } else if self.starts_sequence_item() {
760                items.push(YamlValueNode::Sequence(
761                    self.parse_compact_sequence(indent + 2)?,
762                ));
763            } else if self.line_has_mapping_key() {
764                items.push(YamlValueNode::Mapping(
765                    self.parse_compact_mapping(indent + 2)?,
766                ));
767            } else {
768                let item = self.parse_inline_value(None, indent, true)?;
769                let owns_following_lines = Self::value_owns_following_lines(&item);
770                items.push(item);
771                if !owns_following_lines {
772                    self.consume_line_end_required()?;
773                }
774            }
775            self.skip_blank_lines();
776            if self.current_kind() == "eof"
777                || self.current_line_indent() != indent
778                || !self.starts_sequence_item()
779            {
780                break;
781            }
782        }
783        Ok(YamlSequenceNode {
784            span: self.span_from(start),
785            items,
786            flow: false,
787        })
788    }
789
790    fn parse_compact_sequence(&mut self, indent: usize) -> BackendResult<YamlSequenceNode> {
791        let start = self.mark();
792        let mut items = Vec::new();
793        let mut first_item = true;
794        loop {
795            if !first_item || self.is_line_start() {
796                self.consume_indent(indent)?;
797            }
798            first_item = false;
799            if !self.starts_sequence_item() {
800                break;
801            }
802            self.advance();
803            if matches!(self.current_char(), Some(' ' | '\t')) {
804                self.skip_inline_spaces();
805            }
806            if self.current_starts_comment() {
807                while !matches!(self.current_char(), None | Some('\n')) {
808                    self.advance();
809                }
810                if self.current_char() == Some('\n') {
811                    self.advance();
812                }
813                self.skip_blank_lines();
814                if self.is_line_start() && self.current_char() == Some('\t') {
815                    self.consume_line_start_tabs()?;
816                }
817                let item = if self.current_kind() == "eof" || self.current_line_indent() <= indent {
818                    YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
819                } else {
820                    self.parse_block_node(self.current_line_indent())?
821                };
822                let owns_following_lines = Self::value_owns_following_lines(&item);
823                items.push(item);
824                if !owns_following_lines && self.current_kind() != "eof" {
825                    self.consume_line_end_required()?;
826                }
827            } else if self.current_kind() == "eof" || self.current_char() == Some('\n') {
828                if self.current_char() == Some('\n') {
829                    self.advance();
830                }
831                self.skip_blank_lines();
832                let item = self.parse_block_node(self.current_line_indent())?;
833                let owns_following_lines = Self::value_owns_following_lines(&item);
834                items.push(item);
835                if !owns_following_lines {
836                    self.consume_line_end_required()?;
837                }
838            } else if self.starts_sequence_item() {
839                items.push(YamlValueNode::Sequence(
840                    self.parse_compact_sequence(indent + 2)?,
841                ));
842            } else if self.line_has_mapping_key() {
843                items.push(YamlValueNode::Mapping(
844                    self.parse_compact_mapping(indent + 2)?,
845                ));
846            } else {
847                let item = self.parse_inline_value(None, indent, true)?;
848                let owns_following_lines = Self::value_owns_following_lines(&item);
849                items.push(item);
850                if !owns_following_lines {
851                    self.consume_line_end_required()?;
852                }
853            }
854            self.skip_blank_lines();
855            if self.current_kind() == "eof"
856                || self.current_line_indent() < indent
857                || !self.starts_sequence_item()
858            {
859                break;
860            }
861        }
862        Ok(YamlSequenceNode {
863            span: self.span_from(start),
864            items,
865            flow: false,
866        })
867    }
868
869    fn parse_compact_mapping(&mut self, indent: usize) -> BackendResult<YamlMappingNode> {
870        let start = self.mark();
871        let mut entries = Vec::new();
872        loop {
873            let entry_start = self.mark();
874            let explicit_key =
875                self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '\n'));
876            let key = if explicit_key {
877                self.parse_explicit_key(indent)?
878            } else {
879                self.parse_key()?
880            };
881            let value = if explicit_key {
882                self.parse_explicit_mapping_entry_value(indent)?
883            } else {
884                self.parse_key_value_separator()?;
885                self.parse_mapping_value_after_separator(indent, false)?
886            };
887            if !Self::value_owns_following_lines(&value) {
888                self.consume_line_end_required()?;
889            }
890            entries.push(YamlMappingEntryNode {
891                span: self.span_from(entry_start),
892                key,
893                value,
894            });
895            self.skip_blank_lines();
896            if self.current_kind() == "eof"
897                || self.current_line_indent() != indent
898                || self.starts_sequence_item()
899                || !self.line_has_mapping_key()
900            {
901                break;
902            }
903            self.consume_indent(indent)?;
904        }
905        Ok(YamlMappingNode {
906            span: self.span_from(start),
907            entries,
908            flow: false,
909        })
910    }
911
912    fn parse_block_mapping(&mut self, indent: usize) -> BackendResult<YamlMappingNode> {
913        let start = self.mark();
914        let mut entries = Vec::new();
915        let mut first_entry = true;
916        loop {
917            if !first_entry {
918                self.consume_indent(indent)?;
919            }
920            first_entry = false;
921            let entry_start = self.mark();
922            let explicit_key =
923                self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '\n'));
924            let key = if explicit_key {
925                self.parse_explicit_key(indent)?
926            } else {
927                self.parse_key()?
928            };
929            let value = if explicit_key {
930                self.parse_explicit_mapping_entry_value(indent)?
931            } else {
932                self.parse_key_value_separator()?;
933                self.parse_mapping_value_after_separator(indent, false)?
934            };
935            if !Self::value_owns_following_lines(&value) {
936                self.consume_line_end_required()?;
937            }
938            entries.push(YamlMappingEntryNode {
939                span: self.span_from(entry_start),
940                key,
941                value,
942            });
943            self.skip_blank_lines();
944            if self.current_kind() == "eof"
945                || self.current_line_indent() != indent
946                || self.starts_sequence_item()
947                || !self.line_has_mapping_key()
948            {
949                break;
950            }
951        }
952        Ok(YamlMappingNode {
953            span: self.span_from(start),
954            entries,
955            flow: false,
956        })
957    }
958
959    fn parse_explicit_mapping_entry_value(
960        &mut self,
961        indent: usize,
962    ) -> BackendResult<YamlValueNode> {
963        if self.current_kind() == "eof"
964            || (self.is_line_start() && self.current_line_indent() < indent)
965        {
966            return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
967                null_plain_scalar(),
968            )));
969        }
970
971        self.consume_indent(indent)?;
972        if self.current_char() != Some(':') {
973            return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
974                null_plain_scalar(),
975            )));
976        }
977
978        self.parse_key_value_separator()?;
979        self.parse_mapping_value_after_separator(indent, true)
980    }
981
982    fn parse_mapping_value_after_separator(
983        &mut self,
984        indent: usize,
985        allow_inline_compact_sequence: bool,
986    ) -> BackendResult<YamlValueNode> {
987        if matches!(self.current_char(), Some(' ' | '\t')) {
988            if matches!(self.current_char(), Some(' ' | '\t')) {
989                self.skip_inline_spaces();
990            }
991            return self.parse_mapping_value(indent, allow_inline_compact_sequence);
992        }
993        if matches!(self.current_kind(), "eof") || self.current_char() == Some('\n') {
994            if self.current_char() == Some('\n') {
995                self.advance();
996            }
997            self.skip_blank_lines();
998            self.consume_line_start_tabs()?;
999            if self.starts_sequence_item() && self.current_line_indent() == indent {
1000                return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1001            }
1002            if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1003                return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1004                    null_plain_scalar(),
1005                )));
1006            }
1007            return self.parse_block_node(self.current_line_indent());
1008        }
1009        self.parse_inline_value(None, indent, true)
1010    }
1011
1012    fn parse_mapping_value(
1013        &mut self,
1014        indent: usize,
1015        allow_inline_compact_sequence: bool,
1016    ) -> BackendResult<YamlValueNode> {
1017        if self.current_starts_comment() {
1018            while !matches!(self.current_char(), None | Some('\n')) {
1019                self.advance();
1020            }
1021            if self.current_char() == Some('\n') {
1022                self.advance();
1023            }
1024            self.skip_blank_lines();
1025            if self.is_line_start() && self.current_char() == Some('\t') {
1026                self.consume_line_start_tabs()?;
1027            }
1028            if self.starts_sequence_item() && self.current_line_indent() == indent {
1029                return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1030            }
1031            if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1032                return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1033                    null_plain_scalar(),
1034                )));
1035            }
1036            return self.parse_block_node(self.current_line_indent());
1037        }
1038        if self.starts_sequence_item() {
1039            if self.is_line_start() && self.current_line_indent() == indent {
1040                return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1041            }
1042            if allow_inline_compact_sequence {
1043                return Ok(YamlValueNode::Sequence(
1044                    self.parse_compact_sequence(indent + 2)?,
1045                ));
1046            }
1047            return Err(self.error("Unexpected trailing YAML content."));
1048        }
1049        if allow_inline_compact_sequence && self.line_has_mapping_key() {
1050            return Ok(YamlValueNode::Mapping(
1051                self.parse_compact_mapping(indent + 2)?,
1052            ));
1053        }
1054        if matches!(self.current_char(), Some(' ' | '\t')) {
1055            self.skip_inline_spaces();
1056            if self.current_starts_comment() {
1057                while !matches!(self.current_char(), None | Some('\n')) {
1058                    self.advance();
1059                }
1060                if self.current_char() == Some('\n') {
1061                    self.advance();
1062                }
1063                self.skip_blank_lines();
1064                if self.is_line_start() && self.current_char() == Some('\t') {
1065                    self.consume_line_start_tabs()?;
1066                }
1067                if self.starts_sequence_item() && self.current_line_indent() == indent {
1068                    return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1069                }
1070                if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1071                    return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1072                        null_plain_scalar(),
1073                    )));
1074                }
1075                return self.parse_block_node(self.current_line_indent());
1076            }
1077            if self.starts_sequence_item() {
1078                if self.is_line_start() && self.current_line_indent() == indent {
1079                    return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1080                }
1081                if allow_inline_compact_sequence {
1082                    return Ok(YamlValueNode::Sequence(
1083                        self.parse_compact_sequence(indent + 2)?,
1084                    ));
1085                }
1086                return Err(self.error("Unexpected trailing YAML content."));
1087            }
1088            if allow_inline_compact_sequence && self.line_has_mapping_key() {
1089                return Ok(YamlValueNode::Mapping(
1090                    self.parse_compact_mapping(indent + 2)?,
1091                ));
1092            }
1093            return self.parse_inline_value(None, indent, true);
1094        }
1095        if self.current_kind() == "eof" || self.current_char() == Some('\n') {
1096            if self.current_char() == Some('\n') {
1097                self.advance();
1098            }
1099            self.skip_blank_lines();
1100            if self.is_line_start() && self.current_char() == Some('\t') {
1101                self.consume_line_start_tabs()?;
1102            }
1103            if self.starts_sequence_item() && self.current_line_indent() == indent {
1104                return Ok(YamlValueNode::Sequence(self.parse_block_sequence(indent)?));
1105            }
1106            if self.current_kind() == "eof" || self.current_line_indent() <= indent {
1107                return Ok(YamlValueNode::Scalar(YamlScalarNode::Plain(
1108                    null_plain_scalar(),
1109                )));
1110            }
1111            return self.parse_block_node(self.current_line_indent());
1112        }
1113        self.parse_inline_value(None, indent, true)
1114    }
1115
1116    fn parse_explicit_key(&mut self, indent: usize) -> BackendResult<YamlKeyNode> {
1117        let start = self.mark();
1118        self.consume_char('?')?;
1119        if matches!(self.current_char(), Some(' ' | '\t')) {
1120            self.skip_inline_spaces();
1121        }
1122        let value = if self.current_starts_comment() {
1123            while !matches!(self.current_char(), None | Some('\n')) {
1124                self.advance();
1125            }
1126            if self.current_char() == Some('\n') {
1127                self.advance();
1128            }
1129            self.skip_blank_lines();
1130            if self.is_line_start() && self.current_char() == Some('\t') {
1131                self.consume_line_start_tabs()?;
1132            }
1133            if self.current_kind() == "eof" || self.current_char() == Some(':') {
1134                YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1135            } else {
1136                self.parse_block_node(self.current_line_indent())?
1137            }
1138        } else if self.current_kind() == "eof" || self.current_char() == Some('\n') {
1139            if self.current_char() == Some('\n') {
1140                self.advance();
1141            }
1142            self.skip_blank_lines();
1143            if self.current_kind() == "eof" || self.current_char() == Some(':') {
1144                YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1145            } else {
1146                self.parse_block_node(self.current_line_indent())?
1147            }
1148        } else if self.starts_sequence_item() {
1149            YamlValueNode::Sequence(self.parse_compact_sequence(indent + 2)?)
1150        } else if self.line_has_mapping_key() {
1151            YamlValueNode::Mapping(self.parse_compact_mapping(indent + 2)?)
1152        } else {
1153            let value = self.parse_inline_value(None, indent, true)?;
1154            self.consume_line_end_required()?;
1155            value
1156        };
1157        Ok(self.classify_key_value(start, value))
1158    }
1159
1160    fn parse_key(&mut self) -> BackendResult<YamlKeyNode> {
1161        let start = self.mark();
1162        let key_start = self.index;
1163        let value = self.parse_inline_value(Some(&[':']), 0, false)?;
1164        self.ensure_single_line_implicit_key(key_start)?;
1165        Ok(self.classify_key_value(start, value))
1166    }
1167
1168    fn parse_inline_value(
1169        &mut self,
1170        stop_chars: Option<&[char]>,
1171        parent_indent: usize,
1172        requires_indented_continuation: bool,
1173    ) -> BackendResult<YamlValueNode> {
1174        let decorated = self.parse_decorators()?;
1175        if decorated.is_some() && self.starts_sequence_item() {
1176            return Err(self.error("Unexpected trailing YAML content."));
1177        }
1178        if decorated.is_some() && self.decorator_line_break_ahead() {
1179            self.consume_inline_comment_and_line_break();
1180            self.skip_blank_lines();
1181            self.consume_line_start_tabs()?;
1182            let has_nested_value = self.current_kind() != "eof"
1183                && (self.current_line_indent() > parent_indent
1184                    || (self.current_line_indent() == parent_indent
1185                        && self.starts_sequence_item()));
1186            let value = if has_nested_value {
1187                self.parse_block_node(self.current_line_indent())?
1188            } else {
1189                YamlValueNode::Scalar(YamlScalarNode::Plain(YamlPlainScalarNode {
1190                    span: SourceSpan::point(0, 0),
1191                    chunks: vec![YamlChunk::Text(YamlTextChunkNode {
1192                        span: SourceSpan::point(0, 0),
1193                        value: "null".to_owned(),
1194                    })],
1195                }))
1196            };
1197            return wrap_decorators(decorated, value);
1198        }
1199        if decorated.is_some() && self.inline_value_terminator(stop_chars) {
1200            return wrap_decorators(
1201                decorated,
1202                YamlValueNode::Scalar(YamlScalarNode::Plain(empty_plain_scalar())),
1203            );
1204        }
1205        let value = if self.current_kind() == "interpolation" {
1206            let interpolation = self.consume_interpolation("value")?;
1207            if self.inline_value_terminator(stop_chars) {
1208                YamlValueNode::Interpolation(interpolation)
1209            } else {
1210                YamlValueNode::Scalar(YamlScalarNode::Plain(self.parse_plain_scalar(
1211                    stop_chars,
1212                    Some(vec![YamlChunk::Interpolation(interpolation)]),
1213                    false,
1214                    parent_indent,
1215                    requires_indented_continuation,
1216                )?))
1217            }
1218        } else if self.current_char() == Some('[') {
1219            YamlValueNode::Sequence(self.parse_flow_sequence()?)
1220        } else if self.current_char() == Some('{') {
1221            YamlValueNode::Mapping(self.parse_flow_mapping()?)
1222        } else if self.current_char() == Some('"') {
1223            YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(
1224                self.parse_double_quoted(parent_indent, requires_indented_continuation)?,
1225            ))
1226        } else if self.current_char() == Some('\'') {
1227            YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(self.parse_single_quoted()?))
1228        } else if self.current_char() == Some('*') {
1229            YamlValueNode::Scalar(YamlScalarNode::Alias(self.parse_alias()?))
1230        } else if matches!(self.current_char(), Some('|' | '>')) {
1231            YamlValueNode::Scalar(YamlScalarNode::Block(
1232                self.parse_block_scalar(parent_indent)?,
1233            ))
1234        } else if matches!(self.current_char(), Some(',' | ']' | '}')) {
1235            return Err(self.error("Expected a YAML value."));
1236        } else {
1237            YamlValueNode::Scalar(YamlScalarNode::Plain(self.parse_plain_scalar(
1238                stop_chars,
1239                None,
1240                false,
1241                parent_indent,
1242                requires_indented_continuation,
1243            )?))
1244        };
1245        wrap_decorators(decorated, value)
1246    }
1247
1248    fn inline_value_terminator(&self, stop_chars: Option<&[char]>) -> bool {
1249        let mut probe = self.index;
1250        while matches!(self.items[probe].char(), Some(' ' | '\t')) {
1251            probe += 1;
1252        }
1253        match &self.items[probe] {
1254            StreamItem::Eof { .. } => true,
1255            StreamItem::Char { ch, .. } => {
1256                stop_chars.is_some_and(|chars| chars.contains(ch)) || matches!(ch, '\n' | '#')
1257            }
1258            _ => false,
1259        }
1260    }
1261
1262    fn parse_flow_sequence(&mut self) -> BackendResult<YamlSequenceNode> {
1263        let start = self.mark();
1264        let line_value_start = self.is_line_value_start();
1265        self.consume_char('[')?;
1266        let mut items = Vec::new();
1267        self.skip_flow_separation();
1268        if self.current_char() == Some(']') {
1269            self.advance();
1270            return Ok(YamlSequenceNode {
1271                span: self.span_from(start),
1272                items,
1273                flow: true,
1274            });
1275        }
1276        loop {
1277            if self.current_char() == Some(',') {
1278                return Err(self.error("Expected a YAML value."));
1279            }
1280            if self.current_char() == Some('-') && matches!(self.peek_char(1), Some(',' | ']')) {
1281                return Err(self.error("Expected a YAML value."));
1282            }
1283            items.push(self.parse_flow_sequence_item()?);
1284            self.skip_flow_separation();
1285            if self.current_char() == Some(']') {
1286                self.advance();
1287                break;
1288            }
1289            self.consume_char(',')?;
1290            let saw_line_break = self.skip_flow_separation_with_breaks();
1291            if saw_line_break
1292                && !line_value_start
1293                && self.current_char() != Some(']')
1294                && self.current_line_indent() == 0
1295            {
1296                return Err(self.error("Unexpected trailing YAML content."));
1297            }
1298            if self.current_char() == Some(']') {
1299                self.advance();
1300                break;
1301            }
1302        }
1303        Ok(YamlSequenceNode {
1304            span: self.span_from(start),
1305            items,
1306            flow: true,
1307        })
1308    }
1309
1310    fn is_line_value_start(&self) -> bool {
1311        let mut probe = self.index;
1312        while probe > 0 && self.items[probe - 1].char() != Some('\n') {
1313            probe -= 1;
1314        }
1315        while probe < self.index {
1316            match self.items[probe].char() {
1317                Some(' ' | '\t') => probe += 1,
1318                _ => return false,
1319            }
1320        }
1321        true
1322    }
1323
1324    fn parse_flow_sequence_item(&mut self) -> BackendResult<YamlValueNode> {
1325        let entry_start = self.mark();
1326        let explicit =
1327            self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '[' | '{'));
1328        if explicit {
1329            self.consume_char('?')?;
1330            self.skip_inline_spaces();
1331        }
1332
1333        let key_value = self.parse_inline_value(Some(&[':', ',', ']']), 0, false)?;
1334        let saw_line_break = self.skip_flow_separation_with_breaks();
1335        if self.current_char() != Some(':') {
1336            if explicit {
1337                return Err(self.error("Expected ':' in YAML template."));
1338            }
1339            return Ok(key_value);
1340        }
1341        if saw_line_break {
1342            return Err(self.error("Expected ':' in YAML template."));
1343        }
1344
1345        let key = self.classify_key_value(entry_start.clone(), key_value);
1346        self.parse_key_value_separator()?;
1347        self.skip_flow_separation();
1348        let value = if matches!(self.current_char(), Some(',' | ']')) {
1349            YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1350        } else {
1351            self.parse_inline_value(Some(&[',', ']']), 0, false)?
1352        };
1353        let pair_span = self.span_from(entry_start);
1354        Ok(YamlValueNode::Mapping(YamlMappingNode {
1355            span: pair_span.clone(),
1356            entries: vec![YamlMappingEntryNode {
1357                span: pair_span,
1358                key,
1359                value,
1360            }],
1361            flow: true,
1362        }))
1363    }
1364
1365    fn parse_flow_mapping(&mut self) -> BackendResult<YamlMappingNode> {
1366        let start = self.mark();
1367        self.consume_char('{')?;
1368        let mut entries = Vec::new();
1369        self.skip_flow_separation();
1370        if self.current_char() == Some(',') {
1371            return Err(self.error("Expected ':' in YAML template."));
1372        }
1373        if self.current_char() == Some('}') {
1374            self.advance();
1375            return Ok(YamlMappingNode {
1376                span: self.span_from(start),
1377                entries,
1378                flow: true,
1379            });
1380        }
1381        loop {
1382            let entry_start = self.mark();
1383            let key = self.parse_flow_key()?;
1384            self.skip_flow_separation();
1385            let value = if matches!(self.current_char(), Some(',' | '}')) {
1386                if self.current_char() == Some('}') && flow_implicit_plain_key_needs_separator(&key)
1387                {
1388                    return Err(self.error("Expected ':' in YAML template."));
1389                }
1390                YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1391            } else {
1392                self.parse_key_value_separator()?;
1393                self.skip_flow_separation();
1394                if matches!(self.current_char(), Some(',' | '}')) {
1395                    YamlValueNode::Scalar(YamlScalarNode::Plain(null_plain_scalar()))
1396                } else {
1397                    self.parse_inline_value(Some(&[',', '}']), 0, false)?
1398                }
1399            };
1400            entries.push(YamlMappingEntryNode {
1401                span: self.span_from(entry_start),
1402                key,
1403                value,
1404            });
1405            self.skip_flow_separation();
1406            if self.current_char() == Some('}') {
1407                self.advance();
1408                break;
1409            }
1410            self.consume_char(',')?;
1411            self.skip_flow_separation();
1412            if self.current_char() == Some(',') {
1413                return Err(self.error("Expected ':' in YAML template."));
1414            }
1415            if self.current_char() == Some('}') {
1416                self.advance();
1417                break;
1418            }
1419        }
1420        Ok(YamlMappingNode {
1421            span: self.span_from(start),
1422            entries,
1423            flow: true,
1424        })
1425    }
1426
1427    fn parse_flow_key(&mut self) -> BackendResult<YamlKeyNode> {
1428        let start = self.mark();
1429        let explicit =
1430            self.current_char() == Some('?') && matches!(self.peek_char(1), Some(' ' | '[' | '{'));
1431        if explicit {
1432            self.consume_char('?')?;
1433            self.skip_inline_spaces();
1434        }
1435        let value = self.parse_inline_value(Some(&[':', ',', '}']), 0, false)?;
1436        Ok(self.classify_key_value(start, value))
1437    }
1438
1439    fn ensure_single_line_implicit_key(&self, start_index: usize) -> BackendResult<()> {
1440        let saw_line_break = self.items[start_index..self.index]
1441            .iter()
1442            .any(|item| item.char() == Some('\n'));
1443        if saw_line_break {
1444            return Err(BackendError::parse_at(
1445                "yaml.parse",
1446                "Implicit YAML keys must be on a single line.",
1447                Some(self.current().span().clone()),
1448            ));
1449        }
1450        Ok(())
1451    }
1452
1453    fn parse_double_quoted(
1454        &mut self,
1455        parent_indent: usize,
1456        requires_indented_continuation: bool,
1457    ) -> BackendResult<YamlDoubleQuotedScalarNode> {
1458        let start = self.mark();
1459        self.consume_char('"')?;
1460        let mut chunks = Vec::new();
1461        let mut buffer = String::new();
1462        loop {
1463            if self.current_kind() == "eof" {
1464                return Err(self.error("Unterminated YAML double-quoted scalar."));
1465            }
1466            if self.current_kind() == "interpolation" {
1467                self.flush_buffer(&mut buffer, &mut chunks);
1468                chunks.push(YamlChunk::Interpolation(
1469                    self.consume_interpolation("string_fragment")?,
1470                ));
1471                continue;
1472            }
1473            if self.current_char() == Some('"') {
1474                self.flush_buffer(&mut buffer, &mut chunks);
1475                self.advance();
1476                break;
1477            }
1478            if matches!(self.current_char(), Some('\r' | '\n')) {
1479                buffer.push_str(&self.parse_quoted_line_folding(
1480                    '"',
1481                    parent_indent,
1482                    requires_indented_continuation,
1483                )?);
1484                continue;
1485            }
1486            if self.current_char() == Some('\\') {
1487                buffer.push_str(&self.parse_double_quoted_escape()?);
1488                continue;
1489            }
1490            if let Some(ch) = self.current_char() {
1491                buffer.push(ch);
1492                self.advance();
1493            }
1494        }
1495        Ok(YamlDoubleQuotedScalarNode {
1496            span: self.span_from(start),
1497            chunks,
1498        })
1499    }
1500
1501    fn parse_quoted_line_folding(
1502        &mut self,
1503        terminator: char,
1504        parent_indent: usize,
1505        requires_indented_continuation: bool,
1506    ) -> BackendResult<String> {
1507        let mut breaks = 0usize;
1508        loop {
1509            if self.current_char() == Some('\r') {
1510                self.advance();
1511            }
1512            if self.current_char() != Some('\n') {
1513                return Err(self.error("Invalid YAML quoted-scalar line break."));
1514            }
1515            self.advance();
1516            breaks += 1;
1517            let mut indent = 0usize;
1518            while matches!(self.current_char(), Some(' ' | '\t')) {
1519                indent += 1;
1520                self.advance();
1521            }
1522            let has_required_indent = if requires_indented_continuation {
1523                indent > parent_indent
1524            } else {
1525                indent >= parent_indent
1526            };
1527            if !has_required_indent
1528                && !matches!(self.current_char(), Some('\r' | '\n'))
1529                && self.current_char() != Some(terminator)
1530            {
1531                return Err(self.error("Invalid YAML quoted-scalar line break."));
1532            }
1533            if !matches!(self.current_char(), Some('\r' | '\n')) {
1534                break;
1535            }
1536        }
1537        if breaks == 1 {
1538            Ok(" ".to_owned())
1539        } else {
1540            Ok("\n".repeat(breaks - 1))
1541        }
1542    }
1543
1544    fn parse_single_quoted(&mut self) -> BackendResult<YamlSingleQuotedScalarNode> {
1545        let start = self.mark();
1546        self.consume_char('\'')?;
1547        let mut chunks = Vec::new();
1548        let mut buffer = String::new();
1549        loop {
1550            if self.current_kind() == "eof" {
1551                return Err(self.error("Unterminated YAML single-quoted scalar."));
1552            }
1553            if self.current_kind() == "interpolation" {
1554                self.flush_buffer(&mut buffer, &mut chunks);
1555                chunks.push(YamlChunk::Interpolation(
1556                    self.consume_interpolation("string_fragment")?,
1557                ));
1558                continue;
1559            }
1560            if self.current_char() == Some('\'') {
1561                if self.peek_char(1) == Some('\'') {
1562                    buffer.push('\'');
1563                    self.advance();
1564                    self.advance();
1565                    continue;
1566                }
1567                self.flush_buffer(&mut buffer, &mut chunks);
1568                self.advance();
1569                break;
1570            }
1571            if let Some(ch) = self.current_char() {
1572                buffer.push(ch);
1573                self.advance();
1574            }
1575        }
1576        Ok(YamlSingleQuotedScalarNode {
1577            span: self.span_from(start),
1578            chunks,
1579        })
1580    }
1581
1582    fn parse_plain_scalar(
1583        &mut self,
1584        stop_chars: Option<&[char]>,
1585        leading: Option<Vec<YamlChunk>>,
1586        key_mode: bool,
1587        parent_indent: usize,
1588        requires_indented_continuation: bool,
1589    ) -> BackendResult<YamlPlainScalarNode> {
1590        let start = self.mark();
1591        let mut chunks = leading.unwrap_or_default();
1592        let mut buffer = String::new();
1593        while self.current_kind() != "eof" {
1594            if self.current_kind() == "interpolation" {
1595                self.flush_buffer(&mut buffer, &mut chunks);
1596                chunks.push(YamlChunk::Interpolation(
1597                    self.consume_interpolation("string_fragment")?,
1598                ));
1599                continue;
1600            }
1601            let Some(ch) = self.current_char() else {
1602                break;
1603            };
1604            if ch == '#' && buffer.is_empty() {
1605                return Err(self.error("Expected a YAML value."));
1606            }
1607            if ch == '#'
1608                && self
1609                    .items
1610                    .get(self.index.wrapping_sub(1))
1611                    .and_then(StreamItem::char)
1612                    .is_none_or(char::is_whitespace)
1613            {
1614                break;
1615            }
1616            if ch == '\n' {
1617                if !key_mode {
1618                    if stop_chars.is_none()
1619                        && self.consume_plain_scalar_continuation(
1620                            &mut buffer,
1621                            parent_indent,
1622                            requires_indented_continuation,
1623                        )
1624                    {
1625                        continue;
1626                    }
1627                    if let Some(stop_chars) = stop_chars {
1628                        if self.consume_flow_plain_scalar_continuation(
1629                            &mut buffer,
1630                            stop_chars,
1631                            parent_indent,
1632                        ) {
1633                            continue;
1634                        }
1635                    }
1636                }
1637                break;
1638            }
1639            if stop_chars.is_some_and(|chars| chars.contains(&ch)) {
1640                let colon_without_separator = ch == ':'
1641                    && !matches!(self.peek_char(1), Some(' ' | '\t' | '\n' | ',' | ']' | '}'));
1642                if !colon_without_separator
1643                    && (!key_mode || matches!(self.peek_char(1), Some(' ' | '\t' | '\n')))
1644                {
1645                    break;
1646                }
1647            }
1648            if !key_mode
1649                && stop_chars.is_some()
1650                && ch == ':'
1651                && matches!(self.peek_char(1), Some(' ' | '\n' | ',' | ']' | '}'))
1652            {
1653                break;
1654            }
1655            if !key_mode
1656                && stop_chars.is_none()
1657                && ch == ':'
1658                && matches!(self.peek_char(1), Some(' ' | '\t'))
1659            {
1660                break;
1661            }
1662            if ch == '\t' && stop_chars.is_none() && buffer.contains(':') {
1663                return Err(self.error("Tabs are not allowed as YAML indentation."));
1664            }
1665            buffer.push(ch);
1666            self.advance();
1667        }
1668        self.flush_buffer(&mut buffer, &mut chunks);
1669        Ok(YamlPlainScalarNode {
1670            span: self.span_from(start),
1671            chunks,
1672        })
1673    }
1674
1675    fn consume_plain_scalar_continuation(
1676        &mut self,
1677        buffer: &mut String,
1678        parent_indent: usize,
1679        requires_indented_continuation: bool,
1680    ) -> bool {
1681        let mut probe = self.index;
1682        let mut blank_lines = 0usize;
1683        while self.items.get(probe).and_then(StreamItem::char) == Some('\n') {
1684            probe += 1;
1685            let mut indent = 0usize;
1686            while self.items.get(probe).and_then(StreamItem::char) == Some(' ') {
1687                indent += 1;
1688                probe += 1;
1689            }
1690            match self.items.get(probe).and_then(StreamItem::char) {
1691                Some('\n') => blank_lines += 1,
1692                Some('#') | None => return false,
1693                Some(_)
1694                    if if requires_indented_continuation {
1695                        indent > parent_indent
1696                    } else {
1697                        indent >= parent_indent
1698                    } =>
1699                {
1700                    if indent == parent_indent
1701                        && (self.starts_sequence_item_at(probe)
1702                            || self.line_has_mapping_key_at(probe))
1703                    {
1704                        return false;
1705                    }
1706                    self.index = probe;
1707                    if blank_lines == 0 {
1708                        if !buffer.ends_with(' ') && !buffer.is_empty() {
1709                            buffer.push(' ');
1710                        }
1711                    } else {
1712                        for _ in 0..blank_lines {
1713                            buffer.push('\n');
1714                        }
1715                    }
1716                    return true;
1717                }
1718                _ => return false,
1719            }
1720        }
1721        false
1722    }
1723
1724    fn consume_flow_plain_scalar_continuation(
1725        &mut self,
1726        buffer: &mut String,
1727        stop_chars: &[char],
1728        parent_indent: usize,
1729    ) -> bool {
1730        let mut probe = self.index;
1731        let mut breaks = 0usize;
1732        let continuation_indent;
1733
1734        loop {
1735            if self.items.get(probe).and_then(StreamItem::char) != Some('\n') {
1736                return false;
1737            }
1738            probe += 1;
1739            breaks += 1;
1740
1741            let mut indent = 0usize;
1742            while matches!(
1743                self.items.get(probe).and_then(StreamItem::char),
1744                Some(' ' | '\t')
1745            ) {
1746                indent += 1;
1747                probe += 1;
1748            }
1749
1750            if self.items.get(probe).and_then(StreamItem::char) == Some('#') {
1751                return false;
1752            }
1753
1754            if self.items.get(probe).and_then(StreamItem::char) == Some('\n') {
1755                continue;
1756            }
1757
1758            continuation_indent = indent;
1759            break;
1760        }
1761
1762        let Some(next_char) = self.items.get(probe).and_then(StreamItem::char) else {
1763            return false;
1764        };
1765        if continuation_indent < parent_indent {
1766            return false;
1767        }
1768        if stop_chars.contains(&next_char) {
1769            return false;
1770        }
1771
1772        self.index = probe;
1773        if breaks == 1 {
1774            if !buffer.ends_with(' ') && !buffer.is_empty() {
1775                buffer.push(' ');
1776            }
1777        } else {
1778            for _ in 0..(breaks - 1) {
1779                buffer.push('\n');
1780            }
1781        }
1782        true
1783    }
1784
1785    fn parse_block_scalar(&mut self, parent_indent: usize) -> BackendResult<YamlBlockScalarNode> {
1786        let start = self.mark();
1787        let style = self.current_char().unwrap_or('|').to_string();
1788        self.advance();
1789        let mut chomping = None;
1790        let mut indent_indicator = None;
1791        for _ in 0..2 {
1792            if chomping.is_none() && matches!(self.current_char(), Some('+' | '-')) {
1793                chomping = self.current_char().map(|ch| ch.to_string());
1794                self.advance();
1795                continue;
1796            }
1797            if indent_indicator.is_none()
1798                && self.current_char().is_some_and(|ch| ch.is_ascii_digit())
1799            {
1800                indent_indicator = self
1801                    .current_char()
1802                    .and_then(|ch| ch.to_digit(10))
1803                    .map(|value| value as usize);
1804                self.advance();
1805                continue;
1806            }
1807            break;
1808        }
1809        self.consume_line_end_required()?;
1810        let block_indent = if let Some(indent_indicator) = indent_indicator {
1811            indent_indicator
1812        } else {
1813            self.infer_block_scalar_indent(parent_indent)?
1814        };
1815        let mut chunks = Vec::new();
1816        let mut buffer = String::new();
1817        while self.current_kind() != "eof" {
1818            if self.line_is_blank() {
1819                while self.current_char() == Some(' ') {
1820                    self.advance();
1821                }
1822                if self.current_char() == Some('\n') {
1823                    buffer.push('\n');
1824                    self.advance();
1825                    continue;
1826                }
1827            }
1828            if self.current_line_indent() < block_indent {
1829                break;
1830            }
1831            self.consume_indent(block_indent)?;
1832            while self.current_kind() != "eof" && self.current_char() != Some('\n') {
1833                if self.current_kind() == "interpolation" {
1834                    self.flush_buffer(&mut buffer, &mut chunks);
1835                    chunks.push(YamlChunk::Interpolation(
1836                        self.consume_interpolation("string_fragment")?,
1837                    ));
1838                    continue;
1839                }
1840                if let Some(ch) = self.current_char() {
1841                    buffer.push(ch);
1842                    self.advance();
1843                }
1844            }
1845            if self.current_char() == Some('\n') {
1846                buffer.push('\n');
1847                self.advance();
1848            }
1849        }
1850        self.flush_buffer(&mut buffer, &mut chunks);
1851        Ok(YamlBlockScalarNode {
1852            span: self.span_from(start),
1853            style,
1854            chomping,
1855            indent_indicator,
1856            chunks,
1857        })
1858    }
1859
1860    fn infer_block_scalar_indent(&self, parent_indent: usize) -> BackendResult<usize> {
1861        let mut probe = self.index;
1862        let mut leading_blank_indent = 0usize;
1863        while probe < self.items.len() {
1864            let mut indent = 0usize;
1865            while self.items.get(probe).and_then(StreamItem::char) == Some(' ') {
1866                indent += 1;
1867                probe += 1;
1868            }
1869            match self.items.get(probe).and_then(StreamItem::char) {
1870                Some('\n') => {
1871                    leading_blank_indent = leading_blank_indent.max(indent);
1872                    probe += 1;
1873                }
1874                Some(_) => {
1875                    let is_zero_indented_comment_like_content = parent_indent == 0
1876                        && indent == 0
1877                        && self.items.get(probe).and_then(StreamItem::char) == Some('#');
1878                    if !is_zero_indented_comment_like_content && leading_blank_indent > indent {
1879                        return Err(self.error("Incorrect YAML indentation."));
1880                    }
1881                    return Ok(if parent_indent == 0 {
1882                        indent
1883                    } else {
1884                        indent.max(parent_indent + 1)
1885                    });
1886                }
1887                None => break,
1888            }
1889        }
1890        if leading_blank_indent > parent_indent {
1891            return Err(self.error("Incorrect YAML indentation."));
1892        }
1893        if parent_indent == 0 {
1894            Ok(0)
1895        } else {
1896            Ok(parent_indent + 1)
1897        }
1898    }
1899
1900    fn line_is_blank(&self) -> bool {
1901        let mut probe = self.index;
1902        while self.items.get(probe).and_then(StreamItem::char) == Some(' ') {
1903            probe += 1;
1904        }
1905        matches!(
1906            self.items.get(probe),
1907            Some(StreamItem::Char { ch: '\n', .. }) | Some(StreamItem::Eof { .. })
1908        )
1909    }
1910
1911    fn flush_buffer(&self, buffer: &mut String, chunks: &mut Vec<YamlChunk>) {
1912        if buffer.is_empty() {
1913            return;
1914        }
1915        chunks.push(YamlChunk::Text(YamlTextChunkNode {
1916            span: SourceSpan::point(0, 0),
1917            value: std::mem::take(buffer),
1918        }));
1919    }
1920
1921    fn parse_double_quoted_escape(&mut self) -> BackendResult<String> {
1922        self.consume_char('\\')?;
1923        if self.current_char() == Some('\r') {
1924            self.advance();
1925        }
1926        if self.current_char() == Some('\n') {
1927            self.advance();
1928            while matches!(self.current_char(), Some(' ' | '\t')) {
1929                self.advance();
1930            }
1931            return Ok(String::new());
1932        }
1933        let escape = self
1934            .current_char()
1935            .ok_or_else(|| self.error("Incomplete YAML escape sequence."))?;
1936        self.advance();
1937        let mapped = match escape {
1938            '0' => Some("\0".to_owned()),
1939            'a' => Some("\u{0007}".to_owned()),
1940            'b' => Some("\u{0008}".to_owned()),
1941            't' | '\t' => Some("\t".to_owned()),
1942            'n' => Some("\n".to_owned()),
1943            'v' => Some("\u{000b}".to_owned()),
1944            'f' => Some("\u{000c}".to_owned()),
1945            'r' => Some("\r".to_owned()),
1946            'e' => Some("\u{001b}".to_owned()),
1947            ' ' => Some(" ".to_owned()),
1948            '"' => Some("\"".to_owned()),
1949            '/' => Some("/".to_owned()),
1950            '\\' => Some("\\".to_owned()),
1951            'N' => Some("\u{0085}".to_owned()),
1952            '_' => Some("\u{00a0}".to_owned()),
1953            'L' => Some("\u{2028}".to_owned()),
1954            'P' => Some("\u{2029}".to_owned()),
1955            _ => None,
1956        };
1957        if let Some(value) = mapped {
1958            return Ok(value);
1959        }
1960        let (digits, radix_name) = match escape {
1961            'x' => (2, "hex"),
1962            'u' => (4, "unicode"),
1963            'U' => (8, "unicode"),
1964            _ => {
1965                return Err(self.error("Invalid YAML escape sequence."));
1966            }
1967        };
1968        let chars = self.collect_exact_chars(digits)?;
1969        let codepoint = u32::from_str_radix(&chars, 16)
1970            .map_err(|_| self.error(format!("Invalid YAML {radix_name} escape.")))?;
1971        char::from_u32(codepoint)
1972            .map(|value| value.to_string())
1973            .ok_or_else(|| self.error("Invalid YAML unicode escape."))
1974    }
1975
1976    fn collect_exact_chars(&mut self, count: usize) -> BackendResult<String> {
1977        let mut chars = String::new();
1978        for _ in 0..count {
1979            let ch = self
1980                .current_char()
1981                .ok_or_else(|| self.error("Unexpected end of YAML escape sequence."))?;
1982            chars.push(ch);
1983            self.advance();
1984        }
1985        Ok(chars)
1986    }
1987
1988    fn consume_char(&mut self, expected: char) -> BackendResult<()> {
1989        if self.current_char() != Some(expected) {
1990            return Err(self.error(format!("Expected {expected:?} in YAML template.")));
1991        }
1992        self.advance();
1993        Ok(())
1994    }
1995
1996    fn consume_interpolation(&mut self, role: &str) -> BackendResult<YamlInterpolationNode> {
1997        let (interpolation_index, span) = match self.current() {
1998            StreamItem::Interpolation {
1999                interpolation_index,
2000                span,
2001                ..
2002            } => (*interpolation_index, span.clone()),
2003            _ => return Err(self.error("Expected an interpolation.")),
2004        };
2005        self.advance();
2006        Ok(YamlInterpolationNode {
2007            span,
2008            interpolation_index,
2009            role: role.to_owned(),
2010        })
2011    }
2012}
2013
2014#[derive(Clone, Debug)]
2015struct YamlDocumentFragment {
2016    directives: Vec<String>,
2017    explicit_start: bool,
2018    explicit_end: bool,
2019    items: Vec<StreamItem>,
2020}
2021
2022pub fn parse_template_with_profile(
2023    template: &TemplateInput,
2024    _profile: YamlProfile,
2025) -> BackendResult<YamlStreamNode> {
2026    // YAML only exposes 1.2.2 in this phase, but the parameter keeps the
2027    // parser wired for profile-aware dispatch once additional variants land.
2028    let fragments = split_stream(template)?;
2029    let mut documents = Vec::new();
2030    for fragment in fragments {
2031        let mut parser = YamlParser::from_items(fragment.items);
2032        let mut document = parser.parse()?;
2033        document.directives = fragment.directives;
2034        document.explicit_start = fragment.explicit_start;
2035        document.explicit_end = fragment.explicit_end;
2036        documents.push(document);
2037    }
2038    Ok(YamlStreamNode {
2039        span: documents
2040            .first()
2041            .map(|document| document.span.clone())
2042            .unwrap_or_else(|| SourceSpan::point(0, 0)),
2043        documents,
2044    })
2045}
2046
2047pub fn parse_template(template: &TemplateInput) -> BackendResult<YamlStreamNode> {
2048    parse_template_with_profile(template, YamlProfile::default())
2049}
2050
2051pub fn parse_validated_template_with_profile(
2052    template: &TemplateInput,
2053    profile: YamlProfile,
2054) -> BackendResult<YamlStreamNode> {
2055    let stream = parse_template_with_profile(template, profile)?;
2056    validate_template_stream(&stream)?;
2057    Ok(stream)
2058}
2059
2060pub fn parse_validated_template(template: &TemplateInput) -> BackendResult<YamlStreamNode> {
2061    parse_validated_template_with_profile(template, YamlProfile::default())
2062}
2063
2064pub fn validate_template_with_profile(
2065    template: &TemplateInput,
2066    profile: YamlProfile,
2067) -> BackendResult<()> {
2068    let stream = parse_template_with_profile(template, profile)?;
2069    validate_template_stream(&stream)
2070}
2071
2072pub fn validate_template(template: &TemplateInput) -> BackendResult<()> {
2073    validate_template_with_profile(template, YamlProfile::default())
2074}
2075
2076pub fn check_template_with_profile(
2077    template: &TemplateInput,
2078    profile: YamlProfile,
2079) -> BackendResult<()> {
2080    validate_template_with_profile(template, profile)
2081}
2082
2083pub fn check_template(template: &TemplateInput) -> BackendResult<()> {
2084    check_template_with_profile(template, YamlProfile::default())
2085}
2086
2087pub fn format_template_with_profile(
2088    template: &TemplateInput,
2089    profile: YamlProfile,
2090) -> BackendResult<String> {
2091    let stream = parse_validated_template_with_profile(template, profile)?;
2092    format_yaml_stream(template, &stream)
2093}
2094
2095pub fn format_template(template: &TemplateInput) -> BackendResult<String> {
2096    format_template_with_profile(template, YamlProfile::default())
2097}
2098
2099fn validate_template_stream(stream: &YamlStreamNode) -> BackendResult<()> {
2100    for document in &stream.documents {
2101        validate_value_node(&document.value)?;
2102    }
2103    Ok(())
2104}
2105
2106fn validate_value_node(node: &YamlValueNode) -> BackendResult<()> {
2107    match node {
2108        YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => validate_plain_scalar_node(node),
2109        YamlValueNode::Mapping(node) => {
2110            for entry in &node.entries {
2111                validate_key_node(&entry.key)?;
2112                validate_value_node(&entry.value)?;
2113            }
2114            Ok(())
2115        }
2116        YamlValueNode::Sequence(node) => {
2117            for item in &node.items {
2118                validate_value_node(item)?;
2119            }
2120            Ok(())
2121        }
2122        YamlValueNode::Decorated(node) => validate_value_node(&node.value),
2123        YamlValueNode::Interpolation(_)
2124        | YamlValueNode::Scalar(
2125            YamlScalarNode::DoubleQuoted(_)
2126            | YamlScalarNode::SingleQuoted(_)
2127            | YamlScalarNode::Block(_)
2128            | YamlScalarNode::Alias(_),
2129        ) => Ok(()),
2130    }
2131}
2132
2133fn validate_key_node(node: &YamlKeyNode) -> BackendResult<()> {
2134    match &node.value {
2135        YamlKeyValue::Scalar(YamlScalarNode::Plain(node)) => validate_plain_scalar_node(node),
2136        YamlKeyValue::Complex(node) => validate_value_node(node),
2137        YamlKeyValue::Interpolation(_)
2138        | YamlKeyValue::Scalar(
2139            YamlScalarNode::DoubleQuoted(_)
2140            | YamlScalarNode::SingleQuoted(_)
2141            | YamlScalarNode::Block(_)
2142            | YamlScalarNode::Alias(_),
2143        ) => Ok(()),
2144    }
2145}
2146
2147fn validate_plain_scalar_node(node: &YamlPlainScalarNode) -> BackendResult<()> {
2148    let has_interpolation = node
2149        .chunks
2150        .iter()
2151        .any(|chunk| matches!(chunk, YamlChunk::Interpolation(_)));
2152    let has_whitespace_text = node.chunks.iter().any(|chunk| {
2153        matches!(chunk, YamlChunk::Text(text) if text.value.chars().any(char::is_whitespace))
2154    });
2155
2156    if has_interpolation && has_whitespace_text {
2157        return Err(BackendError::parse_at(
2158            "yaml.parse",
2159            "Quote YAML plain scalars that mix whitespace and interpolations.",
2160            Some(node.span.clone()),
2161        ));
2162    }
2163
2164    Ok(())
2165}
2166
2167pub fn normalize_documents_with_profile(
2168    documents: &[YamlOwned],
2169    _profile: YamlProfile,
2170) -> BackendResult<NormalizedStream> {
2171    // YAML normalization does not vary by profile yet, but the signature does
2172    // so future profile-specific semantics can plug in without reshaping APIs.
2173    documents
2174        .iter()
2175        .map(normalize_document)
2176        .collect::<BackendResult<Vec<_>>>()
2177        .map(NormalizedStream::new)
2178}
2179
2180pub fn normalize_documents(documents: &[YamlOwned]) -> BackendResult<NormalizedStream> {
2181    normalize_documents_with_profile(documents, YamlProfile::default())
2182}
2183
2184pub fn align_normalized_stream_with_ast(
2185    stream: &YamlStreamNode,
2186    normalized: &mut NormalizedStream,
2187) {
2188    for (document_node, document) in stream.documents.iter().zip(normalized.documents.iter_mut()) {
2189        align_document_with_ast(document_node, document);
2190    }
2191}
2192
2193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2194enum CollectionRenderContext {
2195    BlockAllowed,
2196    FlowRequired,
2197}
2198
2199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2200enum RenderedLayout {
2201    Inline,
2202    Block,
2203    Flow,
2204}
2205
2206struct RenderedYamlValue {
2207    text: String,
2208    layout: RenderedLayout,
2209    empty_collection: bool,
2210}
2211
2212impl RenderedYamlValue {
2213    fn inline(text: String) -> Self {
2214        Self {
2215            text,
2216            layout: RenderedLayout::Inline,
2217            empty_collection: false,
2218        }
2219    }
2220
2221    fn block(text: String) -> Self {
2222        Self {
2223            text,
2224            layout: RenderedLayout::Block,
2225            empty_collection: false,
2226        }
2227    }
2228
2229    fn flow(text: String, empty_collection: bool) -> Self {
2230        Self {
2231            text,
2232            layout: RenderedLayout::Flow,
2233            empty_collection,
2234        }
2235    }
2236}
2237
2238fn format_yaml_stream(template: &TemplateInput, stream: &YamlStreamNode) -> BackendResult<String> {
2239    let mut parts = Vec::with_capacity(stream.documents.len());
2240    for document in &stream.documents {
2241        let mut lines = Vec::new();
2242        lines.extend(document.directives.iter().cloned());
2243        if document.explicit_start || !document.directives.is_empty() || stream.documents.len() > 1
2244        {
2245            lines.push("---".to_owned());
2246        }
2247        lines.push(
2248            format_yaml_value(
2249                template,
2250                &document.value,
2251                0,
2252                CollectionRenderContext::BlockAllowed,
2253            )?
2254            .text,
2255        );
2256        if document.explicit_end {
2257            lines.push("...".to_owned());
2258        }
2259        parts.push(lines.join("\n"));
2260    }
2261    Ok(parts.join("\n"))
2262}
2263
2264fn format_yaml_value(
2265    template: &TemplateInput,
2266    node: &YamlValueNode,
2267    indent: usize,
2268    context: CollectionRenderContext,
2269) -> BackendResult<RenderedYamlValue> {
2270    match node {
2271        YamlValueNode::Decorated(node) => {
2272            let mut prefix = String::new();
2273            if let Some(tag) = &node.tag {
2274                prefix.push('!');
2275                prefix.push_str(&assemble_yaml_chunks(template, &tag.chunks, true)?);
2276            }
2277            if let Some(anchor) = &node.anchor {
2278                if !prefix.is_empty() {
2279                    prefix.push(' ');
2280                }
2281                prefix.push('&');
2282                prefix.push_str(&assemble_yaml_chunks(template, &anchor.chunks, true)?);
2283            }
2284            let rendered = format_yaml_value(template, node.value.as_ref(), indent, context)?;
2285            if prefix.is_empty() {
2286                Ok(rendered)
2287            } else {
2288                Ok(apply_rendered_prefix(prefix, rendered))
2289            }
2290        }
2291        YamlValueNode::Mapping(node) => format_yaml_mapping(template, node, indent, context),
2292        YamlValueNode::Sequence(node) => format_yaml_sequence(template, node, indent, context),
2293        YamlValueNode::Interpolation(node) => Ok(RenderedYamlValue::inline(
2294            interpolation_raw_source(template, node.interpolation_index, &node.span, "YAML value")?,
2295        )),
2296        YamlValueNode::Scalar(YamlScalarNode::Alias(node)) => Ok(RenderedYamlValue::inline(
2297            format!("*{}", assemble_yaml_chunks(template, &node.chunks, true)?),
2298        )),
2299        YamlValueNode::Scalar(YamlScalarNode::Block(node)) => Ok(RenderedYamlValue::inline(
2300            format_yaml_block_scalar(template, node, indent)?,
2301        )),
2302        YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(node)) => Ok(RenderedYamlValue::inline(
2303            serde_json::to_string(&assemble_yaml_chunks(template, &node.chunks, false)?).unwrap(),
2304        )),
2305        YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(node)) => {
2306            Ok(RenderedYamlValue::inline(format!(
2307                "'{}'",
2308                assemble_yaml_chunks(template, &node.chunks, false)?.replace('\'', "''")
2309            )))
2310        }
2311        YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => Ok(RenderedYamlValue::inline(
2312            format_yaml_plain_scalar(template, node)?,
2313        )),
2314    }
2315}
2316
2317fn format_yaml_mapping(
2318    template: &TemplateInput,
2319    node: &YamlMappingNode,
2320    indent: usize,
2321    context: CollectionRenderContext,
2322) -> BackendResult<RenderedYamlValue> {
2323    if node.flow || context == CollectionRenderContext::FlowRequired {
2324        let mut entries = Vec::with_capacity(node.entries.len());
2325        for entry in &node.entries {
2326            let rendered_key = match &entry.key.value {
2327                YamlKeyValue::Complex(key) => {
2328                    format_yaml_value(template, key, indent, CollectionRenderContext::FlowRequired)?
2329                        .text
2330                }
2331                _ => format_yaml_key(template, &entry.key)?,
2332            };
2333            let rendered_key = normalize_flow_key_text(rendered_key);
2334            let rendered_value = format_yaml_value(
2335                template,
2336                &entry.value,
2337                indent,
2338                CollectionRenderContext::FlowRequired,
2339            )?;
2340            entries.push(format!("{rendered_key}: {}", rendered_value.text));
2341        }
2342        return Ok(RenderedYamlValue::flow(
2343            format!("{{ {} }}", entries.join(", ")),
2344            node.entries.is_empty(),
2345        ));
2346    }
2347
2348    let mut rendered = String::new();
2349    for entry in &node.entries {
2350        if let YamlKeyValue::Complex(key) = &entry.key.value {
2351            let rendered_key = format_yaml_value(
2352                template,
2353                key,
2354                indent + 2,
2355                CollectionRenderContext::FlowRequired,
2356            )?;
2357            let rendered_value = format_yaml_value(
2358                template,
2359                &entry.value,
2360                indent + 2,
2361                CollectionRenderContext::BlockAllowed,
2362            )?;
2363            push_yaml_section(
2364                &mut rendered,
2365                format!("{}? {}", " ".repeat(indent), rendered_key.text),
2366            );
2367            push_rendered_value_with_prefix(
2368                &mut rendered,
2369                format!("{}:", " ".repeat(indent)),
2370                rendered_value,
2371            );
2372            continue;
2373        }
2374        let key = format_yaml_key(template, &entry.key)?;
2375        let rendered_value = format_yaml_value(
2376            template,
2377            &entry.value,
2378            indent + 2,
2379            CollectionRenderContext::BlockAllowed,
2380        )?;
2381        push_rendered_value_with_prefix(
2382            &mut rendered,
2383            format!("{}{}:", " ".repeat(indent), key),
2384            rendered_value,
2385        );
2386    }
2387    Ok(RenderedYamlValue::block(rendered))
2388}
2389
2390fn format_yaml_sequence(
2391    template: &TemplateInput,
2392    node: &YamlSequenceNode,
2393    indent: usize,
2394    context: CollectionRenderContext,
2395) -> BackendResult<RenderedYamlValue> {
2396    if node.flow || context == CollectionRenderContext::FlowRequired {
2397        let items = node
2398            .items
2399            .iter()
2400            .map(|item| {
2401                format_yaml_value(
2402                    template,
2403                    item,
2404                    indent,
2405                    CollectionRenderContext::FlowRequired,
2406                )
2407                .map(|item| item.text)
2408            })
2409            .collect::<BackendResult<Vec<_>>>()?;
2410        return Ok(RenderedYamlValue::flow(
2411            format!("[ {} ]", items.join(", ")),
2412            node.items.is_empty(),
2413        ));
2414    }
2415
2416    let mut rendered = String::new();
2417    for item in &node.items {
2418        let rendered_item = format_yaml_value(
2419            template,
2420            item,
2421            indent + 2,
2422            CollectionRenderContext::BlockAllowed,
2423        )?;
2424        push_rendered_value_with_prefix(
2425            &mut rendered,
2426            format!("{}-", " ".repeat(indent)),
2427            rendered_item,
2428        );
2429    }
2430    Ok(RenderedYamlValue::block(rendered))
2431}
2432
2433fn format_yaml_key(template: &TemplateInput, node: &YamlKeyNode) -> BackendResult<String> {
2434    match &node.value {
2435        YamlKeyValue::Interpolation(value) => {
2436            interpolation_raw_source(template, value.interpolation_index, &value.span, "YAML key")
2437        }
2438        YamlKeyValue::Scalar(YamlScalarNode::DoubleQuoted(value)) => Ok(serde_json::to_string(
2439            &assemble_yaml_chunks(template, &value.chunks, false)?,
2440        )
2441        .unwrap()),
2442        YamlKeyValue::Scalar(YamlScalarNode::SingleQuoted(value)) => Ok(format!(
2443            "'{}'",
2444            assemble_yaml_chunks(template, &value.chunks, false)?.replace('\'', "''")
2445        )),
2446        YamlKeyValue::Scalar(YamlScalarNode::Plain(value)) => {
2447            format_yaml_plain_scalar(template, value)
2448        }
2449        YamlKeyValue::Scalar(YamlScalarNode::Alias(node)) => Ok(format!(
2450            "*{}",
2451            assemble_yaml_chunks(template, &node.chunks, true)?
2452        )),
2453        YamlKeyValue::Scalar(YamlScalarNode::Block(node)) => {
2454            format_yaml_block_scalar(template, node, 0)
2455        }
2456        YamlKeyValue::Complex(value) => {
2457            format_yaml_value(template, value, 0, CollectionRenderContext::FlowRequired)
2458                .map(|value| value.text)
2459        }
2460    }
2461}
2462
2463fn format_yaml_plain_scalar(
2464    template: &TemplateInput,
2465    node: &YamlPlainScalarNode,
2466) -> BackendResult<String> {
2467    let text = assemble_yaml_chunks(template, &node.chunks, false)?
2468        .trim()
2469        .to_owned();
2470    if text.is_empty() {
2471        return Ok("null".to_owned());
2472    }
2473    if text.contains('\n') {
2474        return Ok(serde_json::to_string(&text).unwrap());
2475    }
2476    Ok(text)
2477}
2478
2479fn format_yaml_block_scalar(
2480    template: &TemplateInput,
2481    node: &YamlBlockScalarNode,
2482    indent: usize,
2483) -> BackendResult<String> {
2484    let mut header = node.style.clone();
2485    if let Some(chomping) = &node.chomping {
2486        header.push_str(chomping);
2487    }
2488    if let Some(indent_indicator) = node.indent_indicator {
2489        header.push_str(&indent_indicator.to_string());
2490    }
2491    let content = assemble_yaml_chunks(template, &node.chunks, false)?;
2492    if content.is_empty() {
2493        let mut rendered = header;
2494        rendered.push('\n');
2495        return Ok(rendered);
2496    }
2497
2498    let indentation_width = node
2499        .indent_indicator
2500        .map_or(indent, |indicator| indent.saturating_sub(2) + indicator);
2501    let indentation = " ".repeat(indentation_width);
2502    let trailing_breaks = content.chars().rev().take_while(|&ch| ch == '\n').count();
2503    let body_content = content.trim_end_matches('\n');
2504    let body = if body_content.is_empty() {
2505        String::new()
2506    } else {
2507        body_content
2508            .split('\n')
2509            .map(|line| format!("{indentation}{line}"))
2510            .collect::<Vec<_>>()
2511            .join("\n")
2512    };
2513    let trailing = match node.chomping.as_deref() {
2514        Some("-") => 0,
2515        Some("+") => trailing_breaks.max(1),
2516        _ => usize::from(!content.is_empty()),
2517    };
2518
2519    let mut rendered = header;
2520    rendered.push('\n');
2521    rendered.push_str(&body);
2522    for _ in 0..trailing {
2523        rendered.push('\n');
2524    }
2525    Ok(rendered)
2526}
2527
2528fn assemble_yaml_chunks(
2529    template: &TemplateInput,
2530    chunks: &[YamlChunk],
2531    _metadata: bool,
2532) -> BackendResult<String> {
2533    let mut text = String::new();
2534    for chunk in chunks {
2535        match chunk {
2536            YamlChunk::Text(chunk) => text.push_str(&chunk.value),
2537            YamlChunk::Interpolation(chunk) => text.push_str(&interpolation_raw_source(
2538                template,
2539                chunk.interpolation_index,
2540                &chunk.span,
2541                "YAML fragment",
2542            )?),
2543        }
2544    }
2545    Ok(text)
2546}
2547
2548fn apply_rendered_prefix(prefix: String, rendered: RenderedYamlValue) -> RenderedYamlValue {
2549    if rendered.layout == RenderedLayout::Block {
2550        return RenderedYamlValue {
2551            text: format!("{prefix}\n{}", rendered.text),
2552            layout: RenderedLayout::Block,
2553            empty_collection: rendered.empty_collection,
2554        };
2555    }
2556    RenderedYamlValue {
2557        text: format!("{prefix} {}", rendered.text),
2558        layout: rendered.layout,
2559        empty_collection: rendered.empty_collection,
2560    }
2561}
2562
2563fn push_rendered_value_with_prefix(
2564    output: &mut String,
2565    prefix: String,
2566    rendered: RenderedYamlValue,
2567) {
2568    if rendered.layout == RenderedLayout::Block && !rendered.empty_collection {
2569        if let Some((first, rest)) = rendered.text.split_once('\n')
2570            && !first.starts_with(' ')
2571        {
2572            push_yaml_section(output, format!("{prefix} {first}"));
2573            if !rest.is_empty() {
2574                push_yaml_section(output, rest.to_owned());
2575            }
2576            return;
2577        }
2578        push_yaml_section(output, prefix);
2579        push_yaml_section(output, rendered.text);
2580        return;
2581    }
2582
2583    push_yaml_section(output, format!("{prefix} {}", rendered.text));
2584}
2585
2586fn push_yaml_section(output: &mut String, section: String) {
2587    if output.is_empty() {
2588        output.push_str(&section);
2589        return;
2590    }
2591    if !output.ends_with('\n') {
2592        output.push('\n');
2593    }
2594    output.push_str(&section);
2595}
2596
2597fn normalize_flow_key_text(rendered_key: String) -> String {
2598    if rendered_key.starts_with('?') && !rendered_key.starts_with("? ") {
2599        return format!("? {}", &rendered_key[1..]);
2600    }
2601    rendered_key
2602}
2603
2604fn interpolation_raw_source(
2605    template: &TemplateInput,
2606    interpolation_index: usize,
2607    span: &SourceSpan,
2608    context: &str,
2609) -> BackendResult<String> {
2610    template
2611        .interpolation_raw_source(interpolation_index)
2612        .map(str::to_owned)
2613        .ok_or_else(|| {
2614            let expression = template.interpolation(interpolation_index).map_or_else(
2615                || format!("slot {interpolation_index}"),
2616                |value| value.expression_label().to_owned(),
2617            );
2618            BackendError::semantic_at(
2619                "yaml.format",
2620                format!(
2621                    "Cannot format {context} interpolation {expression:?} without raw source text."
2622                ),
2623                Some(span.clone()),
2624            )
2625        })
2626}
2627
2628fn normalize_document(document: &YamlOwned) -> BackendResult<NormalizedDocument> {
2629    if matches!(document, YamlOwned::BadValue) {
2630        return Ok(NormalizedDocument::Empty);
2631    }
2632    Ok(NormalizedDocument::Value(normalize_value(document)?))
2633}
2634
2635fn align_document_with_ast(node: &YamlDocumentNode, document: &mut NormalizedDocument) {
2636    if let NormalizedDocument::Value(value) = document {
2637        align_value_with_ast(&node.value, value);
2638    }
2639}
2640
2641fn normalize_value(value: &YamlOwned) -> BackendResult<NormalizedValue> {
2642    match value {
2643        YamlOwned::Value(value) => normalize_scalar(value),
2644        YamlOwned::Representation(value, style, tag) => {
2645            normalize_representation_value(value, *style, tag.as_ref())
2646        }
2647        YamlOwned::Sequence(values) => values
2648            .iter()
2649            .map(normalize_value)
2650            .collect::<BackendResult<Vec<_>>>()
2651            .map(NormalizedValue::Sequence),
2652        YamlOwned::Mapping(values) => normalize_mapping(values),
2653        YamlOwned::Tagged(tag, value) => {
2654            normalize_tagged_value(tag.handle.as_str(), &tag.suffix, value)
2655        }
2656        YamlOwned::Alias(_) => Err(BackendError::semantic(
2657            "Rendered YAML still contains an unresolved alias after validation.",
2658        )),
2659        YamlOwned::BadValue => Ok(NormalizedValue::Null),
2660    }
2661}
2662
2663fn normalize_representation_value(
2664    value: &str,
2665    style: ScalarStyle,
2666    tag: Option<&Tag>,
2667) -> BackendResult<NormalizedValue> {
2668    let parsed = ScalarOwned::parse_from_cow_and_metadata(
2669        Cow::Borrowed(value),
2670        style,
2671        tag.map(Cow::Borrowed).as_ref(),
2672    )
2673    .ok_or_else(|| {
2674        BackendError::semantic(format!(
2675            "Rendered YAML representation {value:?} could not be normalized with the active tag/schema."
2676        ))
2677    })?;
2678    normalize_scalar(&parsed)
2679}
2680
2681fn normalize_scalar(value: &saphyr::ScalarOwned) -> BackendResult<NormalizedValue> {
2682    match value {
2683        saphyr::ScalarOwned::Null => Ok(NormalizedValue::Null),
2684        saphyr::ScalarOwned::Boolean(value) => Ok(NormalizedValue::Bool(*value)),
2685        saphyr::ScalarOwned::Integer(value) => Ok(NormalizedValue::Integer((*value).into())),
2686        saphyr::ScalarOwned::FloatingPoint(value) => {
2687            let value = value.into_inner();
2688            if !value.is_finite() {
2689                return Err(BackendError::semantic(
2690                    "Rendered YAML contained a non-finite float, which this backend does not normalize.",
2691                ));
2692            }
2693            Ok(NormalizedValue::Float(NormalizedFloat::finite(value)))
2694        }
2695        saphyr::ScalarOwned::String(value) => Ok(NormalizedValue::String(value.clone())),
2696    }
2697}
2698
2699fn normalize_mapping(values: &saphyr::MappingOwned) -> BackendResult<NormalizedValue> {
2700    let mut entries = Vec::new();
2701    for (key, value) in values {
2702        if is_merge_key(key) {
2703            apply_merge_entries(value, &mut entries)?;
2704            continue;
2705        }
2706        insert_mapping_entry(
2707            &mut entries,
2708            normalize_key(key)?,
2709            normalize_value(value)?,
2710            true,
2711        );
2712    }
2713    Ok(NormalizedValue::Mapping(entries))
2714}
2715
2716fn align_value_with_ast(node: &YamlValueNode, normalized: &mut NormalizedValue) {
2717    match node {
2718        YamlValueNode::Decorated(decorated) => {
2719            if decorated.tag.as_ref().is_some_and(is_set_tag_literal)
2720                && let NormalizedValue::Mapping(entries) = normalized
2721            {
2722                let keys = entries.iter().map(|entry| entry.key.clone()).collect();
2723                *normalized = NormalizedValue::Set(keys);
2724                return;
2725            }
2726            align_value_with_ast(&decorated.value, normalized);
2727        }
2728        YamlValueNode::Mapping(mapping) => {
2729            if let NormalizedValue::Mapping(entries) = normalized {
2730                for (entry_node, entry) in mapping.entries.iter().zip(entries.iter_mut()) {
2731                    align_key_with_ast(mapping.flow, &entry_node.key, &mut entry.key);
2732                    align_value_with_ast(&entry_node.value, &mut entry.value);
2733                }
2734            }
2735        }
2736        YamlValueNode::Sequence(sequence) => {
2737            if let NormalizedValue::Sequence(values) = normalized {
2738                for (item_node, value) in sequence.items.iter().zip(values.iter_mut()) {
2739                    align_value_with_ast(item_node, value);
2740                }
2741            }
2742        }
2743        YamlValueNode::Scalar(_) | YamlValueNode::Interpolation(_) => {}
2744    }
2745}
2746
2747fn align_key_with_ast(flow_mapping: bool, key: &YamlKeyNode, normalized: &mut NormalizedKey) {
2748    match &key.value {
2749        YamlKeyValue::Scalar(YamlScalarNode::Plain(node))
2750            if flow_mapping && plain_scalar_starts_with_question(node) =>
2751        {
2752            if let NormalizedKey::String(value) = normalized
2753                && let Some(stripped) = value.strip_prefix('?')
2754            {
2755                *value = stripped.to_owned();
2756            }
2757        }
2758        YamlKeyValue::Complex(value) => align_key_value_with_ast(value, normalized),
2759        YamlKeyValue::Scalar(_) | YamlKeyValue::Interpolation(_) => {}
2760    }
2761}
2762
2763fn align_key_value_with_ast(node: &YamlValueNode, normalized: &mut NormalizedKey) {
2764    match node {
2765        YamlValueNode::Decorated(decorated) => {
2766            align_key_value_with_ast(&decorated.value, normalized)
2767        }
2768        YamlValueNode::Sequence(sequence) => {
2769            if let NormalizedKey::Sequence(keys) = normalized {
2770                for (item_node, key) in sequence.items.iter().zip(keys.iter_mut()) {
2771                    align_key_value_with_ast(item_node, key);
2772                }
2773            }
2774        }
2775        YamlValueNode::Mapping(mapping) => {
2776            if let NormalizedKey::Mapping(entries) = normalized {
2777                for (entry_node, entry) in mapping.entries.iter().zip(entries.iter_mut()) {
2778                    align_key_with_ast(mapping.flow, &entry_node.key, &mut entry.key);
2779                    align_key_value_with_ast(&entry_node.value, &mut entry.value);
2780                }
2781            }
2782        }
2783        YamlValueNode::Scalar(_) | YamlValueNode::Interpolation(_) => {}
2784    }
2785}
2786
2787fn plain_scalar_starts_with_question(node: &YamlPlainScalarNode) -> bool {
2788    let Some(YamlChunk::Text(text)) = node.chunks.first() else {
2789        return false;
2790    };
2791    text.value.starts_with('?')
2792}
2793
2794fn is_set_tag_literal(tag: &YamlTagNode) -> bool {
2795    let mut literal = String::new();
2796    for chunk in &tag.chunks {
2797        let YamlChunk::Text(text) = chunk else {
2798            return false;
2799        };
2800        literal.push_str(&text.value);
2801    }
2802    matches!(
2803        literal.as_str(),
2804        "!set" | "!!set" | "<tag:yaml.org,2002:set>" | "tag:yaml.org,2002:set"
2805    )
2806}
2807
2808fn normalize_key(value: &YamlOwned) -> BackendResult<NormalizedKey> {
2809    match value {
2810        YamlOwned::Value(value) => match value {
2811            saphyr::ScalarOwned::Null => Ok(NormalizedKey::Null),
2812            saphyr::ScalarOwned::Boolean(value) => Ok(NormalizedKey::Bool(*value)),
2813            saphyr::ScalarOwned::Integer(value) => Ok(NormalizedKey::Integer((*value).into())),
2814            saphyr::ScalarOwned::FloatingPoint(value) => {
2815                let value = value.into_inner();
2816                if !value.is_finite() {
2817                    return Err(BackendError::semantic(
2818                        "Rendered YAML contained a non-finite float mapping key, which this backend does not normalize.",
2819                    ));
2820                }
2821                Ok(NormalizedKey::Float(NormalizedFloat::finite(value)))
2822            }
2823            saphyr::ScalarOwned::String(value) => Ok(NormalizedKey::String(value.clone())),
2824        },
2825        YamlOwned::Representation(value, style, tag) => {
2826            normalize_representation_key(value, *style, tag.as_ref())
2827        }
2828        YamlOwned::Sequence(values) => values
2829            .iter()
2830            .map(normalize_key)
2831            .collect::<BackendResult<Vec<_>>>()
2832            .map(NormalizedKey::Sequence),
2833        YamlOwned::Mapping(values) => values
2834            .iter()
2835            .map(|(key, value)| {
2836                Ok(NormalizedKeyEntry {
2837                    key: normalize_key(key)?,
2838                    value: normalize_key(value)?,
2839                })
2840            })
2841            .collect::<BackendResult<Vec<_>>>()
2842            .map(NormalizedKey::Mapping),
2843        YamlOwned::Tagged(tag, value) => {
2844            normalize_tagged_key(tag.handle.as_str(), &tag.suffix, value)
2845        }
2846        YamlOwned::Alias(_) => Err(BackendError::semantic(
2847            "Rendered YAML still contains an unresolved alias after validation.",
2848        )),
2849        YamlOwned::BadValue => Ok(NormalizedKey::Null),
2850    }
2851}
2852
2853fn normalize_representation_key(
2854    value: &str,
2855    style: ScalarStyle,
2856    tag: Option<&Tag>,
2857) -> BackendResult<NormalizedKey> {
2858    let parsed = ScalarOwned::parse_from_cow_and_metadata(
2859        Cow::Borrowed(value),
2860        style,
2861        tag.map(Cow::Borrowed).as_ref(),
2862    )
2863    .ok_or_else(|| {
2864        BackendError::semantic(format!(
2865            "Rendered YAML key representation {value:?} could not be normalized with the active tag/schema."
2866        ))
2867    })?;
2868    match parsed {
2869        ScalarOwned::Null => Ok(NormalizedKey::Null),
2870        ScalarOwned::Boolean(value) => Ok(NormalizedKey::Bool(value)),
2871        ScalarOwned::Integer(value) => Ok(NormalizedKey::Integer(value.into())),
2872        ScalarOwned::FloatingPoint(value) => {
2873            let value = value.into_inner();
2874            if !value.is_finite() {
2875                return Err(BackendError::semantic(
2876                    "Rendered YAML contained a non-finite float mapping key, which this backend does not normalize.",
2877                ));
2878            }
2879            Ok(NormalizedKey::Float(NormalizedFloat::finite(value)))
2880        }
2881        ScalarOwned::String(value) => Ok(NormalizedKey::String(value)),
2882    }
2883}
2884
2885fn normalize_tagged_value(
2886    handle: &str,
2887    suffix: &str,
2888    value: &YamlOwned,
2889) -> BackendResult<NormalizedValue> {
2890    if is_yaml_core_tag(handle, suffix, "set") {
2891        let YamlOwned::Mapping(values) = value else {
2892            return Err(BackendError::semantic(
2893                "YAML !!set nodes must normalize from a mapping value.",
2894            ));
2895        };
2896        return values
2897            .iter()
2898            .map(|(key, _)| normalize_key(key))
2899            .collect::<BackendResult<Vec<_>>>()
2900            .map(NormalizedValue::Set);
2901    }
2902    if let Some(normalized) = normalize_core_tagged_scalar_value(handle, suffix, value)? {
2903        return Ok(normalized);
2904    }
2905    normalize_value(value)
2906}
2907
2908fn normalize_tagged_key(
2909    handle: &str,
2910    suffix: &str,
2911    value: &YamlOwned,
2912) -> BackendResult<NormalizedKey> {
2913    if is_yaml_core_tag(handle, suffix, "set") {
2914        return Err(BackendError::semantic(
2915            "YAML set values cannot be used as mapping keys in normalized output.",
2916        ));
2917    }
2918    if let Some(normalized) = normalize_core_tagged_scalar_key(handle, suffix, value)? {
2919        return Ok(normalized);
2920    }
2921    normalize_key(value)
2922}
2923
2924fn normalize_core_tagged_scalar_value(
2925    handle: &str,
2926    suffix: &str,
2927    value: &YamlOwned,
2928) -> BackendResult<Option<NormalizedValue>> {
2929    if !matches!(suffix, "null" | "bool" | "int" | "float" | "str")
2930        || !matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2931    {
2932        return Ok(None);
2933    }
2934    if is_empty_tagged_scalar(value) {
2935        return Ok(match suffix {
2936            "null" => Some(NormalizedValue::Null),
2937            "str" => Some(NormalizedValue::String(String::new())),
2938            _ => None,
2939        });
2940    }
2941    let Some(text) = scalar_text_for_core_tagged_value(value) else {
2942        return Ok(None);
2943    };
2944    normalize_representation_value(
2945        text.as_ref(),
2946        ScalarStyle::Plain,
2947        Some(&Tag {
2948            handle: canonical_core_tag_handle(handle).to_owned(),
2949            suffix: suffix.to_owned(),
2950        }),
2951    )
2952    .map(Some)
2953}
2954
2955fn normalize_core_tagged_scalar_key(
2956    handle: &str,
2957    suffix: &str,
2958    value: &YamlOwned,
2959) -> BackendResult<Option<NormalizedKey>> {
2960    if !matches!(suffix, "null" | "bool" | "int" | "float" | "str")
2961        || !matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
2962    {
2963        return Ok(None);
2964    }
2965    if is_empty_tagged_scalar(value) {
2966        return Ok(match suffix {
2967            "null" => Some(NormalizedKey::Null),
2968            "str" => Some(NormalizedKey::String(String::new())),
2969            _ => None,
2970        });
2971    }
2972    let Some(text) = scalar_text_for_core_tagged_value(value) else {
2973        return Ok(None);
2974    };
2975    normalize_representation_key(
2976        text.as_ref(),
2977        ScalarStyle::Plain,
2978        Some(&Tag {
2979            handle: canonical_core_tag_handle(handle).to_owned(),
2980            suffix: suffix.to_owned(),
2981        }),
2982    )
2983    .map(Some)
2984}
2985
2986fn canonical_core_tag_handle(handle: &str) -> &str {
2987    match handle {
2988        "" | "!!" | "tag:yaml.org,2002:" => "tag:yaml.org,2002:",
2989        other => other,
2990    }
2991}
2992
2993fn scalar_text_for_tagged_value(value: &YamlOwned) -> Option<Cow<'_, str>> {
2994    match value {
2995        YamlOwned::Representation(value, _, _) => Some(Cow::Borrowed(value.as_str())),
2996        YamlOwned::Value(ScalarOwned::Null) => Some(Cow::Borrowed("null")),
2997        YamlOwned::Value(ScalarOwned::Boolean(value)) => {
2998            Some(Cow::Borrowed(if *value { "true" } else { "false" }))
2999        }
3000        YamlOwned::Value(ScalarOwned::Integer(value)) => Some(Cow::Owned(value.to_string())),
3001        YamlOwned::Value(ScalarOwned::FloatingPoint(value)) => {
3002            Some(Cow::Owned(value.into_inner().to_string()))
3003        }
3004        YamlOwned::Value(ScalarOwned::String(value)) => Some(Cow::Borrowed(value.as_str())),
3005        YamlOwned::Tagged(_, value) => scalar_text_for_tagged_value(value),
3006        YamlOwned::Sequence(_)
3007        | YamlOwned::Mapping(_)
3008        | YamlOwned::Alias(_)
3009        | YamlOwned::BadValue => None,
3010    }
3011}
3012
3013fn scalar_text_for_core_tagged_value(value: &YamlOwned) -> Option<Cow<'_, str>> {
3014    match value {
3015        YamlOwned::Value(ScalarOwned::Null) => Some(Cow::Borrowed("")),
3016        YamlOwned::Tagged(_, value) => scalar_text_for_core_tagged_value(value),
3017        other => scalar_text_for_tagged_value(other),
3018    }
3019}
3020
3021fn is_empty_tagged_scalar(value: &YamlOwned) -> bool {
3022    match value {
3023        YamlOwned::Value(ScalarOwned::Null) => true,
3024        YamlOwned::Representation(value, _, _) => value.is_empty(),
3025        YamlOwned::Tagged(_, value) => is_empty_tagged_scalar(value),
3026        _ => false,
3027    }
3028}
3029
3030fn is_yaml_core_tag(handle: &str, suffix: &str, expected_suffix: &str) -> bool {
3031    suffix == expected_suffix && matches!(handle, "" | "!!" | "tag:yaml.org,2002:")
3032}
3033
3034fn is_merge_key(value: &YamlOwned) -> bool {
3035    match value {
3036        YamlOwned::Value(saphyr::ScalarOwned::String(value)) => value == "<<",
3037        YamlOwned::Representation(value, _, _) => value == "<<",
3038        YamlOwned::Tagged(_, value) => is_merge_key(value),
3039        _ => false,
3040    }
3041}
3042
3043fn apply_merge_entries(value: &YamlOwned, entries: &mut Vec<NormalizedEntry>) -> BackendResult<()> {
3044    match value {
3045        YamlOwned::Mapping(values) => merge_mapping_entries(values, entries),
3046        YamlOwned::Sequence(values) => {
3047            for value in values {
3048                let YamlOwned::Mapping(values) = value else {
3049                    return Err(BackendError::semantic(
3050                        "YAML merge sequences must contain only mappings.",
3051                    ));
3052                };
3053                merge_mapping_entries(values, entries)?;
3054            }
3055            Ok(())
3056        }
3057        YamlOwned::Tagged(_, value) => apply_merge_entries(value, entries),
3058        _ => Err(BackendError::semantic(
3059            "YAML merge values must be a mapping or sequence of mappings.",
3060        )),
3061    }
3062}
3063
3064fn merge_mapping_entries(
3065    values: &saphyr::MappingOwned,
3066    entries: &mut Vec<NormalizedEntry>,
3067) -> BackendResult<()> {
3068    for (key, value) in values {
3069        insert_mapping_entry(entries, normalize_key(key)?, normalize_value(value)?, false);
3070    }
3071    Ok(())
3072}
3073
3074fn insert_mapping_entry(
3075    entries: &mut Vec<NormalizedEntry>,
3076    key: NormalizedKey,
3077    value: NormalizedValue,
3078    override_existing: bool,
3079) {
3080    if let Some(existing) = entries.iter_mut().find(|entry| entry.key == key) {
3081        if override_existing {
3082            existing.value = value;
3083        }
3084        return;
3085    }
3086    entries.push(NormalizedEntry { key, value });
3087}
3088
3089fn wrap_decorators(
3090    decorated: Option<(Option<YamlTagNode>, Option<YamlAnchorNode>)>,
3091    value: YamlValueNode,
3092) -> BackendResult<YamlValueNode> {
3093    if let Some((tag, anchor)) = decorated {
3094        let decorator_span = tag
3095            .as_ref()
3096            .map(|tag| tag.span.clone())
3097            .or_else(|| anchor.as_ref().map(|anchor| anchor.span.clone()))
3098            .unwrap_or_else(|| value_span(&value).clone());
3099        let span = decorator_span.merge(value_span(&value));
3100
3101        match value {
3102            YamlValueNode::Decorated(inner) => {
3103                if anchor.is_some() && inner.anchor.is_some() {
3104                    return Err(BackendError::parse_at(
3105                        "yaml.parse",
3106                        "YAML nodes cannot define more than one anchor.",
3107                        Some(span),
3108                    ));
3109                }
3110                if tag.is_some() && inner.tag.is_some() {
3111                    return Err(BackendError::parse_at(
3112                        "yaml.parse",
3113                        "YAML nodes cannot define more than one tag.",
3114                        Some(span),
3115                    ));
3116                }
3117
3118                Ok(YamlValueNode::Decorated(YamlDecoratedNode {
3119                    span,
3120                    value: inner.value,
3121                    tag: tag.or(inner.tag),
3122                    anchor: anchor.or(inner.anchor),
3123                }))
3124            }
3125            YamlValueNode::Scalar(YamlScalarNode::Alias(_))
3126                if tag.is_some() || anchor.is_some() =>
3127            {
3128                Err(BackendError::parse_at(
3129                    "yaml.parse",
3130                    "YAML aliases cannot define tags or anchors.",
3131                    Some(span),
3132                ))
3133            }
3134            value => Ok(YamlValueNode::Decorated(YamlDecoratedNode {
3135                span,
3136                value: Box::new(value),
3137                tag,
3138                anchor,
3139            })),
3140        }
3141    } else {
3142        Ok(value)
3143    }
3144}
3145
3146fn null_plain_scalar() -> YamlPlainScalarNode {
3147    YamlPlainScalarNode {
3148        span: SourceSpan::point(0, 0),
3149        chunks: vec![YamlChunk::Text(YamlTextChunkNode {
3150            span: SourceSpan::point(0, 0),
3151            value: "null".to_owned(),
3152        })],
3153    }
3154}
3155
3156fn empty_plain_scalar() -> YamlPlainScalarNode {
3157    YamlPlainScalarNode {
3158        span: SourceSpan::point(0, 0),
3159        chunks: Vec::new(),
3160    }
3161}
3162
3163fn flow_implicit_plain_key_needs_separator(key: &YamlKeyNode) -> bool {
3164    matches!(
3165        &key.value,
3166        YamlKeyValue::Scalar(YamlScalarNode::Plain(node))
3167            if node.chunks.iter().any(|chunk| match chunk {
3168                YamlChunk::Text(text) => text.value.chars().any(char::is_whitespace),
3169                YamlChunk::Interpolation(_) => true,
3170            })
3171    )
3172}
3173
3174fn split_stream(template: &TemplateInput) -> BackendResult<Vec<YamlDocumentFragment>> {
3175    let items = template.flatten();
3176    let mut fragments = Vec::new();
3177    let mut directives = Vec::new();
3178    let mut current_start = None;
3179    let mut explicit_start = false;
3180    let mut explicit_end = false;
3181    let mut line_start = 0;
3182
3183    while line_start < items.len() {
3184        let (line_end, next_line) = line_bounds(&items, line_start);
3185        let line = collect_line(&items[line_start..line_end]);
3186        let trimmed = line.trim_end_matches(['\r', '\n']);
3187        let document_start_payload = document_start_payload_start(&items, line_start, line_end);
3188        let document_end = is_document_end_marker(trimmed);
3189        let malformed_document_end = trimmed.starts_with("...") && !document_end;
3190
3191        if malformed_document_end {
3192            let span = items
3193                .get(line_start)
3194                .map(|item| item.span().clone())
3195                .unwrap_or_else(|| SourceSpan::point(0, 0));
3196            return Err(BackendError::parse_at(
3197                "yaml.parse",
3198                "Unexpected trailing YAML content.",
3199                Some(span),
3200            ));
3201        }
3202
3203        if current_start.is_none() {
3204            if trimmed.is_empty() || trimmed.starts_with('#') {
3205                line_start = next_line;
3206                continue;
3207            }
3208            if trimmed.starts_with('%') {
3209                if !is_valid_directive(trimmed) {
3210                    let span = items
3211                        .get(line_start)
3212                        .map(|item| item.span().clone())
3213                        .unwrap_or_else(|| SourceSpan::point(0, 0));
3214                    return Err(BackendError::parse_at(
3215                        "yaml.parse",
3216                        "Unexpected trailing YAML content.",
3217                        Some(span),
3218                    ));
3219                }
3220                if duplicates_existing_directive(&directives, trimmed) {
3221                    let span = items
3222                        .get(line_start)
3223                        .map(|item| item.span().clone())
3224                        .unwrap_or_else(|| SourceSpan::point(0, 0));
3225                    return Err(BackendError::parse_at(
3226                        "yaml.parse",
3227                        "Unexpected trailing YAML content.",
3228                        Some(span),
3229                    ));
3230                }
3231                directives.push(trimmed.to_owned());
3232                line_start = next_line;
3233                continue;
3234            }
3235            if trimmed == "---" {
3236                current_start = Some(next_line);
3237                explicit_start = true;
3238                line_start = next_line;
3239                continue;
3240            }
3241            if let Some(payload_start) = document_start_payload {
3242                if explicit_start_payload_looks_like_block_mapping(trimmed) {
3243                    let span = items
3244                        .get(line_start)
3245                        .map(|item| item.span().clone())
3246                        .unwrap_or_else(|| SourceSpan::point(0, 0));
3247                    return Err(BackendError::parse_at(
3248                        "yaml.parse",
3249                        "Unexpected trailing YAML content.",
3250                        Some(span),
3251                    ));
3252                }
3253                current_start = Some(payload_start.unwrap_or(next_line));
3254                explicit_start = true;
3255                line_start = next_line;
3256                continue;
3257            }
3258            if document_end {
3259                line_start = next_line;
3260                continue;
3261            }
3262            current_start = Some(line_start);
3263        } else if trimmed == "---" {
3264            fragments.push(build_fragment(
3265                &items,
3266                current_start.unwrap_or(line_start),
3267                line_start,
3268                std::mem::take(&mut directives),
3269                explicit_start,
3270                explicit_end,
3271            ));
3272            current_start = Some(next_line);
3273            explicit_start = true;
3274            explicit_end = false;
3275            line_start = next_line;
3276            continue;
3277        } else if let Some(payload_start) = document_start_payload {
3278            fragments.push(build_fragment(
3279                &items,
3280                current_start.unwrap_or(line_start),
3281                line_start,
3282                std::mem::take(&mut directives),
3283                explicit_start,
3284                explicit_end,
3285            ));
3286            current_start = Some(payload_start.unwrap_or(next_line));
3287            explicit_start = true;
3288            explicit_end = false;
3289            line_start = next_line;
3290            continue;
3291        } else if document_end {
3292            fragments.push(build_fragment(
3293                &items,
3294                current_start.unwrap_or(line_start),
3295                line_start,
3296                std::mem::take(&mut directives),
3297                explicit_start,
3298                true,
3299            ));
3300            current_start = None;
3301            explicit_start = false;
3302            explicit_end = false;
3303            line_start = next_line;
3304            continue;
3305        }
3306
3307        line_start = next_line;
3308    }
3309
3310    if let Some(start) = current_start {
3311        fragments.push(build_fragment(
3312            &items,
3313            start,
3314            items.len().saturating_sub(1),
3315            std::mem::take(&mut directives),
3316            explicit_start,
3317            explicit_end,
3318        ));
3319    }
3320
3321    if current_start.is_none() && !directives.is_empty() {
3322        let span = items
3323            .iter()
3324            .rev()
3325            .find(|item| item.kind() != "eof")
3326            .map(|item| item.span().clone())
3327            .unwrap_or_else(|| SourceSpan::point(0, 0));
3328        return Err(BackendError::parse_at(
3329            "yaml.parse",
3330            "Unexpected trailing YAML content.",
3331            Some(span),
3332        ));
3333    }
3334
3335    if fragments.is_empty() {
3336        fragments.push(YamlDocumentFragment {
3337            directives: Vec::new(),
3338            explicit_start: false,
3339            explicit_end: false,
3340            items: vec![StreamItem::Eof {
3341                span: SourceSpan::point(0, 0),
3342            }],
3343        });
3344    }
3345
3346    Ok(fragments)
3347}
3348
3349fn build_fragment(
3350    items: &[StreamItem],
3351    start: usize,
3352    end: usize,
3353    directives: Vec<String>,
3354    explicit_start: bool,
3355    explicit_end: bool,
3356) -> YamlDocumentFragment {
3357    let mut fragment_items = items[start..end].to_vec();
3358    let eof_span = fragment_items
3359        .last()
3360        .map_or_else(|| SourceSpan::point(0, 0), |item| item.span().clone());
3361    fragment_items.push(StreamItem::Eof { span: eof_span });
3362    YamlDocumentFragment {
3363        directives,
3364        explicit_start,
3365        explicit_end,
3366        items: fragment_items,
3367    }
3368}
3369
3370fn line_bounds(items: &[StreamItem], start: usize) -> (usize, usize) {
3371    let mut probe = start;
3372    while probe < items.len() {
3373        if items[probe].char() == Some('\n') {
3374            return (probe, probe + 1);
3375        }
3376        if items[probe].kind() == "eof" {
3377            return (probe, probe + 1);
3378        }
3379        probe += 1;
3380    }
3381    (items.len(), items.len())
3382}
3383
3384fn document_start_payload_start(
3385    items: &[StreamItem],
3386    line_start: usize,
3387    line_end: usize,
3388) -> Option<Option<usize>> {
3389    let mut probe = line_start;
3390    for expected in ['-', '-', '-'] {
3391        if items.get(probe).and_then(StreamItem::char) != Some(expected) {
3392            return None;
3393        }
3394        probe += 1;
3395    }
3396    if probe >= line_end {
3397        return Some(None);
3398    }
3399    if !matches!(
3400        items.get(probe).and_then(StreamItem::char),
3401        Some(' ' | '\t')
3402    ) {
3403        return None;
3404    }
3405    while probe < line_end
3406        && matches!(
3407            items.get(probe).and_then(StreamItem::char),
3408            Some(' ' | '\t')
3409        )
3410    {
3411        probe += 1;
3412    }
3413    if probe >= line_end || items.get(probe).and_then(StreamItem::char) == Some('#') {
3414        return Some(None);
3415    }
3416    Some(Some(probe))
3417}
3418
3419fn is_document_end_marker(trimmed: &str) -> bool {
3420    if !trimmed.starts_with("...") {
3421        return false;
3422    }
3423    let Some(remainder) = trimmed.get(3..) else {
3424        return true;
3425    };
3426    let remainder = remainder.trim_start_matches([' ', '\t']);
3427    remainder.is_empty() || remainder.starts_with('#')
3428}
3429
3430fn explicit_start_payload_looks_like_block_mapping(trimmed: &str) -> bool {
3431    let Some(rest) = trimmed.strip_prefix("---") else {
3432        return false;
3433    };
3434    let rest = rest.trim_start_matches([' ', '\t']);
3435    if !(rest.starts_with('&') || rest.starts_with('!')) {
3436        return false;
3437    }
3438    if rest.starts_with('[') || rest.starts_with('{') {
3439        return false;
3440    }
3441    let Some((before_colon, after_colon)) = rest.split_once(':') else {
3442        return false;
3443    };
3444    !before_colon.contains('[')
3445        && !before_colon.contains('{')
3446        && !before_colon.ends_with(':')
3447        && matches!(after_colon.chars().next(), Some(' ' | '\t') | None)
3448}
3449
3450fn is_valid_directive(trimmed: &str) -> bool {
3451    let directive = trimmed
3452        .split_once('#')
3453        .map(|(directive, _)| directive)
3454        .unwrap_or(trimmed)
3455        .trim_end();
3456    let mut parts = directive.split_whitespace();
3457    match parts.next() {
3458        Some("%YAML") => matches!(
3459            (parts.next(), parts.next(), parts.next()),
3460            (Some(_version), None, None)
3461        ),
3462        Some("%TAG") => matches!(
3463            (parts.next(), parts.next(), parts.next(), parts.next()),
3464            (Some(_handle), Some(_prefix), None, None)
3465        ),
3466        Some(_) => true,
3467        None => false,
3468    }
3469}
3470
3471fn duplicates_existing_directive(existing: &[String], candidate: &str) -> bool {
3472    let Some((name, handle)) = directive_identity(candidate) else {
3473        return false;
3474    };
3475    existing.iter().any(|directive| {
3476        directive_identity(directive).is_some_and(|(existing_name, existing_handle)| {
3477            existing_name == name && existing_handle == handle
3478        })
3479    })
3480}
3481
3482fn directive_identity(trimmed: &str) -> Option<(&str, Option<&str>)> {
3483    let directive = trimmed
3484        .split_once('#')
3485        .map(|(directive, _)| directive)
3486        .unwrap_or(trimmed)
3487        .trim_end();
3488    let mut parts = directive.split_whitespace();
3489    match parts.next() {
3490        Some("%YAML") => Some(("%YAML", None)),
3491        Some("%TAG") => parts.next().map(|handle| ("%TAG", Some(handle))),
3492        _ => None,
3493    }
3494}
3495
3496fn collect_line(items: &[StreamItem]) -> String {
3497    let mut line = String::new();
3498    for item in items {
3499        if let Some(ch) = item.char() {
3500            line.push(ch);
3501        } else {
3502            line.push('\u{fffc}');
3503        }
3504    }
3505    line
3506}
3507
3508fn value_span(value: &YamlValueNode) -> &SourceSpan {
3509    match value {
3510        YamlValueNode::Scalar(YamlScalarNode::Plain(node)) => &node.span,
3511        YamlValueNode::Scalar(YamlScalarNode::DoubleQuoted(node)) => &node.span,
3512        YamlValueNode::Scalar(YamlScalarNode::SingleQuoted(node)) => &node.span,
3513        YamlValueNode::Scalar(YamlScalarNode::Block(node)) => &node.span,
3514        YamlValueNode::Scalar(YamlScalarNode::Alias(node)) => &node.span,
3515        YamlValueNode::Interpolation(node) => &node.span,
3516        YamlValueNode::Mapping(node) => &node.span,
3517        YamlValueNode::Sequence(node) => &node.span,
3518        YamlValueNode::Decorated(node) => &node.span,
3519    }
3520}
3521
3522#[cfg(test)]
3523mod tests {
3524    use super::{YamlValueNode, parse_template};
3525    use pyo3::prelude::*;
3526    use saphyr::{LoadableYamlNode, ScalarOwned, YamlOwned};
3527    use tstring_pyo3_bindings::{extract_template, yaml::render_document};
3528    use tstring_syntax::{BackendError, BackendResult, ErrorKind};
3529
3530    fn parse_rendered_yaml(text: &str) -> BackendResult<Vec<YamlOwned>> {
3531        YamlOwned::load_from_str(text).map_err(|err| {
3532            BackendError::parse(format!(
3533                "Rendered YAML could not be reparsed during test verification: {err}"
3534            ))
3535        })
3536    }
3537
3538    fn yaml_scalar_text(value: &YamlOwned) -> Option<&str> {
3539        match value {
3540            YamlOwned::Value(value) => value.as_str(),
3541            YamlOwned::Representation(value, _, _) => Some(value.as_str()),
3542            YamlOwned::Tagged(_, value) => yaml_scalar_text(value),
3543            _ => None,
3544        }
3545    }
3546
3547    fn yaml_integer(value: &YamlOwned) -> Option<i64> {
3548        match value {
3549            YamlOwned::Value(value) => value.as_integer(),
3550            YamlOwned::Tagged(_, value) => yaml_integer(value),
3551            _ => None,
3552        }
3553    }
3554
3555    fn yaml_float(value: &YamlOwned) -> Option<f64> {
3556        match value {
3557            YamlOwned::Value(ScalarOwned::FloatingPoint(value)) => Some(value.into_inner()),
3558            YamlOwned::Tagged(_, value) => yaml_float(value),
3559            _ => None,
3560        }
3561    }
3562
3563    fn yaml_sequence_len(value: &YamlOwned) -> Option<usize> {
3564        match value {
3565            YamlOwned::Sequence(value) => Some(value.len()),
3566            YamlOwned::Tagged(_, value) => yaml_sequence_len(value),
3567            _ => None,
3568        }
3569    }
3570
3571    fn yaml_mapping(value: &YamlOwned) -> Option<&saphyr::MappingOwned> {
3572        match value {
3573            YamlOwned::Mapping(value) => Some(value),
3574            YamlOwned::Tagged(_, value) => yaml_mapping(value),
3575            _ => None,
3576        }
3577    }
3578
3579    fn assert_string_entry(document: &YamlOwned, key: &str, expected: &str) {
3580        let mapping = yaml_mapping(document).expect("expected YAML mapping");
3581        let value = mapping
3582            .iter()
3583            .find_map(|(entry_key, entry_value)| {
3584                (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3585            })
3586            .expect("expected YAML mapping entry");
3587        assert_eq!(yaml_scalar_text(value), Some(expected));
3588    }
3589
3590    fn assert_integer_entry(document: &YamlOwned, key: &str, expected: i64) {
3591        let mapping = yaml_mapping(document).expect("expected YAML mapping");
3592        let value = mapping
3593            .iter()
3594            .find_map(|(entry_key, entry_value)| {
3595                (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3596            })
3597            .expect("expected YAML mapping entry");
3598        assert_eq!(yaml_integer(value), Some(expected));
3599    }
3600
3601    fn yaml_mapping_entry<'a>(document: &'a YamlOwned, key: &str) -> Option<&'a YamlOwned> {
3602        yaml_mapping(document).and_then(|mapping| {
3603            mapping.iter().find_map(|(entry_key, entry_value)| {
3604                (yaml_scalar_text(entry_key) == Some(key)).then_some(entry_value)
3605            })
3606        })
3607    }
3608
3609    #[test]
3610    fn parses_yaml_flow_and_scalar_nodes() {
3611        Python::with_gil(|py| {
3612            let module = PyModule::from_code(
3613                py,
3614                pyo3::ffi::c_str!(
3615                    "user='Alice'\ntemplate=t'name: \"hi-{user}\"\\nitems: [1, {user}]'\n"
3616                ),
3617                pyo3::ffi::c_str!("test_yaml.py"),
3618                pyo3::ffi::c_str!("test_yaml"),
3619            )
3620            .unwrap();
3621            let template = module.getattr("template").unwrap();
3622            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3623            let stream = parse_template(&template).unwrap();
3624            let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3625                panic!("expected mapping");
3626            };
3627            assert_eq!(mapping.entries.len(), 2);
3628        });
3629    }
3630
3631    #[test]
3632    fn parses_tags_and_anchors_on_scalars() {
3633        Python::with_gil(|py| {
3634            let module = PyModule::from_code(
3635                py,
3636                pyo3::ffi::c_str!(
3637                    "tag='str'\nanchor='user'\ntemplate=t'value: !{tag} &{anchor} \"hi\"\\n'\n"
3638                ),
3639                pyo3::ffi::c_str!("test_yaml_decorators.py"),
3640                pyo3::ffi::c_str!("test_yaml_decorators"),
3641            )
3642            .unwrap();
3643            let template = module.getattr("template").unwrap();
3644            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3645            let stream = parse_template(&template).unwrap();
3646            let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3647                panic!("expected mapping");
3648            };
3649            let YamlValueNode::Decorated(node) = &mapping.entries[0].value else {
3650                panic!("expected decorated scalar value");
3651            };
3652            assert!(node.tag.is_some());
3653            assert!(node.anchor.is_some());
3654        });
3655    }
3656
3657    #[test]
3658    fn renders_nested_yaml_values_and_validates() {
3659        Python::with_gil(|py| {
3660            let module = PyModule::from_code(
3661                py,
3662                pyo3::ffi::c_str!(
3663                    "name='Ada'\nmeta={'active': True, 'count': 2}\ntemplate=t'name: {name}\\nmeta: {meta}\\n'\n"
3664                ),
3665                pyo3::ffi::c_str!("test_yaml_render.py"),
3666                pyo3::ffi::c_str!("test_yaml_render"),
3667            )
3668            .unwrap();
3669            let template = module.getattr("template").unwrap();
3670            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3671            let stream = parse_template(&template).unwrap();
3672            let rendered = render_document(py, &stream).unwrap();
3673            let documents = parse_rendered_yaml(&rendered.text).unwrap();
3674
3675            assert!(rendered.text.contains("name: \"Ada\""));
3676            assert_string_entry(&documents[0], "name", "Ada");
3677        });
3678    }
3679
3680    #[test]
3681    fn rejects_metadata_with_whitespace() {
3682        Python::with_gil(|py| {
3683            let module = PyModule::from_code(
3684                py,
3685                pyo3::ffi::c_str!("anchor='bad anchor'\ntemplate=t'value: &{anchor} \"hi\"\\n'\n"),
3686                pyo3::ffi::c_str!("test_yaml_error.py"),
3687                pyo3::ffi::c_str!("test_yaml_error"),
3688            )
3689            .unwrap();
3690            let template = module.getattr("template").unwrap();
3691            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3692            let stream = parse_template(&template).unwrap();
3693            let err = match render_document(py, &stream) {
3694                Ok(_) => panic!("expected YAML render failure"),
3695                Err(err) => err,
3696            };
3697
3698            assert_eq!(err.kind, ErrorKind::Unrepresentable);
3699            assert!(err.message.contains("YAML metadata"));
3700        });
3701    }
3702
3703    #[test]
3704    fn rejects_flow_mappings_missing_commas_during_parse() {
3705        Python::with_gil(|py| {
3706            let module = PyModule::from_code(
3707                py,
3708                pyo3::ffi::c_str!(
3709                    "from string.templatelib import Template\nmapping=Template('{a: 1 b: 2}\\n')\nmissing_colon=Template('value: {a b}\\n')\nleading_comma_mapping=Template('value: {, a: 1}\\n')\nsequence=Template('[1,,2]\\n')\ntrailing_sequence=Template('value: [1, 2,,]\\n')\nempty_sequence=Template('[,]\\n')\nempty_mapping=Template('value: {,}\\n')\n"
3710                ),
3711                pyo3::ffi::c_str!("test_yaml_flow_error.py"),
3712                pyo3::ffi::c_str!("test_yaml_flow_error"),
3713            )
3714            .unwrap();
3715            for name in [
3716                "mapping",
3717                "missing_colon",
3718                "leading_comma_mapping",
3719                "sequence",
3720                "trailing_sequence",
3721                "empty_sequence",
3722                "empty_mapping",
3723            ] {
3724                let template = module.getattr(name).unwrap();
3725                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3726                let err = parse_template(&template).expect_err("expected YAML parse failure");
3727                assert_eq!(err.kind, ErrorKind::Parse);
3728                assert!(err.message.contains("Expected"));
3729            }
3730        });
3731    }
3732
3733    #[test]
3734    fn rejects_tabs_as_mapping_separation_whitespace() {
3735        Python::with_gil(|py| {
3736            let module = PyModule::from_code(
3737                py,
3738                pyo3::ffi::c_str!(
3739                    "from string.templatelib import Template\nmapping=Template('a:\\t1\\n')\nplain=Template('url: a:b\\t\\n')\nindent=Template('a:\\n\\t- 1\\n')\n"
3740                ),
3741                pyo3::ffi::c_str!("test_yaml_tab_error.py"),
3742                pyo3::ffi::c_str!("test_yaml_tab_error"),
3743            )
3744            .unwrap();
3745            for name in ["mapping", "plain", "indent"] {
3746                let template = module.getattr(name).unwrap();
3747                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3748                let err = parse_template(&template).expect_err("expected YAML parse failure");
3749                assert_eq!(err.kind, ErrorKind::Parse);
3750                assert!(
3751                    err.message.contains("Tabs are not allowed"),
3752                    "{name}: {}",
3753                    err.message
3754                );
3755            }
3756        });
3757    }
3758
3759    #[test]
3760    fn splits_explicit_document_streams() {
3761        Python::with_gil(|py| {
3762            let module = PyModule::from_code(
3763                py,
3764                pyo3::ffi::c_str!("template=t'---\\nname: Alice\\n---\\nname: Bob\\n'\n"),
3765                pyo3::ffi::c_str!("test_yaml_stream.py"),
3766                pyo3::ffi::c_str!("test_yaml_stream"),
3767            )
3768            .unwrap();
3769            let template = module.getattr("template").unwrap();
3770            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3771            let stream = parse_template(&template).unwrap();
3772            assert_eq!(stream.documents.len(), 2);
3773        });
3774    }
3775
3776    #[test]
3777    fn parses_and_renders_flow_complex_keys_with_interpolation() {
3778        Python::with_gil(|py| {
3779            let module = PyModule::from_code(
3780                py,
3781                pyo3::ffi::c_str!(
3782                    "left='Alice'\nright='Bob'\ntemplate=t'{{ {{name: [{left}, {right}]}}: 1, [{left}, {right}]: 2 }}'\n"
3783                ),
3784                pyo3::ffi::c_str!("test_yaml_flow_complex_keys.py"),
3785                pyo3::ffi::c_str!("test_yaml_flow_complex_keys"),
3786            )
3787            .unwrap();
3788            let template = module.getattr("template").unwrap();
3789            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3790            let stream = parse_template(&template).unwrap();
3791            let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3792                panic!("expected mapping");
3793            };
3794            assert!(matches!(
3795                mapping.entries[0].key.value,
3796                super::YamlKeyValue::Complex(_)
3797            ));
3798            assert!(matches!(
3799                mapping.entries[1].key.value,
3800                super::YamlKeyValue::Complex(_)
3801            ));
3802
3803            let rendered = render_document(py, &stream).unwrap();
3804            assert_eq!(
3805                rendered.text,
3806                "{ { name: [ \"Alice\", \"Bob\" ] }: 1, [ \"Alice\", \"Bob\" ]: 2 }"
3807            );
3808            let documents = parse_rendered_yaml(&rendered.text).unwrap();
3809            assert_eq!(
3810                documents[0]
3811                    .as_mapping()
3812                    .expect("expected YAML mapping")
3813                    .len(),
3814                2
3815            );
3816        });
3817    }
3818
3819    #[test]
3820    fn parses_explicit_mapping_keys_with_nested_collections() {
3821        Python::with_gil(|py| {
3822            let module = PyModule::from_code(
3823                py,
3824                pyo3::ffi::c_str!(
3825                    "left='Alice'\nright='Bob'\ntemplate=t'? {{name: [{left}, {right}]}}\\n: 1\\n'\n"
3826                ),
3827                pyo3::ffi::c_str!("test_yaml_explicit_complex_key.py"),
3828                pyo3::ffi::c_str!("test_yaml_explicit_complex_key"),
3829            )
3830            .unwrap();
3831            let template = module.getattr("template").unwrap();
3832            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3833            let stream = parse_template(&template).unwrap();
3834            let YamlValueNode::Mapping(mapping) = &stream.documents[0].value else {
3835                panic!("expected mapping");
3836            };
3837            assert!(matches!(
3838                mapping.entries[0].key.value,
3839                super::YamlKeyValue::Complex(_)
3840            ));
3841            let rendered = render_document(py, &stream).unwrap();
3842            assert!(rendered.text.contains("? { name: [ \"Alice\", \"Bob\" ] }"));
3843        });
3844    }
3845
3846    #[test]
3847    fn renders_explicit_complex_keys_text_and_validated_shape() {
3848        Python::with_gil(|py| {
3849            let module = PyModule::from_code(
3850                py,
3851                pyo3::ffi::c_str!(
3852                    "left='Alice'\nright='Bob'\ncomplex_key=t'? {{name: [{left}, {right}]}}\\n: 1\\n'\nempty_key=t'?\\n: 1\\n'\n"
3853                ),
3854                pyo3::ffi::c_str!("test_yaml_explicit_key_render.py"),
3855                pyo3::ffi::c_str!("test_yaml_explicit_key_render"),
3856            )
3857            .unwrap();
3858
3859            let complex_key = module.getattr("complex_key").unwrap();
3860            let complex_key = extract_template(py, &complex_key, "yaml_t/yaml_t_str").unwrap();
3861            let rendered = render_document(py, &parse_template(&complex_key).unwrap()).unwrap();
3862            assert!(
3863                rendered
3864                    .text
3865                    .contains("? { name: [ \"Alice\", \"Bob\" ] }\n: 1")
3866            );
3867            let documents = parse_rendered_yaml(&rendered.text).unwrap();
3868            assert_eq!(
3869                documents[0]
3870                    .as_mapping()
3871                    .expect("expected YAML mapping")
3872                    .len(),
3873                1
3874            );
3875
3876            let empty_key = module.getattr("empty_key").unwrap();
3877            let empty_key = extract_template(py, &empty_key, "yaml_t/yaml_t_str").unwrap();
3878            let rendered = render_document(py, &parse_template(&empty_key).unwrap()).unwrap();
3879            assert_eq!(rendered.text, "? null\n: 1");
3880            let documents = parse_rendered_yaml(&rendered.text).unwrap();
3881            assert_eq!(
3882                documents[0]
3883                    .as_mapping()
3884                    .expect("expected YAML mapping")
3885                    .len(),
3886                1
3887            );
3888        });
3889    }
3890
3891    #[test]
3892    fn parses_yaml_quoted_scalar_escapes() {
3893        Python::with_gil(|py| {
3894            let module = PyModule::from_code(
3895                py,
3896                pyo3::ffi::c_str!(
3897                    "from string.templatelib import Template\ntemplate=t'value: \"line\\\\nnext \\\\u03B1 \\\\x41\"\\nquote: \\'it\\'\\'s ok\\''\nunicode=t'value: \"\\\\U0001D11E\"\\n'\ncrlf_join=Template('value: \"a\\\\\\r\\n  b\"\\n')\nnel=t'value: \"\\\\N\"\\n'\nnbsp=t'value: \"\\\\_\"\\n'\nempty_single=Template(\"value: ''\\n\")\nempty_double=t'value: \"\"\\n'\nsingle_blank=Template(\"value: 'a\\n\\n  b\\n\\n  c'\\n\")\n"
3898                ),
3899                pyo3::ffi::c_str!("test_yaml_quoted_scalars.py"),
3900                pyo3::ffi::c_str!("test_yaml_quoted_scalars"),
3901            )
3902            .unwrap();
3903            let template = module.getattr("template").unwrap();
3904            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3905            let stream = parse_template(&template).unwrap();
3906            let rendered = render_document(py, &stream).unwrap();
3907            let documents = parse_rendered_yaml(&rendered.text).unwrap();
3908            assert_string_entry(&documents[0], "value", "line\nnext α A");
3909            assert_string_entry(&documents[0], "quote", "it's ok");
3910
3911            for (name, expected) in [
3912                ("unicode", "𝄞"),
3913                ("crlf_join", "ab"),
3914                ("nel", "\u{0085}"),
3915                ("nbsp", "\u{00a0}"),
3916                ("empty_single", ""),
3917                ("empty_double", ""),
3918                ("single_blank", "a\nb\nc"),
3919            ] {
3920                let template = module.getattr(name).unwrap();
3921                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3922                let stream = parse_template(&template).unwrap();
3923                let rendered = render_document(py, &stream).unwrap();
3924                let documents = parse_rendered_yaml(&rendered.text).unwrap();
3925                assert_string_entry(&documents[0], "value", expected);
3926            }
3927        });
3928    }
3929
3930    #[test]
3931    fn renders_quoted_scalar_escape_and_folding_families() {
3932        Python::with_gil(|py| {
3933            let module = PyModule::from_code(
3934                py,
3935                pyo3::ffi::c_str!(
3936                    "from string.templatelib import Template\nbase=t'value: \"line\\\\nnext \\\\u03B1 \\\\x41\"\\nquote: \\'it\\'\\'s ok\\''\nmultiline_double=t'value: \"a\\n  b\"\\n'\nmultiline_double_blank=t'value: \"a\\n\\n  b\"\\n'\nmultiline_double_more_blank=t'value: \"a\\n\\n\\n  b\"\\n'\nunicode=t'value: \"\\\\U0001D11E\"\\n'\ncrlf_join=Template('value: \"a\\\\\\r\\n  b\"\\n')\nnel=t'value: \"\\\\N\"\\n'\nnbsp=t'value: \"\\\\_\"\\n'\nspace=t'value: \"\\\\ \"\\n'\nslash=t'value: \"\\\\/\"\\n'\ntab=t'value: \"\\\\t\"\\n'\nempty_single=Template(\"value: ''\\n\")\nempty_double=t'value: \"\"\\n'\nsingle_blank=Template(\"value: 'a\\n\\n  b\\n\\n  c'\\n\")\n"
3937                ),
3938                pyo3::ffi::c_str!("test_yaml_quoted_scalar_families.py"),
3939                pyo3::ffi::c_str!("test_yaml_quoted_scalar_families"),
3940            )
3941            .unwrap();
3942
3943            let base = module.getattr("base").unwrap();
3944            let base = extract_template(py, &base, "yaml_t/yaml_t_str").unwrap();
3945            let rendered = render_document(py, &parse_template(&base).unwrap()).unwrap();
3946            let documents = parse_rendered_yaml(&rendered.text).unwrap();
3947            assert_eq!(
3948                rendered.text,
3949                "value: \"line\\nnext α A\"\nquote: 'it''s ok'"
3950            );
3951            assert_string_entry(&documents[0], "value", "line\nnext α A");
3952            assert_string_entry(&documents[0], "quote", "it's ok");
3953
3954            for (name, expected_text, expected) in [
3955                ("multiline_double", "value: \"a b\"", "a b"),
3956                ("multiline_double_blank", "value: \"a\\nb\"", "a\nb"),
3957                (
3958                    "multiline_double_more_blank",
3959                    "value: \"a\\n\\nb\"",
3960                    "a\n\nb",
3961                ),
3962                ("unicode", "value: \"𝄞\"", "𝄞"),
3963                ("crlf_join", "value: \"ab\"", "ab"),
3964                ("nel", "value: \"\u{0085}\"", "\u{0085}"),
3965                ("nbsp", "value: \"\u{00a0}\"", "\u{00a0}"),
3966                ("space", "value: \" \"", " "),
3967                ("slash", "value: \"/\"", "/"),
3968                ("tab", "value: \"\\t\"", "\t"),
3969                ("empty_single", "value: ''", ""),
3970                ("empty_double", "value: \"\"", ""),
3971                ("single_blank", "value: 'a\n\n  b\n\n  c'", "a\nb\nc"),
3972            ] {
3973                let template = module.getattr(name).unwrap();
3974                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
3975                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
3976                assert_eq!(rendered.text, expected_text, "{name}");
3977                let documents = parse_rendered_yaml(&rendered.text).unwrap();
3978                assert_string_entry(&documents[0], "value", expected);
3979            }
3980        });
3981    }
3982
3983    #[test]
3984    fn renders_spec_quoted_scalar_examples_round_trip() {
3985        Python::with_gil(|py| {
3986            let module = PyModule::from_code(
3987                py,
3988                pyo3::ffi::c_str!(
3989                    "unicode=t'unicode: \"Sosa did fine.\\\\u263A\"'\ncontrol=t'control: \"\\\\b1998\\\\t1999\\\\t2000\\\\n\"'\nsingle=t'''single: '\"Howdy!\" he cried.' '''\nquoted=t'''quoted: ' # Not a ''comment''.' '''\ntie=t'''tie: '|\\\\-*-/|' '''\n"
3990                ),
3991                pyo3::ffi::c_str!("test_yaml_spec_quoted_examples.py"),
3992                pyo3::ffi::c_str!("test_yaml_spec_quoted_examples"),
3993            )
3994            .unwrap();
3995
3996            for (name, key, expected_text, expected_value) in [
3997                (
3998                    "unicode",
3999                    "unicode",
4000                    "unicode: \"Sosa did fine.☺\"",
4001                    "Sosa did fine.\u{263a}",
4002                ),
4003                (
4004                    "control",
4005                    "control",
4006                    "control: \"\\b1998\\t1999\\t2000\\n\"",
4007                    "\u{0008}1998\t1999\t2000\n",
4008                ),
4009                (
4010                    "single",
4011                    "single",
4012                    "single: '\"Howdy!\" he cried.'",
4013                    "\"Howdy!\" he cried.",
4014                ),
4015                (
4016                    "quoted",
4017                    "quoted",
4018                    "quoted: ' # Not a ''comment''.'",
4019                    " # Not a 'comment'.",
4020                ),
4021                ("tie", "tie", "tie: '|\\-*-/|'", "|\\-*-/|"),
4022            ] {
4023                let template = module.getattr(name).unwrap();
4024                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4025                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4026                assert_eq!(rendered.text, expected_text, "{name}");
4027                let documents = parse_rendered_yaml(&rendered.text).unwrap();
4028                assert_string_entry(&documents[0], key, expected_value);
4029            }
4030        });
4031    }
4032
4033    #[test]
4034    fn folds_multiline_double_quoted_scalars() {
4035        Python::with_gil(|py| {
4036            let module = PyModule::from_code(
4037                py,
4038                pyo3::ffi::c_str!(
4039                    "single=t'value: \"a\\n  b\"\\n'\nblank=t'value: \"a\\n\\n  b\"\\n'\n"
4040                ),
4041                pyo3::ffi::c_str!("test_yaml_multiline_double_quoted.py"),
4042                pyo3::ffi::c_str!("test_yaml_multiline_double_quoted"),
4043            )
4044            .unwrap();
4045
4046            for (name, expected) in [("single", "a b"), ("blank", "a\nb")] {
4047                let template = module.getattr(name).unwrap();
4048                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4049                let stream = parse_template(&template).unwrap();
4050                let rendered = render_document(py, &stream).unwrap();
4051                let documents = parse_rendered_yaml(&rendered.text).unwrap();
4052                assert_string_entry(&documents[0], "value", expected);
4053            }
4054        });
4055    }
4056
4057    #[test]
4058    fn renders_block_chomping_and_indent_indicator_families() {
4059        Python::with_gil(|py| {
4060            let module = PyModule::from_code(
4061                py,
4062                pyo3::ffi::c_str!(
4063                    "literal_strip=t'value: |-\\n  a\\n  b\\n'\nliteral_keep=t'value: |+\\n  a\\n  b\\n'\nliteral_keep_leading_blank=t'value: |+\\n\\n  a\\n'\nfolded_strip=t'value: >-\\n  a\\n  b\\n'\nfolded_keep=t'value: >+\\n  a\\n  b\\n'\nfolded_more=t'value: >\\n  a\\n    b\\n  c\\n'\nindent_indicator=t'value: |2\\n  a\\n  b\\n'\nliteral_blank_keep=t'value: |+\\n  a\\n\\n  b\\n'\nfolded_blank_keep=t'value: >+\\n  a\\n\\n  b\\n'\n"
4064                ),
4065                pyo3::ffi::c_str!("test_yaml_block_chomping_families.py"),
4066                pyo3::ffi::c_str!("test_yaml_block_chomping_families"),
4067            )
4068            .unwrap();
4069
4070            for (name, expected_text, key, expected_value) in [
4071                ("literal_strip", "value: |-\n  a\n  b", "value", "a\nb"),
4072                ("literal_keep", "value: |+\n  a\n  b\n", "value", "a\nb\n"),
4073                (
4074                    "literal_keep_leading_blank",
4075                    "value: |+\n  \n  a\n",
4076                    "value",
4077                    "\na\n",
4078                ),
4079                ("folded_strip", "value: >-\n  a\n  b", "value", "a b"),
4080                ("folded_keep", "value: >+\n  a\n  b\n", "value", "a b\n"),
4081                (
4082                    "folded_more",
4083                    "value: >\n  a\n    b\n  c\n",
4084                    "value",
4085                    "a\n  b\nc\n",
4086                ),
4087                (
4088                    "indent_indicator",
4089                    "value: |2\n  a\n  b\n",
4090                    "value",
4091                    "a\nb\n",
4092                ),
4093                (
4094                    "literal_blank_keep",
4095                    "value: |+\n  a\n  \n  b\n",
4096                    "value",
4097                    "a\n\nb\n",
4098                ),
4099                (
4100                    "folded_blank_keep",
4101                    "value: >+\n  a\n  \n  b\n",
4102                    "value",
4103                    "a\nb\n",
4104                ),
4105            ] {
4106                let template = module.getattr(name).unwrap();
4107                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4108                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4109                assert_eq!(rendered.text, expected_text, "{name}");
4110                let documents = parse_rendered_yaml(&rendered.text).unwrap();
4111                assert_string_entry(&documents[0], key, expected_value);
4112            }
4113        });
4114    }
4115
4116    #[test]
4117    fn folds_multiline_plain_scalars() {
4118        Python::with_gil(|py| {
4119            let module = PyModule::from_code(
4120                py,
4121                pyo3::ffi::c_str!("template=t'value: a\\n  b\\n\\n  c\\n'\n"),
4122                pyo3::ffi::c_str!("test_yaml_multiline_plain.py"),
4123                pyo3::ffi::c_str!("test_yaml_multiline_plain"),
4124            )
4125            .unwrap();
4126            let template = module.getattr("template").unwrap();
4127            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4128            let stream = parse_template(&template).unwrap();
4129            let rendered = render_document(py, &stream).unwrap();
4130            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4131            assert_string_entry(&documents[0], "value", "a b\nc");
4132            assert!(rendered.text.contains("\"a b\\nc\""));
4133        });
4134    }
4135
4136    #[test]
4137    fn accepts_top_level_flow_collections_with_trailing_newlines() {
4138        Python::with_gil(|py| {
4139            let module = PyModule::from_code(
4140                py,
4141                pyo3::ffi::c_str!(
4142                    "from string.templatelib import Template\ntemplate=Template('{a: 1, b: [2, 3]}\\n')\n"
4143                ),
4144                pyo3::ffi::c_str!("test_yaml_flow_trailing_newline.py"),
4145                pyo3::ffi::c_str!("test_yaml_flow_trailing_newline"),
4146            )
4147            .unwrap();
4148            let template = module.getattr("template").unwrap();
4149            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4150            let stream = parse_template(&template).unwrap();
4151            let rendered = render_document(py, &stream).unwrap();
4152            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4153            assert_eq!(
4154                documents[0]
4155                    .as_mapping()
4156                    .expect("expected YAML mapping")
4157                    .len(),
4158                2
4159            );
4160        });
4161    }
4162
4163    #[test]
4164    fn accepts_line_wrapped_flow_collections() {
4165        Python::with_gil(|py| {
4166            let module = PyModule::from_code(
4167                py,
4168                pyo3::ffi::c_str!(
4169                    "from string.templatelib import Template\nsequence=Template('key: [a,\\n  b]\\n')\nmapping=Template('key: {a: 1,\\n  b: 2}\\n')\n"
4170                ),
4171                pyo3::ffi::c_str!("test_yaml_wrapped_flow.py"),
4172                pyo3::ffi::c_str!("test_yaml_wrapped_flow"),
4173            )
4174            .unwrap();
4175
4176            for name in ["sequence", "mapping"] {
4177                let template = module.getattr(name).unwrap();
4178                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4179                let stream = parse_template(&template).unwrap();
4180                let rendered = render_document(py, &stream).unwrap();
4181                let documents = parse_rendered_yaml(&rendered.text).unwrap();
4182                assert_eq!(
4183                    documents[0]
4184                        .as_mapping()
4185                        .expect("expected YAML mapping")
4186                        .len(),
4187                    1
4188                );
4189            }
4190        });
4191    }
4192
4193    #[test]
4194    fn accepts_flow_collections_with_comments() {
4195        Python::with_gil(|py| {
4196            let module = PyModule::from_code(
4197                py,
4198                pyo3::ffi::c_str!(
4199                    "from string.templatelib import Template\nsequence=Template('key: [a, # first\\n  b]\\n')\nmapping=Template('key: {a: 1, # first\\n  b: 2}\\n')\n"
4200                ),
4201                pyo3::ffi::c_str!("test_yaml_flow_comments.py"),
4202                pyo3::ffi::c_str!("test_yaml_flow_comments"),
4203            )
4204            .unwrap();
4205
4206            for name in ["sequence", "mapping"] {
4207                let template = module.getattr(name).unwrap();
4208                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4209                let stream = parse_template(&template).unwrap();
4210                let rendered = render_document(py, &stream).unwrap();
4211                let documents = parse_rendered_yaml(&rendered.text).unwrap();
4212                assert_eq!(
4213                    documents[0]
4214                        .as_mapping()
4215                        .expect("expected YAML mapping")
4216                        .len(),
4217                    1
4218                );
4219            }
4220        });
4221    }
4222
4223    #[test]
4224    fn treats_empty_documents_as_null() {
4225        Python::with_gil(|py| {
4226            let module = PyModule::from_code(
4227                py,
4228                pyo3::ffi::c_str!(
4229                    "from string.templatelib import Template\nempty=Template('---\\n...\\n')\nstream=Template('---\\n\\n---\\na: 1\\n')\n"
4230                ),
4231                pyo3::ffi::c_str!("test_yaml_empty_docs.py"),
4232                pyo3::ffi::c_str!("test_yaml_empty_docs"),
4233            )
4234            .unwrap();
4235
4236            let empty = module.getattr("empty").unwrap();
4237            let empty = extract_template(py, &empty, "yaml_t/yaml_t_str").unwrap();
4238            let rendered = render_document(py, &parse_template(&empty).unwrap()).unwrap();
4239            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4240            assert!(documents.as_slice()[0].is_null());
4241
4242            let stream = module.getattr("stream").unwrap();
4243            let stream = extract_template(py, &stream, "yaml_t/yaml_t_str").unwrap();
4244            let rendered = render_document(py, &parse_template(&stream).unwrap()).unwrap();
4245            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4246            assert!(documents[0].is_null());
4247            assert!(documents[1].as_mapping().is_some());
4248        });
4249    }
4250
4251    #[test]
4252    fn supports_indentless_sequence_values_and_empty_explicit_keys() {
4253        Python::with_gil(|py| {
4254            let module = PyModule::from_code(
4255                py,
4256                pyo3::ffi::c_str!(
4257                    "from string.templatelib import Template\nindentless=Template('a:\\n- 1\\n- 2\\n')\nempty_key=Template('?\\n: 1\\n')\n"
4258                ),
4259                pyo3::ffi::c_str!("test_yaml_indentless_and_empty_key.py"),
4260                pyo3::ffi::c_str!("test_yaml_indentless_and_empty_key"),
4261            )
4262            .unwrap();
4263
4264            let indentless = module.getattr("indentless").unwrap();
4265            let indentless = extract_template(py, &indentless, "yaml_t/yaml_t_str").unwrap();
4266            let rendered = render_document(py, &parse_template(&indentless).unwrap()).unwrap();
4267            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4268            let mapping = documents[0].as_mapping().expect("expected YAML mapping");
4269            let value = mapping
4270                .iter()
4271                .find_map(|(key, value)| (key.as_str() == Some("a")).then_some(value))
4272                .expect("expected key a");
4273            assert_eq!(value.as_vec().expect("expected YAML sequence").len(), 2);
4274
4275            let empty_key = module.getattr("empty_key").unwrap();
4276            let empty_key = extract_template(py, &empty_key, "yaml_t/yaml_t_str").unwrap();
4277            let rendered = render_document(py, &parse_template(&empty_key).unwrap()).unwrap();
4278            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4279            let mapping = documents[0].as_mapping().expect("expected YAML mapping");
4280            let value = mapping
4281                .iter()
4282                .find_map(|(key, value)| key.is_null().then_some(value))
4283                .expect("expected null key");
4284            assert_eq!(value.as_integer(), Some(1));
4285        });
4286    }
4287
4288    #[test]
4289    fn supports_compact_mappings_in_sequences_and_plain_hash_chars() {
4290        Python::with_gil(|py| {
4291            let module = PyModule::from_code(
4292                py,
4293                pyo3::ffi::c_str!(
4294                    "from string.templatelib import Template\nseq=Template('- a: 1\\n  b: 2\\n- c: 3\\n')\nmapped=Template('items:\\n- a: 1\\n  b: 2\\n- c: 3\\n')\nseqs=Template('- - 1\\n  - 2\\n- - 3\\n')\nhash_value=Template('value: a#b\\n')\n"
4295                ),
4296                pyo3::ffi::c_str!("test_yaml_compact_sequence_maps.py"),
4297                pyo3::ffi::c_str!("test_yaml_compact_sequence_maps"),
4298            )
4299            .unwrap();
4300
4301            for name in ["seq", "mapped", "seqs", "hash_value"] {
4302                let template = module.getattr(name).unwrap();
4303                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4304                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
4305                let documents = parse_rendered_yaml(&rendered.text).unwrap();
4306                assert_eq!(documents.len(), 1);
4307            }
4308        });
4309    }
4310
4311    #[test]
4312    fn preserves_explicit_document_end_markers_in_streams() {
4313        Python::with_gil(|py| {
4314            let module = PyModule::from_code(
4315                py,
4316                pyo3::ffi::c_str!(
4317                    "from string.templatelib import Template\ntemplate=Template('---\\na: 1\\n...\\n---\\nb: 2\\n')\ncommented=Template('---\\na: 1\\n... # end\\n---\\nb: 2\\n')\n"
4318                ),
4319                pyo3::ffi::c_str!("test_yaml_explicit_end.py"),
4320                pyo3::ffi::c_str!("test_yaml_explicit_end"),
4321            )
4322            .unwrap();
4323            let template = module.getattr("template").unwrap();
4324            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4325            let stream = parse_template(&template).unwrap();
4326            let rendered = render_document(py, &stream).unwrap();
4327
4328            assert!(rendered.text.contains("...\n---"));
4329
4330            let commented = module.getattr("commented").unwrap();
4331            let commented = extract_template(py, &commented, "yaml_t/yaml_t_str").unwrap();
4332            let rendered = render_document(py, &parse_template(&commented).unwrap()).unwrap();
4333            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4334            assert_eq!(documents.len(), 2);
4335        });
4336    }
4337
4338    #[test]
4339    fn parses_verbatim_tags() {
4340        Python::with_gil(|py| {
4341            let module = PyModule::from_code(
4342                py,
4343                pyo3::ffi::c_str!(
4344                    "from string.templatelib import Template\ntemplate=Template('value: !<tag:yaml.org,2002:str> hello\\n')\n"
4345                ),
4346                pyo3::ffi::c_str!("test_yaml_verbatim_tag.py"),
4347                pyo3::ffi::c_str!("test_yaml_verbatim_tag"),
4348            )
4349            .unwrap();
4350            let template = module.getattr("template").unwrap();
4351            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
4352            let stream = parse_template(&template).unwrap();
4353            let rendered = render_document(py, &stream).unwrap();
4354            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4355
4356            assert!(rendered.text.contains("!<tag:yaml.org,2002:str>"));
4357            assert_string_entry(&documents[0], "value", "hello");
4358        });
4359    }
4360
4361    #[test]
4362    fn validates_user_defined_tags_via_saphyr() {
4363        Python::with_gil(|py| {
4364            let module = PyModule::from_code(
4365                py,
4366                pyo3::ffi::c_str!(
4367                    "from string.templatelib import Template\nscalar=Template('!custom 3\\n')\ncustom_tag_scalar=Template('value: !custom 3\\n')\ncustom_tag_sequence=Template('value: !custom [1, 2]\\n')\ncommented_root=Template('--- # comment\\n!custom [1, 2]\\n')\n"
4368                ),
4369                pyo3::ffi::c_str!("test_yaml_custom_tags.py"),
4370                pyo3::ffi::c_str!("test_yaml_custom_tags"),
4371            )
4372            .unwrap();
4373
4374            let scalar = module.getattr("scalar").unwrap();
4375            let scalar = extract_template(py, &scalar, "yaml_t/yaml_t_str").unwrap();
4376            let rendered = render_document(py, &parse_template(&scalar).unwrap()).unwrap();
4377            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4378            assert_eq!(rendered.text.trim_end(), "!custom 3");
4379            assert!(rendered.text.contains("!custom 3"));
4380            assert_eq!(yaml_integer(&documents[0]), Some(3));
4381
4382            let custom_tag_scalar = module.getattr("custom_tag_scalar").unwrap();
4383            let custom_tag_scalar =
4384                extract_template(py, &custom_tag_scalar, "yaml_t/yaml_t_str").unwrap();
4385            let rendered =
4386                render_document(py, &parse_template(&custom_tag_scalar).unwrap()).unwrap();
4387            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4388            assert_eq!(rendered.text.trim_end(), "value: !custom 3");
4389            assert!(rendered.text.contains("value: !custom 3"));
4390            assert_integer_entry(&documents[0], "value", 3);
4391
4392            let custom_tag_sequence = module.getattr("custom_tag_sequence").unwrap();
4393            let custom_tag_sequence =
4394                extract_template(py, &custom_tag_sequence, "yaml_t/yaml_t_str").unwrap();
4395            let rendered =
4396                render_document(py, &parse_template(&custom_tag_sequence).unwrap()).unwrap();
4397            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4398            assert!(rendered.text.contains("value: !custom [ 1, 2 ]"));
4399            let value = documents[0]
4400                .as_mapping()
4401                .expect("expected YAML mapping")
4402                .iter()
4403                .find_map(|(key, value)| (yaml_scalar_text(key) == Some("value")).then_some(value))
4404                .expect("expected value key");
4405            assert_eq!(yaml_sequence_len(value), Some(2));
4406
4407            let commented_root = module.getattr("commented_root").unwrap();
4408            let commented_root =
4409                extract_template(py, &commented_root, "yaml_t/yaml_t_str").unwrap();
4410            let rendered = render_document(py, &parse_template(&commented_root).unwrap()).unwrap();
4411            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4412            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4413        });
4414    }
4415
4416    #[test]
4417    fn rejects_aliases_that_cross_document_boundaries() {
4418        Python::with_gil(|py| {
4419            let module = PyModule::from_code(
4420                py,
4421                pyo3::ffi::c_str!(
4422                    "from string.templatelib import Template\nstream=Template('--- &a\\n- 1\\n- 2\\n---\\n*a\\n')\n"
4423                ),
4424                pyo3::ffi::c_str!("test_yaml_cross_doc_alias.py"),
4425                pyo3::ffi::c_str!("test_yaml_cross_doc_alias"),
4426            )
4427            .unwrap();
4428
4429            let stream = module.getattr("stream").unwrap();
4430            let stream = extract_template(py, &stream, "yaml_t/yaml_t_str").unwrap();
4431            let rendered = render_document(py, &parse_template(&stream).unwrap()).unwrap();
4432            let err = parse_rendered_yaml(&rendered.text).expect_err("expected YAML parse failure");
4433            assert_eq!(err.kind, ErrorKind::Parse);
4434            assert!(err.message.contains("unknown anchor"));
4435        });
4436    }
4437
4438    #[test]
4439    fn preserves_tag_directives_and_handle_tags() {
4440        Python::with_gil(|py| {
4441            let module = PyModule::from_code(
4442                py,
4443                pyo3::ffi::c_str!(
4444                    "from string.templatelib import Template\nscalar=Template('%TAG !e! tag:example.com,2020:\\n---\\nvalue: !e!foo 1\\n')\ndoc_start_comment=Template('--- # comment\\nvalue: 1\\n')\ndoc_start_tag_comment=Template('--- !!str true # comment\\n')\nroot=Template('%YAML 1.2\\n%TAG !e! tag:example.com,2020:\\n---\\n!e!root {value: !e!leaf 1}\\n')\nblock_map=Template('--- !!map\\na: 1\\n')\nblock_seq=Template('--- !!seq\\n- 1\\n- 2\\n')\nanchor_map=Template('--- &root\\n  a: 1\\n')\nanchor_seq=Template('--- !custom &root\\n  - 1\\n  - 2\\n')\n"
4445                ),
4446                pyo3::ffi::c_str!("test_yaml_tag_directives.py"),
4447                pyo3::ffi::c_str!("test_yaml_tag_directives"),
4448            )
4449            .unwrap();
4450
4451            let scalar = module.getattr("scalar").unwrap();
4452            let scalar = extract_template(py, &scalar, "yaml_t/yaml_t_str").unwrap();
4453            let stream = parse_template(&scalar).unwrap();
4454            let rendered = render_document(py, &stream).unwrap();
4455            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4456            assert_eq!(
4457                rendered.text,
4458                "%TAG !e! tag:example.com,2020:\n---\nvalue: !e!foo 1"
4459            );
4460            assert_integer_entry(&documents[0], "value", 1);
4461
4462            let doc_start_comment = module.getattr("doc_start_comment").unwrap();
4463            let doc_start_comment =
4464                extract_template(py, &doc_start_comment, "yaml_t/yaml_t_str").unwrap();
4465            let stream = parse_template(&doc_start_comment).unwrap();
4466            let rendered = render_document(py, &stream).unwrap();
4467            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4468            assert_eq!(rendered.text, "---\nvalue: 1");
4469            assert_integer_entry(&documents[0], "value", 1);
4470
4471            let doc_start_tag_comment = module.getattr("doc_start_tag_comment").unwrap();
4472            let doc_start_tag_comment =
4473                extract_template(py, &doc_start_tag_comment, "yaml_t/yaml_t_str").unwrap();
4474            let stream = parse_template(&doc_start_tag_comment).unwrap();
4475            let rendered = render_document(py, &stream).unwrap();
4476            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4477            assert_eq!(rendered.text, "---\n!!str true");
4478            assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
4479
4480            let root = module.getattr("root").unwrap();
4481            let root = extract_template(py, &root, "yaml_t/yaml_t_str").unwrap();
4482            let stream = parse_template(&root).unwrap();
4483            let rendered = render_document(py, &stream).unwrap();
4484            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4485            assert_eq!(
4486                rendered.text,
4487                "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
4488            );
4489            assert_integer_entry(&documents[0], "value", 1);
4490
4491            let block_map = module.getattr("block_map").unwrap();
4492            let block_map = extract_template(py, &block_map, "yaml_t/yaml_t_str").unwrap();
4493            let stream = parse_template(&block_map).unwrap();
4494            let rendered = render_document(py, &stream).unwrap();
4495            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4496            assert_eq!(rendered.text, "---\n!!map\na: 1");
4497            assert_integer_entry(&documents[0], "a", 1);
4498
4499            let block_seq = module.getattr("block_seq").unwrap();
4500            let block_seq = extract_template(py, &block_seq, "yaml_t/yaml_t_str").unwrap();
4501            let stream = parse_template(&block_seq).unwrap();
4502            let rendered = render_document(py, &stream).unwrap();
4503            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4504            assert_eq!(rendered.text, "---\n!!seq\n- 1\n- 2");
4505            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4506
4507            let anchor_map = module.getattr("anchor_map").unwrap();
4508            let anchor_map = extract_template(py, &anchor_map, "yaml_t/yaml_t_str").unwrap();
4509            let stream = parse_template(&anchor_map).unwrap();
4510            let rendered = render_document(py, &stream).unwrap();
4511            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4512            assert_eq!(rendered.text, "---\n&root\na: 1");
4513            assert_integer_entry(&documents[0], "a", 1);
4514
4515            let anchor_seq = module.getattr("anchor_seq").unwrap();
4516            let anchor_seq = extract_template(py, &anchor_seq, "yaml_t/yaml_t_str").unwrap();
4517            let stream = parse_template(&anchor_seq).unwrap();
4518            let rendered = render_document(py, &stream).unwrap();
4519            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4520            assert_eq!(rendered.text, "---\n!custom &root\n- 1\n- 2");
4521            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4522        });
4523    }
4524
4525    #[test]
4526    fn preserves_explicit_core_schema_tags() {
4527        Python::with_gil(|py| {
4528            let module = PyModule::from_code(
4529                py,
4530                pyo3::ffi::c_str!(
4531                    "from string.templatelib import Template\nmapping=Template('value_bool: !!bool true\\nvalue_str: !!str true\\nvalue_float: !!float 1\\nvalue_null: !!null null\\n')\nroot_int=Template('--- !!int 3\\n')\nroot_str=Template('--- !!str true\\n')\nroot_bool=Template('--- !!bool true\\n')\n"
4532                ),
4533                pyo3::ffi::c_str!("test_yaml_core_tags.py"),
4534                pyo3::ffi::c_str!("test_yaml_core_tags"),
4535            )
4536            .unwrap();
4537
4538            let mapping = module.getattr("mapping").unwrap();
4539            let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
4540            let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
4541            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4542            let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
4543            let float_value = mapping
4544                .iter()
4545                .find_map(|(key, value)| {
4546                    (yaml_scalar_text(key) == Some("value_float")).then_some(value)
4547                })
4548                .expect("expected value_float key");
4549            assert_eq!(yaml_float(float_value), Some(1.0));
4550            assert_string_entry(&documents[0], "value_str", "true");
4551
4552            let root_int = module.getattr("root_int").unwrap();
4553            let root_int = extract_template(py, &root_int, "yaml_t/yaml_t_str").unwrap();
4554            let rendered = render_document(py, &parse_template(&root_int).unwrap()).unwrap();
4555            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4556            assert_eq!(yaml_integer(&documents[0]), Some(3));
4557
4558            let root_str = module.getattr("root_str").unwrap();
4559            let root_str = extract_template(py, &root_str, "yaml_t/yaml_t_str").unwrap();
4560            let rendered = render_document(py, &parse_template(&root_str).unwrap()).unwrap();
4561            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4562            assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
4563
4564            let root_bool = module.getattr("root_bool").unwrap();
4565            let root_bool = extract_template(py, &root_bool, "yaml_t/yaml_t_str").unwrap();
4566            let rendered = render_document(py, &parse_template(&root_bool).unwrap()).unwrap();
4567            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4568            assert!(documents[0].is_boolean());
4569        });
4570    }
4571
4572    #[test]
4573    fn supports_flow_trailing_commas_sequence_values_and_indent_indicators() {
4574        Python::with_gil(|py| {
4575            let module = PyModule::from_code(
4576                py,
4577                pyo3::ffi::c_str!(
4578                    "from string.templatelib import Template\ncomment_only=Template('# comment\\n')\ncomment_only_explicit=Template('--- # comment\\n')\ncomment_only_explicit_end=Template('--- # comment\\n...\\n')\ncomment_only_explicit_end_stream=Template('--- # comment\\n...\\n---\\na: 1\\n')\ncomment_only_mid_stream=Template('---\\na: 1\\n--- # comment\\n...\\n---\\nb: 2\\n')\ncomment_only_tail_stream=Template('---\\na: 1\\n--- # comment\\n...\\n')\nflow_seq=Template('[1, 2,]\\n')\nempty_flow_seq=Template('value: []\\n')\nflow_map=Template('{a: 1,}\\n')\nempty_flow_map=Template('value: {}\\n')\nflow_scalar_mix=Template('value: [\"\", \\'\\', plain]\\n')\nflow_plain_scalar=Template('value: [1 2]\\n')\nflow_hash_plain_mapping_value=Template('value: {a: b#c}\\n')\nflow_hash_plain_mapping_values=Template('value: {a: b#c, d: e#f}\\n')\nflow_hash_plain_scalars=Template('value: [a#b, c#d]\\n')\nflow_hash_value_sequence=Template('value: [a#b, c#d, e#f]\\n')\nflow_hash_long_sequence=Template('value: [a#b, c#d, e#f, g#h]\\n')\nflow_hash_five_sequence=Template('value: [a#b, c#d, e#f, g#h, i#j]\\n')\nflow_hash_seq_six=Template('value: [a#b, c#d, e#f, g#h, i#j, k#l]\\n')\nflow_hash_seq_seven=Template('value: [a#b, c#d, e#f, g#h, i#j, k#l, m#n]\\n')\nflow_mapping_hash_key=Template('value: {a#b: 1}\\n')\nflow_sequence_comments_value=Template('value: [1, # c\\n 2]\\n')\nflow_mapping_comments_value=Template('value: {a: 1, # c\\n b: 2}\\n')\ncomment_after_value=Template('value: a # c\\n')\nplain_colon_hash=Template('value: a:b#c\\n')\nplain_colon_hash_deeper=Template('value: a:b:c#d\\n')\nplain_hash_chain=Template('value: a#b#c\\n')\nplain_hash_chain_deeper=Template('value: a#b#c#d\\n')\nplain_hash_chain_deeper_comment=Template('value: a#b#c#d # comment\\n')\nflow_hash_mapping_long=Template('value: {a: b#c, d: e#f, g: h#i}\\n')\nflow_hash_mapping_four=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l}\\n')\nflow_hash_mapping_five=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l, m: n#o}\\n')\nflow_hash_mapping_six=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l, m: n#o, p: q#r}\\n')\ncomment_after_plain_colon=Template('value: a:b # c\\n')\ncomment_after_flow_plain_colon=Template('value: [a:b # c\\n]\\n')\nflow_plain_hash_chain=Template('value: [a#b#c, d#e#f]\\n')\nflow_plain_hash_chain_single_deeper=Template('value: [a#b#c#d]\\n')\nflow_plain_hash_chain_single_deeper_comment=Template('value: [a#b#c#d # comment\\n]\\n')\nflow_plain_hash_chain_long=Template('value: [a#b#c, d#e#f, g#h#i]\\n')\nflow_plain_hash_chain_four=Template('value: [a#b#c, d#e#f, g#h#i, j#k#l]\\n')\nblock_plain_comment_after_colon_long=Template('value: a:b:c # comment\\n')\nblock_plain_comment_after_colon_deeper=Template('value: a:b:c:d # comment\\n')\nflow_plain_comment_after_colon_long=Template('value: [a:b:c # comment\\n]\\n')\nflow_plain_comment_after_colon_deeper=Template('value: [a:b:c:d # comment\\n]\\n')\nflow_plain_colon_hash_deeper=Template('value: [a:b:c#d]\\n')\nflow_mapping_plain_key_question=Template('value: {?x: 1}\\n')\nflow_mapping_plain_key_questions=Template('value: {?x: 1, ?y: 2}\\n')\nmapping_empty_flow_values=Template('value: {a: [], b: {}}\\n')\nflow_mapping_empty_key=Template('{\"\": 1}\\n')\nflow_mapping_empty_key_and_values=Template('{\"\": [], foo: {}}\\n')\nflow_mapping_nested_empty=Template('{a: {}, b: []}\\n')\nflow_null_key=Template('{null: 1, \"\": 2}\\n')\nflow_sequence_nested_empty=Template('[[], {}]\\n')\nplain_scalar_colon_no_space=Template('value: a:b\\n')\nplain_question_mark_scalar=Template('value: ?x\\n')\nplain_colon_scalar_flow=Template('value: [a:b, c:d]\\n')\nflow_mapping_colon_plain_key=Template('value: {a:b: c}\\n')\nflow_mapping_colon_and_hash=Template('value: {a:b: c#d}\\n')\nblock_plain_colon_no_space=Template('value: a:b:c\\n')\nblock_null_key=Template('? null\\n: 1\\n')\nquoted_null_key=Template('? \"\"\\n: 1\\n')\nalias_in_flow_mapping_value=Template('base: &a {x: 1}\\nvalue: {ref: *a}\\n')\nflow_null_and_alias=Template('base: &a {x: 1}\\nvalue: {null: *a}\\n')\nalias_seq_value=Template('a: &x [1, 2]\\nb: *x\\n')\nflow_mapping_missing_value=Template('value: {a: }\\n')\nflow_seq_missing_value_before_end=Template('value: [1, 2, ]\\n')\nflow_alias_map=Template('value: {left: &a 1, right: *a}\\n')\nflow_alias_seq=Template('value: [&a 1, *a]\\n')\nflow_merge=Template('value: {<<: &base {a: 1}, b: 2}\\n')\nnested_flow_alias_merge=Template('value: [{<<: &base {a: 1}, b: 2}, *base]\\n')\nexplicit_seq=Template('? a\\n: - 1\\n  - 2\\n')\nindented_block=Template('value: |1\\n a\\n b\\n')\n"
4579                ),
4580                pyo3::ffi::c_str!("test_yaml_edge_cases.py"),
4581                pyo3::ffi::c_str!("test_yaml_edge_cases"),
4582            )
4583            .unwrap();
4584
4585            let flow_seq = module.getattr("flow_seq").unwrap();
4586            let flow_seq = extract_template(py, &flow_seq, "yaml_t/yaml_t_str").unwrap();
4587            let rendered = render_document(py, &parse_template(&flow_seq).unwrap()).unwrap();
4588            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4589            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
4590
4591            let comment_only = module.getattr("comment_only").unwrap();
4592            let comment_only = extract_template(py, &comment_only, "yaml_t/yaml_t_str").unwrap();
4593            let rendered = render_document(py, &parse_template(&comment_only).unwrap()).unwrap();
4594            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4595            assert!(documents[0].is_null());
4596
4597            let comment_only_explicit = module.getattr("comment_only_explicit").unwrap();
4598            let comment_only_explicit =
4599                extract_template(py, &comment_only_explicit, "yaml_t/yaml_t_str").unwrap();
4600            let rendered =
4601                render_document(py, &parse_template(&comment_only_explicit).unwrap()).unwrap();
4602            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4603            assert!(documents[0].is_null());
4604
4605            let comment_only_explicit_end = module.getattr("comment_only_explicit_end").unwrap();
4606            let comment_only_explicit_end =
4607                extract_template(py, &comment_only_explicit_end, "yaml_t/yaml_t_str").unwrap();
4608            let rendered =
4609                render_document(py, &parse_template(&comment_only_explicit_end).unwrap()).unwrap();
4610            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4611            assert!(documents[0].is_null());
4612
4613            let comment_only_explicit_end_stream =
4614                module.getattr("comment_only_explicit_end_stream").unwrap();
4615            let comment_only_explicit_end_stream =
4616                extract_template(py, &comment_only_explicit_end_stream, "yaml_t/yaml_t_str")
4617                    .unwrap();
4618            let rendered = render_document(
4619                py,
4620                &parse_template(&comment_only_explicit_end_stream).unwrap(),
4621            )
4622            .unwrap();
4623            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4624            assert_eq!(documents.len(), 2);
4625            assert!(documents[0].is_null());
4626            assert_integer_entry(&documents[1], "a", 1);
4627
4628            let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
4629            let comment_only_mid_stream =
4630                extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
4631            let rendered =
4632                render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
4633            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4634            assert_eq!(documents.len(), 3);
4635            assert_integer_entry(&documents[0], "a", 1);
4636            assert!(documents[1].is_null());
4637            assert_integer_entry(&documents[2], "b", 2);
4638
4639            let comment_only_tail_stream = module.getattr("comment_only_tail_stream").unwrap();
4640            let comment_only_tail_stream =
4641                extract_template(py, &comment_only_tail_stream, "yaml_t/yaml_t_str").unwrap();
4642            let rendered =
4643                render_document(py, &parse_template(&comment_only_tail_stream).unwrap()).unwrap();
4644            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4645            assert_eq!(documents.len(), 2);
4646            assert_integer_entry(&documents[0], "a", 1);
4647            assert!(documents[1].is_null());
4648
4649            let flow_map = module.getattr("flow_map").unwrap();
4650            let flow_map = extract_template(py, &flow_map, "yaml_t/yaml_t_str").unwrap();
4651            let rendered = render_document(py, &parse_template(&flow_map).unwrap()).unwrap();
4652            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4653            assert_integer_entry(&documents[0], "a", 1);
4654
4655            let empty_flow_seq = module.getattr("empty_flow_seq").unwrap();
4656            let empty_flow_seq =
4657                extract_template(py, &empty_flow_seq, "yaml_t/yaml_t_str").unwrap();
4658            let rendered = render_document(py, &parse_template(&empty_flow_seq).unwrap()).unwrap();
4659            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4660            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4661            assert_eq!(yaml_sequence_len(value), Some(0));
4662
4663            let empty_flow_map = module.getattr("empty_flow_map").unwrap();
4664            let empty_flow_map =
4665                extract_template(py, &empty_flow_map, "yaml_t/yaml_t_str").unwrap();
4666            let rendered = render_document(py, &parse_template(&empty_flow_map).unwrap()).unwrap();
4667            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4668            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4669            assert!(yaml_mapping(value).is_some());
4670
4671            let flow_scalar_mix = module.getattr("flow_scalar_mix").unwrap();
4672            let flow_scalar_mix =
4673                extract_template(py, &flow_scalar_mix, "yaml_t/yaml_t_str").unwrap();
4674            let rendered = render_document(py, &parse_template(&flow_scalar_mix).unwrap()).unwrap();
4675            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4676            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4677            match value {
4678                YamlOwned::Sequence(sequence) => {
4679                    assert_eq!(sequence.len(), 3);
4680                    assert_eq!(yaml_scalar_text(&sequence[0]), Some(""));
4681                    assert_eq!(yaml_scalar_text(&sequence[1]), Some(""));
4682                    assert_eq!(yaml_scalar_text(&sequence[2]), Some("plain"));
4683                }
4684                _ => panic!("expected YAML sequence"),
4685            }
4686
4687            let flow_plain_scalar = module.getattr("flow_plain_scalar").unwrap();
4688            let flow_plain_scalar =
4689                extract_template(py, &flow_plain_scalar, "yaml_t/yaml_t_str").unwrap();
4690            let rendered =
4691                render_document(py, &parse_template(&flow_plain_scalar).unwrap()).unwrap();
4692            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4693            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4694            match value {
4695                YamlOwned::Sequence(sequence) => {
4696                    assert_eq!(sequence.len(), 1);
4697                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("1 2"));
4698                }
4699                _ => panic!("expected YAML sequence"),
4700            }
4701
4702            let flow_hash_plain_mapping_value =
4703                module.getattr("flow_hash_plain_mapping_value").unwrap();
4704            let flow_hash_plain_mapping_value =
4705                extract_template(py, &flow_hash_plain_mapping_value, "yaml_t/yaml_t_str").unwrap();
4706            let rendered =
4707                render_document(py, &parse_template(&flow_hash_plain_mapping_value).unwrap())
4708                    .unwrap();
4709            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4710            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4711            assert_string_entry(value, "a", "b#c");
4712
4713            let flow_hash_plain_mapping_values =
4714                module.getattr("flow_hash_plain_mapping_values").unwrap();
4715            let flow_hash_plain_mapping_values =
4716                extract_template(py, &flow_hash_plain_mapping_values, "yaml_t/yaml_t_str").unwrap();
4717            let rendered = render_document(
4718                py,
4719                &parse_template(&flow_hash_plain_mapping_values).unwrap(),
4720            )
4721            .unwrap();
4722            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4723            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4724            assert_string_entry(value, "a", "b#c");
4725            assert_string_entry(value, "d", "e#f");
4726
4727            let flow_hash_plain_scalars = module.getattr("flow_hash_plain_scalars").unwrap();
4728            let flow_hash_plain_scalars =
4729                extract_template(py, &flow_hash_plain_scalars, "yaml_t/yaml_t_str").unwrap();
4730            let rendered =
4731                render_document(py, &parse_template(&flow_hash_plain_scalars).unwrap()).unwrap();
4732            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4733            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4734            match value {
4735                YamlOwned::Sequence(sequence) => {
4736                    assert_eq!(sequence.len(), 2);
4737                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b"));
4738                    assert_eq!(yaml_scalar_text(&sequence[1]), Some("c#d"));
4739                }
4740                _ => panic!("expected YAML sequence"),
4741            }
4742
4743            let flow_hash_value_sequence = module.getattr("flow_hash_value_sequence").unwrap();
4744            let flow_hash_value_sequence =
4745                extract_template(py, &flow_hash_value_sequence, "yaml_t/yaml_t_str").unwrap();
4746            let rendered =
4747                render_document(py, &parse_template(&flow_hash_value_sequence).unwrap()).unwrap();
4748            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4749            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4750            match value {
4751                YamlOwned::Sequence(sequence) => {
4752                    assert_eq!(sequence.len(), 3);
4753                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b"));
4754                    assert_eq!(yaml_scalar_text(&sequence[1]), Some("c#d"));
4755                    assert_eq!(yaml_scalar_text(&sequence[2]), Some("e#f"));
4756                }
4757                _ => panic!("expected YAML sequence"),
4758            }
4759
4760            let flow_hash_long_sequence = module.getattr("flow_hash_long_sequence").unwrap();
4761            let flow_hash_long_sequence =
4762                extract_template(py, &flow_hash_long_sequence, "yaml_t/yaml_t_str").unwrap();
4763            let rendered =
4764                render_document(py, &parse_template(&flow_hash_long_sequence).unwrap()).unwrap();
4765            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4766            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4767            match value {
4768                YamlOwned::Sequence(sequence) => {
4769                    assert_eq!(sequence.len(), 4);
4770                    assert_eq!(yaml_scalar_text(&sequence[3]), Some("g#h"));
4771                }
4772                _ => panic!("expected YAML sequence"),
4773            }
4774
4775            let flow_hash_five_sequence = module.getattr("flow_hash_five_sequence").unwrap();
4776            let flow_hash_five_sequence =
4777                extract_template(py, &flow_hash_five_sequence, "yaml_t/yaml_t_str").unwrap();
4778            let rendered =
4779                render_document(py, &parse_template(&flow_hash_five_sequence).unwrap()).unwrap();
4780            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4781            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4782            match value {
4783                YamlOwned::Sequence(sequence) => {
4784                    assert_eq!(sequence.len(), 5);
4785                    assert_eq!(yaml_scalar_text(&sequence[4]), Some("i#j"));
4786                }
4787                _ => panic!("expected YAML sequence"),
4788            }
4789
4790            let flow_mapping_hash_key = module.getattr("flow_mapping_hash_key").unwrap();
4791            let flow_mapping_hash_key =
4792                extract_template(py, &flow_mapping_hash_key, "yaml_t/yaml_t_str").unwrap();
4793            let rendered =
4794                render_document(py, &parse_template(&flow_mapping_hash_key).unwrap()).unwrap();
4795            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4796            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4797            assert_integer_entry(value, "a#b", 1);
4798
4799            let flow_sequence_comments_value =
4800                module.getattr("flow_sequence_comments_value").unwrap();
4801            let flow_sequence_comments_value =
4802                extract_template(py, &flow_sequence_comments_value, "yaml_t/yaml_t_str").unwrap();
4803            let rendered =
4804                render_document(py, &parse_template(&flow_sequence_comments_value).unwrap())
4805                    .unwrap();
4806            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4807            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4808            match value {
4809                YamlOwned::Sequence(sequence) => {
4810                    assert_eq!(sequence.len(), 2);
4811                    assert_eq!(yaml_integer(&sequence[0]), Some(1));
4812                    assert_eq!(yaml_integer(&sequence[1]), Some(2));
4813                }
4814                _ => panic!("expected YAML sequence"),
4815            }
4816
4817            let flow_mapping_comments_value =
4818                module.getattr("flow_mapping_comments_value").unwrap();
4819            let flow_mapping_comments_value =
4820                extract_template(py, &flow_mapping_comments_value, "yaml_t/yaml_t_str").unwrap();
4821            let rendered =
4822                render_document(py, &parse_template(&flow_mapping_comments_value).unwrap())
4823                    .unwrap();
4824            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4825            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4826            assert_integer_entry(value, "a", 1);
4827            assert_integer_entry(value, "b", 2);
4828
4829            let comment_after_value = module.getattr("comment_after_value").unwrap();
4830            let comment_after_value =
4831                extract_template(py, &comment_after_value, "yaml_t/yaml_t_str").unwrap();
4832            let rendered =
4833                render_document(py, &parse_template(&comment_after_value).unwrap()).unwrap();
4834            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4835            assert_string_entry(&documents[0], "value", "a");
4836
4837            let plain_colon_hash = module.getattr("plain_colon_hash").unwrap();
4838            let plain_colon_hash =
4839                extract_template(py, &plain_colon_hash, "yaml_t/yaml_t_str").unwrap();
4840            let rendered =
4841                render_document(py, &parse_template(&plain_colon_hash).unwrap()).unwrap();
4842            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4843            assert_string_entry(&documents[0], "value", "a:b#c");
4844
4845            let plain_colon_hash_deeper = module.getattr("plain_colon_hash_deeper").unwrap();
4846            let plain_colon_hash_deeper =
4847                extract_template(py, &plain_colon_hash_deeper, "yaml_t/yaml_t_str").unwrap();
4848            let rendered =
4849                render_document(py, &parse_template(&plain_colon_hash_deeper).unwrap()).unwrap();
4850            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4851            assert_string_entry(&documents[0], "value", "a:b:c#d");
4852
4853            let plain_hash_chain = module.getattr("plain_hash_chain").unwrap();
4854            let plain_hash_chain =
4855                extract_template(py, &plain_hash_chain, "yaml_t/yaml_t_str").unwrap();
4856            let rendered =
4857                render_document(py, &parse_template(&plain_hash_chain).unwrap()).unwrap();
4858            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4859            assert_string_entry(&documents[0], "value", "a#b#c");
4860
4861            let plain_hash_chain_deeper = module.getattr("plain_hash_chain_deeper").unwrap();
4862            let plain_hash_chain_deeper =
4863                extract_template(py, &plain_hash_chain_deeper, "yaml_t/yaml_t_str").unwrap();
4864            let rendered =
4865                render_document(py, &parse_template(&plain_hash_chain_deeper).unwrap()).unwrap();
4866            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4867            assert_string_entry(&documents[0], "value", "a#b#c#d");
4868
4869            let plain_hash_chain_deeper_comment =
4870                module.getattr("plain_hash_chain_deeper_comment").unwrap();
4871            let plain_hash_chain_deeper_comment =
4872                extract_template(py, &plain_hash_chain_deeper_comment, "yaml_t/yaml_t_str")
4873                    .unwrap();
4874            let rendered = render_document(
4875                py,
4876                &parse_template(&plain_hash_chain_deeper_comment).unwrap(),
4877            )
4878            .unwrap();
4879            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4880            assert_string_entry(&documents[0], "value", "a#b#c#d");
4881
4882            let flow_hash_mapping_long = module.getattr("flow_hash_mapping_long").unwrap();
4883            let flow_hash_mapping_long =
4884                extract_template(py, &flow_hash_mapping_long, "yaml_t/yaml_t_str").unwrap();
4885            let rendered =
4886                render_document(py, &parse_template(&flow_hash_mapping_long).unwrap()).unwrap();
4887            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4888            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4889            assert_string_entry(value, "a", "b#c");
4890            assert_string_entry(value, "d", "e#f");
4891            assert_string_entry(value, "g", "h#i");
4892
4893            let flow_hash_mapping_four = module.getattr("flow_hash_mapping_four").unwrap();
4894            let flow_hash_mapping_four =
4895                extract_template(py, &flow_hash_mapping_four, "yaml_t/yaml_t_str").unwrap();
4896            let rendered =
4897                render_document(py, &parse_template(&flow_hash_mapping_four).unwrap()).unwrap();
4898            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4899            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4900            assert_string_entry(value, "a", "b#c");
4901            assert_string_entry(value, "d", "e#f");
4902            assert_string_entry(value, "g", "h#i");
4903            assert_string_entry(value, "j", "k#l");
4904
4905            let flow_hash_mapping_five = module.getattr("flow_hash_mapping_five").unwrap();
4906            let flow_hash_mapping_five =
4907                extract_template(py, &flow_hash_mapping_five, "yaml_t/yaml_t_str").unwrap();
4908            let rendered =
4909                render_document(py, &parse_template(&flow_hash_mapping_five).unwrap()).unwrap();
4910            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4911            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4912            assert_string_entry(value, "m", "n#o");
4913
4914            let flow_hash_mapping_six = module.getattr("flow_hash_mapping_six").unwrap();
4915            let flow_hash_mapping_six =
4916                extract_template(py, &flow_hash_mapping_six, "yaml_t/yaml_t_str").unwrap();
4917            let rendered =
4918                render_document(py, &parse_template(&flow_hash_mapping_six).unwrap()).unwrap();
4919            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4920            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4921            assert_string_entry(value, "p", "q#r");
4922
4923            let comment_after_plain_colon = module.getattr("comment_after_plain_colon").unwrap();
4924            let comment_after_plain_colon =
4925                extract_template(py, &comment_after_plain_colon, "yaml_t/yaml_t_str").unwrap();
4926            let rendered =
4927                render_document(py, &parse_template(&comment_after_plain_colon).unwrap()).unwrap();
4928            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4929            assert_string_entry(&documents[0], "value", "a:b");
4930
4931            let comment_after_flow_plain_colon =
4932                module.getattr("comment_after_flow_plain_colon").unwrap();
4933            let comment_after_flow_plain_colon =
4934                extract_template(py, &comment_after_flow_plain_colon, "yaml_t/yaml_t_str").unwrap();
4935            let rendered = render_document(
4936                py,
4937                &parse_template(&comment_after_flow_plain_colon).unwrap(),
4938            )
4939            .unwrap();
4940            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4941            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4942            match value {
4943                YamlOwned::Sequence(sequence) => {
4944                    assert_eq!(sequence.len(), 1);
4945                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b"));
4946                }
4947                _ => panic!("expected YAML sequence"),
4948            }
4949
4950            let flow_plain_hash_chain = module.getattr("flow_plain_hash_chain").unwrap();
4951            let flow_plain_hash_chain =
4952                extract_template(py, &flow_plain_hash_chain, "yaml_t/yaml_t_str").unwrap();
4953            let rendered =
4954                render_document(py, &parse_template(&flow_plain_hash_chain).unwrap()).unwrap();
4955            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4956            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4957            match value {
4958                YamlOwned::Sequence(sequence) => {
4959                    assert_eq!(sequence.len(), 2);
4960                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c"));
4961                    assert_eq!(yaml_scalar_text(&sequence[1]), Some("d#e#f"));
4962                }
4963                _ => panic!("expected YAML sequence"),
4964            }
4965
4966            let flow_plain_hash_chain_single_deeper = module
4967                .getattr("flow_plain_hash_chain_single_deeper")
4968                .unwrap();
4969            let flow_plain_hash_chain_single_deeper = extract_template(
4970                py,
4971                &flow_plain_hash_chain_single_deeper,
4972                "yaml_t/yaml_t_str",
4973            )
4974            .unwrap();
4975            let rendered = render_document(
4976                py,
4977                &parse_template(&flow_plain_hash_chain_single_deeper).unwrap(),
4978            )
4979            .unwrap();
4980            let documents = parse_rendered_yaml(&rendered.text).unwrap();
4981            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
4982            match value {
4983                YamlOwned::Sequence(sequence) => {
4984                    assert_eq!(sequence.len(), 1);
4985                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c#d"));
4986                }
4987                _ => panic!("expected YAML sequence"),
4988            }
4989
4990            let flow_plain_hash_chain_single_deeper_comment = module
4991                .getattr("flow_plain_hash_chain_single_deeper_comment")
4992                .unwrap();
4993            let flow_plain_hash_chain_single_deeper_comment = extract_template(
4994                py,
4995                &flow_plain_hash_chain_single_deeper_comment,
4996                "yaml_t/yaml_t_str",
4997            )
4998            .unwrap();
4999            let rendered = render_document(
5000                py,
5001                &parse_template(&flow_plain_hash_chain_single_deeper_comment).unwrap(),
5002            )
5003            .unwrap();
5004            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5005            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5006            match value {
5007                YamlOwned::Sequence(sequence) => {
5008                    assert_eq!(sequence.len(), 1);
5009                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a#b#c#d"));
5010                }
5011                _ => panic!("expected YAML sequence"),
5012            }
5013
5014            let flow_hash_seq_six = module.getattr("flow_hash_seq_six").unwrap();
5015            let flow_hash_seq_six =
5016                extract_template(py, &flow_hash_seq_six, "yaml_t/yaml_t_str").unwrap();
5017            let rendered =
5018                render_document(py, &parse_template(&flow_hash_seq_six).unwrap()).unwrap();
5019            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5020            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5021            match value {
5022                YamlOwned::Sequence(sequence) => {
5023                    assert_eq!(sequence.len(), 6);
5024                    assert_eq!(yaml_scalar_text(&sequence[5]), Some("k#l"));
5025                }
5026                _ => panic!("expected YAML sequence"),
5027            }
5028
5029            let flow_hash_seq_seven = module.getattr("flow_hash_seq_seven").unwrap();
5030            let flow_hash_seq_seven =
5031                extract_template(py, &flow_hash_seq_seven, "yaml_t/yaml_t_str").unwrap();
5032            let rendered =
5033                render_document(py, &parse_template(&flow_hash_seq_seven).unwrap()).unwrap();
5034            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5035            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5036            match value {
5037                YamlOwned::Sequence(sequence) => {
5038                    assert_eq!(sequence.len(), 7);
5039                    assert_eq!(yaml_scalar_text(&sequence[6]), Some("m#n"));
5040                }
5041                _ => panic!("expected YAML sequence"),
5042            }
5043
5044            let flow_plain_hash_chain_four = module.getattr("flow_plain_hash_chain_four").unwrap();
5045            let flow_plain_hash_chain_four =
5046                extract_template(py, &flow_plain_hash_chain_four, "yaml_t/yaml_t_str").unwrap();
5047            let rendered =
5048                render_document(py, &parse_template(&flow_plain_hash_chain_four).unwrap()).unwrap();
5049            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5050            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5051            match value {
5052                YamlOwned::Sequence(sequence) => {
5053                    assert_eq!(sequence.len(), 4);
5054                    assert_eq!(yaml_scalar_text(&sequence[3]), Some("j#k#l"));
5055                }
5056                _ => panic!("expected YAML sequence"),
5057            }
5058
5059            let block_plain_comment_after_colon_deeper = module
5060                .getattr("block_plain_comment_after_colon_deeper")
5061                .unwrap();
5062            let block_plain_comment_after_colon_deeper = extract_template(
5063                py,
5064                &block_plain_comment_after_colon_deeper,
5065                "yaml_t/yaml_t_str",
5066            )
5067            .unwrap();
5068            let rendered = render_document(
5069                py,
5070                &parse_template(&block_plain_comment_after_colon_deeper).unwrap(),
5071            )
5072            .unwrap();
5073            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5074            assert_string_entry(&documents[0], "value", "a:b:c:d");
5075
5076            let flow_plain_comment_after_colon_deeper = module
5077                .getattr("flow_plain_comment_after_colon_deeper")
5078                .unwrap();
5079            let flow_plain_comment_after_colon_deeper = extract_template(
5080                py,
5081                &flow_plain_comment_after_colon_deeper,
5082                "yaml_t/yaml_t_str",
5083            )
5084            .unwrap();
5085            let rendered = render_document(
5086                py,
5087                &parse_template(&flow_plain_comment_after_colon_deeper).unwrap(),
5088            )
5089            .unwrap();
5090            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5091            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5092            match value {
5093                YamlOwned::Sequence(sequence) => {
5094                    assert_eq!(sequence.len(), 1);
5095                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b:c:d"));
5096                }
5097                _ => panic!("expected YAML sequence"),
5098            }
5099
5100            let flow_mapping_plain_key_question =
5101                module.getattr("flow_mapping_plain_key_question").unwrap();
5102            let flow_mapping_plain_key_question =
5103                extract_template(py, &flow_mapping_plain_key_question, "yaml_t/yaml_t_str")
5104                    .unwrap();
5105            let rendered = render_document(
5106                py,
5107                &parse_template(&flow_mapping_plain_key_question).unwrap(),
5108            )
5109            .unwrap();
5110            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5111            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5112            let mapping = yaml_mapping(value).expect("expected YAML mapping");
5113            assert_eq!(mapping.len(), 1);
5114            let (_, entry_value) = mapping.iter().next().expect("expected YAML mapping entry");
5115            assert_eq!(yaml_integer(entry_value), Some(1));
5116
5117            let flow_mapping_plain_key_questions =
5118                module.getattr("flow_mapping_plain_key_questions").unwrap();
5119            let flow_mapping_plain_key_questions =
5120                extract_template(py, &flow_mapping_plain_key_questions, "yaml_t/yaml_t_str")
5121                    .unwrap();
5122            let rendered = render_document(
5123                py,
5124                &parse_template(&flow_mapping_plain_key_questions).unwrap(),
5125            )
5126            .unwrap();
5127            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5128            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5129            let mapping = yaml_mapping(value).expect("expected YAML mapping");
5130            assert_eq!(mapping.len(), 2);
5131
5132            let mapping_empty_flow_values = module.getattr("mapping_empty_flow_values").unwrap();
5133            let mapping_empty_flow_values =
5134                extract_template(py, &mapping_empty_flow_values, "yaml_t/yaml_t_str").unwrap();
5135            let rendered =
5136                render_document(py, &parse_template(&mapping_empty_flow_values).unwrap()).unwrap();
5137            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5138            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5139            let mapping = yaml_mapping(value).expect("expected YAML mapping");
5140            let left = mapping
5141                .iter()
5142                .find_map(|(key, value)| (yaml_scalar_text(key) == Some("a")).then_some(value))
5143                .expect("expected key a");
5144            let right = mapping
5145                .iter()
5146                .find_map(|(key, value)| (yaml_scalar_text(key) == Some("b")).then_some(value))
5147                .expect("expected key b");
5148            assert_eq!(yaml_sequence_len(left), Some(0));
5149            assert!(yaml_mapping(right).is_some());
5150
5151            let flow_mapping_empty_key = module.getattr("flow_mapping_empty_key").unwrap();
5152            let flow_mapping_empty_key =
5153                extract_template(py, &flow_mapping_empty_key, "yaml_t/yaml_t_str").unwrap();
5154            let rendered =
5155                render_document(py, &parse_template(&flow_mapping_empty_key).unwrap()).unwrap();
5156            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5157            assert_integer_entry(&documents[0], "", 1);
5158
5159            let flow_mapping_empty_key_and_values =
5160                module.getattr("flow_mapping_empty_key_and_values").unwrap();
5161            let flow_mapping_empty_key_and_values =
5162                extract_template(py, &flow_mapping_empty_key_and_values, "yaml_t/yaml_t_str")
5163                    .unwrap();
5164            let rendered = render_document(
5165                py,
5166                &parse_template(&flow_mapping_empty_key_and_values).unwrap(),
5167            )
5168            .unwrap();
5169            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5170            assert!(yaml_mapping_entry(&documents[0], "").is_some());
5171            let foo = yaml_mapping_entry(&documents[0], "foo").expect("expected foo");
5172            assert!(yaml_mapping(foo).is_some());
5173
5174            let flow_mapping_nested_empty = module.getattr("flow_mapping_nested_empty").unwrap();
5175            let flow_mapping_nested_empty =
5176                extract_template(py, &flow_mapping_nested_empty, "yaml_t/yaml_t_str").unwrap();
5177            let rendered =
5178                render_document(py, &parse_template(&flow_mapping_nested_empty).unwrap()).unwrap();
5179            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5180            let a = yaml_mapping_entry(&documents[0], "a").expect("expected key a");
5181            let b = yaml_mapping_entry(&documents[0], "b").expect("expected key b");
5182            assert!(yaml_mapping(a).is_some());
5183            assert_eq!(yaml_sequence_len(b), Some(0));
5184
5185            let flow_null_key = module.getattr("flow_null_key").unwrap();
5186            let flow_null_key = extract_template(py, &flow_null_key, "yaml_t/yaml_t_str").unwrap();
5187            let rendered = render_document(py, &parse_template(&flow_null_key).unwrap()).unwrap();
5188            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5189            let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
5190            assert!(
5191                mapping
5192                    .iter()
5193                    .any(|(key, value)| key.is_null() && yaml_integer(value) == Some(1))
5194            );
5195            assert!(mapping.iter().any(|(key, value)| {
5196                yaml_scalar_text(key) == Some("") && yaml_integer(value) == Some(2)
5197            }));
5198
5199            let block_null_key = module.getattr("block_null_key").unwrap();
5200            let block_null_key =
5201                extract_template(py, &block_null_key, "yaml_t/yaml_t_str").unwrap();
5202            let rendered = render_document(py, &parse_template(&block_null_key).unwrap()).unwrap();
5203            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5204            let mapping = yaml_mapping(&documents[0]).expect("expected YAML mapping");
5205            assert!(
5206                mapping
5207                    .iter()
5208                    .any(|(key, value)| key.is_null() && yaml_integer(value) == Some(1))
5209            );
5210
5211            let quoted_null_key = module.getattr("quoted_null_key").unwrap();
5212            let quoted_null_key =
5213                extract_template(py, &quoted_null_key, "yaml_t/yaml_t_str").unwrap();
5214            let rendered = render_document(py, &parse_template(&quoted_null_key).unwrap()).unwrap();
5215            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5216            assert_integer_entry(&documents[0], "", 1);
5217
5218            let flow_sequence_nested_empty = module.getattr("flow_sequence_nested_empty").unwrap();
5219            let flow_sequence_nested_empty =
5220                extract_template(py, &flow_sequence_nested_empty, "yaml_t/yaml_t_str").unwrap();
5221            let rendered =
5222                render_document(py, &parse_template(&flow_sequence_nested_empty).unwrap()).unwrap();
5223            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5224            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5225
5226            let plain_scalar_colon_no_space =
5227                module.getattr("plain_scalar_colon_no_space").unwrap();
5228            let plain_scalar_colon_no_space =
5229                extract_template(py, &plain_scalar_colon_no_space, "yaml_t/yaml_t_str").unwrap();
5230            let rendered =
5231                render_document(py, &parse_template(&plain_scalar_colon_no_space).unwrap())
5232                    .unwrap();
5233            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5234            assert_string_entry(&documents[0], "value", "a:b");
5235
5236            let plain_question_mark_scalar = module.getattr("plain_question_mark_scalar").unwrap();
5237            let plain_question_mark_scalar =
5238                extract_template(py, &plain_question_mark_scalar, "yaml_t/yaml_t_str").unwrap();
5239            let rendered =
5240                render_document(py, &parse_template(&plain_question_mark_scalar).unwrap()).unwrap();
5241            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5242            assert_string_entry(&documents[0], "value", "?x");
5243
5244            let plain_colon_scalar_flow = module.getattr("plain_colon_scalar_flow").unwrap();
5245            let plain_colon_scalar_flow =
5246                extract_template(py, &plain_colon_scalar_flow, "yaml_t/yaml_t_str").unwrap();
5247            let rendered =
5248                render_document(py, &parse_template(&plain_colon_scalar_flow).unwrap()).unwrap();
5249            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5250            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5251            assert_eq!(yaml_sequence_len(value), Some(2));
5252
5253            let flow_mapping_colon_plain_key =
5254                module.getattr("flow_mapping_colon_plain_key").unwrap();
5255            let flow_mapping_colon_plain_key =
5256                extract_template(py, &flow_mapping_colon_plain_key, "yaml_t/yaml_t_str").unwrap();
5257            let rendered =
5258                render_document(py, &parse_template(&flow_mapping_colon_plain_key).unwrap())
5259                    .unwrap();
5260            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5261            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5262            assert_string_entry(value, "a:b", "c");
5263
5264            let flow_mapping_colon_and_hash =
5265                module.getattr("flow_mapping_colon_and_hash").unwrap();
5266            let flow_mapping_colon_and_hash =
5267                extract_template(py, &flow_mapping_colon_and_hash, "yaml_t/yaml_t_str").unwrap();
5268            let rendered =
5269                render_document(py, &parse_template(&flow_mapping_colon_and_hash).unwrap())
5270                    .unwrap();
5271            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5272            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5273            assert_string_entry(value, "a:b", "c#d");
5274
5275            let block_plain_colon_no_space = module.getattr("block_plain_colon_no_space").unwrap();
5276            let block_plain_colon_no_space =
5277                extract_template(py, &block_plain_colon_no_space, "yaml_t/yaml_t_str").unwrap();
5278            let rendered =
5279                render_document(py, &parse_template(&block_plain_colon_no_space).unwrap()).unwrap();
5280            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5281            assert_string_entry(&documents[0], "value", "a:b:c");
5282
5283            let flow_plain_colon_hash_deeper =
5284                module.getattr("flow_plain_colon_hash_deeper").unwrap();
5285            let flow_plain_colon_hash_deeper =
5286                extract_template(py, &flow_plain_colon_hash_deeper, "yaml_t/yaml_t_str").unwrap();
5287            let rendered =
5288                render_document(py, &parse_template(&flow_plain_colon_hash_deeper).unwrap())
5289                    .unwrap();
5290            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5291            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5292            match value {
5293                YamlOwned::Sequence(sequence) => {
5294                    assert_eq!(sequence.len(), 1);
5295                    assert_eq!(yaml_scalar_text(&sequence[0]), Some("a:b:c#d"));
5296                }
5297                _ => panic!("expected YAML sequence"),
5298            }
5299
5300            let alias_in_flow_mapping_value =
5301                module.getattr("alias_in_flow_mapping_value").unwrap();
5302            let alias_in_flow_mapping_value =
5303                extract_template(py, &alias_in_flow_mapping_value, "yaml_t/yaml_t_str").unwrap();
5304            let rendered =
5305                render_document(py, &parse_template(&alias_in_flow_mapping_value).unwrap())
5306                    .unwrap();
5307            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5308            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5309            let reference = yaml_mapping_entry(value, "ref").expect("expected ref");
5310            assert_integer_entry(reference, "x", 1);
5311
5312            let flow_null_and_alias = module.getattr("flow_null_and_alias").unwrap();
5313            let flow_null_and_alias =
5314                extract_template(py, &flow_null_and_alias, "yaml_t/yaml_t_str").unwrap();
5315            let rendered =
5316                render_document(py, &parse_template(&flow_null_and_alias).unwrap()).unwrap();
5317            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5318            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5319            let null_value = yaml_mapping(value)
5320                .and_then(|entries| {
5321                    entries
5322                        .iter()
5323                        .find_map(|(key, value)| key.is_null().then_some(value))
5324                })
5325                .expect("expected null key");
5326            assert_integer_entry(null_value, "x", 1);
5327
5328            let alias_seq_value = module.getattr("alias_seq_value").unwrap();
5329            let alias_seq_value =
5330                extract_template(py, &alias_seq_value, "yaml_t/yaml_t_str").unwrap();
5331            let rendered = render_document(py, &parse_template(&alias_seq_value).unwrap()).unwrap();
5332            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5333            assert_eq!(
5334                yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("expected key a")),
5335                Some(2)
5336            );
5337            assert_eq!(
5338                yaml_sequence_len(yaml_mapping_entry(&documents[0], "b").expect("expected key b")),
5339                Some(2)
5340            );
5341
5342            let flow_mapping_missing_value = module.getattr("flow_mapping_missing_value").unwrap();
5343            let flow_mapping_missing_value =
5344                extract_template(py, &flow_mapping_missing_value, "yaml_t/yaml_t_str").unwrap();
5345            let rendered =
5346                render_document(py, &parse_template(&flow_mapping_missing_value).unwrap()).unwrap();
5347            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5348            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5349            let a = yaml_mapping_entry(value, "a").expect("expected key a");
5350            assert!(a.is_null());
5351
5352            let flow_seq_missing_value_before_end =
5353                module.getattr("flow_seq_missing_value_before_end").unwrap();
5354            let flow_seq_missing_value_before_end =
5355                extract_template(py, &flow_seq_missing_value_before_end, "yaml_t/yaml_t_str")
5356                    .unwrap();
5357            let rendered = render_document(
5358                py,
5359                &parse_template(&flow_seq_missing_value_before_end).unwrap(),
5360            )
5361            .unwrap();
5362            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5363            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5364            assert_eq!(yaml_sequence_len(value), Some(2));
5365
5366            let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5367            let flow_alias_map =
5368                extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5369            let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5370            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5371            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5372            assert_integer_entry(value, "left", 1);
5373            assert_integer_entry(value, "right", 1);
5374
5375            let flow_alias_seq = module.getattr("flow_alias_seq").unwrap();
5376            let flow_alias_seq =
5377                extract_template(py, &flow_alias_seq, "yaml_t/yaml_t_str").unwrap();
5378            let rendered = render_document(py, &parse_template(&flow_alias_seq).unwrap()).unwrap();
5379            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5380            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5381            assert_eq!(yaml_sequence_len(value), Some(2));
5382
5383            let flow_merge = module.getattr("flow_merge").unwrap();
5384            let flow_merge = extract_template(py, &flow_merge, "yaml_t/yaml_t_str").unwrap();
5385            let rendered = render_document(py, &parse_template(&flow_merge).unwrap()).unwrap();
5386            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5387            assert!(rendered.text.contains("<<: &base"));
5388            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5389            assert_integer_entry(value, "b", 2);
5390
5391            let template = module.getattr("nested_flow_alias_merge").unwrap();
5392            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5393            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5394            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5395            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5396            let sequence = match value {
5397                YamlOwned::Sequence(sequence) => sequence,
5398                _ => panic!("expected YAML sequence"),
5399            };
5400            assert_eq!(sequence.len(), 2);
5401            assert_integer_entry(&sequence[0], "b", 2);
5402            assert_integer_entry(&sequence[1], "a", 1);
5403
5404            let explicit_seq = module.getattr("explicit_seq").unwrap();
5405            let explicit_seq = extract_template(py, &explicit_seq, "yaml_t/yaml_t_str").unwrap();
5406            let rendered = render_document(py, &parse_template(&explicit_seq).unwrap()).unwrap();
5407            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5408            let mapping = documents[0].as_mapping().expect("expected YAML mapping");
5409            let value = mapping
5410                .iter()
5411                .find_map(|(key, value)| (yaml_scalar_text(key) == Some("a")).then_some(value))
5412                .expect("expected key a");
5413            assert_eq!(yaml_sequence_len(value), Some(2));
5414
5415            let indented_block = module.getattr("indented_block").unwrap();
5416            let indented_block =
5417                extract_template(py, &indented_block, "yaml_t/yaml_t_str").unwrap();
5418            let rendered = render_document(py, &parse_template(&indented_block).unwrap()).unwrap();
5419            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5420            assert_string_entry(&documents[0], "value", "a\nb\n");
5421            assert_eq!(rendered.text, "value: |1\n a\n b\n");
5422        });
5423    }
5424
5425    #[test]
5426    fn rejects_parse_render_and_validation_contracts() {
5427        Python::with_gil(|py| {
5428            let module = PyModule::from_code(
5429                py,
5430                pyo3::ffi::c_str!(
5431                    "from string.templatelib import Template\nclass BadStringValue:\n    def __str__(self):\n        raise ValueError('cannot stringify')\nbad=BadStringValue()\ntag='bad tag'\nparse_open=t'value: [1, 2'\nparse_tab=Template('a:\\t1\\n')\nparse_nested_tab=Template('a:\\n  b:\\n\\t- 1\\n')\nparse_trailing=Template('value: *not alias\\n')\nparse_empty_flow=Template('[1,,2]\\n')\nparse_trailing_entry=Template('value: [1, 2,,]\\n')\nparse_empty_mapping=Template('value: {,}\\n')\nparse_missing_colon=Template('value: {a b}\\n')\nparse_extra_comma=Template('value: {a: 1,, b: 2}\\n')\nunknown_anchor=Template('value: *not_alias\\n')\ncross_doc_anchor=Template('--- &a\\n- 1\\n- 2\\n---\\n*a\\n')\nfragment_template=t'label: \"hi-{bad}\"'\nmetadata_template=t'value: !{tag} ok'\nfloat_template=t'value: {float(\"inf\")}'\n"
5432                ),
5433                pyo3::ffi::c_str!("test_yaml_error_contracts.py"),
5434                pyo3::ffi::c_str!("test_yaml_error_contracts"),
5435            )
5436            .unwrap();
5437
5438            for (name, expected) in [
5439                ("parse_open", "Expected"),
5440                ("parse_tab", "Tabs are not allowed"),
5441                ("parse_nested_tab", "Tabs are not allowed"),
5442                ("parse_trailing", "Unexpected trailing YAML content"),
5443                ("parse_empty_flow", "Expected a YAML value"),
5444                ("parse_trailing_entry", "Expected"),
5445                ("parse_empty_mapping", "Expected ':' in YAML template"),
5446                ("parse_missing_colon", "Expected ':' in YAML template"),
5447                ("parse_extra_comma", "Expected ':' in YAML template"),
5448            ] {
5449                let template = module.getattr(name).unwrap();
5450                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5451                let err = parse_template(&template).expect_err("expected YAML parse failure");
5452                assert_eq!(err.kind, ErrorKind::Parse);
5453                assert!(err.message.contains(expected), "{name}: {}", err.message);
5454            }
5455
5456            let unknown_anchor = module.getattr("unknown_anchor").unwrap();
5457            let unknown_anchor =
5458                extract_template(py, &unknown_anchor, "yaml_t/yaml_t_str").unwrap();
5459            let rendered = render_document(py, &parse_template(&unknown_anchor).unwrap()).unwrap();
5460            let err = parse_rendered_yaml(&rendered.text)
5461                .expect_err("expected YAML unknown-anchor parse failure");
5462            assert_eq!(err.kind, ErrorKind::Parse);
5463            assert!(err.message.contains("unknown anchor"));
5464
5465            let cross_doc_anchor = module.getattr("cross_doc_anchor").unwrap();
5466            let cross_doc_anchor =
5467                extract_template(py, &cross_doc_anchor, "yaml_t/yaml_t_str").unwrap();
5468            let rendered =
5469                render_document(py, &parse_template(&cross_doc_anchor).unwrap()).unwrap();
5470            let err = parse_rendered_yaml(&rendered.text)
5471                .expect_err("expected YAML cross-document anchor parse failure");
5472            assert_eq!(err.kind, ErrorKind::Parse);
5473            assert!(err.message.contains("unknown anchor"));
5474
5475            for (name, expected) in [
5476                ("fragment_template", "fragment"),
5477                ("metadata_template", "metadata"),
5478                ("float_template", "non-finite float"),
5479            ] {
5480                let template = module.getattr(name).unwrap();
5481                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5482                let document = parse_template(&template).unwrap();
5483                let err = match render_document(py, &document) {
5484                    Ok(_) => panic!("expected YAML render failure"),
5485                    Err(err) => err,
5486                };
5487                assert_eq!(err.kind, ErrorKind::Unrepresentable);
5488                assert!(err.message.contains(expected), "{name}: {}", err.message);
5489            }
5490        });
5491    }
5492
5493    #[test]
5494    fn renders_custom_tag_stream_and_complex_key_text() {
5495        Python::with_gil(|py| {
5496            let module = PyModule::from_code(
5497                py,
5498                pyo3::ffi::c_str!(
5499                    "from string.templatelib import Template\ncomment_only_tail_stream=Template('---\\na: 1\\n--- # comment\\n...\\n')\ncustom_tag_sequence=Template('value: !custom [1, 2]\\n')\nflow_alias_map=Template('value: {left: &a 1, right: *a}\\n')\n"
5500                ),
5501                pyo3::ffi::c_str!("test_yaml_render_streams_and_keys.py"),
5502                pyo3::ffi::c_str!("test_yaml_render_streams_and_keys"),
5503            )
5504            .unwrap();
5505
5506            let comment_only_tail_stream = module.getattr("comment_only_tail_stream").unwrap();
5507            let comment_only_tail_stream =
5508                extract_template(py, &comment_only_tail_stream, "yaml_t/yaml_t_str").unwrap();
5509            let rendered =
5510                render_document(py, &parse_template(&comment_only_tail_stream).unwrap()).unwrap();
5511            assert_eq!(rendered.text, "---\na: 1\n---\nnull\n...");
5512            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5513            assert_eq!(documents.len(), 2);
5514            assert_integer_entry(&documents[0], "a", 1);
5515            assert!(documents[1].is_null());
5516
5517            let custom_tag_sequence = module.getattr("custom_tag_sequence").unwrap();
5518            let custom_tag_sequence =
5519                extract_template(py, &custom_tag_sequence, "yaml_t/yaml_t_str").unwrap();
5520            let rendered =
5521                render_document(py, &parse_template(&custom_tag_sequence).unwrap()).unwrap();
5522            assert_eq!(rendered.text, "value: !custom [ 1, 2 ]");
5523            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5524            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5525            assert_eq!(yaml_sequence_len(value), Some(2));
5526
5527            let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5528            let flow_alias_map =
5529                extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5530            let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5531            assert_eq!(rendered.text, "value: { left: &a 1, right: *a }");
5532            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5533            let value = yaml_mapping_entry(&documents[0], "value").expect("expected value key");
5534            assert_integer_entry(value, "left", 1);
5535            assert_integer_entry(value, "right", 1);
5536        });
5537    }
5538
5539    #[test]
5540    fn renders_custom_tag_scalar_mapping_and_root_sequence_shapes() {
5541        Python::with_gil(|py| {
5542            let module = PyModule::from_code(
5543                py,
5544                pyo3::ffi::c_str!(
5545                    "scalar_root=t'!custom 3\\n'\nmapping=t'value: !custom 3\\n'\nsequence=t'value: !custom [1, 2]\\n'\ncommented_root_sequence=t'--- # comment\\n!custom [1, 2]\\n'\n"
5546                ),
5547                pyo3::ffi::c_str!("test_yaml_custom_tag_shapes.py"),
5548                pyo3::ffi::c_str!("test_yaml_custom_tag_shapes"),
5549            )
5550            .unwrap();
5551
5552            let scalar_root = module.getattr("scalar_root").unwrap();
5553            let scalar_root = extract_template(py, &scalar_root, "yaml_t/yaml_t_str").unwrap();
5554            let rendered = render_document(py, &parse_template(&scalar_root).unwrap()).unwrap();
5555            assert_eq!(rendered.text, "!custom 3");
5556            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5557            assert_eq!(yaml_integer(&documents[0]), Some(3));
5558
5559            let mapping = module.getattr("mapping").unwrap();
5560            let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
5561            let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
5562            assert_eq!(rendered.text, "value: !custom 3");
5563            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5564            assert_integer_entry(&documents[0], "value", 3);
5565
5566            let sequence = module.getattr("sequence").unwrap();
5567            let sequence = extract_template(py, &sequence, "yaml_t/yaml_t_str").unwrap();
5568            let rendered = render_document(py, &parse_template(&sequence).unwrap()).unwrap();
5569            assert_eq!(rendered.text, "value: !custom [ 1, 2 ]");
5570            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5571            assert_eq!(
5572                yaml_sequence_len(yaml_mapping_entry(&documents[0], "value").expect("value key")),
5573                Some(2)
5574            );
5575
5576            let commented_root_sequence = module.getattr("commented_root_sequence").unwrap();
5577            let commented_root_sequence =
5578                extract_template(py, &commented_root_sequence, "yaml_t/yaml_t_str").unwrap();
5579            let rendered =
5580                render_document(py, &parse_template(&commented_root_sequence).unwrap()).unwrap();
5581            assert_eq!(rendered.text, "---\n!custom [ 1, 2 ]");
5582            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5583            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5584        });
5585    }
5586
5587    #[test]
5588    fn renders_core_schema_scalars_and_top_level_sequence() {
5589        Python::with_gil(|py| {
5590            let module = PyModule::from_code(
5591                py,
5592                pyo3::ffi::c_str!(
5593                    "from string.templatelib import Template\ncore_scalars=Template('value: true\\nnone: null\\nlegacy_bool: on\\nlegacy_yes: yes\\n')\ntop_level_sequence=Template('- 1\\n- true\\n- null\\n- on\\n')\n"
5594                ),
5595                pyo3::ffi::c_str!("test_yaml_core_scalars_and_sequence.py"),
5596                pyo3::ffi::c_str!("test_yaml_core_scalars_and_sequence"),
5597            )
5598            .unwrap();
5599
5600            let core_scalars = module.getattr("core_scalars").unwrap();
5601            let core_scalars = extract_template(py, &core_scalars, "yaml_t/yaml_t_str").unwrap();
5602            let rendered = render_document(py, &parse_template(&core_scalars).unwrap()).unwrap();
5603            assert_eq!(
5604                rendered.text,
5605                "value: true\nnone: null\nlegacy_bool: on\nlegacy_yes: yes"
5606            );
5607            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5608            assert_eq!(documents.len(), 1);
5609            assert_eq!(
5610                yaml_mapping_entry(&documents[0], "value").and_then(YamlOwned::as_bool),
5611                Some(true)
5612            );
5613            assert!(
5614                yaml_mapping_entry(&documents[0], "none")
5615                    .expect("none key")
5616                    .is_null()
5617            );
5618            assert_string_entry(&documents[0], "legacy_bool", "on");
5619            assert_string_entry(&documents[0], "legacy_yes", "yes");
5620
5621            let top_level_sequence = module.getattr("top_level_sequence").unwrap();
5622            let top_level_sequence =
5623                extract_template(py, &top_level_sequence, "yaml_t/yaml_t_str").unwrap();
5624            let rendered =
5625                render_document(py, &parse_template(&top_level_sequence).unwrap()).unwrap();
5626            assert_eq!(rendered.text, "- 1\n- true\n- null\n- on");
5627            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5628            assert_eq!(documents.len(), 1);
5629            let sequence = documents[0].as_vec().expect("expected top-level sequence");
5630            assert_eq!(sequence.len(), 4);
5631            assert_eq!(yaml_integer(&sequence[0]), Some(1));
5632            assert_eq!(sequence[1].as_bool(), Some(true));
5633            assert!(sequence[2].is_null());
5634            assert_eq!(yaml_scalar_text(&sequence[3]), Some("on"));
5635        });
5636    }
5637
5638    #[test]
5639    fn renders_end_to_end_supported_positions_text_and_data() {
5640        Python::with_gil(|py| {
5641            let module = PyModule::from_code(
5642                py,
5643                pyo3::ffi::c_str!(
5644                    "user='Alice'\nkey='owner'\nanchor='item'\ntag='str'\ntemplate=t'''\n{key}: {user}\nlabel: \"prefix-{user}\"\nplain: item-{user}\nitems:\n  - &{anchor} {user}\n  - *{anchor}\ntagged: !{tag} {user}\nflow: [{user}, {{label: {user}}}]\n'''\n"
5645                ),
5646                pyo3::ffi::c_str!("test_yaml_end_to_end_positions.py"),
5647                pyo3::ffi::c_str!("test_yaml_end_to_end_positions"),
5648            )
5649            .unwrap();
5650
5651            let template = module.getattr("template").unwrap();
5652            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5653            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5654            assert_eq!(
5655                rendered.text,
5656                "\"owner\": \"Alice\"\nlabel: \"prefix-Alice\"\nplain: item-Alice\nitems:\n  - &item \"Alice\"\n  - *item\ntagged: !str \"Alice\"\nflow: [ \"Alice\", { label: \"Alice\" } ]"
5657            );
5658            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5659            assert_eq!(documents.len(), 1);
5660            assert_string_entry(&documents[0], "owner", "Alice");
5661            assert_string_entry(&documents[0], "label", "prefix-Alice");
5662            assert_string_entry(&documents[0], "plain", "item-Alice");
5663            assert_string_entry(&documents[0], "tagged", "Alice");
5664            let items = yaml_mapping_entry(&documents[0], "items").expect("items key");
5665            let items = items.as_vec().expect("items sequence");
5666            assert_eq!(items.len(), 2);
5667            assert_eq!(yaml_scalar_text(&items[0]), Some("Alice"));
5668            assert_eq!(yaml_scalar_text(&items[1]), Some("Alice"));
5669            let flow = yaml_mapping_entry(&documents[0], "flow").expect("flow key");
5670            let flow = flow.as_vec().expect("flow sequence");
5671            assert_eq!(yaml_scalar_text(&flow[0]), Some("Alice"));
5672            assert_string_entry(&flow[1], "label", "Alice");
5673        });
5674    }
5675
5676    #[test]
5677    fn renders_block_scalars_and_sequence_item_text_and_data() {
5678        Python::with_gil(|py| {
5679            let module = PyModule::from_code(
5680                py,
5681                pyo3::ffi::c_str!(
5682                    "user='Alice'\ntemplate=t'''\nliteral: |\n  hello {user}\n  world\nfolded: >\n  hello {user}\n  world\nlines:\n  - |\n      item {user}\n'''\n"
5683                ),
5684                pyo3::ffi::c_str!("test_yaml_block_scalars_render.py"),
5685                pyo3::ffi::c_str!("test_yaml_block_scalars_render"),
5686            )
5687            .unwrap();
5688
5689            let template = module.getattr("template").unwrap();
5690            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5691            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5692            assert_eq!(
5693                rendered.text,
5694                "literal: |\n  hello Alice\n  world\nfolded: >\n  hello Alice\n  world\nlines:\n  - |\n    item Alice\n"
5695            );
5696            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5697            assert_eq!(documents.len(), 1);
5698            assert_string_entry(&documents[0], "literal", "hello Alice\nworld\n");
5699            assert_string_entry(&documents[0], "folded", "hello Alice world\n");
5700            let lines = yaml_mapping_entry(&documents[0], "lines").expect("lines key");
5701            let lines = lines.as_vec().expect("lines sequence");
5702            assert_eq!(yaml_scalar_text(&lines[0]), Some("item Alice\n"));
5703        });
5704    }
5705
5706    #[test]
5707    fn renders_comment_only_document_variants_and_mid_stream_shapes() {
5708        Python::with_gil(|py| {
5709            let module = PyModule::from_code(
5710                py,
5711                pyo3::ffi::c_str!(
5712                    "comment_only=t'# comment\\n'\ncomment_only_explicit=t'--- # comment\\n'\ncomment_only_explicit_end=t'--- # comment\\n...\\n'\ncomment_only_mid_stream=t'---\\na: 1\\n--- # comment\\n...\\n---\\nb: 2\\n'\n"
5713                ),
5714                pyo3::ffi::c_str!("test_yaml_comment_variants.py"),
5715                pyo3::ffi::c_str!("test_yaml_comment_variants"),
5716            )
5717            .unwrap();
5718
5719            for (name, expected_text) in [
5720                ("comment_only", "null"),
5721                ("comment_only_explicit", "---\nnull"),
5722                ("comment_only_explicit_end", "---\nnull\n..."),
5723            ] {
5724                let template = module.getattr(name).unwrap();
5725                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5726                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5727                assert_eq!(rendered.text, expected_text);
5728                let documents = parse_rendered_yaml(&rendered.text).unwrap();
5729                assert_eq!(documents.len(), 1);
5730                assert!(documents[0].is_null());
5731            }
5732
5733            let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
5734            let comment_only_mid_stream =
5735                extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
5736            let rendered =
5737                render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
5738            assert_eq!(rendered.text, "---\na: 1\n---\nnull\n...\n---\nb: 2");
5739            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5740            assert_eq!(documents.len(), 3);
5741            assert_integer_entry(&documents[0], "a", 1);
5742            assert!(documents[1].is_null());
5743            assert_integer_entry(&documents[2], "b", 2);
5744        });
5745    }
5746
5747    #[test]
5748    fn renders_tag_directives_and_handle_tag_roots() {
5749        Python::with_gil(|py| {
5750            let module = PyModule::from_code(
5751                py,
5752                pyo3::ffi::c_str!(
5753                    "tag_directive_scalar=t'%TAG !e! tag:example.com,2020:\\n---\\nvalue: !e!foo 1\\n'\ntag_directive_root=t'%YAML 1.2\\n%TAG !e! tag:example.com,2020:\\n---\\n!e!root {{value: !e!leaf 1}}\\n'\ntag_directive_root_comment=t'%YAML 1.2\\n%TAG !e! tag:example.com,2020:\\n--- # comment\\n!e!root {{value: !e!leaf 1}}\\n'\nverbatim_root_mapping=t'--- !<tag:yaml.org,2002:map>\\na: 1\\n'\nverbatim_root_sequence=t'--- !<tag:yaml.org,2002:seq>\\n- 1\\n- 2\\n'\n"
5754                ),
5755                pyo3::ffi::c_str!("test_yaml_tag_directives.py"),
5756                pyo3::ffi::c_str!("test_yaml_tag_directives"),
5757            )
5758            .unwrap();
5759
5760            let tag_directive_scalar = module.getattr("tag_directive_scalar").unwrap();
5761            let tag_directive_scalar =
5762                extract_template(py, &tag_directive_scalar, "yaml_t/yaml_t_str").unwrap();
5763            let rendered =
5764                render_document(py, &parse_template(&tag_directive_scalar).unwrap()).unwrap();
5765            assert_eq!(
5766                rendered.text,
5767                "%TAG !e! tag:example.com,2020:\n---\nvalue: !e!foo 1"
5768            );
5769            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5770            assert_integer_entry(&documents[0], "value", 1);
5771
5772            let tag_directive_root = module.getattr("tag_directive_root").unwrap();
5773            let tag_directive_root =
5774                extract_template(py, &tag_directive_root, "yaml_t/yaml_t_str").unwrap();
5775            let rendered =
5776                render_document(py, &parse_template(&tag_directive_root).unwrap()).unwrap();
5777            assert_eq!(
5778                rendered.text,
5779                "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
5780            );
5781            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5782            assert_integer_entry(&documents[0], "value", 1);
5783
5784            let tag_directive_root_comment = module.getattr("tag_directive_root_comment").unwrap();
5785            let tag_directive_root_comment =
5786                extract_template(py, &tag_directive_root_comment, "yaml_t/yaml_t_str").unwrap();
5787            let rendered =
5788                render_document(py, &parse_template(&tag_directive_root_comment).unwrap()).unwrap();
5789            assert_eq!(
5790                rendered.text,
5791                "%YAML 1.2\n%TAG !e! tag:example.com,2020:\n---\n!e!root { value: !e!leaf 1 }"
5792            );
5793            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5794            assert_integer_entry(&documents[0], "value", 1);
5795
5796            let verbatim_root_mapping = module.getattr("verbatim_root_mapping").unwrap();
5797            let verbatim_root_mapping =
5798                extract_template(py, &verbatim_root_mapping, "yaml_t/yaml_t_str").unwrap();
5799            let rendered =
5800                render_document(py, &parse_template(&verbatim_root_mapping).unwrap()).unwrap();
5801            assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:map>\na: 1");
5802            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5803            assert_integer_entry(&documents[0], "a", 1);
5804
5805            let verbatim_root_sequence = module.getattr("verbatim_root_sequence").unwrap();
5806            let verbatim_root_sequence =
5807                extract_template(py, &verbatim_root_sequence, "yaml_t/yaml_t_str").unwrap();
5808            let rendered =
5809                render_document(py, &parse_template(&verbatim_root_sequence).unwrap()).unwrap();
5810            assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:seq>\n- 1\n- 2");
5811            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5812            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5813        });
5814    }
5815
5816    #[test]
5817    fn renders_explicit_core_tag_root_shapes() {
5818        Python::with_gil(|py| {
5819            let module = PyModule::from_code(
5820                py,
5821                pyo3::ffi::c_str!(
5822                    "root_bool=t'--- !!bool true\\n'\nroot_str=t'--- !!str true\\n'\nroot_int=t'--- !!int 3\\n'\n"
5823                ),
5824                pyo3::ffi::c_str!("test_yaml_core_tag_roots.py"),
5825                pyo3::ffi::c_str!("test_yaml_core_tag_roots"),
5826            )
5827            .unwrap();
5828
5829            for (name, expected_text) in [
5830                ("root_bool", "---\n!!bool true"),
5831                ("root_str", "---\n!!str true"),
5832                ("root_int", "---\n!!int 3"),
5833            ] {
5834                let template = module.getattr(name).unwrap();
5835                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5836                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5837                assert_eq!(rendered.text, expected_text);
5838            }
5839
5840            let root_bool = module.getattr("root_bool").unwrap();
5841            let root_bool = extract_template(py, &root_bool, "yaml_t/yaml_t_str").unwrap();
5842            let rendered = render_document(py, &parse_template(&root_bool).unwrap()).unwrap();
5843            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5844            assert_eq!(documents[0].as_bool(), Some(true));
5845
5846            let root_str = module.getattr("root_str").unwrap();
5847            let root_str = extract_template(py, &root_str, "yaml_t/yaml_t_str").unwrap();
5848            let rendered = render_document(py, &parse_template(&root_str).unwrap()).unwrap();
5849            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5850            assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
5851
5852            let root_int = module.getattr("root_int").unwrap();
5853            let root_int = extract_template(py, &root_int, "yaml_t/yaml_t_str").unwrap();
5854            let rendered = render_document(py, &parse_template(&root_int).unwrap()).unwrap();
5855            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5856            assert_eq!(yaml_integer(&documents[0]), Some(3));
5857        });
5858    }
5859
5860    #[test]
5861    fn renders_explicit_core_tag_mapping_and_root_text_families() {
5862        Python::with_gil(|py| {
5863            let module = PyModule::from_code(
5864                py,
5865                pyo3::ffi::c_str!(
5866                    "mapping=t'value_bool: !!bool true\\nvalue_str: !!str true\\nvalue_float: !!float 1\\nvalue_null: !!null null\\n'\nroot_int=t'--- !!int 3\\n'\nroot_str=t'--- !!str true\\n'\nroot_bool=t'--- !!bool true\\n'\n"
5867                ),
5868                pyo3::ffi::c_str!("test_yaml_explicit_core_tag_families.py"),
5869                pyo3::ffi::c_str!("test_yaml_explicit_core_tag_families"),
5870            )
5871            .unwrap();
5872
5873            let mapping = module.getattr("mapping").unwrap();
5874            let mapping = extract_template(py, &mapping, "yaml_t/yaml_t_str").unwrap();
5875            let rendered = render_document(py, &parse_template(&mapping).unwrap()).unwrap();
5876            assert_eq!(
5877                rendered.text,
5878                "value_bool: !!bool true\nvalue_str: !!str true\nvalue_float: !!float 1\nvalue_null: !!null null"
5879            );
5880            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5881            assert_eq!(
5882                yaml_mapping_entry(&documents[0], "value_bool").and_then(YamlOwned::as_bool),
5883                Some(true)
5884            );
5885            assert_eq!(
5886                yaml_scalar_text(
5887                    yaml_mapping_entry(&documents[0], "value_str").expect("value_str")
5888                ),
5889                Some("true")
5890            );
5891            assert_eq!(
5892                yaml_mapping_entry(&documents[0], "value_float").and_then(yaml_float),
5893                Some(1.0)
5894            );
5895            assert!(
5896                yaml_mapping_entry(&documents[0], "value_null")
5897                    .expect("value_null")
5898                    .is_null()
5899            );
5900
5901            for (name, expected_text) in [
5902                ("root_int", "---\n!!int 3"),
5903                ("root_str", "---\n!!str true"),
5904                ("root_bool", "---\n!!bool true"),
5905            ] {
5906                let template = module.getattr(name).unwrap();
5907                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
5908                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
5909                assert_eq!(rendered.text, expected_text, "{name}");
5910            }
5911        });
5912    }
5913
5914    #[test]
5915    fn renders_flow_trailing_comma_explicit_key_and_indent_indicator_families() {
5916        Python::with_gil(|py| {
5917            let module = PyModule::from_code(
5918                py,
5919                pyo3::ffi::c_str!(
5920                    "from string.templatelib import Template\nflow_sequence=t'[1, 2,]\\n'\nflow_mapping=Template('{a: 1,}\\n')\nexplicit_key_sequence_value=t'? a\\n: - 1\\n  - 2\\n'\nindent_indicator=t'value: |1\\n a\\n b\\n'\n"
5921                ),
5922                pyo3::ffi::c_str!("test_yaml_flow_indent_families.py"),
5923                pyo3::ffi::c_str!("test_yaml_flow_indent_families"),
5924            )
5925            .unwrap();
5926
5927            let flow_sequence = module.getattr("flow_sequence").unwrap();
5928            let flow_sequence = extract_template(py, &flow_sequence, "yaml_t/yaml_t_str").unwrap();
5929            let rendered = render_document(py, &parse_template(&flow_sequence).unwrap()).unwrap();
5930            assert_eq!(rendered.text, "[ 1, 2 ]");
5931            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5932            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
5933
5934            let flow_mapping = module.getattr("flow_mapping").unwrap();
5935            let flow_mapping = extract_template(py, &flow_mapping, "yaml_t/yaml_t_str").unwrap();
5936            let rendered = render_document(py, &parse_template(&flow_mapping).unwrap()).unwrap();
5937            assert_eq!(rendered.text, "{ a: 1 }");
5938            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5939            assert_integer_entry(&documents[0], "a", 1);
5940
5941            let explicit_key_sequence_value =
5942                module.getattr("explicit_key_sequence_value").unwrap();
5943            let explicit_key_sequence_value =
5944                extract_template(py, &explicit_key_sequence_value, "yaml_t/yaml_t_str").unwrap();
5945            let rendered =
5946                render_document(py, &parse_template(&explicit_key_sequence_value).unwrap())
5947                    .unwrap();
5948            assert_eq!(rendered.text, "? a\n:\n  - 1\n  - 2");
5949            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5950            let entry = documents[0].as_mapping().expect("mapping");
5951            assert_eq!(entry.len(), 1);
5952
5953            let indent_indicator = module.getattr("indent_indicator").unwrap();
5954            let indent_indicator =
5955                extract_template(py, &indent_indicator, "yaml_t/yaml_t_str").unwrap();
5956            let rendered =
5957                render_document(py, &parse_template(&indent_indicator).unwrap()).unwrap();
5958            assert_eq!(rendered.text, "value: |1\n a\n b\n");
5959            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5960            assert_string_entry(&documents[0], "value", "a\nb\n");
5961        });
5962    }
5963
5964    #[test]
5965    fn renders_flow_alias_and_merge_shapes() {
5966        Python::with_gil(|py| {
5967            let module = PyModule::from_code(
5968                py,
5969                pyo3::ffi::c_str!(
5970                    "from string.templatelib import Template\nflow_alias_map=Template('value: {left: &a 1, right: *a}\\n')\nflow_alias_seq=Template('value: [&a 1, *a]\\n')\nflow_merge=Template('value: {<<: &base {a: 1}, b: 2}\\n')\n"
5971                ),
5972                pyo3::ffi::c_str!("test_yaml_flow_alias_merge.py"),
5973                pyo3::ffi::c_str!("test_yaml_flow_alias_merge"),
5974            )
5975            .unwrap();
5976
5977            let flow_alias_map = module.getattr("flow_alias_map").unwrap();
5978            let flow_alias_map =
5979                extract_template(py, &flow_alias_map, "yaml_t/yaml_t_str").unwrap();
5980            let rendered = render_document(py, &parse_template(&flow_alias_map).unwrap()).unwrap();
5981            assert_eq!(rendered.text, "value: { left: &a 1, right: *a }");
5982            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5983            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5984            assert_integer_entry(value, "left", 1);
5985            assert_integer_entry(value, "right", 1);
5986
5987            let flow_alias_seq = module.getattr("flow_alias_seq").unwrap();
5988            let flow_alias_seq =
5989                extract_template(py, &flow_alias_seq, "yaml_t/yaml_t_str").unwrap();
5990            let rendered = render_document(py, &parse_template(&flow_alias_seq).unwrap()).unwrap();
5991            assert_eq!(rendered.text, "value: [ &a 1, *a ]");
5992            let documents = parse_rendered_yaml(&rendered.text).unwrap();
5993            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
5994            let value = value.as_vec().expect("value seq");
5995            assert_eq!(yaml_integer(&value[0]), Some(1));
5996            assert_eq!(yaml_integer(&value[1]), Some(1));
5997
5998            let flow_merge = module.getattr("flow_merge").unwrap();
5999            let flow_merge = extract_template(py, &flow_merge, "yaml_t/yaml_t_str").unwrap();
6000            let rendered = render_document(py, &parse_template(&flow_merge).unwrap()).unwrap();
6001            assert_eq!(rendered.text, "value: { <<: &base { a: 1 }, b: 2 }");
6002            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6003            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6004            assert_integer_entry(value, "b", 2);
6005            assert!(
6006                yaml_mapping_entry(value, "a").is_some()
6007                    || yaml_mapping_entry(value, "<<").is_some()
6008            );
6009        });
6010    }
6011
6012    #[test]
6013    fn renders_document_stream_and_root_decorator_shapes() {
6014        Python::with_gil(|py| {
6015            let module = PyModule::from_code(
6016                py,
6017                pyo3::ffi::c_str!(
6018                    "from string.templatelib import Template\ncomment_only_explicit_end_document=Template('--- # comment\\n...\\n')\ncomment_only_explicit_end_stream=Template('--- # comment\\n...\\n---\\na: 1\\n')\ncomment_only_mid_stream=Template('---\\na: 1\\n--- # comment\\n...\\n---\\nb: 2\\n')\nexplicit_end_comment_stream=Template('---\\na: 1\\n... # end\\n---\\nb: 2\\n')\ndoc_start_comment=Template('--- # comment\\nvalue: 1\\n')\ndoc_start_tag_comment=Template('--- !!str true # comment\\n')\ntagged_block_root_mapping=Template('--- !!map\\na: 1\\n')\ntagged_block_root_sequence=Template('--- !!seq\\n- 1\\n- 2\\n')\nroot_anchor_sequence=Template('--- &root\\n  - 1\\n  - 2\\n')\nroot_anchor_custom_mapping=Template('--- &root !custom\\n  a: 1\\n')\nroot_custom_anchor_sequence=Template('--- !custom &root\\n  - 1\\n  - 2\\n')\nflow_newline=Template('{a: 1, b: [2, 3]}\\n')\n"
6019                ),
6020                pyo3::ffi::c_str!("test_yaml_stream_root_shapes.py"),
6021                pyo3::ffi::c_str!("test_yaml_stream_root_shapes"),
6022            )
6023            .unwrap();
6024
6025            let comment_only_explicit_end_document = module
6026                .getattr("comment_only_explicit_end_document")
6027                .unwrap();
6028            let comment_only_explicit_end_document =
6029                extract_template(py, &comment_only_explicit_end_document, "yaml_t/yaml_t_str")
6030                    .unwrap();
6031            let rendered = render_document(
6032                py,
6033                &parse_template(&comment_only_explicit_end_document).unwrap(),
6034            )
6035            .unwrap();
6036            assert_eq!(rendered.text, "---\nnull\n...");
6037            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6038            assert!(matches!(documents[0], YamlOwned::Value(_)));
6039
6040            let comment_only_explicit_end_stream =
6041                module.getattr("comment_only_explicit_end_stream").unwrap();
6042            let comment_only_explicit_end_stream =
6043                extract_template(py, &comment_only_explicit_end_stream, "yaml_t/yaml_t_str")
6044                    .unwrap();
6045            let rendered = render_document(
6046                py,
6047                &parse_template(&comment_only_explicit_end_stream).unwrap(),
6048            )
6049            .unwrap();
6050            assert_eq!(rendered.text, "---\nnull\n...\n---\na: 1");
6051            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6052            assert!(matches!(documents[0], YamlOwned::Value(_)));
6053            assert_integer_entry(&documents[1], "a", 1);
6054
6055            let comment_only_mid_stream = module.getattr("comment_only_mid_stream").unwrap();
6056            let comment_only_mid_stream =
6057                extract_template(py, &comment_only_mid_stream, "yaml_t/yaml_t_str").unwrap();
6058            let rendered =
6059                render_document(py, &parse_template(&comment_only_mid_stream).unwrap()).unwrap();
6060            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6061            assert_integer_entry(&documents[0], "a", 1);
6062            assert!(matches!(documents[1], YamlOwned::Value(_)));
6063            assert_integer_entry(&documents[2], "b", 2);
6064
6065            let explicit_end_comment_stream =
6066                module.getattr("explicit_end_comment_stream").unwrap();
6067            let explicit_end_comment_stream =
6068                extract_template(py, &explicit_end_comment_stream, "yaml_t/yaml_t_str").unwrap();
6069            let rendered =
6070                render_document(py, &parse_template(&explicit_end_comment_stream).unwrap())
6071                    .unwrap();
6072            assert_eq!(rendered.text, "---\na: 1\n...\n---\nb: 2");
6073            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6074            assert_integer_entry(&documents[0], "a", 1);
6075            assert_integer_entry(&documents[1], "b", 2);
6076
6077            let doc_start_comment = module.getattr("doc_start_comment").unwrap();
6078            let doc_start_comment =
6079                extract_template(py, &doc_start_comment, "yaml_t/yaml_t_str").unwrap();
6080            let rendered =
6081                render_document(py, &parse_template(&doc_start_comment).unwrap()).unwrap();
6082            assert_eq!(rendered.text, "---\nvalue: 1");
6083            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6084            assert_integer_entry(&documents[0], "value", 1);
6085
6086            let doc_start_tag_comment = module.getattr("doc_start_tag_comment").unwrap();
6087            let doc_start_tag_comment =
6088                extract_template(py, &doc_start_tag_comment, "yaml_t/yaml_t_str").unwrap();
6089            let rendered =
6090                render_document(py, &parse_template(&doc_start_tag_comment).unwrap()).unwrap();
6091            assert_eq!(rendered.text, "---\n!!str true");
6092            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6093            assert_eq!(yaml_scalar_text(&documents[0]), Some("true"));
6094
6095            let tagged_block_root_mapping = module.getattr("tagged_block_root_mapping").unwrap();
6096            let tagged_block_root_mapping =
6097                extract_template(py, &tagged_block_root_mapping, "yaml_t/yaml_t_str").unwrap();
6098            let rendered =
6099                render_document(py, &parse_template(&tagged_block_root_mapping).unwrap()).unwrap();
6100            assert_eq!(rendered.text, "---\n!!map\na: 1");
6101            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6102            assert_integer_entry(&documents[0], "a", 1);
6103
6104            let tagged_block_root_sequence = module.getattr("tagged_block_root_sequence").unwrap();
6105            let tagged_block_root_sequence =
6106                extract_template(py, &tagged_block_root_sequence, "yaml_t/yaml_t_str").unwrap();
6107            let rendered =
6108                render_document(py, &parse_template(&tagged_block_root_sequence).unwrap()).unwrap();
6109            assert_eq!(rendered.text, "---\n!!seq\n- 1\n- 2");
6110            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6111            assert_eq!(documents[0].as_vec().expect("root seq").len(), 2);
6112
6113            let root_anchor_sequence = module.getattr("root_anchor_sequence").unwrap();
6114            let root_anchor_sequence =
6115                extract_template(py, &root_anchor_sequence, "yaml_t/yaml_t_str").unwrap();
6116            let rendered =
6117                render_document(py, &parse_template(&root_anchor_sequence).unwrap()).unwrap();
6118            assert_eq!(rendered.text, "---\n&root\n- 1\n- 2");
6119            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6120            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
6121
6122            let root_anchor_custom_mapping = module.getattr("root_anchor_custom_mapping").unwrap();
6123            let root_anchor_custom_mapping =
6124                extract_template(py, &root_anchor_custom_mapping, "yaml_t/yaml_t_str").unwrap();
6125            let rendered =
6126                render_document(py, &parse_template(&root_anchor_custom_mapping).unwrap()).unwrap();
6127            assert_eq!(rendered.text, "---\n!custom &root\na: 1");
6128            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6129            assert_integer_entry(&documents[0], "a", 1);
6130
6131            let root_custom_anchor_sequence =
6132                module.getattr("root_custom_anchor_sequence").unwrap();
6133            let root_custom_anchor_sequence =
6134                extract_template(py, &root_custom_anchor_sequence, "yaml_t/yaml_t_str").unwrap();
6135            let rendered =
6136                render_document(py, &parse_template(&root_custom_anchor_sequence).unwrap())
6137                    .unwrap();
6138            assert_eq!(rendered.text, "---\n!custom &root\n- 1\n- 2");
6139            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6140            assert_eq!(yaml_sequence_len(&documents[0]), Some(2));
6141
6142            let flow_newline = module.getattr("flow_newline").unwrap();
6143            let flow_newline = extract_template(py, &flow_newline, "yaml_t/yaml_t_str").unwrap();
6144            let rendered = render_document(py, &parse_template(&flow_newline).unwrap()).unwrap();
6145            assert_eq!(rendered.text, "{ a: 1, b: [ 2, 3 ] }");
6146            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6147            let mapping = yaml_mapping(&documents[0]).expect("expected root flow mapping");
6148            assert_eq!(mapping.len(), 2);
6149        });
6150    }
6151
6152    #[test]
6153    fn renders_merge_and_collection_shape_families() {
6154        Python::with_gil(|py| {
6155            let module = PyModule::from_code(
6156                py,
6157                pyo3::ffi::c_str!(
6158                    "from string.templatelib import Template\nmerge=Template('base: &base\\n  a: 1\\n  b: 2\\nderived:\\n  <<: *base\\n  c: 3\\n')\nflow_nested_alias_merge=Template('value: [{<<: &base {a: 1}, b: 2}, *base]\\n')\nalias_seq_value=Template('a: &x [1, 2]\\nb: *x\\n')\nempty_flow_sequence=Template('value: []\\n')\nempty_flow_mapping=Template('value: {}\\n')\nflow_mapping_missing_value=Template('value: {a: }\\n')\nflow_seq_missing_value_before_end=Template('value: [1, 2, ]\\n')\nindentless_sequence_value=Template('a:\\n- 1\\n- 2\\n')\nsequence_of_mappings=Template('- a: 1\\n  b: 2\\n- c: 3\\n')\nmapping_of_sequence_of_mappings=Template('items:\\n- a: 1\\n  b: 2\\n- c: 3\\n')\nsequence_of_sequences=Template('- - 1\\n  - 2\\n- - 3\\n')\n"
6159                ),
6160                pyo3::ffi::c_str!("test_yaml_merge_collection_shapes.py"),
6161                pyo3::ffi::c_str!("test_yaml_merge_collection_shapes"),
6162            )
6163            .unwrap();
6164
6165            let merge = module.getattr("merge").unwrap();
6166            let merge = extract_template(py, &merge, "yaml_t/yaml_t_str").unwrap();
6167            let rendered = render_document(py, &parse_template(&merge).unwrap()).unwrap();
6168            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6169            let derived = yaml_mapping_entry(&documents[0], "derived").expect("derived");
6170            assert_integer_entry(derived, "c", 3);
6171            assert!(
6172                yaml_mapping_entry(derived, "a").is_some()
6173                    || yaml_mapping_entry(derived, "<<").is_some()
6174            );
6175            assert!(
6176                yaml_mapping_entry(derived, "b").is_some()
6177                    || yaml_mapping_entry(derived, "<<").is_some()
6178            );
6179
6180            let flow_nested_alias_merge = module.getattr("flow_nested_alias_merge").unwrap();
6181            let flow_nested_alias_merge =
6182                extract_template(py, &flow_nested_alias_merge, "yaml_t/yaml_t_str").unwrap();
6183            let rendered =
6184                render_document(py, &parse_template(&flow_nested_alias_merge).unwrap()).unwrap();
6185            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6186            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6187            let value = value.as_vec().expect("value seq");
6188            assert_eq!(value.len(), 2);
6189
6190            let alias_seq_value = module.getattr("alias_seq_value").unwrap();
6191            let alias_seq_value =
6192                extract_template(py, &alias_seq_value, "yaml_t/yaml_t_str").unwrap();
6193            let rendered = render_document(py, &parse_template(&alias_seq_value).unwrap()).unwrap();
6194            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6195            assert_eq!(
6196                yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("a")),
6197                Some(2)
6198            );
6199            assert_eq!(
6200                yaml_sequence_len(yaml_mapping_entry(&documents[0], "b").expect("b")),
6201                Some(2)
6202            );
6203
6204            for (name, expected_len) in [
6205                ("empty_flow_sequence", 0usize),
6206                ("flow_seq_missing_value_before_end", 2usize),
6207            ] {
6208                let template = module.getattr(name).unwrap();
6209                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6210                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6211                let documents = parse_rendered_yaml(&rendered.text).unwrap();
6212                let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6213                assert_eq!(yaml_sequence_len(value), Some(expected_len), "{name}");
6214            }
6215
6216            let empty_flow_mapping = module.getattr("empty_flow_mapping").unwrap();
6217            let empty_flow_mapping =
6218                extract_template(py, &empty_flow_mapping, "yaml_t/yaml_t_str").unwrap();
6219            let rendered =
6220                render_document(py, &parse_template(&empty_flow_mapping).unwrap()).unwrap();
6221            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6222            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6223            assert_eq!(yaml_mapping(value).expect("mapping").len(), 0);
6224
6225            let flow_mapping_missing_value = module.getattr("flow_mapping_missing_value").unwrap();
6226            let flow_mapping_missing_value =
6227                extract_template(py, &flow_mapping_missing_value, "yaml_t/yaml_t_str").unwrap();
6228            let rendered =
6229                render_document(py, &parse_template(&flow_mapping_missing_value).unwrap()).unwrap();
6230            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6231            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6232            assert!(yaml_mapping_entry(value, "a").is_some());
6233
6234            let indentless_sequence_value = module.getattr("indentless_sequence_value").unwrap();
6235            let indentless_sequence_value =
6236                extract_template(py, &indentless_sequence_value, "yaml_t/yaml_t_str").unwrap();
6237            let rendered =
6238                render_document(py, &parse_template(&indentless_sequence_value).unwrap()).unwrap();
6239            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6240            assert_eq!(
6241                yaml_sequence_len(yaml_mapping_entry(&documents[0], "a").expect("a")),
6242                Some(2)
6243            );
6244
6245            let sequence_of_mappings = module.getattr("sequence_of_mappings").unwrap();
6246            let sequence_of_mappings =
6247                extract_template(py, &sequence_of_mappings, "yaml_t/yaml_t_str").unwrap();
6248            let rendered =
6249                render_document(py, &parse_template(&sequence_of_mappings).unwrap()).unwrap();
6250            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6251            assert_eq!(documents[0].as_vec().expect("top-level seq").len(), 2);
6252
6253            let mapping_of_sequence_of_mappings =
6254                module.getattr("mapping_of_sequence_of_mappings").unwrap();
6255            let mapping_of_sequence_of_mappings =
6256                extract_template(py, &mapping_of_sequence_of_mappings, "yaml_t/yaml_t_str")
6257                    .unwrap();
6258            let rendered = render_document(
6259                py,
6260                &parse_template(&mapping_of_sequence_of_mappings).unwrap(),
6261            )
6262            .unwrap();
6263            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6264            assert_eq!(
6265                yaml_sequence_len(yaml_mapping_entry(&documents[0], "items").expect("items")),
6266                Some(2)
6267            );
6268
6269            let sequence_of_sequences = module.getattr("sequence_of_sequences").unwrap();
6270            let sequence_of_sequences =
6271                extract_template(py, &sequence_of_sequences, "yaml_t/yaml_t_str").unwrap();
6272            let rendered =
6273                render_document(py, &parse_template(&sequence_of_sequences).unwrap()).unwrap();
6274            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6275            let top = documents[0].as_vec().expect("top-level seq");
6276            assert_eq!(top.len(), 2);
6277            assert_eq!(yaml_sequence_len(&top[0]), Some(2));
6278            assert_eq!(yaml_sequence_len(&top[1]), Some(1));
6279        });
6280    }
6281
6282    #[test]
6283    fn renders_flow_scalar_key_and_comment_edge_families() {
6284        Python::with_gil(|py| {
6285            let module = PyModule::from_code(
6286                py,
6287                pyo3::ffi::c_str!(
6288                    "from string.templatelib import Template\nflow_plain_scalar_with_space=Template('value: [1 2]\\n')\nmapping_empty_flow_values=Template('value: {a: [], b: {}}\\n')\nflow_mapping_empty_key_and_values=Template('{\"\": [], foo: {}}\\n')\nflow_null_key=Template('{null: 1, \"\": 2}\\n')\nblock_null_key=Template('? null\\n: 1\\n')\nquoted_null_key=Template('? \"\"\\n: 1\\n')\nplain_question_mark_scalar=Template('value: ?x\\n')\nplain_colon_scalar_flow=Template('value: [a:b, c:d]\\n')\nflow_mapping_plain_key_questions=Template('value: {?x: 1, ?y: 2}\\n')\nflow_hash_mapping_four=Template('value: {a: b#c, d: e#f, g: h#i, j: k#l}\\n')\nflow_hash_seq_seven=Template('value: [a#b, c#d, e#f, g#h, i#j, k#l, m#n]\\n')\ncomment_after_flow_plain_colon=Template('value: [a:b # c\\n]\\n')\nflow_plain_comment_after_colon_deeper=Template('value: [a:b:c:d # comment\\n]\\n')\n"
6289                ),
6290                pyo3::ffi::c_str!("test_yaml_flow_scalar_edge_families.py"),
6291                pyo3::ffi::c_str!("test_yaml_flow_scalar_edge_families"),
6292            )
6293            .unwrap();
6294
6295            let flow_plain_scalar_with_space =
6296                module.getattr("flow_plain_scalar_with_space").unwrap();
6297            let flow_plain_scalar_with_space =
6298                extract_template(py, &flow_plain_scalar_with_space, "yaml_t/yaml_t_str").unwrap();
6299            let rendered =
6300                render_document(py, &parse_template(&flow_plain_scalar_with_space).unwrap())
6301                    .unwrap();
6302            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6303            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6304            let value = value.as_vec().expect("value seq");
6305            assert_eq!(yaml_scalar_text(&value[0]), Some("1 2"));
6306
6307            let mapping_empty_flow_values = module.getattr("mapping_empty_flow_values").unwrap();
6308            let mapping_empty_flow_values =
6309                extract_template(py, &mapping_empty_flow_values, "yaml_t/yaml_t_str").unwrap();
6310            let rendered =
6311                render_document(py, &parse_template(&mapping_empty_flow_values).unwrap()).unwrap();
6312            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6313            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6314            assert_eq!(
6315                yaml_sequence_len(yaml_mapping_entry(value, "a").expect("a")),
6316                Some(0)
6317            );
6318            assert_eq!(
6319                yaml_mapping(yaml_mapping_entry(value, "b").expect("b"))
6320                    .expect("b mapping")
6321                    .len(),
6322                0
6323            );
6324
6325            let flow_mapping_empty_key_and_values =
6326                module.getattr("flow_mapping_empty_key_and_values").unwrap();
6327            let flow_mapping_empty_key_and_values =
6328                extract_template(py, &flow_mapping_empty_key_and_values, "yaml_t/yaml_t_str")
6329                    .unwrap();
6330            let rendered = render_document(
6331                py,
6332                &parse_template(&flow_mapping_empty_key_and_values).unwrap(),
6333            )
6334            .unwrap();
6335            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6336            assert_eq!(
6337                yaml_sequence_len(yaml_mapping_entry(&documents[0], "").expect("empty key")),
6338                Some(0)
6339            );
6340            assert_eq!(
6341                yaml_mapping(yaml_mapping_entry(&documents[0], "foo").expect("foo"))
6342                    .expect("foo mapping")
6343                    .len(),
6344                0
6345            );
6346
6347            let flow_null_key = module.getattr("flow_null_key").unwrap();
6348            let flow_null_key = extract_template(py, &flow_null_key, "yaml_t/yaml_t_str").unwrap();
6349            let rendered = render_document(py, &parse_template(&flow_null_key).unwrap()).unwrap();
6350            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6351            assert_eq!(documents[0].as_mapping().expect("mapping").len(), 2);
6352
6353            let block_null_key = module.getattr("block_null_key").unwrap();
6354            let block_null_key =
6355                extract_template(py, &block_null_key, "yaml_t/yaml_t_str").unwrap();
6356            let rendered = render_document(py, &parse_template(&block_null_key).unwrap()).unwrap();
6357            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6358            assert_eq!(documents[0].as_mapping().expect("mapping").len(), 1);
6359
6360            let quoted_null_key = module.getattr("quoted_null_key").unwrap();
6361            let quoted_null_key =
6362                extract_template(py, &quoted_null_key, "yaml_t/yaml_t_str").unwrap();
6363            let rendered = render_document(py, &parse_template(&quoted_null_key).unwrap()).unwrap();
6364            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6365            assert_eq!(documents[0].as_mapping().expect("mapping").len(), 1);
6366
6367            let plain_question_mark_scalar = module.getattr("plain_question_mark_scalar").unwrap();
6368            let plain_question_mark_scalar =
6369                extract_template(py, &plain_question_mark_scalar, "yaml_t/yaml_t_str").unwrap();
6370            let rendered =
6371                render_document(py, &parse_template(&plain_question_mark_scalar).unwrap()).unwrap();
6372            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6373            assert_string_entry(&documents[0], "value", "?x");
6374
6375            let plain_colon_scalar_flow = module.getattr("plain_colon_scalar_flow").unwrap();
6376            let plain_colon_scalar_flow =
6377                extract_template(py, &plain_colon_scalar_flow, "yaml_t/yaml_t_str").unwrap();
6378            let rendered =
6379                render_document(py, &parse_template(&plain_colon_scalar_flow).unwrap()).unwrap();
6380            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6381            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6382            let value = value.as_vec().expect("value seq");
6383            assert_eq!(yaml_scalar_text(&value[0]), Some("a:b"));
6384            assert_eq!(yaml_scalar_text(&value[1]), Some("c:d"));
6385
6386            let flow_mapping_plain_key_questions =
6387                module.getattr("flow_mapping_plain_key_questions").unwrap();
6388            let flow_mapping_plain_key_questions =
6389                extract_template(py, &flow_mapping_plain_key_questions, "yaml_t/yaml_t_str")
6390                    .unwrap();
6391            let rendered = render_document(
6392                py,
6393                &parse_template(&flow_mapping_plain_key_questions).unwrap(),
6394            )
6395            .unwrap();
6396            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6397            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6398            assert_eq!(yaml_mapping(value).expect("mapping").len(), 2);
6399
6400            let flow_hash_mapping_four = module.getattr("flow_hash_mapping_four").unwrap();
6401            let flow_hash_mapping_four =
6402                extract_template(py, &flow_hash_mapping_four, "yaml_t/yaml_t_str").unwrap();
6403            let rendered =
6404                render_document(py, &parse_template(&flow_hash_mapping_four).unwrap()).unwrap();
6405            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6406            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6407            assert_eq!(yaml_mapping(value).expect("mapping").len(), 4);
6408
6409            let flow_hash_seq_seven = module.getattr("flow_hash_seq_seven").unwrap();
6410            let flow_hash_seq_seven =
6411                extract_template(py, &flow_hash_seq_seven, "yaml_t/yaml_t_str").unwrap();
6412            let rendered =
6413                render_document(py, &parse_template(&flow_hash_seq_seven).unwrap()).unwrap();
6414            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6415            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6416            assert_eq!(yaml_sequence_len(value), Some(7));
6417
6418            let comment_after_flow_plain_colon =
6419                module.getattr("comment_after_flow_plain_colon").unwrap();
6420            let comment_after_flow_plain_colon =
6421                extract_template(py, &comment_after_flow_plain_colon, "yaml_t/yaml_t_str").unwrap();
6422            let rendered = render_document(
6423                py,
6424                &parse_template(&comment_after_flow_plain_colon).unwrap(),
6425            )
6426            .unwrap();
6427            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6428            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6429            let value = value.as_vec().expect("value seq");
6430            assert_eq!(yaml_scalar_text(&value[0]), Some("a:b"));
6431
6432            let flow_plain_comment_after_colon_deeper = module
6433                .getattr("flow_plain_comment_after_colon_deeper")
6434                .unwrap();
6435            let flow_plain_comment_after_colon_deeper = extract_template(
6436                py,
6437                &flow_plain_comment_after_colon_deeper,
6438                "yaml_t/yaml_t_str",
6439            )
6440            .unwrap();
6441            let rendered = render_document(
6442                py,
6443                &parse_template(&flow_plain_comment_after_colon_deeper).unwrap(),
6444            )
6445            .unwrap();
6446            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6447            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6448            let value = value.as_vec().expect("value seq");
6449            assert_eq!(yaml_scalar_text(&value[0]), Some("a:b:c:d"));
6450        });
6451    }
6452
6453    #[test]
6454    fn renders_flow_collection_comment_and_verbatim_tag_families() {
6455        Python::with_gil(|py| {
6456            let module = PyModule::from_code(
6457                py,
6458                pyo3::ffi::c_str!(
6459                    "from string.templatelib import Template\nverbatim_tag=Template('value: !<tag:yaml.org,2002:str> hello\\n')\nflow_wrapped_sequence=Template('key: [a,\\n  b]\\n')\nflow_wrapped_mapping=Template('key: {a: 1,\\n  b: 2}\\n')\nflow_sequence_comment=Template('key: [a, # first\\n  b]\\n')\nflow_mapping_comment=Template('key: {a: 1, # first\\n  b: 2}\\n')\nalias_in_flow_mapping_value=Template('base: &a {x: 1}\\nvalue: {ref: *a}\\n')\nflow_null_and_alias=Template('base: &a {x: 1}\\nvalue: {null: *a}\\n')\n"
6460                ),
6461                pyo3::ffi::c_str!("test_yaml_flow_collection_comment_and_tag_families.py"),
6462                pyo3::ffi::c_str!("test_yaml_flow_collection_comment_and_tag_families"),
6463            )
6464            .unwrap();
6465
6466            let verbatim_tag = module.getattr("verbatim_tag").unwrap();
6467            let verbatim_tag = extract_template(py, &verbatim_tag, "yaml_t/yaml_t_str").unwrap();
6468            let rendered = render_document(py, &parse_template(&verbatim_tag).unwrap()).unwrap();
6469            assert_eq!(rendered.text, "value: !<tag:yaml.org,2002:str> hello");
6470            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6471            assert_string_entry(&documents[0], "value", "hello");
6472
6473            for (name, expected_text) in [
6474                ("flow_wrapped_sequence", "key: [ a, b ]"),
6475                ("flow_sequence_comment", "key: [ a, b ]"),
6476            ] {
6477                let template = module.getattr(name).unwrap();
6478                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6479                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6480                assert_eq!(rendered.text, expected_text, "{name}");
6481                let documents = parse_rendered_yaml(&rendered.text).unwrap();
6482                let value = yaml_mapping_entry(&documents[0], "key").expect("sequence key");
6483                assert_eq!(yaml_sequence_len(value), Some(2), "{name}");
6484            }
6485
6486            for (name, expected_text) in [
6487                ("flow_wrapped_mapping", "key: { a: 1, b: 2 }"),
6488                ("flow_mapping_comment", "key: { a: 1, b: 2 }"),
6489            ] {
6490                let template = module.getattr(name).unwrap();
6491                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6492                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6493                assert_eq!(rendered.text, expected_text, "{name}");
6494                let documents = parse_rendered_yaml(&rendered.text).unwrap();
6495                let value = yaml_mapping_entry(&documents[0], "key").expect("mapping key");
6496                assert_eq!(yaml_mapping(value).expect("mapping").len(), 2, "{name}");
6497            }
6498
6499            let alias_in_flow_mapping_value =
6500                module.getattr("alias_in_flow_mapping_value").unwrap();
6501            let alias_in_flow_mapping_value =
6502                extract_template(py, &alias_in_flow_mapping_value, "yaml_t/yaml_t_str").unwrap();
6503            let rendered =
6504                render_document(py, &parse_template(&alias_in_flow_mapping_value).unwrap())
6505                    .unwrap();
6506            assert_eq!(rendered.text, "base: &a { x: 1 }\nvalue: { ref: *a }");
6507            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6508            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6509            let referenced = yaml_mapping_entry(value, "ref").expect("ref key");
6510            assert_integer_entry(referenced, "x", 1);
6511
6512            let flow_null_and_alias = module.getattr("flow_null_and_alias").unwrap();
6513            let flow_null_and_alias =
6514                extract_template(py, &flow_null_and_alias, "yaml_t/yaml_t_str").unwrap();
6515            let rendered =
6516                render_document(py, &parse_template(&flow_null_and_alias).unwrap()).unwrap();
6517            assert_eq!(rendered.text, "base: &a { x: 1 }\nvalue: { null: *a }");
6518            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6519            let value = yaml_mapping_entry(&documents[0], "value").expect("value key");
6520            assert_eq!(yaml_mapping(value).expect("mapping").len(), 1);
6521        });
6522    }
6523
6524    #[test]
6525    fn renders_verbatim_root_scalar_text_and_data() {
6526        Python::with_gil(|py| {
6527            let module = PyModule::from_code(
6528                py,
6529                pyo3::ffi::c_str!("template=t'--- !<tag:yaml.org,2002:str> hello\\n'\n"),
6530                pyo3::ffi::c_str!("test_yaml_verbatim_root_scalar.py"),
6531                pyo3::ffi::c_str!("test_yaml_verbatim_root_scalar"),
6532            )
6533            .unwrap();
6534
6535            let template = module.getattr("template").unwrap();
6536            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6537            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6538            assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:str> hello");
6539            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6540            assert_eq!(yaml_scalar_text(&documents[0]), Some("hello"));
6541        });
6542    }
6543
6544    #[test]
6545    fn renders_verbatim_root_anchor_scalar_text_and_data() {
6546        Python::with_gil(|py| {
6547            let module = PyModule::from_code(
6548                py,
6549                pyo3::ffi::c_str!("template=t'--- !<tag:yaml.org,2002:str> &root hello\\n'\n"),
6550                pyo3::ffi::c_str!("test_yaml_verbatim_root_anchor_scalar.py"),
6551                pyo3::ffi::c_str!("test_yaml_verbatim_root_anchor_scalar"),
6552            )
6553            .unwrap();
6554
6555            let template = module.getattr("template").unwrap();
6556            let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6557            let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6558            assert_eq!(rendered.text, "---\n!<tag:yaml.org,2002:str> &root hello");
6559            let documents = parse_rendered_yaml(&rendered.text).unwrap();
6560            assert_eq!(yaml_scalar_text(&documents[0]), Some("hello"));
6561        });
6562    }
6563
6564    #[test]
6565    fn renders_spec_chapter_2_examples_text_and_data() {
6566        Python::with_gil(|py| {
6567            let module = PyModule::from_code(
6568                py,
6569                pyo3::ffi::c_str!(
6570                    "players=t'- Mark McGwire\\n- Sammy Sosa\\n- Ken Griffey\\n'\nclubs=t'american:\\n- Boston Red Sox\\n- Detroit Tigers\\n- New York Yankees\\nnational:\\n- New York Mets\\n- Chicago Cubs\\n- Atlanta Braves\\n'\nstats_seq=t'-\\n  name: Mark McGwire\\n  hr:   65\\n  avg:  0.278\\n-\\n  name: Sammy Sosa\\n  hr:   63\\n  avg:  0.288\\n'\nmap_of_maps=t'Mark McGwire: {{hr: 65, avg: 0.278}}\\nSammy Sosa: {{\\n  hr: 63,\\n  avg: 0.288,\\n}}\\n'\ntwo_docs=t'# Ranking of 1998 home runs\\n---\\n- Mark McGwire\\n- Sammy Sosa\\n- Ken Griffey\\n\\n# Team ranking\\n---\\n- Chicago Cubs\\n- St Louis Cardinals\\n'\nplay_feed=t'---\\ntime: 20:03:20\\nplayer: Sammy Sosa\\naction: strike (miss)\\n...\\n---\\ntime: 20:03:47\\nplayer: Sammy Sosa\\naction: grand slam\\n...\\n'\n"
6571                ),
6572                pyo3::ffi::c_str!("test_yaml_spec_chapter_2_examples.py"),
6573                pyo3::ffi::c_str!("test_yaml_spec_chapter_2_examples"),
6574            )
6575            .unwrap();
6576
6577            for (name, expected_text) in [
6578                ("players", "- Mark McGwire\n- Sammy Sosa\n- Ken Griffey"),
6579                (
6580                    "clubs",
6581                    "american:\n  - Boston Red Sox\n  - Detroit Tigers\n  - New York Yankees\nnational:\n  - New York Mets\n  - Chicago Cubs\n  - Atlanta Braves",
6582                ),
6583                (
6584                    "stats_seq",
6585                    "-\n  name: Mark McGwire\n  hr: 65\n  avg: 0.278\n-\n  name: Sammy Sosa\n  hr: 63\n  avg: 0.288",
6586                ),
6587                (
6588                    "map_of_maps",
6589                    "Mark McGwire: { hr: 65, avg: 0.278 }\nSammy Sosa: { hr: 63, avg: 0.288 }",
6590                ),
6591                (
6592                    "two_docs",
6593                    "---\n- Mark McGwire\n- Sammy Sosa\n- Ken Griffey\n---\n- Chicago Cubs\n- St Louis Cardinals",
6594                ),
6595                (
6596                    "play_feed",
6597                    "---\ntime: 20:03:20\nplayer: Sammy Sosa\naction: strike (miss)\n...\n---\ntime: 20:03:47\nplayer: Sammy Sosa\naction: grand slam\n...",
6598                ),
6599            ] {
6600                let template = module.getattr(name).unwrap();
6601                let template = extract_template(py, &template, "yaml_t/yaml_t_str").unwrap();
6602                let rendered = render_document(py, &parse_template(&template).unwrap()).unwrap();
6603                assert_eq!(rendered.text, expected_text, "{name}");
6604                let documents = parse_rendered_yaml(&rendered.text).unwrap();
6605                match name {
6606                    "players" => assert_eq!(yaml_sequence_len(&documents[0]), Some(3)),
6607                    "clubs" => assert_eq!(yaml_mapping(&documents[0]).expect("clubs").len(), 2),
6608                    "stats_seq" => assert_eq!(yaml_sequence_len(&documents[0]), Some(2)),
6609                    "map_of_maps" => {
6610                        assert_eq!(yaml_mapping(&documents[0]).expect("map_of_maps").len(), 2)
6611                    }
6612                    "two_docs" => assert_eq!(documents.len(), 2),
6613                    "play_feed" => assert_eq!(documents.len(), 2),
6614                    _ => unreachable!(),
6615                }
6616            }
6617        });
6618    }
6619
6620    #[test]
6621    fn test_parse_rendered_yaml_surfaces_parse_failures() {
6622        let err =
6623            parse_rendered_yaml("value: *missing\n").expect_err("expected YAML parse failure");
6624        assert_eq!(err.kind, ErrorKind::Parse);
6625        assert!(err.message.contains("Rendered YAML could not be reparsed"));
6626    }
6627}