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