1use std::io::{self, Write};
10
11use crate::event::{Chomp, ScalarStyle};
12use crate::node::{Document, Node};
13use crate::pos::Span;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CollectionStyle {
22 Block,
24 Flow,
26}
27
28#[derive(Debug, Clone)]
30pub struct EmitConfig {
31 pub indent_width: usize,
33 pub line_width: usize,
35 pub default_scalar_style: ScalarStyle,
37 pub default_collection_style: CollectionStyle,
39}
40
41impl Default for EmitConfig {
42 fn default() -> Self {
43 Self {
44 indent_width: 2,
45 line_width: 80,
46 default_scalar_style: ScalarStyle::Plain,
47 default_collection_style: CollectionStyle::Block,
48 }
49 }
50}
51
52#[must_use]
63pub fn emit(documents: &[Document<Span>], config: &EmitConfig) -> String {
64 let mut buf = Vec::new();
65 let _: io::Result<()> = emit_to_writer(documents, config, &mut buf);
67 String::from_utf8(buf).unwrap_or_default()
69}
70
71pub fn emit_to_writer(
77 documents: &[Document<Span>],
78 config: &EmitConfig,
79 writer: &mut dyn Write,
80) -> io::Result<()> {
81 let mut emitter = Emitter { config, writer };
82 for (i, doc) in documents.iter().enumerate() {
83 emitter.emit_document(doc, i > 0)?;
84 }
85 Ok(())
86}
87
88struct Emitter<'a> {
93 config: &'a EmitConfig,
94 writer: &'a mut dyn Write,
95}
96
97impl Emitter<'_> {
98 fn emit_document(&mut self, doc: &Document<Span>, is_multi: bool) -> io::Result<()> {
99 if is_multi || doc.version.is_some() {
102 writeln!(self.writer, "---")?;
103 }
104
105 for comment in &doc.comments {
107 writeln!(self.writer, "# {comment}")?;
108 }
109
110 self.emit_node(&doc.root, 0, false)?;
111
112 writeln!(self.writer)?;
114 Ok(())
115 }
116
117 fn emit_node(&mut self, node: &Node<Span>, indent: usize, in_flow: bool) -> io::Result<()> {
118 match node {
119 Node::Scalar {
120 value,
121 style,
122 anchor,
123 tag,
124 ..
125 } => self.emit_scalar(value, *style, anchor.as_deref(), tag.as_deref()),
126 Node::Mapping {
127 entries,
128 anchor,
129 tag,
130 ..
131 } => self.emit_mapping(entries, anchor.as_deref(), tag.as_deref(), indent, in_flow),
132 Node::Sequence {
133 items, anchor, tag, ..
134 } => self.emit_sequence(items, anchor.as_deref(), tag.as_deref(), indent, in_flow),
135 Node::Alias { name, .. } => write!(self.writer, "*{name}"),
136 }
137 }
138
139 fn emit_scalar(
144 &mut self,
145 value: &str,
146 style: ScalarStyle,
147 anchor: Option<&str>,
148 tag: Option<&str>,
149 ) -> io::Result<()> {
150 if let Some(name) = anchor {
151 write!(self.writer, "&{name} ")?;
152 }
153 if let Some(t) = tag {
154 write!(self.writer, "{} ", format_tag(t))?;
155 }
156 match style {
157 ScalarStyle::Plain => {
158 let s = if needs_quoting(value) {
159 format!("'{}'", value.replace('\'', "''"))
160 } else {
161 value.to_owned()
162 };
163 write!(self.writer, "{s}")
164 }
165 ScalarStyle::SingleQuoted => {
166 write!(self.writer, "'{}'", value.replace('\'', "''"))
167 }
168 ScalarStyle::DoubleQuoted => {
169 write!(self.writer, "\"{}\"", escape_double(value))
170 }
171 ScalarStyle::Literal(chomp) => {
172 let indicator = chomp_indicator(chomp);
173 writeln!(self.writer, "|{indicator}")?;
174 for line in value.split('\n') {
175 writeln!(self.writer, " {line}")?;
176 }
177 Ok(())
178 }
179 ScalarStyle::Folded(chomp) => {
180 let indicator = chomp_indicator(chomp);
181 writeln!(self.writer, ">{indicator}")?;
182 for line in value.split('\n') {
183 writeln!(self.writer, " {line}")?;
184 }
185 Ok(())
186 }
187 }
188 }
189
190 fn emit_mapping(
195 &mut self,
196 entries: &[(Node<Span>, Node<Span>)],
197 anchor: Option<&str>,
198 tag: Option<&str>,
199 indent: usize,
200 in_flow: bool,
201 ) -> io::Result<()> {
202 let style = if in_flow {
204 CollectionStyle::Flow
205 } else {
206 self.config.default_collection_style
207 };
208
209 if let Some(name) = anchor {
211 write!(self.writer, "&{name} ")?;
212 }
213 if let Some(t) = tag {
214 write!(self.writer, "{} ", format_tag(t))?;
215 }
216
217 if entries.is_empty() {
218 return write!(self.writer, "{{}}");
219 }
220
221 match style {
222 CollectionStyle::Flow => {
223 write!(self.writer, "{{")?;
224 for (i, (key, value)) in entries.iter().enumerate() {
225 if i > 0 {
226 write!(self.writer, ", ")?;
227 }
228 self.emit_node(key, indent, true)?;
229 write!(self.writer, ": ")?;
230 self.emit_node(value, indent, true)?;
231 }
232 write!(self.writer, "}}")
233 }
234 CollectionStyle::Block => {
235 let child_indent = indent + self.config.indent_width;
236 let pad: String = " ".repeat(indent);
237 for (key, value) in entries {
238 write!(self.writer, "{pad}")?;
239 self.emit_node(key, child_indent, false)?;
240 write!(self.writer, ": ")?;
241 if is_block_collection(value, self.config.default_collection_style) {
243 writeln!(self.writer)?;
244 write!(self.writer, "{}", " ".repeat(child_indent))?;
245 }
246 self.emit_node(value, child_indent, false)?;
247 writeln!(self.writer)?;
248 }
249 Ok(())
250 }
251 }
252 }
253
254 fn emit_sequence(
259 &mut self,
260 items: &[Node<Span>],
261 anchor: Option<&str>,
262 tag: Option<&str>,
263 indent: usize,
264 in_flow: bool,
265 ) -> io::Result<()> {
266 let style = if in_flow {
267 CollectionStyle::Flow
268 } else {
269 self.config.default_collection_style
270 };
271
272 if let Some(name) = anchor {
273 write!(self.writer, "&{name} ")?;
274 }
275 if let Some(t) = tag {
276 write!(self.writer, "{} ", format_tag(t))?;
277 }
278
279 if items.is_empty() {
280 return write!(self.writer, "[]");
281 }
282
283 match style {
284 CollectionStyle::Flow => {
285 write!(self.writer, "[")?;
286 for (i, item) in items.iter().enumerate() {
287 if i > 0 {
288 write!(self.writer, ", ")?;
289 }
290 self.emit_node(item, indent, true)?;
291 }
292 write!(self.writer, "]")
293 }
294 CollectionStyle::Block => {
295 let pad: String = " ".repeat(indent);
296 let child_indent = indent + self.config.indent_width;
297 for item in items {
298 write!(self.writer, "{pad}- ")?;
299 self.emit_node(item, child_indent, false)?;
300 writeln!(self.writer)?;
301 }
302 Ok(())
303 }
304 }
305 }
306}
307
308fn needs_quoting(value: &str) -> bool {
314 if value.is_empty() {
316 return true;
317 }
318 matches!(
320 value,
321 "null"
322 | "Null"
323 | "NULL"
324 | "~"
325 | "true"
326 | "True"
327 | "TRUE"
328 | "false"
329 | "False"
330 | "FALSE"
331 | ".inf"
332 | ".Inf"
333 | ".INF"
334 | "-.inf"
335 | "-.Inf"
336 | "-.INF"
337 | ".nan"
338 | ".NaN"
339 | ".NAN"
340 )
341}
342
343fn escape_double(value: &str) -> String {
345 let mut out = String::with_capacity(value.len());
346 for ch in value.chars() {
347 match ch {
348 '"' => out.push_str("\\\""),
349 '\\' => out.push_str("\\\\"),
350 '\n' => out.push_str("\\n"),
351 '\r' => out.push_str("\\r"),
352 '\t' => out.push_str("\\t"),
353 c => out.push(c),
354 }
355 }
356 out
357}
358
359const fn chomp_indicator(chomp: Chomp) -> &'static str {
361 match chomp {
362 Chomp::Strip => "-",
363 Chomp::Clip => "",
364 Chomp::Keep => "+",
365 }
366}
367
368fn format_tag(tag: &str) -> String {
370 tag.strip_prefix("tag:yaml.org,2002:").map_or_else(
371 || {
372 if tag.starts_with('!') {
373 tag.to_owned()
374 } else {
375 format!("!<{tag}>")
376 }
377 },
378 |suffix| format!("!!{suffix}"),
379 )
380}
381
382const fn is_block_collection(node: &Node<Span>, default_style: CollectionStyle) -> bool {
384 match (node, default_style) {
385 (Node::Mapping { entries, .. }, CollectionStyle::Block) if !entries.is_empty() => true,
386 (Node::Sequence { items, .. }, CollectionStyle::Block) if !items.is_empty() => true,
387 _ => false,
388 }
389}
390
391#[cfg(test)]
396#[allow(
397 clippy::expect_used,
398 clippy::unwrap_used,
399 clippy::indexing_slicing,
400 clippy::panic
401)]
402mod tests {
403 use super::*;
404 use crate::loader::load;
405
406 fn null_span() -> Span {
411 use crate::pos::Pos;
412 let p = Pos {
413 byte_offset: 0,
414 char_offset: 0,
415 line: 1,
416 column: 0,
417 };
418 Span { start: p, end: p }
419 }
420
421 fn scalar(value: &str) -> Node<Span> {
422 Node::Scalar {
423 value: value.to_owned(),
424 style: ScalarStyle::Plain,
425 anchor: None,
426 tag: None,
427 loc: null_span(),
428 leading_comments: Vec::new(),
429 trailing_comment: None,
430 }
431 }
432
433 fn doc(root: Node<Span>) -> Document<Span> {
434 Document {
435 root,
436 version: None,
437 tags: vec![],
438 comments: vec![],
439 }
440 }
441
442 fn default_config() -> EmitConfig {
443 EmitConfig::default()
444 }
445
446 fn reload_one(yaml: &str) -> Node<Span> {
447 load(yaml)
448 .expect("reload failed")
449 .into_iter()
450 .next()
451 .unwrap()
452 .root
453 }
454
455 #[test]
460 fn emit_plain_scalar_round_trips() {
461 let docs = load("hello\n").expect("parse failed");
462 let config = default_config();
463
464 let result = emit(&docs, &config);
465
466 assert!(result.contains("hello"), "result: {result:?}");
467 assert!(load(&result).is_ok(), "reload failed: {result:?}");
468 }
469
470 #[test]
475 fn config_default_indent_width_is_2() {
476 assert_eq!(EmitConfig::default().indent_width, 2);
477 }
478
479 #[test]
480 fn config_default_line_width_is_80() {
481 assert_eq!(EmitConfig::default().line_width, 80);
482 }
483
484 #[test]
485 fn config_default_scalar_style_is_plain() {
486 assert!(matches!(
487 EmitConfig::default().default_scalar_style,
488 ScalarStyle::Plain
489 ));
490 }
491
492 #[test]
493 fn config_default_collection_style_is_block() {
494 assert!(matches!(
495 EmitConfig::default().default_collection_style,
496 CollectionStyle::Block
497 ));
498 }
499
500 #[test]
501 fn config_indent_width_4_used_in_block_mapping() {
502 let config = EmitConfig {
503 indent_width: 4,
504 ..EmitConfig::default()
505 };
506 let root = Node::Mapping {
507 entries: vec![(scalar("key"), scalar("value"))],
508 anchor: None,
509 tag: None,
510 loc: null_span(),
511 leading_comments: Vec::new(),
512 trailing_comment: None,
513 };
514 let result = emit(&[doc(root)], &config);
515 assert!(result.contains("key: value"), "result: {result:?}");
517 }
518
519 #[test]
524 fn plain_scalar_emits_unquoted() {
525 let result = emit(&[doc(scalar("hello"))], &default_config());
526 assert!(result.contains("hello"), "result: {result:?}");
527 assert!(!result.contains('"'), "result: {result:?}");
528 assert!(!result.contains('\''), "result: {result:?}");
529 }
530
531 #[test]
532 fn plain_scalar_empty_string_emits_quoted() {
533 let node = Node::Scalar {
534 value: String::new(),
535 style: ScalarStyle::Plain,
536 anchor: None,
537 tag: None,
538 loc: null_span(),
539 leading_comments: Vec::new(),
540 trailing_comment: None,
541 };
542 let result = emit(&[doc(node)], &default_config());
543 assert!(result.contains("''"), "result: {result:?}");
545 }
546
547 #[test]
548 fn plain_scalar_null_word_emits_single_quoted() {
549 let node = Node::Scalar {
550 value: "null".to_owned(),
551 style: ScalarStyle::Plain,
552 anchor: None,
553 tag: None,
554 loc: null_span(),
555 leading_comments: Vec::new(),
556 trailing_comment: None,
557 };
558 let result = emit(&[doc(node)], &default_config());
559 assert!(result.contains("'null'"), "result: {result:?}");
560 }
561
562 #[test]
563 fn plain_scalar_true_emits_single_quoted() {
564 let node = Node::Scalar {
565 value: "true".to_owned(),
566 style: ScalarStyle::Plain,
567 anchor: None,
568 tag: None,
569 loc: null_span(),
570 leading_comments: Vec::new(),
571 trailing_comment: None,
572 };
573 let result = emit(&[doc(node)], &default_config());
574 assert!(result.contains("'true'"), "result: {result:?}");
575 }
576
577 #[test]
578 fn plain_scalar_false_emits_single_quoted() {
579 let node = Node::Scalar {
580 value: "false".to_owned(),
581 style: ScalarStyle::Plain,
582 anchor: None,
583 tag: None,
584 loc: null_span(),
585 leading_comments: Vec::new(),
586 trailing_comment: None,
587 };
588 let result = emit(&[doc(node)], &default_config());
589 assert!(result.contains("'false'"), "result: {result:?}");
590 }
591
592 #[test]
593 fn plain_scalar_tilde_emits_single_quoted() {
594 let node = Node::Scalar {
595 value: "~".to_owned(),
596 style: ScalarStyle::Plain,
597 anchor: None,
598 tag: None,
599 loc: null_span(),
600 leading_comments: Vec::new(),
601 trailing_comment: None,
602 };
603 let result = emit(&[doc(node)], &default_config());
604 assert!(result.contains("'~'"), "result: {result:?}");
605 }
606
607 #[test]
608 fn plain_scalar_inf_emits_single_quoted() {
609 let node = Node::Scalar {
610 value: ".inf".to_owned(),
611 style: ScalarStyle::Plain,
612 anchor: None,
613 tag: None,
614 loc: null_span(),
615 leading_comments: Vec::new(),
616 trailing_comment: None,
617 };
618 let result = emit(&[doc(node)], &default_config());
619 assert!(result.contains("'.inf'"), "result: {result:?}");
620 }
621
622 #[test]
627 fn single_quoted_scalar_wraps_in_single_quotes() {
628 let node = Node::Scalar {
629 value: "hello world".to_owned(),
630 style: ScalarStyle::SingleQuoted,
631 anchor: None,
632 tag: None,
633 loc: null_span(),
634 leading_comments: Vec::new(),
635 trailing_comment: None,
636 };
637 let result = emit(&[doc(node)], &default_config());
638 assert!(result.contains("'hello world'"), "result: {result:?}");
639 }
640
641 #[test]
642 fn single_quoted_scalar_escapes_embedded_single_quote() {
643 let node = Node::Scalar {
644 value: "it's".to_owned(),
645 style: ScalarStyle::SingleQuoted,
646 anchor: None,
647 tag: None,
648 loc: null_span(),
649 leading_comments: Vec::new(),
650 trailing_comment: None,
651 };
652 let result = emit(&[doc(node)], &default_config());
653 assert!(result.contains("'it''s'"), "result: {result:?}");
654 }
655
656 #[test]
661 fn double_quoted_scalar_wraps_in_double_quotes() {
662 let node = Node::Scalar {
663 value: "hello world".to_owned(),
664 style: ScalarStyle::DoubleQuoted,
665 anchor: None,
666 tag: None,
667 loc: null_span(),
668 leading_comments: Vec::new(),
669 trailing_comment: None,
670 };
671 let result = emit(&[doc(node)], &default_config());
672 assert!(result.contains("\"hello world\""), "result: {result:?}");
673 }
674
675 #[test]
676 fn double_quoted_scalar_escapes_embedded_quote() {
677 let node = Node::Scalar {
678 value: "say \"hi\"".to_owned(),
679 style: ScalarStyle::DoubleQuoted,
680 anchor: None,
681 tag: None,
682 loc: null_span(),
683 leading_comments: Vec::new(),
684 trailing_comment: None,
685 };
686 let result = emit(&[doc(node)], &default_config());
687 assert!(result.contains(r#""say \"hi\"""#), "result: {result:?}");
688 }
689
690 #[test]
691 fn double_quoted_scalar_escapes_newline() {
692 let node = Node::Scalar {
693 value: "line1\nline2".to_owned(),
694 style: ScalarStyle::DoubleQuoted,
695 anchor: None,
696 tag: None,
697 loc: null_span(),
698 leading_comments: Vec::new(),
699 trailing_comment: None,
700 };
701 let result = emit(&[doc(node)], &default_config());
702 assert!(result.contains("\"line1\\nline2\""), "result: {result:?}");
703 }
704
705 #[test]
710 fn literal_block_scalar_clip_emits_pipe() {
711 let node = Node::Scalar {
712 value: "line1\nline2".to_owned(),
713 style: ScalarStyle::Literal(Chomp::Clip),
714 anchor: None,
715 tag: None,
716 loc: null_span(),
717 leading_comments: Vec::new(),
718 trailing_comment: None,
719 };
720 let result = emit(&[doc(node)], &default_config());
721 assert!(result.contains("|\n"), "result: {result:?}");
722 assert!(result.contains("line1"), "result: {result:?}");
723 }
724
725 #[test]
726 fn literal_block_scalar_strip_emits_pipe_minus() {
727 let node = Node::Scalar {
728 value: "line1\nline2".to_owned(),
729 style: ScalarStyle::Literal(Chomp::Strip),
730 anchor: None,
731 tag: None,
732 loc: null_span(),
733 leading_comments: Vec::new(),
734 trailing_comment: None,
735 };
736 let result = emit(&[doc(node)], &default_config());
737 assert!(result.contains("|-\n"), "result: {result:?}");
738 }
739
740 #[test]
741 fn literal_block_scalar_keep_emits_pipe_plus() {
742 let node = Node::Scalar {
743 value: "line1\nline2".to_owned(),
744 style: ScalarStyle::Literal(Chomp::Keep),
745 anchor: None,
746 tag: None,
747 loc: null_span(),
748 leading_comments: Vec::new(),
749 trailing_comment: None,
750 };
751 let result = emit(&[doc(node)], &default_config());
752 assert!(result.contains("|+\n"), "result: {result:?}");
753 }
754
755 #[test]
760 fn folded_block_scalar_clip_emits_gt() {
761 let node = Node::Scalar {
762 value: "line1\nline2".to_owned(),
763 style: ScalarStyle::Folded(Chomp::Clip),
764 anchor: None,
765 tag: None,
766 loc: null_span(),
767 leading_comments: Vec::new(),
768 trailing_comment: None,
769 };
770 let result = emit(&[doc(node)], &default_config());
771 assert!(result.contains(">\n"), "result: {result:?}");
772 assert!(result.contains("line1"), "result: {result:?}");
773 }
774
775 #[test]
776 fn folded_block_scalar_strip_emits_gt_minus() {
777 let node = Node::Scalar {
778 value: "line1\nline2".to_owned(),
779 style: ScalarStyle::Folded(Chomp::Strip),
780 anchor: None,
781 tag: None,
782 loc: null_span(),
783 leading_comments: Vec::new(),
784 trailing_comment: None,
785 };
786 let result = emit(&[doc(node)], &default_config());
787 assert!(result.contains(">-\n"), "result: {result:?}");
788 }
789
790 #[test]
791 fn folded_block_scalar_keep_emits_gt_plus() {
792 let node = Node::Scalar {
793 value: "line1\nline2".to_owned(),
794 style: ScalarStyle::Folded(Chomp::Keep),
795 anchor: None,
796 tag: None,
797 loc: null_span(),
798 leading_comments: Vec::new(),
799 trailing_comment: None,
800 };
801 let result = emit(&[doc(node)], &default_config());
802 assert!(result.contains(">+\n"), "result: {result:?}");
803 }
804
805 #[test]
810 fn block_mapping_emits_key_colon_value() {
811 let root = Node::Mapping {
812 entries: vec![(scalar("name"), scalar("Alice"))],
813 anchor: None,
814 tag: None,
815 loc: null_span(),
816 leading_comments: Vec::new(),
817 trailing_comment: None,
818 };
819 let result = emit(&[doc(root)], &default_config());
820 assert!(result.contains("name: Alice"), "result: {result:?}");
821 }
822
823 #[test]
824 fn block_mapping_multiple_entries() {
825 let root = Node::Mapping {
826 entries: vec![(scalar("a"), scalar("1")), (scalar("b"), scalar("2"))],
827 anchor: None,
828 tag: None,
829 loc: null_span(),
830 leading_comments: Vec::new(),
831 trailing_comment: None,
832 };
833 let result = emit(&[doc(root)], &default_config());
834 assert!(result.contains("a: 1"), "result: {result:?}");
835 assert!(result.contains("b: 2"), "result: {result:?}");
836 }
837
838 #[test]
839 fn block_sequence_emits_dash_items() {
840 let root = Node::Sequence {
841 items: vec![scalar("a"), scalar("b"), scalar("c")],
842 anchor: None,
843 tag: None,
844 loc: null_span(),
845 leading_comments: Vec::new(),
846 trailing_comment: None,
847 };
848 let result = emit(&[doc(root)], &default_config());
849 assert!(result.contains("- a"), "result: {result:?}");
850 assert!(result.contains("- b"), "result: {result:?}");
851 assert!(result.contains("- c"), "result: {result:?}");
852 }
853
854 #[test]
855 fn block_mapping_empty_emits_braces() {
856 let root = Node::Mapping {
857 entries: vec![],
858 anchor: None,
859 tag: None,
860 loc: null_span(),
861 leading_comments: Vec::new(),
862 trailing_comment: None,
863 };
864 let result = emit(&[doc(root)], &default_config());
865 assert!(result.contains("{}"), "result: {result:?}");
866 }
867
868 #[test]
869 fn block_sequence_empty_emits_brackets() {
870 let root = Node::Sequence {
871 items: vec![],
872 anchor: None,
873 tag: None,
874 loc: null_span(),
875 leading_comments: Vec::new(),
876 trailing_comment: None,
877 };
878 let result = emit(&[doc(root)], &default_config());
879 assert!(result.contains("[]"), "result: {result:?}");
880 }
881
882 #[test]
887 fn flow_mapping_emits_braces() {
888 let config = EmitConfig {
889 default_collection_style: CollectionStyle::Flow,
890 ..EmitConfig::default()
891 };
892 let root = Node::Mapping {
893 entries: vec![(scalar("key"), scalar("val"))],
894 anchor: None,
895 tag: None,
896 loc: null_span(),
897 leading_comments: Vec::new(),
898 trailing_comment: None,
899 };
900 let result = emit(&[doc(root)], &config);
901 assert!(result.contains("{key: val}"), "result: {result:?}");
902 }
903
904 #[test]
905 fn flow_sequence_emits_brackets() {
906 let config = EmitConfig {
907 default_collection_style: CollectionStyle::Flow,
908 ..EmitConfig::default()
909 };
910 let root = Node::Sequence {
911 items: vec![scalar("a"), scalar("b")],
912 anchor: None,
913 tag: None,
914 loc: null_span(),
915 leading_comments: Vec::new(),
916 trailing_comment: None,
917 };
918 let result = emit(&[doc(root)], &config);
919 assert!(result.contains("[a, b]"), "result: {result:?}");
920 }
921
922 #[test]
923 fn flow_mapping_empty_emits_braces() {
924 let config = EmitConfig {
925 default_collection_style: CollectionStyle::Flow,
926 ..EmitConfig::default()
927 };
928 let root = Node::Mapping {
929 entries: vec![],
930 anchor: None,
931 tag: None,
932 loc: null_span(),
933 leading_comments: Vec::new(),
934 trailing_comment: None,
935 };
936 let result = emit(&[doc(root)], &config);
937 assert!(result.contains("{}"), "result: {result:?}");
938 }
939
940 #[test]
941 fn flow_sequence_empty_emits_brackets() {
942 let config = EmitConfig {
943 default_collection_style: CollectionStyle::Flow,
944 ..EmitConfig::default()
945 };
946 let root = Node::Sequence {
947 items: vec![],
948 anchor: None,
949 tag: None,
950 loc: null_span(),
951 leading_comments: Vec::new(),
952 trailing_comment: None,
953 };
954 let result = emit(&[doc(root)], &config);
955 assert!(result.contains("[]"), "result: {result:?}");
956 }
957
958 #[test]
959 fn flow_mapping_multiple_entries_comma_separated() {
960 let config = EmitConfig {
961 default_collection_style: CollectionStyle::Flow,
962 ..EmitConfig::default()
963 };
964 let root = Node::Mapping {
965 entries: vec![(scalar("a"), scalar("1")), (scalar("b"), scalar("2"))],
966 anchor: None,
967 tag: None,
968 loc: null_span(),
969 leading_comments: Vec::new(),
970 trailing_comment: None,
971 };
972 let result = emit(&[doc(root)], &config);
973 assert!(result.contains("{a: 1, b: 2}"), "result: {result:?}");
974 }
975
976 #[test]
981 fn anchor_emits_ampersand_prefix() {
982 let node = Node::Scalar {
983 value: "shared".to_owned(),
984 style: ScalarStyle::Plain,
985 anchor: Some("ref".to_owned()),
986 tag: None,
987 loc: null_span(),
988 leading_comments: Vec::new(),
989 trailing_comment: None,
990 };
991 let result = emit(&[doc(node)], &default_config());
992 assert!(result.contains("&ref shared"), "result: {result:?}");
993 }
994
995 #[test]
996 fn alias_emits_asterisk_prefix() {
997 let root = Node::Sequence {
998 items: vec![
999 Node::Scalar {
1000 value: "val".to_owned(),
1001 style: ScalarStyle::Plain,
1002 anchor: Some("a".to_owned()),
1003 tag: None,
1004 loc: null_span(),
1005 leading_comments: Vec::new(),
1006 trailing_comment: None,
1007 },
1008 Node::Alias {
1009 name: "a".to_owned(),
1010 loc: null_span(),
1011 leading_comments: Vec::new(),
1012 trailing_comment: None,
1013 },
1014 ],
1015 anchor: None,
1016 tag: None,
1017 loc: null_span(),
1018 leading_comments: Vec::new(),
1019 trailing_comment: None,
1020 };
1021 let result = emit(&[doc(root)], &default_config());
1022 assert!(result.contains("&a val"), "result: {result:?}");
1023 assert!(result.contains("*a"), "result: {result:?}");
1024 }
1025
1026 #[test]
1027 fn anchor_on_mapping_emits_before_entries() {
1028 let root = Node::Mapping {
1029 entries: vec![(scalar("k"), scalar("v"))],
1030 anchor: Some("m".to_owned()),
1031 tag: None,
1032 loc: null_span(),
1033 leading_comments: Vec::new(),
1034 trailing_comment: None,
1035 };
1036 let result = emit(&[doc(root)], &default_config());
1037 assert!(result.contains("&m"), "result: {result:?}");
1038 }
1039
1040 #[test]
1041 fn anchor_on_sequence_emits_before_items() {
1042 let root = Node::Sequence {
1043 items: vec![scalar("x")],
1044 anchor: Some("s".to_owned()),
1045 tag: None,
1046 loc: null_span(),
1047 leading_comments: Vec::new(),
1048 trailing_comment: None,
1049 };
1050 let result = emit(&[doc(root)], &default_config());
1051 assert!(result.contains("&s"), "result: {result:?}");
1052 }
1053
1054 #[test]
1059 fn yaml_core_tag_emits_double_bang_shorthand() {
1060 let node = Node::Scalar {
1061 value: "42".to_owned(),
1062 style: ScalarStyle::Plain,
1063 anchor: None,
1064 tag: Some("tag:yaml.org,2002:int".to_owned()),
1065 loc: null_span(),
1066 leading_comments: Vec::new(),
1067 trailing_comment: None,
1068 };
1069 let result = emit(&[doc(node)], &default_config());
1070 assert!(result.contains("!!int"), "result: {result:?}");
1071 }
1072
1073 #[test]
1074 fn local_tag_emits_exclamation_prefix() {
1075 let node = Node::Scalar {
1076 value: "val".to_owned(),
1077 style: ScalarStyle::Plain,
1078 anchor: None,
1079 tag: Some("!local".to_owned()),
1080 loc: null_span(),
1081 leading_comments: Vec::new(),
1082 trailing_comment: None,
1083 };
1084 let result = emit(&[doc(node)], &default_config());
1085 assert!(result.contains("!local"), "result: {result:?}");
1086 }
1087
1088 #[test]
1089 fn unknown_uri_tag_emits_angle_bracket_form() {
1090 let node = Node::Scalar {
1091 value: "val".to_owned(),
1092 style: ScalarStyle::Plain,
1093 anchor: None,
1094 tag: Some("http://example.com/tag".to_owned()),
1095 loc: null_span(),
1096 leading_comments: Vec::new(),
1097 trailing_comment: None,
1098 };
1099 let result = emit(&[doc(node)], &default_config());
1100 assert!(
1101 result.contains("!<http://example.com/tag>"),
1102 "result: {result:?}"
1103 );
1104 }
1105
1106 #[test]
1111 fn single_document_no_separator() {
1112 let result = emit(&[doc(scalar("only"))], &default_config());
1113 assert!(!result.starts_with("---"), "result: {result:?}");
1114 }
1115
1116 #[test]
1117 fn two_documents_second_has_separator() {
1118 let docs = vec![doc(scalar("first")), doc(scalar("second"))];
1119 let result = emit(&docs, &default_config());
1120 let separator_count = result.matches("---").count();
1122 assert_eq!(separator_count, 1, "result: {result:?}");
1123 assert!(result.contains("first"), "result: {result:?}");
1124 assert!(result.contains("second"), "result: {result:?}");
1125 }
1126
1127 #[test]
1128 fn three_documents_two_separators() {
1129 let docs = vec![doc(scalar("a")), doc(scalar("b")), doc(scalar("c"))];
1130 let result = emit(&docs, &default_config());
1131 let separator_count = result.matches("---").count();
1132 assert_eq!(separator_count, 2, "result: {result:?}");
1133 }
1134
1135 #[test]
1136 fn empty_document_list_emits_empty_string() {
1137 let result = emit(&[], &default_config());
1138 assert!(result.is_empty(), "result: {result:?}");
1139 }
1140
1141 #[test]
1142 fn document_with_version_emits_separator() {
1143 let mut d = doc(scalar("val"));
1144 d.version = Some((1, 2));
1145 let result = emit(&[d], &default_config());
1146 assert!(result.starts_with("---"), "result: {result:?}");
1147 }
1148
1149 #[test]
1154 fn document_comment_emits_hash_prefix() {
1155 let mut d = doc(scalar("val"));
1156 d.comments = vec!["a comment".to_owned()];
1157 let result = emit(&[d], &default_config());
1158 assert!(result.contains("# a comment"), "result: {result:?}");
1159 }
1160
1161 #[test]
1162 fn multiple_comments_all_emitted() {
1163 let mut d = doc(scalar("val"));
1164 d.comments = vec!["first".to_owned(), "second".to_owned()];
1165 let result = emit(&[d], &default_config());
1166 assert!(result.contains("# first"), "result: {result:?}");
1167 assert!(result.contains("# second"), "result: {result:?}");
1168 }
1169
1170 #[test]
1171 fn comments_appear_before_root_node() {
1172 let mut d = doc(scalar("val"));
1173 d.comments = vec!["note".to_owned()];
1174 let result = emit(&[d], &default_config());
1175 let comment_pos = result.find("# note").expect("comment missing");
1176 let val_pos = result.find("val").expect("value missing");
1177 assert!(comment_pos < val_pos, "result: {result:?}");
1178 }
1179
1180 #[test]
1185 fn nested_mapping_indents_child() {
1186 let inner = Node::Mapping {
1187 entries: vec![(scalar("x"), scalar("1"))],
1188 anchor: None,
1189 tag: None,
1190 loc: null_span(),
1191 leading_comments: Vec::new(),
1192 trailing_comment: None,
1193 };
1194 let root = Node::Mapping {
1195 entries: vec![(scalar("outer"), inner)],
1196 anchor: None,
1197 tag: None,
1198 loc: null_span(),
1199 leading_comments: Vec::new(),
1200 trailing_comment: None,
1201 };
1202 let result = emit(&[doc(root)], &default_config());
1203 assert!(result.contains("outer:"), "result: {result:?}");
1204 assert!(result.contains("x: 1"), "result: {result:?}");
1205 }
1206
1207 #[test]
1208 fn nested_sequence_indents_child() {
1209 let inner = Node::Sequence {
1210 items: vec![scalar("a"), scalar("b")],
1211 anchor: None,
1212 tag: None,
1213 loc: null_span(),
1214 leading_comments: Vec::new(),
1215 trailing_comment: None,
1216 };
1217 let root = Node::Sequence {
1218 items: vec![inner],
1219 anchor: None,
1220 tag: None,
1221 loc: null_span(),
1222 leading_comments: Vec::new(),
1223 trailing_comment: None,
1224 };
1225 let result = emit(&[doc(root)], &default_config());
1226 assert!(result.contains("- "), "result: {result:?}");
1227 assert!(result.contains('a'), "result: {result:?}");
1228 }
1229
1230 #[test]
1231 fn emit_output_is_valid_utf8() {
1232 let docs = load("key: value\n").expect("parse failed");
1233 let result = emit(&docs, &default_config());
1234 assert!(!result.is_empty());
1236 }
1237
1238 #[test]
1239 fn emit_to_writer_matches_emit() {
1240 let docs = load("a: b\n").expect("parse failed");
1241 let config = default_config();
1242
1243 let expected = emit(&docs, &config);
1244 let mut buf = Vec::new();
1245 emit_to_writer(&docs, &config, &mut buf).expect("write failed");
1246 let actual = String::from_utf8(buf).expect("utf-8");
1247
1248 assert_eq!(actual, expected);
1249 }
1250
1251 #[test]
1252 fn scalar_round_trip_plain() {
1253 let yaml = "greeting: hello\n";
1254 let docs = load(yaml).expect("parse failed");
1255 let result = emit(&docs, &default_config());
1256 let reloaded = reload_one(&result);
1257 let Node::Mapping { entries, .. } = reloaded else {
1260 panic!("expected Mapping after reload");
1261 };
1262 let [(key, val)] = entries.as_slice() else {
1263 panic!("expected exactly one entry, got {}", entries.len());
1264 };
1265 assert!(
1266 matches!(key, Node::Scalar { value, .. } if value == "greeting"),
1267 "key mismatch: {key:?}"
1268 );
1269 assert!(
1270 matches!(val, Node::Scalar { value, .. } if value == "hello"),
1271 "value mismatch: {val:?}"
1272 );
1273 }
1274
1275 #[test]
1276 fn flow_style_sequence_round_trips() {
1277 let config = EmitConfig {
1278 default_collection_style: CollectionStyle::Flow,
1279 ..EmitConfig::default()
1280 };
1281 let root = Node::Sequence {
1282 items: vec![scalar("1"), scalar("2"), scalar("3")],
1283 anchor: None,
1284 tag: None,
1285 loc: null_span(),
1286 leading_comments: Vec::new(),
1287 trailing_comment: None,
1288 };
1289 let result = emit(&[doc(root)], &config);
1290 assert!(load(&result).is_ok(), "reload failed: {result:?}");
1291 }
1292
1293 #[test]
1294 fn reserved_word_round_trips_as_string() {
1295 let node = Node::Scalar {
1296 value: "null".to_owned(),
1297 style: ScalarStyle::Plain,
1298 anchor: None,
1299 tag: None,
1300 loc: null_span(),
1301 leading_comments: Vec::new(),
1302 trailing_comment: None,
1303 };
1304 let result = emit(&[doc(node)], &default_config());
1305 assert!(load(&result).is_ok(), "reload failed: {result:?}");
1308 }
1309}