1mod comments;
33mod reloc;
34mod stream;
35
36use comments::{attach_leading_comments, attach_trailing_comment};
37use reloc::reloc;
38use stream::{
39 consume_leading_comments, consume_leading_doc_comments, next_from, peek_trailing_comment,
40 with_hash_prefix,
41};
42
43use std::collections::{HashMap, HashSet};
44use std::iter::Peekable;
45
46use crate::error::Error;
47use crate::event::{Event, ScalarStyle};
48use crate::node::{Document, Node};
49use crate::pos::{Pos, Span};
50
51#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
57pub enum LoadError {
58 #[error("parse error at {pos:?}: {message}")]
60 Parse {
61 pos: Pos,
63 message: String,
65 },
66
67 #[error("unexpected end of event stream")]
69 UnexpectedEndOfStream,
70
71 #[error("nesting depth limit exceeded (max: {limit})")]
73 NestingDepthLimitExceeded {
74 limit: usize,
76 },
77
78 #[error("anchor count limit exceeded (max: {limit})")]
80 AnchorCountLimitExceeded {
81 limit: usize,
83 },
84
85 #[error("alias expansion node limit exceeded (max: {limit})")]
87 AliasExpansionLimitExceeded {
88 limit: usize,
90 },
91
92 #[error("circular alias reference: '{name}'")]
94 CircularAlias {
95 name: String,
97 },
98
99 #[error("undefined alias: '{name}'")]
101 UndefinedAlias {
102 name: String,
104 },
105}
106
107type Result<T> = std::result::Result<T, LoadError>;
109
110type EventStream<'a> =
112 Peekable<Box<dyn Iterator<Item = std::result::Result<(Event<'a>, Span), Error>> + 'a>>;
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum LoadMode {
121 Lossless,
123 Resolved,
125}
126
127#[derive(Debug, Clone)]
129pub struct LoaderOptions {
130 pub max_nesting_depth: usize,
133 pub max_anchors: usize,
136 pub max_expanded_nodes: usize,
139 pub mode: LoadMode,
141}
142
143impl Default for LoaderOptions {
144 fn default() -> Self {
145 Self {
146 max_nesting_depth: 512,
147 max_anchors: 10_000,
148 max_expanded_nodes: 1_000_000,
149 mode: LoadMode::Lossless,
150 }
151 }
152}
153
154pub struct LoaderBuilder {
167 options: LoaderOptions,
168}
169
170impl LoaderBuilder {
171 #[must_use]
173 pub fn new() -> Self {
174 Self {
175 options: LoaderOptions::default(),
176 }
177 }
178
179 #[must_use]
181 pub const fn lossless(mut self) -> Self {
182 self.options.mode = LoadMode::Lossless;
183 self
184 }
185
186 #[must_use]
188 pub const fn resolved(mut self) -> Self {
189 self.options.mode = LoadMode::Resolved;
190 self
191 }
192
193 #[must_use]
195 pub const fn max_nesting_depth(mut self, limit: usize) -> Self {
196 self.options.max_nesting_depth = limit;
197 self
198 }
199
200 #[must_use]
202 pub const fn max_anchors(mut self, limit: usize) -> Self {
203 self.options.max_anchors = limit;
204 self
205 }
206
207 #[must_use]
209 pub const fn max_expanded_nodes(mut self, limit: usize) -> Self {
210 self.options.max_expanded_nodes = limit;
211 self
212 }
213
214 #[must_use]
216 pub const fn build(self) -> Loader {
217 Loader {
218 options: self.options,
219 }
220 }
221}
222
223impl Default for LoaderBuilder {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229pub struct Loader {
235 options: LoaderOptions,
236}
237
238impl Loader {
239 pub fn load(&self, input: &str) -> std::result::Result<Vec<Document<Span>>, LoadError> {
246 let mut state = LoadState::new(&self.options);
247 let iter: Box<dyn Iterator<Item = std::result::Result<(Event<'_>, Span), Error>> + '_> =
248 Box::new(crate::parse_events(input));
249 state.run(iter.peekable())
250 }
251}
252
253pub fn load(input: &str) -> std::result::Result<Vec<Document<Span>>, LoadError> {
273 LoaderBuilder::new().lossless().build().load(input)
274}
275
276struct LoadState<'opt> {
281 options: &'opt LoaderOptions,
282 anchor_map: HashMap<String, Node<Span>>,
284 anchor_count: usize,
286 depth: usize,
288 expanded_nodes: usize,
290 pending_leading: Vec<String>,
296}
297
298impl<'opt> LoadState<'opt> {
299 fn new(options: &'opt LoaderOptions) -> Self {
300 Self {
301 options,
302 anchor_map: HashMap::new(),
303 anchor_count: 0,
304 depth: 0,
305 expanded_nodes: 0,
306 pending_leading: Vec::new(),
307 }
308 }
309
310 fn reset_for_document(&mut self) {
311 self.anchor_map.clear();
312 self.anchor_count = 0;
313 self.expanded_nodes = 0;
314 self.pending_leading.clear();
315 }
316
317 fn run(&mut self, mut stream: EventStream<'_>) -> Result<Vec<Document<Span>>> {
318 let mut docs: Vec<Document<Span>> = Vec::new();
319
320 match stream.next() {
322 Some(Ok(_)) | None => {}
323 Some(Err(e)) => {
324 return Err(LoadError::Parse {
325 pos: e.pos,
326 message: e.message,
327 });
328 }
329 }
330
331 loop {
332 match next_from(&mut stream)? {
334 None | Some((Event::StreamEnd, _)) => break,
335 Some((
336 Event::DocumentStart {
337 explicit,
338 version,
339 tag_directives,
340 },
341 _,
342 )) => {
343 let doc_explicit_start = explicit;
344 let doc_version = version;
345 let doc_tags = tag_directives;
346 self.reset_for_document();
347
348 let mut doc_comments: Vec<String> = Vec::new();
349
350 consume_leading_doc_comments(&mut stream, &mut doc_comments)?;
352
353 let root = if is_document_end(stream.peek()) {
355 empty_scalar()
357 } else {
358 self.parse_node(&mut stream)?
359 };
360
361 let doc_explicit_end =
363 if let Some(Ok((Event::DocumentEnd { explicit }, _))) = stream.peek() {
364 let end_explicit = *explicit;
365 let _ = stream.next();
366 end_explicit
367 } else {
368 false
369 };
370
371 docs.push(Document {
372 root,
373 version: doc_version,
374 tags: doc_tags,
375 comments: doc_comments,
376 explicit_start: doc_explicit_start,
377 explicit_end: doc_explicit_end,
378 });
379 }
380 Some(_) => {
381 }
383 }
384 }
385
386 Ok(docs)
387 }
388
389 #[expect(
393 clippy::too_many_lines,
394 reason = "match-on-event-type; splitting would obscure flow"
395 )]
396 fn parse_node(&mut self, stream: &mut EventStream<'_>) -> Result<Node<Span>> {
397 if matches!(
401 stream.peek(),
402 Some(Ok((
403 Event::MappingEnd | Event::SequenceEnd | Event::DocumentEnd { .. },
404 _
405 )))
406 ) {
407 return Ok(empty_scalar());
408 }
409
410 let Some((event, span)) = next_from(stream)? else {
411 return Ok(empty_scalar());
412 };
413
414 match event {
415 Event::Scalar {
416 value,
417 style,
418 anchor,
419 tag,
420 } => {
421 let node = Node::Scalar {
422 value: value.into_owned(),
423 style,
424 anchor: anchor.map(str::to_owned),
425 tag: tag.map(std::borrow::Cow::into_owned),
426 loc: span,
427 leading_comments: None,
428 trailing_comment: None,
429 };
430 if let Some(name) = node.anchor() {
431 self.register_anchor(name.to_owned(), &node)?;
432 }
433 Ok(node)
434 }
435
436 Event::MappingStart { anchor, tag, style } => {
437 let anchor = anchor.map(str::to_owned);
438 let tag = tag.map(std::borrow::Cow::into_owned);
439
440 self.depth += 1;
441 if self.depth > self.options.max_nesting_depth {
442 return Err(LoadError::NestingDepthLimitExceeded {
443 limit: self.options.max_nesting_depth,
444 });
445 }
446
447 let mut entries: Vec<(Node<Span>, Node<Span>)> = Vec::new();
448 let mut end_span = span;
449
450 loop {
451 let raw_leading = consume_leading_comments(stream)?;
455 let leading = if self.pending_leading.is_empty() {
456 raw_leading
457 } else {
458 let mut combined = std::mem::take(&mut self.pending_leading);
459 combined.extend(raw_leading);
460 combined
461 };
462
463 match stream.peek() {
464 None | Some(Ok((Event::MappingEnd | Event::StreamEnd, _))) => {
465 if !leading.is_empty() {
470 self.pending_leading = leading;
471 }
472 break;
473 }
474 Some(Err(_)) => {
475 return Err(match stream.next() {
477 Some(Err(e)) => LoadError::Parse {
478 pos: e.pos,
479 message: e.message,
480 },
481 _ => LoadError::UnexpectedEndOfStream,
482 });
483 }
484 Some(Ok(_)) => {}
485 }
486
487 let mut key = self.parse_node(stream)?;
488 attach_leading_comments(&mut key, leading);
489
490 let mut value = self.parse_node(stream)?;
491
492 if !is_block_scalar(&value)
501 && matches!(stream.peek(), Some(Ok((Event::Comment { .. }, _))))
502 {
503 let value_end_line = node_end_line(&value);
504 if let Some(trail) = peek_trailing_comment(stream, value_end_line)? {
505 attach_trailing_comment(&mut value, trail);
506 }
507 }
508
509 entries.push((key, value));
510 }
511
512 if let Some(Ok((Event::MappingEnd, end))) = stream.peek() {
514 end_span = *end;
515 let _ = stream.next();
516 }
517 self.depth -= 1;
518
519 let node = Node::Mapping {
520 entries,
521 style,
522 anchor: anchor.clone(),
523 tag,
524 loc: Span {
525 start: span.start,
526 end: end_span.end,
527 },
528 leading_comments: None,
529 trailing_comment: None,
530 };
531 if let Some(name) = anchor {
532 self.register_anchor(name, &node)?;
533 }
534 Ok(node)
535 }
536
537 Event::SequenceStart { anchor, tag, style } => {
538 let anchor = anchor.map(str::to_owned);
539 let tag = tag.map(std::borrow::Cow::into_owned);
540
541 self.depth += 1;
542 if self.depth > self.options.max_nesting_depth {
543 return Err(LoadError::NestingDepthLimitExceeded {
544 limit: self.options.max_nesting_depth,
545 });
546 }
547
548 let mut items: Vec<Node<Span>> = Vec::new();
549 let mut end_span = span;
550
551 loop {
552 let raw_leading = consume_leading_comments(stream)?;
556 let leading = if self.pending_leading.is_empty() {
557 raw_leading
558 } else {
559 let mut combined = std::mem::take(&mut self.pending_leading);
560 combined.extend(raw_leading);
561 combined
562 };
563
564 match stream.peek() {
565 None | Some(Ok((Event::SequenceEnd | Event::StreamEnd, _))) => {
566 if !leading.is_empty() {
572 self.pending_leading = leading;
573 }
574 break;
575 }
576 Some(Err(_)) => {
577 return Err(match stream.next() {
579 Some(Err(e)) => LoadError::Parse {
580 pos: e.pos,
581 message: e.message,
582 },
583 _ => LoadError::UnexpectedEndOfStream,
584 });
585 }
586 Some(Ok(_)) => {}
587 }
588
589 let mut item = self.parse_node(stream)?;
590 attach_leading_comments(&mut item, leading);
591
592 if !is_block_scalar(&item)
598 && matches!(stream.peek(), Some(Ok((Event::Comment { .. }, _))))
599 {
600 let item_end_line = node_end_line(&item);
601 if let Some(trail) = peek_trailing_comment(stream, item_end_line)? {
602 attach_trailing_comment(&mut item, trail);
603 }
604 }
605
606 items.push(item);
607 }
608
609 if let Some(Ok((Event::SequenceEnd, end))) = stream.peek() {
611 end_span = *end;
612 let _ = stream.next();
613 }
614 self.depth -= 1;
615
616 let node = Node::Sequence {
617 items,
618 style,
619 anchor: anchor.clone(),
620 tag,
621 loc: Span {
622 start: span.start,
623 end: end_span.end,
624 },
625 leading_comments: None,
626 trailing_comment: None,
627 };
628 if let Some(name) = anchor {
629 self.register_anchor(name, &node)?;
630 }
631 Ok(node)
632 }
633
634 Event::Alias { name } => {
635 let name = name.to_owned();
636 self.resolve_alias(&name, span)
637 }
638
639 Event::Comment { text } => {
640 self.pending_leading.push(with_hash_prefix(text));
646 self.parse_node(stream)
647 }
648
649 Event::StreamStart
650 | Event::StreamEnd
651 | Event::DocumentStart { .. }
652 | Event::DocumentEnd { .. }
653 | Event::MappingEnd
654 | Event::SequenceEnd => {
655 Ok(empty_scalar())
657 }
658 }
659 }
660
661 fn register_anchor(&mut self, name: String, node: &Node<Span>) -> Result<()> {
662 if !self.anchor_map.contains_key(&name) {
663 self.anchor_count += 1;
664 if self.anchor_count > self.options.max_anchors {
665 return Err(LoadError::AnchorCountLimitExceeded {
666 limit: self.options.max_anchors,
667 });
668 }
669 }
670 if self.options.mode == LoadMode::Resolved {
674 self.expanded_nodes += 1;
675 if self.expanded_nodes > self.options.max_expanded_nodes {
676 return Err(LoadError::AliasExpansionLimitExceeded {
677 limit: self.options.max_expanded_nodes,
678 });
679 }
680 self.anchor_map.insert(name, node.clone());
681 } else {
682 self.anchor_map.insert(name, empty_scalar());
685 }
686 Ok(())
687 }
688
689 fn resolve_alias(&mut self, name: &str, loc: Span) -> Result<Node<Span>> {
690 match self.options.mode {
691 LoadMode::Lossless => Ok(Node::Alias {
692 name: name.to_owned(),
693 loc,
694 leading_comments: None,
695 trailing_comment: None,
696 }),
697 LoadMode::Resolved => {
698 let anchored = self.anchor_map.get(name).cloned().ok_or_else(|| {
699 LoadError::UndefinedAlias {
700 name: name.to_owned(),
701 }
702 })?;
703 let mut in_progress: HashSet<String> = HashSet::new();
704 self.expand_node(anchored, &mut in_progress)
705 }
706 }
707 }
708
709 fn expand_node(
712 &mut self,
713 node: Node<Span>,
714 in_progress: &mut HashSet<String>,
715 ) -> Result<Node<Span>> {
716 self.expanded_nodes += 1;
720 if self.expanded_nodes > self.options.max_expanded_nodes {
721 return Err(LoadError::AliasExpansionLimitExceeded {
722 limit: self.options.max_expanded_nodes,
723 });
724 }
725
726 match node {
727 Node::Alias { ref name, loc, .. } => {
728 if in_progress.contains(name) {
729 return Err(LoadError::CircularAlias { name: name.clone() });
730 }
731 let target = self
732 .anchor_map
733 .get(name)
734 .cloned()
735 .ok_or_else(|| LoadError::UndefinedAlias { name: name.clone() })?;
736 in_progress.insert(name.clone());
737 let expanded = self.expand_node(target, in_progress)?;
738 in_progress.remove(name);
739 Ok(reloc(expanded, loc))
741 }
742 Node::Mapping {
743 entries,
744 style,
745 anchor,
746 tag,
747 loc,
748 leading_comments,
749 trailing_comment,
750 } => {
751 let mut expanded_entries = Vec::with_capacity(entries.len());
752 for (k, v) in entries {
753 let ek = self.expand_node(k, in_progress)?;
754 let ev = self.expand_node(v, in_progress)?;
755 expanded_entries.push((ek, ev));
756 }
757 Ok(Node::Mapping {
758 entries: expanded_entries,
759 style,
760 anchor,
761 tag,
762 loc,
763 leading_comments,
764 trailing_comment,
765 })
766 }
767 Node::Sequence {
768 items,
769 style,
770 anchor,
771 tag,
772 loc,
773 leading_comments,
774 trailing_comment,
775 } => {
776 let mut expanded_items = Vec::with_capacity(items.len());
777 for item in items {
778 expanded_items.push(self.expand_node(item, in_progress)?);
779 }
780 Ok(Node::Sequence {
781 items: expanded_items,
782 style,
783 anchor,
784 tag,
785 loc,
786 leading_comments,
787 trailing_comment,
788 })
789 }
790 scalar @ Node::Scalar { .. } => Ok(scalar),
792 }
793 }
794}
795
796const fn is_document_end(peeked: Option<&std::result::Result<(Event<'_>, Span), Error>>) -> bool {
798 matches!(
799 peeked,
800 None | Some(Ok((Event::DocumentEnd { .. } | Event::StreamEnd, _)))
801 )
802}
803
804#[inline]
809const fn node_end_line(node: &Node<Span>) -> usize {
810 match node {
811 Node::Scalar { loc, .. }
812 | Node::Mapping { loc, .. }
813 | Node::Sequence { loc, .. }
814 | Node::Alias { loc, .. } => loc.end.line,
815 }
816}
817
818#[inline]
827const fn is_block_scalar(node: &Node<Span>) -> bool {
828 matches!(
829 node,
830 Node::Scalar {
831 style: ScalarStyle::Literal(_) | ScalarStyle::Folded(_),
832 ..
833 }
834 )
835}
836
837const fn empty_scalar() -> Node<Span> {
842 Node::Scalar {
843 value: String::new(),
844 style: ScalarStyle::Plain,
845 anchor: None,
846 tag: None,
847 loc: Span {
848 start: Pos::ORIGIN,
849 end: Pos::ORIGIN,
850 },
851 leading_comments: None,
852 trailing_comment: None,
853 }
854}
855
856#[cfg(test)]
861#[expect(
862 clippy::expect_used,
863 clippy::unwrap_used,
864 clippy::indexing_slicing,
865 clippy::panic,
866 reason = "test code"
867)]
868mod tests {
869 use super::*;
870
871 #[test]
873 fn loader_state_resets_anchor_map_between_documents() {
874 let result = LoaderBuilder::new()
876 .resolved()
877 .build()
878 .load("---\n- &foo hello\n...\n---\n- *foo\n...\n");
879 assert!(
880 result.is_err(),
881 "expected Err: *foo in doc 2 should be undefined"
882 );
883 assert!(matches!(
884 result.unwrap_err(),
885 LoadError::UndefinedAlias { .. }
886 ));
887 }
888
889 #[test]
891 fn register_anchor_increments_count() {
892 let options = LoaderOptions {
893 max_anchors: 2,
894 ..LoaderOptions::default()
895 };
896 let mut state = LoadState::new(&options);
897 let node = Node::Scalar {
898 value: "x".to_owned(),
899 style: ScalarStyle::Plain,
900 anchor: None,
901 tag: None,
902 loc: Span {
903 start: Pos::ORIGIN,
904 end: Pos::ORIGIN,
905 },
906 leading_comments: None,
907 trailing_comment: None,
908 };
909 assert!(state.register_anchor("a".to_owned(), &node).is_ok());
910 assert!(state.register_anchor("b".to_owned(), &node).is_ok());
911 let err = state
912 .register_anchor("c".to_owned(), &node)
913 .expect_err("expected AnchorCountLimitExceeded");
914 assert!(matches!(
915 err,
916 LoadError::AnchorCountLimitExceeded { limit: 2 }
917 ));
918 }
919
920 #[test]
922 fn expand_node_detects_circular_alias() {
923 let options = LoaderOptions {
924 mode: LoadMode::Resolved,
925 ..LoaderOptions::default()
926 };
927 let mut state = LoadState::new(&options);
928 let alias_node = Node::Alias {
930 name: "a".to_owned(),
931 loc: Span {
932 start: Pos::ORIGIN,
933 end: Pos::ORIGIN,
934 },
935 leading_comments: None,
936 trailing_comment: None,
937 };
938 state.anchor_map.insert("a".to_owned(), alias_node.clone());
939 let mut in_progress = HashSet::new();
940 let result = state.expand_node(alias_node, &mut in_progress);
941 assert!(
942 matches!(result, Err(LoadError::CircularAlias { .. })),
943 "expected CircularAlias, got: {result:?}"
944 );
945 }
946
947 #[test]
953 fn comment_between_key_and_nested_mapping_is_attached_to_first_key() {
954 let docs = load("outer:\n # Style 1\n inner: val\n").unwrap();
955 let root = &docs[0].root;
956 let Node::Mapping { entries, .. } = root else {
960 panic!("expected root mapping");
961 };
962 assert_eq!(entries.len(), 1);
963 let (_outer_key, outer_value) = &entries[0];
964 let Node::Mapping {
965 entries: nested, ..
966 } = outer_value
967 else {
968 panic!("expected nested mapping");
969 };
970 assert_eq!(nested.len(), 1);
971 let (inner_key, _) = &nested[0];
972 assert_eq!(
973 inner_key.leading_comments(),
974 &["# Style 1"],
975 "comment should be attached to the first nested key"
976 );
977 }
978
979 #[test]
981 fn comment_between_key_and_nested_sequence_is_attached_to_first_item() {
982 let docs = load("key:\n # leading\n - item1\n - item2\n").unwrap();
983 let root = &docs[0].root;
984 let Node::Mapping { entries, .. } = root else {
985 panic!("expected root mapping");
986 };
987 let (_key, seq_value) = &entries[0];
988 let Node::Sequence { items, .. } = seq_value else {
989 panic!("expected sequence value");
990 };
991 assert_eq!(
994 items[0].leading_comments(),
995 &["# leading"],
996 "comment should be attached to first sequence item"
997 );
998 }
999
1000 #[test]
1002 fn multiple_comments_between_key_and_collection_all_preserved() {
1003 let docs = load("key:\n # first\n # second\n - item\n").unwrap();
1004 let root = &docs[0].root;
1005 let Node::Mapping { entries, .. } = root else {
1006 panic!("expected root mapping");
1007 };
1008 let (_key, seq_value) = &entries[0];
1009 let Node::Sequence { items, .. } = seq_value else {
1010 panic!("expected sequence value");
1011 };
1012 assert_eq!(
1013 items[0].leading_comments(),
1014 &["# first", "# second"],
1015 "both comments should be on first item"
1016 );
1017 }
1018
1019 #[test]
1021 fn comment_between_key_and_collection_does_not_corrupt_key_node() {
1022 let docs = load("outer:\n # Style 1\n inner: val\n").unwrap();
1023 let root = &docs[0].root;
1024 let Node::Mapping { entries, .. } = root else {
1025 panic!("expected root mapping");
1026 };
1027 let (outer_key, _) = &entries[0];
1028 assert!(
1029 outer_key.leading_comments().is_empty(),
1030 "outer key should have no leading comments"
1031 );
1032 assert!(
1033 outer_key.trailing_comment().is_none(),
1034 "outer key should have no trailing comment"
1035 );
1036 }
1037
1038 #[test]
1040 fn no_comment_between_key_and_value_leaves_leading_comments_empty() {
1041 let docs = load("key:\n inner: val\n").unwrap();
1042 let root = &docs[0].root;
1043 let Node::Mapping { entries, .. } = root else {
1044 panic!("expected root mapping");
1045 };
1046 let (_key, nested) = &entries[0];
1047 let Node::Mapping {
1048 entries: nested_entries,
1049 ..
1050 } = nested
1051 else {
1052 panic!("expected nested mapping");
1053 };
1054 let (inner_key, _) = &nested_entries[0];
1055 assert!(
1056 inner_key.leading_comments().is_empty(),
1057 "inner key should have no leading comments when there is no comment"
1058 );
1059 }
1060
1061 #[test]
1067 fn trailing_comment_of_sequence_preserved_as_leading_on_next_sibling() {
1068 let input =
1069 "Lists:\n list-a:\n - item1\n - item2\n\n # Style 2\n list-b:\n - item1\n";
1070 let docs = load(input).unwrap();
1071 let root = &docs[0].root;
1072 let Node::Mapping { entries, .. } = root else {
1073 panic!("expected root mapping");
1074 };
1075 let (_lists_key, nested) = &entries[0];
1076 let Node::Mapping {
1077 entries: nested_entries,
1078 ..
1079 } = nested
1080 else {
1081 panic!("expected nested mapping");
1082 };
1083 assert_eq!(nested_entries.len(), 2);
1084 let (list_b_key, _) = &nested_entries[1];
1085 assert_eq!(
1086 list_b_key.leading_comments(),
1087 &["# Style 2"],
1088 "# Style 2 should be leading comment on list-b key"
1089 );
1090 }
1091
1092 #[test]
1094 fn overflow_comments_from_nested_sequence_end_reach_next_mapping_entry() {
1095 let input = "outer:\n a:\n - x\n # between\n b: y\n";
1096 let docs = load(input).unwrap();
1097 let root = &docs[0].root;
1098 let Node::Mapping { entries, .. } = root else {
1099 panic!("expected root mapping");
1100 };
1101 let (_outer_key, outer_val) = &entries[0];
1102 let Node::Mapping {
1103 entries: nested, ..
1104 } = outer_val
1105 else {
1106 panic!("expected nested mapping");
1107 };
1108 assert_eq!(nested.len(), 2);
1109 let (b_key, _) = &nested[1];
1110 assert_eq!(
1111 b_key.leading_comments(),
1112 &["# between"],
1113 "# between should be leading comment on b key"
1114 );
1115 }
1116
1117 #[test]
1119 fn overflow_comments_from_nested_mapping_end_reach_next_sibling() {
1120 let input = "parent:\n child1:\n k: v\n # end-of-child1\n child2: val\n";
1121 let docs = load(input).unwrap();
1122 let root = &docs[0].root;
1123 let Node::Mapping { entries, .. } = root else {
1124 panic!("expected root mapping");
1125 };
1126 let (_parent_key, parent_val) = &entries[0];
1127 let Node::Mapping {
1128 entries: siblings, ..
1129 } = parent_val
1130 else {
1131 panic!("expected parent mapping value");
1132 };
1133 assert_eq!(siblings.len(), 2);
1134 let (child2_key, _) = &siblings[1];
1135 assert_eq!(
1136 child2_key.leading_comments(),
1137 &["# end-of-child1"],
1138 "# end-of-child1 should be leading comment on child2 key"
1139 );
1140 }
1141
1142 #[test]
1144 fn overflow_comments_at_top_level_sequence_end_are_not_lost() {
1145 let input = "items:\n - a\n - b\n # tail\n";
1151 let docs = load(input).unwrap();
1152 assert!(!docs.is_empty(), "document should parse without error");
1154 let root = &docs[0].root;
1156 let Node::Mapping { entries, .. } = root else {
1157 panic!("expected root mapping");
1158 };
1159 let (_items_key, seq_val) = &entries[0];
1160 let Node::Sequence { items, .. } = seq_val else {
1161 panic!("expected sequence value");
1162 };
1163 assert_eq!(items.len(), 2, "sequence items must not be lost");
1164 }
1165
1166 #[test]
1168 fn no_overflow_comments_when_collection_ends_cleanly() {
1169 let docs = load("key:\n - item1\n - item2\n").unwrap();
1170 let root = &docs[0].root;
1171 let Node::Mapping { entries, .. } = root else {
1172 panic!("expected root mapping");
1173 };
1174 let (_key, seq_val) = &entries[0];
1175 let Node::Sequence { items, .. } = seq_val else {
1176 panic!("expected sequence value");
1177 };
1178 for item in items {
1179 assert!(
1180 item.leading_comments().is_empty(),
1181 "items should have no leading comments"
1182 );
1183 }
1184 }
1185
1186 #[test]
1192 fn original_bug_report_input_preserves_both_comments() {
1193 let input = "Lists:\n # Style 1\n list-a:\n - item1\n - item2\n\n # Style 2\n list-b:\n - item1\n - item2\n";
1194 let docs = load(input).unwrap();
1195 let root = &docs[0].root;
1196 let Node::Mapping { entries, .. } = root else {
1197 panic!("expected root mapping");
1198 };
1199 let (_lists_key, nested) = &entries[0];
1200 let Node::Mapping {
1201 entries: nested_entries,
1202 ..
1203 } = nested
1204 else {
1205 panic!("expected nested mapping");
1206 };
1207 assert_eq!(nested_entries.len(), 2);
1208 let (first_key, _) = &nested_entries[0];
1209 let (second_key, _) = &nested_entries[1];
1210 assert_eq!(
1211 first_key.leading_comments(),
1212 &["# Style 1"],
1213 "list-a should have # Style 1 as leading comment"
1214 );
1215 assert_eq!(
1216 second_key.leading_comments(),
1217 &["# Style 2"],
1218 "list-b should have # Style 2 as leading comment"
1219 );
1220 }
1221
1222 #[test]
1224 fn leading_and_trailing_comments_both_preserved_on_sibling_entries() {
1225 let input = "map:\n # leading\n key: value # trailing\n # next-leading\n key2: v2\n";
1226 let docs = load(input).unwrap();
1227 let root = &docs[0].root;
1228 let Node::Mapping { entries, .. } = root else {
1229 panic!("expected root mapping");
1230 };
1231 let (_map_key, map_val) = &entries[0];
1232 let Node::Mapping {
1233 entries: siblings, ..
1234 } = map_val
1235 else {
1236 panic!("expected mapping value");
1237 };
1238 assert_eq!(siblings.len(), 2);
1239 let (key1, val1) = &siblings[0];
1240 let (key2, _) = &siblings[1];
1241 assert_eq!(key1.leading_comments(), &["# leading"]);
1242 assert_eq!(val1.trailing_comment(), Some("# trailing"));
1243 assert_eq!(key2.leading_comments(), &["# next-leading"]);
1244 }
1245
1246 #[test]
1248 fn deeply_nested_overflow_comments_reach_correct_sibling() {
1249 let input = "top:\n mid:\n - x\n # deep-overflow\n next: y\n";
1250 let docs = load(input).unwrap();
1251 let root = &docs[0].root;
1252 let Node::Mapping { entries, .. } = root else {
1253 panic!("expected root mapping");
1254 };
1255 let (_top_key, top_val) = &entries[0];
1256 let Node::Mapping {
1257 entries: top_entries,
1258 ..
1259 } = top_val
1260 else {
1261 panic!("expected top-level mapping");
1262 };
1263 assert_eq!(top_entries.len(), 2);
1264 let (next_key, _) = &top_entries[1];
1265 assert_eq!(
1266 next_key.leading_comments(),
1267 &["# deep-overflow"],
1268 "# deep-overflow should propagate from nested sequence to next sibling"
1269 );
1270 }
1271
1272 #[test]
1278 fn bare_document_has_both_flags_false() {
1279 let docs = load("key: value\n").expect("load failed");
1280 assert_eq!(docs.len(), 1);
1281 assert!(!docs[0].explicit_start, "expected explicit_start=false");
1282 assert!(!docs[0].explicit_end, "expected explicit_end=false");
1283 }
1284
1285 #[test]
1287 fn document_with_start_marker_has_explicit_start_true() {
1288 let docs = load("---\nkey: value\n").expect("load failed");
1289 assert_eq!(docs.len(), 1);
1290 assert!(docs[0].explicit_start, "expected explicit_start=true");
1291 assert!(!docs[0].explicit_end, "expected explicit_end=false");
1292 }
1293
1294 #[test]
1296 fn document_with_end_marker_has_explicit_end_true() {
1297 let docs = load("key: value\n...\n").expect("load failed");
1298 assert_eq!(docs.len(), 1);
1299 assert!(!docs[0].explicit_start, "expected explicit_start=false");
1300 assert!(docs[0].explicit_end, "expected explicit_end=true");
1301 }
1302
1303 #[test]
1305 fn document_with_both_markers_has_both_flags_true() {
1306 let docs = load("---\nkey: value\n...\n").expect("load failed");
1307 assert_eq!(docs.len(), 1);
1308 assert!(docs[0].explicit_start, "expected explicit_start=true");
1309 assert!(docs[0].explicit_end, "expected explicit_end=true");
1310 }
1311
1312 #[test]
1314 fn multi_document_flags_are_independent() {
1315 let docs = load("doc1: a\n---\ndoc2: b\n...\n---\ndoc3: c\n").expect("load failed");
1319 assert_eq!(docs.len(), 3);
1320 assert!(!docs[0].explicit_start, "doc1 explicit_start");
1321 assert!(!docs[0].explicit_end, "doc1 explicit_end");
1322 assert!(docs[1].explicit_start, "doc2 explicit_start");
1323 assert!(docs[1].explicit_end, "doc2 explicit_end");
1324 assert!(docs[2].explicit_start, "doc3 explicit_start");
1325 assert!(!docs[2].explicit_end, "doc3 explicit_end");
1326 }
1327
1328 #[test]
1330 fn empty_document_with_explicit_markers_has_both_flags_true() {
1331 let docs = load("---\n...\n").expect("load failed");
1332 assert_eq!(docs.len(), 1);
1333 assert!(docs[0].explicit_start, "expected explicit_start=true");
1334 assert!(docs[0].explicit_end, "expected explicit_end=true");
1335 }
1336}