1#![allow(dead_code)]
33
34use crate::syntax::{SyntaxKind, SyntaxNode};
35use rowan::GreenNodeBuilder;
36
37use super::model::{YamlDiagnostic, YamlParseReport};
38use super::profile::YamlValidationContext;
39use super::scanner::{Scanner, TokenKind, TriviaKind};
40
41fn strip_line_prefix(input: &str, prefix: &str) -> String {
46 input
47 .lines()
48 .map(|line| match line.strip_prefix(prefix) {
49 Some(rest) => rest.strip_prefix(' ').unwrap_or(rest),
50 None => line,
51 })
52 .collect::<Vec<_>>()
53 .join("\n")
54}
55
56pub fn validate_yaml_with_prefix(input: &str, prefix: &str) -> Option<YamlDiagnostic> {
63 if prefix.is_empty() {
64 return super::validator::validate_yaml(input);
65 }
66 super::validator::validate_yaml(&strip_line_prefix(input, prefix))
67}
68
69fn strip_line_prefix_with_offsets(input: &str, prefix: &str) -> (String, Vec<usize>) {
76 let mut stripped = String::new();
77 let mut offsets = Vec::new();
78 let base = input.as_ptr() as usize;
79 for (line_idx, line) in input.lines().enumerate() {
80 let line_off = line.as_ptr() as usize - base;
82 if line_idx > 0 {
83 offsets.push(line_off.saturating_sub(1));
85 stripped.push('\n');
86 }
87 let (payload, payload_off) = match line.strip_prefix(prefix) {
88 Some(rest) => {
89 let after = rest.strip_prefix(' ').unwrap_or(rest);
90 (after, line.len() - after.len())
91 }
92 None => (line, 0),
93 };
94 offsets.extend((0..payload.len()).map(|i| line_off + payload_off + i));
95 stripped.push_str(payload);
96 }
97 offsets.push(input.len());
98 (stripped, offsets)
99}
100
101pub fn locate_yaml_diagnostic(input: &str, prefix: &str) -> Option<(YamlDiagnostic, usize, usize)> {
108 locate_yaml_diagnostic_ctx(input, prefix, YamlValidationContext::substrate())
109}
110
111pub fn locate_yaml_diagnostic_ctx(
117 input: &str,
118 prefix: &str,
119 ctx: YamlValidationContext,
120) -> Option<(YamlDiagnostic, usize, usize)> {
121 if prefix.is_empty() {
122 let diag = super::validator::validate_yaml_with_context(input, ctx)?;
123 let start = diag.byte_start.min(input.len());
124 let end = diag.byte_end.min(input.len()).max(start);
125 return Some((diag, start, end));
126 }
127 let diag =
131 super::validator::validate_yaml_with_context(&strip_line_prefix(input, prefix), ctx)?;
132 let (_stripped, offsets) = strip_line_prefix_with_offsets(input, prefix);
133 let start = offsets.get(diag.byte_start).copied().unwrap_or(input.len());
134 let end = offsets
135 .get(diag.byte_end)
136 .copied()
137 .unwrap_or(input.len())
138 .max(start);
139 Some((diag, start, end))
140}
141
142pub fn parse_yaml_tree(input: &str) -> Option<SyntaxNode> {
144 parse_yaml_report(input).tree
145}
146
147pub fn parse_yaml_report(input: &str) -> YamlParseReport {
159 if let Some(err) = super::validator::validate_yaml(input) {
160 return YamlParseReport {
161 tree: None,
162 diagnostics: vec![err],
163 };
164 }
165
166 YamlParseReport {
167 tree: Some(parse_stream(input)),
168 diagnostics: Vec::new(),
169 }
170}
171
172pub fn parse_stream(input: &str) -> SyntaxNode {
176 parse_stream_inner(input, None)
177}
178
179pub fn parse_stream_with_prefix(input: &str, prefix: &str) -> SyntaxNode {
194 parse_stream_inner(input, (!prefix.is_empty()).then_some(prefix))
195}
196
197fn parse_stream_inner(input: &str, line_prefix: Option<&str>) -> SyntaxNode {
198 let mut builder = GreenNodeBuilder::new();
199 builder.start_node(SyntaxKind::YAML_STREAM.into());
200 let mut scanner = match line_prefix {
201 Some(prefix) => Scanner::with_prefix(input, prefix),
202 None => Scanner::new(input),
203 };
204 let mut doc_open = false;
205 let mut doc_only_has_directives = false;
212 let mut block_stack: Vec<BlockFrame> = Vec::new();
217 let mut prev_significant: Option<TokenKind> = None;
229 let mut decoration_col_floor: Option<usize> = None;
235 while let Some(tok) = scanner.next_token() {
236 let last_significant = prev_significant;
237 let decorations_so_far = decoration_col_floor;
238 let is_decoration = matches!(
239 tok.kind,
240 TokenKind::Anchor | TokenKind::Tag | TokenKind::Alias
241 );
242 if !matches!(
243 tok.kind,
244 TokenKind::Trivia(_) | TokenKind::StreamStart | TokenKind::StreamEnd
245 ) {
246 if is_decoration {
247 decoration_col_floor = Some(
248 decoration_col_floor.map_or(tok.start.column, |c| c.min(tok.start.column)),
249 );
250 } else {
251 prev_significant = Some(tok.kind);
252 decoration_col_floor = None;
253 }
254 }
255 match tok.kind {
256 TokenKind::StreamStart | TokenKind::StreamEnd => continue,
257 TokenKind::BlockMappingStart => {
258 ensure_doc_open(&mut builder, &mut doc_open);
259 doc_only_has_directives = false;
260 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
261 builder.start_node(SyntaxKind::YAML_BLOCK_MAP.into());
262 block_stack.push(BlockFrame::BlockMap {
263 entry_open: false,
264 in_value: false,
265 });
266 continue;
267 }
268 TokenKind::BlockSequenceStart => {
269 ensure_doc_open(&mut builder, &mut doc_open);
270 doc_only_has_directives = false;
271 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
272 builder.start_node(SyntaxKind::YAML_BLOCK_SEQUENCE.into());
273 block_stack.push(BlockFrame::BlockSequence {
274 item_open: false,
275 indentless: false,
276 });
277 continue;
278 }
279 TokenKind::BlockEnd => {
280 close_indentless_sequences(&mut builder, &mut block_stack);
285 close_open_sub_wrapper(&mut builder, &mut block_stack);
286 if block_stack.pop().is_some() {
290 builder.finish_node();
291 }
292 continue;
293 }
294 TokenKind::FlowSequenceStart => {
295 ensure_doc_open(&mut builder, &mut doc_open);
296 doc_only_has_directives = false;
297 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
298 builder.start_node(SyntaxKind::YAML_FLOW_SEQUENCE.into());
301 block_stack.push(BlockFrame::FlowSequence { item_open: false });
302 let text = &input[tok.start.index..tok.end.index];
303 builder.token(SyntaxKind::YAML_FLOW_INDICATOR.into(), text);
304 continue;
305 }
306 TokenKind::FlowSequenceEnd => {
307 close_open_sub_wrapper(&mut builder, &mut block_stack);
308 let text = &input[tok.start.index..tok.end.index];
309 builder.token(SyntaxKind::YAML_FLOW_INDICATOR.into(), text);
310 if matches!(
311 block_stack.last(),
312 Some(BlockFrame::FlowSequence { .. } | BlockFrame::FlowMap { .. })
313 ) {
314 block_stack.pop();
315 builder.finish_node();
316 }
317 continue;
318 }
319 TokenKind::FlowMappingStart => {
320 ensure_doc_open(&mut builder, &mut doc_open);
321 doc_only_has_directives = false;
322 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
323 builder.start_node(SyntaxKind::YAML_FLOW_MAP.into());
324 block_stack.push(BlockFrame::FlowMap {
325 entry_open: false,
326 in_value: false,
327 });
328 let text = &input[tok.start.index..tok.end.index];
329 builder.token(SyntaxKind::YAML_FLOW_INDICATOR.into(), text);
330 continue;
331 }
332 TokenKind::FlowMappingEnd => {
333 close_open_sub_wrapper(&mut builder, &mut block_stack);
334 let text = &input[tok.start.index..tok.end.index];
335 builder.token(SyntaxKind::YAML_FLOW_INDICATOR.into(), text);
336 if matches!(
337 block_stack.last(),
338 Some(BlockFrame::FlowMap { .. } | BlockFrame::FlowSequence { .. })
339 ) {
340 block_stack.pop();
341 builder.finish_node();
342 }
343 continue;
344 }
345 TokenKind::FlowEntry => {
346 close_open_sub_wrapper(&mut builder, &mut block_stack);
349 let text = &input[tok.start.index..tok.end.index];
350 builder.token(SyntaxKind::YAML_FLOW_INDICATOR.into(), text);
351 continue;
352 }
353 TokenKind::Key => {
354 close_indentless_sequences(&mut builder, &mut block_stack);
358 if matches!(
363 block_stack.last(),
364 Some(BlockFrame::BlockMap { .. } | BlockFrame::FlowMap { .. })
365 ) {
366 open_map_entry_with_key(&mut builder, &mut block_stack);
367 }
368 if tok.start.index == tok.end.index {
369 continue;
371 }
372 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
374 }
377 TokenKind::Value => {
378 close_indentless_sequences(&mut builder, &mut block_stack);
381 let map_state = match block_stack.last().copied() {
382 Some(BlockFrame::BlockMap {
383 entry_open,
384 in_value,
385 }) => Some((false, entry_open, in_value)),
386 Some(BlockFrame::FlowMap {
387 entry_open,
388 in_value,
389 }) => Some((true, entry_open, in_value)),
390 _ => None,
391 };
392 if let Some((is_flow, mut entry_open, mut in_value)) = map_state {
393 if !is_flow && entry_open && in_value {
405 close_open_sub_wrapper(&mut builder, &mut block_stack);
406 entry_open = false;
407 in_value = false;
408 }
409 if !entry_open {
412 open_map_entry_with_key(&mut builder, &mut block_stack);
413 }
414 if !in_value {
415 let text = &input[tok.start.index..tok.end.index];
418 if !text.is_empty() {
419 builder.token(SyntaxKind::YAML_COLON.into(), text);
420 }
421 builder.finish_node(); let value_kind = if is_flow {
423 SyntaxKind::YAML_FLOW_MAP_VALUE
424 } else {
425 SyntaxKind::YAML_BLOCK_MAP_VALUE
426 };
427 builder.start_node(value_kind.into());
428 if let Some(
429 BlockFrame::BlockMap { in_value, .. }
430 | BlockFrame::FlowMap { in_value, .. },
431 ) = block_stack.last_mut()
432 {
433 *in_value = true;
434 }
435 continue;
436 }
437 }
441 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
444 }
445 TokenKind::BlockEntry => {
446 let decorations_inside_value =
462 decorations_so_far.is_none_or(|c| c > tok.start.column);
463 let indentless_value = last_significant == Some(TokenKind::Value)
464 && matches!(
465 block_stack.last(),
466 Some(BlockFrame::BlockMap { in_value: true, .. })
467 )
468 && decorations_inside_value;
469 let indentless_key = last_significant == Some(TokenKind::Key)
476 && matches!(
477 block_stack.last(),
478 Some(BlockFrame::BlockMap {
479 entry_open: true,
480 in_value: false,
481 })
482 )
483 && decorations_inside_value;
484 if indentless_value || indentless_key {
485 builder.start_node(SyntaxKind::YAML_BLOCK_SEQUENCE.into());
486 block_stack.push(BlockFrame::BlockSequence {
487 item_open: false,
488 indentless: true,
489 });
490 }
491 if matches!(block_stack.last(), Some(BlockFrame::BlockSequence { .. })) {
492 close_open_sub_wrapper(&mut builder, &mut block_stack);
493 builder.start_node(SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM.into());
494 if let Some(BlockFrame::BlockSequence { item_open, .. }) =
495 block_stack.last_mut()
496 {
497 *item_open = true;
498 }
499 }
500 }
503 TokenKind::Trivia(_) => {
504 }
507 _ => {
508 if !matches!(tok.kind, TokenKind::DocumentStart | TokenKind::DocumentEnd) {
513 ensure_flow_seq_item_open(&mut builder, &mut block_stack);
514 }
515 }
516 }
517 let text = &input[tok.start.index..tok.end.index];
518 if text.is_empty() {
519 continue;
521 }
522 let kind = map_token_to_syntax_kind(tok.kind);
523 match tok.kind {
524 TokenKind::DocumentStart => {
525 if doc_open && doc_only_has_directives {
536 builder.token(kind.into(), text);
537 doc_only_has_directives = false;
538 } else {
539 close_block_containers(&mut builder, &mut block_stack);
540 if doc_open {
541 builder.finish_node();
542 }
543 builder.start_node(SyntaxKind::YAML_DOCUMENT.into());
544 doc_open = true;
545 doc_only_has_directives = false;
546 builder.token(kind.into(), text);
547 }
548 }
549 TokenKind::DocumentEnd => {
550 close_block_containers(&mut builder, &mut block_stack);
554 if !doc_open {
555 builder.start_node(SyntaxKind::YAML_DOCUMENT.into());
556 }
557 builder.token(kind.into(), text);
558 builder.finish_node();
559 doc_open = false;
560 doc_only_has_directives = false;
561 }
562 TokenKind::Trivia(_) => {
563 builder.token(kind.into(), text);
568 }
569 TokenKind::Directive => {
570 let was_open = doc_open;
574 ensure_doc_open(&mut builder, &mut doc_open);
575 if !was_open {
576 doc_only_has_directives = true;
577 }
578 builder.token(kind.into(), text);
579 }
580 TokenKind::Scalar(_) => {
581 ensure_doc_open(&mut builder, &mut doc_open);
588 doc_only_has_directives = false;
589 emit_scalar_node(&mut builder, text, line_prefix);
590 }
591 _ => {
592 ensure_doc_open(&mut builder, &mut doc_open);
597 doc_only_has_directives = false;
598 builder.token(kind.into(), text);
599 }
600 }
601 }
602 close_block_containers(&mut builder, &mut block_stack);
608 if doc_open {
609 builder.finish_node();
610 }
611 builder.finish_node();
612 SyntaxNode::new_root(builder.finish())
613}
614
615#[derive(Debug, Clone, Copy)]
625enum BlockFrame {
626 BlockMap {
627 entry_open: bool,
628 in_value: bool,
629 },
630 BlockSequence {
637 item_open: bool,
638 indentless: bool,
639 },
640 FlowMap {
641 entry_open: bool,
642 in_value: bool,
643 },
644 FlowSequence {
645 item_open: bool,
646 },
647}
648
649fn ensure_doc_open(builder: &mut GreenNodeBuilder<'_>, doc_open: &mut bool) {
650 if !*doc_open {
651 builder.start_node(SyntaxKind::YAML_DOCUMENT.into());
652 *doc_open = true;
653 }
654}
655
656fn ensure_flow_seq_item_open(builder: &mut GreenNodeBuilder<'_>, stack: &mut [BlockFrame]) {
661 if let Some(BlockFrame::FlowSequence { item_open }) = stack.last_mut()
662 && !*item_open
663 {
664 builder.start_node(SyntaxKind::YAML_FLOW_SEQUENCE_ITEM.into());
665 *item_open = true;
666 }
667}
668
669fn open_map_entry_with_key(builder: &mut GreenNodeBuilder<'_>, stack: &mut [BlockFrame]) {
673 close_open_sub_wrapper(builder, stack);
674 let (entry_kind, key_kind) = match stack.last() {
675 Some(BlockFrame::BlockMap { .. }) => (
676 SyntaxKind::YAML_BLOCK_MAP_ENTRY,
677 SyntaxKind::YAML_BLOCK_MAP_KEY,
678 ),
679 Some(BlockFrame::FlowMap { .. }) => (
680 SyntaxKind::YAML_FLOW_MAP_ENTRY,
681 SyntaxKind::YAML_FLOW_MAP_KEY,
682 ),
683 _ => return,
684 };
685 builder.start_node(entry_kind.into());
686 builder.start_node(key_kind.into());
687 if let Some(
688 BlockFrame::BlockMap {
689 entry_open,
690 in_value,
691 }
692 | BlockFrame::FlowMap {
693 entry_open,
694 in_value,
695 },
696 ) = stack.last_mut()
697 {
698 *entry_open = true;
699 *in_value = false;
700 }
701}
702
703fn close_indentless_sequences(builder: &mut GreenNodeBuilder<'_>, stack: &mut Vec<BlockFrame>) {
712 while let Some(BlockFrame::BlockSequence {
713 indentless: true, ..
714 }) = stack.last()
715 {
716 close_open_sub_wrapper(builder, stack);
717 stack.pop();
718 builder.finish_node(); }
720}
721
722fn close_open_sub_wrapper(builder: &mut GreenNodeBuilder<'_>, stack: &mut [BlockFrame]) {
732 let Some(frame) = stack.last_mut() else {
733 return;
734 };
735 match frame {
736 BlockFrame::BlockMap {
737 entry_open: true,
738 in_value,
739 } => {
740 if *in_value {
741 builder.finish_node(); } else {
743 builder.finish_node(); builder.start_node(SyntaxKind::YAML_BLOCK_MAP_VALUE.into());
745 builder.finish_node(); }
747 builder.finish_node(); *frame = BlockFrame::BlockMap {
749 entry_open: false,
750 in_value: false,
751 };
752 }
753 BlockFrame::FlowMap {
754 entry_open: true,
755 in_value,
756 } => {
757 if *in_value {
758 builder.finish_node();
759 } else {
760 builder.finish_node();
761 builder.start_node(SyntaxKind::YAML_FLOW_MAP_VALUE.into());
762 builder.finish_node();
763 }
764 builder.finish_node();
765 *frame = BlockFrame::FlowMap {
766 entry_open: false,
767 in_value: false,
768 };
769 }
770 BlockFrame::BlockSequence {
771 item_open: true,
772 indentless,
773 } => {
774 let indentless = *indentless;
775 builder.finish_node();
776 *frame = BlockFrame::BlockSequence {
777 item_open: false,
778 indentless,
779 };
780 }
781 BlockFrame::FlowSequence { item_open: true } => {
782 builder.finish_node();
783 *frame = BlockFrame::FlowSequence { item_open: false };
784 }
785 _ => {}
786 }
787}
788
789fn close_block_containers(builder: &mut GreenNodeBuilder<'_>, stack: &mut Vec<BlockFrame>) {
790 while let Some(frame) = stack.pop() {
791 match frame {
792 BlockFrame::BlockMap {
793 entry_open: true,
794 in_value,
795 } => {
796 if in_value {
797 builder.finish_node(); } else {
799 builder.finish_node(); builder.start_node(SyntaxKind::YAML_BLOCK_MAP_VALUE.into());
801 builder.finish_node();
802 }
803 builder.finish_node(); }
805 BlockFrame::FlowMap {
806 entry_open: true,
807 in_value,
808 } => {
809 if in_value {
810 builder.finish_node();
811 } else {
812 builder.finish_node();
813 builder.start_node(SyntaxKind::YAML_FLOW_MAP_VALUE.into());
814 builder.finish_node();
815 }
816 builder.finish_node();
817 }
818 BlockFrame::BlockSequence {
819 item_open: true, ..
820 }
821 | BlockFrame::FlowSequence { item_open: true } => {
822 builder.finish_node();
823 }
824 _ => {}
825 }
826 builder.finish_node();
827 }
828}
829
830fn emit_scalar_node(
839 builder: &mut GreenNodeBuilder<'static>,
840 text: &str,
841 line_prefix: Option<&str>,
842) {
843 builder.start_node(SyntaxKind::YAML_SCALAR.into());
844 emit_scalar_fragments(builder, text, line_prefix);
845 builder.finish_node();
846}
847
848fn emit_scalar_fragments(
860 builder: &mut GreenNodeBuilder<'static>,
861 text: &str,
862 line_prefix: Option<&str>,
863) {
864 let bytes = text.as_bytes();
865 let mut i = 0;
866 let mut line_index = 0usize;
867 while i < bytes.len() {
868 if line_index > 0
870 && let Some(prefix) = line_prefix
871 && let Some(len) = prefix_match_len(&text[i..], prefix)
872 {
873 builder.token(SyntaxKind::YAML_LINE_PREFIX.into(), &text[i..i + len]);
874 i += len;
875 }
876 let content_start = i;
878 while i < bytes.len() && !matches!(bytes[i], b'\n' | b'\r') {
879 i += 1;
880 }
881 if content_start < i {
882 builder.token(SyntaxKind::YAML_SCALAR_TEXT.into(), &text[content_start..i]);
883 }
884 if i < bytes.len() {
886 let nl_len = if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
887 2
888 } else {
889 1
890 };
891 builder.token(SyntaxKind::NEWLINE.into(), &text[i..i + nl_len]);
892 i += nl_len;
893 line_index += 1;
894 }
895 }
896}
897
898fn prefix_match_len(s: &str, marker: &str) -> Option<usize> {
902 let after = s.strip_prefix(marker)?;
903 Some(marker.len() + usize::from(after.starts_with(' ')))
904}
905
906fn map_token_to_syntax_kind(kind: TokenKind) -> SyntaxKind {
907 match kind {
908 TokenKind::Trivia(TriviaKind::Whitespace) => SyntaxKind::WHITESPACE,
909 TokenKind::Trivia(TriviaKind::Newline) => SyntaxKind::NEWLINE,
910 TokenKind::Trivia(TriviaKind::Comment) => SyntaxKind::YAML_COMMENT,
911 TokenKind::Trivia(TriviaKind::LinePrefix) => SyntaxKind::YAML_LINE_PREFIX,
912 TokenKind::DocumentStart => SyntaxKind::YAML_DOCUMENT_START,
913 TokenKind::DocumentEnd => SyntaxKind::YAML_DOCUMENT_END,
914 TokenKind::Directive => SyntaxKind::YAML_DIRECTIVE,
915 TokenKind::BlockEntry => SyntaxKind::YAML_BLOCK_SEQ_ENTRY,
916 TokenKind::FlowEntry => SyntaxKind::YAML_FLOW_INDICATOR,
917 TokenKind::FlowSequenceStart | TokenKind::FlowSequenceEnd => {
918 SyntaxKind::YAML_FLOW_INDICATOR
919 }
920 TokenKind::FlowMappingStart | TokenKind::FlowMappingEnd => SyntaxKind::YAML_FLOW_INDICATOR,
921 TokenKind::Value => SyntaxKind::YAML_COLON,
922 TokenKind::Anchor => SyntaxKind::YAML_ANCHOR,
923 TokenKind::Alias => SyntaxKind::YAML_ALIAS,
924 TokenKind::Tag => SyntaxKind::YAML_TAG,
925 TokenKind::Scalar(_) => SyntaxKind::YAML_SCALAR_TEXT,
930 TokenKind::Key => SyntaxKind::YAML_KEY,
933 TokenKind::StreamStart
936 | TokenKind::StreamEnd
937 | TokenKind::BlockSequenceStart
938 | TokenKind::BlockMappingStart
939 | TokenKind::BlockEnd => SyntaxKind::YAML_FLOW_INDICATOR,
940 }
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946 use crate::syntax::SyntaxKind;
947
948 fn assert_lossless(input: &str) {
950 assert_eq!(
951 parse_stream(input).text().to_string(),
952 input,
953 "input {input:?} not preserved"
954 );
955 }
956
957 #[test]
958 fn strip_with_offsets_matches_strip_line_prefix() {
959 for input in [
960 "#| a: 1\n",
961 "#| a: 1\n#| b\n",
962 " #| x: 1\n",
963 "#| a\r\n#| b\r\n",
964 "#| a",
965 ] {
966 let (text, offsets) = strip_line_prefix_with_offsets(input, "#|");
967 assert_eq!(text, strip_line_prefix(input, "#|"), "text for {input:?}");
968 assert_eq!(offsets.len(), text.len() + 1, "offset count for {input:?}");
969 assert!(
970 offsets.iter().all(|&o| o <= input.len()),
971 "offsets in bounds for {input:?}"
972 );
973 }
974 }
975
976 #[test]
977 fn locate_maps_hashpipe_error_to_region_offset() {
978 let input = "#| echo: [\n";
979 let (_diag, start, _end) = locate_yaml_diagnostic(input, "#|").expect("diagnostic");
980 assert_eq!(start, input.find('[').unwrap());
981 }
982
983 #[test]
984 fn locate_maps_composite_marker_error() {
985 let input = " #| echo: [\n";
987 let (_diag, start, _end) = locate_yaml_diagnostic(input, " #|").expect("diagnostic");
988 assert_eq!(start, input.find('[').unwrap());
989 }
990
991 #[test]
992 fn locate_maps_crlf_region_error() {
993 let input = "#| ok: 1\r\n#| echo: [\r\n";
994 let (_diag, start, _end) = locate_yaml_diagnostic(input, "#|").expect("diagnostic");
995 assert_eq!(start, input.find('[').unwrap());
996 }
997
998 #[test]
999 fn locate_frontmatter_uses_identity_offsets() {
1000 let input = "title: [\n";
1001 let (diag, start, _end) = locate_yaml_diagnostic(input, "").expect("diagnostic");
1002 assert_eq!(start, diag.byte_start);
1003 assert_eq!(start, input.find('[').unwrap());
1004 }
1005
1006 #[test]
1007 fn locate_returns_none_for_valid_yaml() {
1008 assert!(locate_yaml_diagnostic("#| echo: false\n", "#|").is_none());
1009 assert!(locate_yaml_diagnostic("title: ok\n", "").is_none());
1010 }
1011
1012 #[test]
1013 fn block_scalar_followed_by_option_is_not_swallowed_as_comment() {
1014 let input = "#| fig-cap: |\n#| A caption\n#| echo: false\n";
1018 let tree = parse_stream_with_prefix(input, "#|");
1019 assert_eq!(tree.to_string(), input, "byte-lossless");
1020 let entries = tree
1021 .descendants()
1022 .filter(|node| node.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
1023 .count();
1024 assert_eq!(entries, 2, "expected fig-cap and echo entries");
1025 assert!(
1026 !tree
1027 .descendants_with_tokens()
1028 .any(|element| element.kind() == SyntaxKind::YAML_COMMENT),
1029 "the option line must not be scanned as a comment"
1030 );
1031 }
1032
1033 #[test]
1034 fn returns_byte_lossless_cst_for_empty_input() {
1035 assert_lossless("");
1036 }
1037
1038 #[test]
1039 fn returns_byte_lossless_cst_for_simple_mapping() {
1040 assert_lossless("key: value\n");
1041 }
1042
1043 #[test]
1044 fn returns_byte_lossless_cst_for_block_sequence() {
1045 assert_lossless("- a\n- b\n");
1046 }
1047
1048 #[test]
1049 fn returns_byte_lossless_cst_for_flow_mapping() {
1050 assert_lossless("{a: b, c: d}\n");
1051 }
1052
1053 #[test]
1054 fn returns_byte_lossless_cst_for_block_scalar() {
1055 assert_lossless("key: |\n hello\n world\n");
1056 }
1057
1058 #[test]
1059 fn returns_byte_lossless_cst_for_quoted_scalar() {
1060 assert_lossless("\"key\": \"value\"\n");
1061 }
1062
1063 #[test]
1064 fn returns_byte_lossless_cst_for_multi_line_plain_scalar() {
1065 assert_lossless("key: hello\n world\n");
1066 }
1067
1068 #[test]
1069 fn preserves_explicit_key_indicator_byte_in_flow_context() {
1070 assert_lossless("{ ?foo: bar }\n");
1075 }
1076
1077 #[test]
1078 fn does_not_absorb_terminator_line_break_into_flow_scalar() {
1079 assert_lossless("{a: 42\n}\n");
1085 }
1086
1087 fn document_count(tree: &SyntaxNode) -> usize {
1088 tree.children()
1089 .filter(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
1090 .count()
1091 }
1092
1093 #[test]
1094 fn implicit_document_wraps_body_with_no_markers() {
1095 let input = "key: value\n";
1098 let tree = parse_stream(input);
1099 assert_eq!(document_count(&tree), 1);
1100 assert_eq!(tree.text().to_string(), input);
1101 }
1102
1103 #[test]
1104 fn explicit_doc_start_opens_document_marker_lives_inside() {
1105 let input = "---\nkey: value\n";
1106 let tree = parse_stream(input);
1107 assert_eq!(document_count(&tree), 1);
1108 let doc = tree
1109 .children()
1110 .find(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
1111 .expect("document node");
1112 assert!(
1113 doc.children_with_tokens().any(|el| el
1114 .as_token()
1115 .is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_START)),
1116 "`---` token should live inside YAML_DOCUMENT"
1117 );
1118 assert_eq!(tree.text().to_string(), input);
1119 }
1120
1121 #[test]
1122 fn explicit_doc_end_closes_document_marker_lives_inside() {
1123 let input = "key: value\n...\n";
1124 let tree = parse_stream(input);
1125 assert_eq!(document_count(&tree), 1);
1126 let doc = tree
1127 .children()
1128 .find(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
1129 .expect("document node");
1130 assert!(
1131 doc.children_with_tokens().any(|el| el
1132 .as_token()
1133 .is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_END)),
1134 "`...` token should live inside YAML_DOCUMENT"
1135 );
1136 assert_eq!(tree.text().to_string(), input);
1137 }
1138
1139 #[test]
1140 fn consecutive_doc_starts_emit_two_documents() {
1141 let input = "---\na\n---\nb\n";
1142 let tree = parse_stream(input);
1143 assert_eq!(document_count(&tree), 2);
1144 assert_eq!(tree.text().to_string(), input);
1145 }
1146
1147 #[test]
1148 fn pre_document_trivia_stays_at_stream_level() {
1149 let input = "\n---\nkey: value\n";
1153 let tree = parse_stream(input);
1154 let stream_token_kinds: Vec<SyntaxKind> = tree
1155 .children_with_tokens()
1156 .filter_map(|el| el.into_token())
1157 .map(|t| t.kind())
1158 .collect();
1159 assert!(
1160 stream_token_kinds.contains(&SyntaxKind::NEWLINE),
1161 "leading newline should be a direct child of YAML_STREAM, got {stream_token_kinds:?}"
1162 );
1163 assert_eq!(tree.text().to_string(), input);
1164 }
1165
1166 #[test]
1167 fn bare_doc_end_at_stream_start_opens_synthetic_empty_document() {
1168 let input = "...\n";
1172 let tree = parse_stream(input);
1173 assert_eq!(document_count(&tree), 1);
1174 assert_eq!(tree.text().to_string(), input);
1175 }
1176
1177 fn first_document(tree: &SyntaxNode) -> SyntaxNode {
1178 tree.children()
1179 .find(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
1180 .expect("at least one document")
1181 }
1182
1183 fn block_map_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
1184 parent
1185 .children()
1186 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
1187 }
1188
1189 fn block_seq_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
1190 parent
1191 .children()
1192 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
1193 }
1194
1195 fn block_map_entries(map: &SyntaxNode) -> Vec<SyntaxNode> {
1196 map.children()
1197 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
1198 .collect()
1199 }
1200
1201 fn block_seq_items(seq: &SyntaxNode) -> Vec<SyntaxNode> {
1202 seq.children()
1203 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
1204 .collect()
1205 }
1206
1207 fn entry_key(entry: &SyntaxNode) -> SyntaxNode {
1208 entry
1209 .children()
1210 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY)
1211 .expect("entry should have a YAML_BLOCK_MAP_KEY child")
1212 }
1213
1214 fn entry_value(entry: &SyntaxNode) -> SyntaxNode {
1215 entry
1216 .children()
1217 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
1218 .expect("entry should have a YAML_BLOCK_MAP_VALUE child")
1219 }
1220
1221 #[test]
1222 fn consecutive_empty_key_colons_open_separate_entries() {
1223 let input = ": a\n: b\n";
1230 let tree = parse_stream(input);
1231 let doc = first_document(&tree);
1232 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1233 let entries = block_map_entries(&map);
1234 assert_eq!(entries.len(), 2, "expected two empty-key ENTRY nodes");
1235 for (entry, scalar) in entries.iter().zip(["a", "b"]) {
1236 let key = entry_key(entry);
1237 assert!(
1239 !key.children().any(|n| n.kind() == SyntaxKind::YAML_SCALAR),
1240 "empty key should carry no scalar, got {key:?}",
1241 );
1242 let value = entry_value(entry);
1243 assert!(
1244 value
1245 .children()
1246 .any(|n| n.kind() == SyntaxKind::YAML_SCALAR && n.text() == scalar),
1247 "value should be {scalar:?}, got {value:?}",
1248 );
1249 }
1250 assert_eq!(tree.text().to_string(), input);
1251 }
1252
1253 #[test]
1254 fn block_mapping_wraps_key_value_with_key_and_value_sub_wrappers() {
1255 let input = "key: value\n";
1256 let tree = parse_stream(input);
1257 let doc = first_document(&tree);
1258 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1259 let entries = block_map_entries(&map);
1260 assert_eq!(entries.len(), 1, "expected one ENTRY for `key: value`");
1261 let key = entry_key(&entries[0]);
1262 let value = entry_value(&entries[0]);
1263 assert!(
1265 key.children_with_tokens().any(|el| el
1266 .as_token()
1267 .is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
1268 "colon should be the trailing token of YAML_BLOCK_MAP_KEY",
1269 );
1270 assert!(
1271 value
1272 .children()
1273 .any(|n| n.kind() == SyntaxKind::YAML_SCALAR),
1274 "scalar `value` should live inside YAML_BLOCK_MAP_VALUE",
1275 );
1276 assert_eq!(tree.text().to_string(), input);
1277 }
1278
1279 #[test]
1280 fn block_sequence_wraps_entries_in_yaml_block_sequence() {
1281 let input = "- a\n- b\n";
1282 let tree = parse_stream(input);
1283 let doc = first_document(&tree);
1284 let seq = block_seq_under(&doc).expect("YAML_BLOCK_SEQUENCE child");
1285 let items = block_seq_items(&seq);
1286 assert_eq!(items.len(), 2, "expected 2 YAML_BLOCK_SEQUENCE_ITEM");
1287 for item in &items {
1289 let dash_count = item
1290 .children_with_tokens()
1291 .filter(|el| {
1292 el.as_token()
1293 .is_some_and(|t| t.kind() == SyntaxKind::YAML_BLOCK_SEQ_ENTRY)
1294 })
1295 .count();
1296 assert_eq!(dash_count, 1, "each item owns exactly one `-` token");
1297 }
1298 assert_eq!(tree.text().to_string(), input);
1299 }
1300
1301 #[test]
1302 fn nested_block_mapping_nests_inner_block_map_inside_outer_value() {
1303 let input = "outer:\n inner: x\n";
1304 let tree = parse_stream(input);
1305 let doc = first_document(&tree);
1306 let outer = block_map_under(&doc).expect("outer YAML_BLOCK_MAP");
1307 let outer_entries = block_map_entries(&outer);
1308 assert_eq!(outer_entries.len(), 1);
1309 let outer_value = entry_value(&outer_entries[0]);
1310 let inner = outer_value
1311 .children()
1312 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
1313 .expect("inner YAML_BLOCK_MAP nested under outer VALUE");
1314 let inner_entries = block_map_entries(&inner);
1315 assert_eq!(inner_entries.len(), 1);
1316 let inner_key = entry_key(&inner_entries[0]);
1317 assert!(
1318 inner_key.children_with_tokens().any(|el| el
1319 .as_token()
1320 .is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
1321 "inner key should own its colon",
1322 );
1323 assert_eq!(tree.text().to_string(), input);
1324 }
1325
1326 #[test]
1327 fn block_sequence_inside_mapping_nests_under_outer_map_value() {
1328 let input = "items:\n - a\n - b\n";
1329 let tree = parse_stream(input);
1330 let doc = first_document(&tree);
1331 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1332 let entries = block_map_entries(&map);
1333 assert_eq!(entries.len(), 1, "one entry: `items: <seq>`");
1334 let value = entry_value(&entries[0]);
1335 let seq = value
1336 .children()
1337 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
1338 .expect("YAML_BLOCK_SEQUENCE nested under map VALUE");
1339 let items = block_seq_items(&seq);
1340 assert_eq!(items.len(), 2);
1341 assert_eq!(tree.text().to_string(), input);
1342 }
1343
1344 #[test]
1345 fn dedent_closes_inner_block_map_before_next_outer_key() {
1346 let input = "outer:\n inner: x\nsibling: y\n";
1353 let tree = parse_stream(input);
1354 let doc = first_document(&tree);
1355 let outer = block_map_under(&doc).expect("outer YAML_BLOCK_MAP");
1356 let entries = block_map_entries(&outer);
1357 assert_eq!(
1358 entries.len(),
1359 2,
1360 "outer map should have two entries (`outer:` and `sibling:`)",
1361 );
1362 let first_value = entry_value(&entries[0]);
1364 let nested_in_first = first_value
1365 .children()
1366 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
1367 .count();
1368 assert_eq!(nested_in_first, 1);
1369 let second_value = entry_value(&entries[1]);
1370 let nested_in_second = second_value
1371 .children()
1372 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
1373 .count();
1374 assert_eq!(nested_in_second, 0);
1375 assert_eq!(tree.text().to_string(), input);
1376 }
1377
1378 #[test]
1379 fn block_map_with_two_top_level_entries_emits_two_entry_wrappers() {
1380 let input = "a: 1\nb: 2\n";
1381 let tree = parse_stream(input);
1382 let doc = first_document(&tree);
1383 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1384 assert_eq!(block_map_entries(&map).len(), 2);
1385 assert_eq!(tree.text().to_string(), input);
1386 }
1387
1388 #[test]
1389 fn explicit_key_indicator_question_mark_lives_inside_key() {
1390 let input = "? a\n: b\n";
1394 let tree = parse_stream(input);
1395 let doc = first_document(&tree);
1396 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1397 let entries = block_map_entries(&map);
1398 assert_eq!(entries.len(), 1);
1399 let key = entry_key(&entries[0]);
1400 let has_question = key.children_with_tokens().any(|el| {
1401 el.as_token()
1402 .is_some_and(|t| t.kind() == SyntaxKind::YAML_KEY)
1403 });
1404 assert!(has_question, "`?` should live inside YAML_BLOCK_MAP_KEY");
1405 assert_eq!(tree.text().to_string(), input);
1406 }
1407
1408 #[test]
1409 fn explicit_key_indentless_sequence_wraps_inside_key() {
1410 let input = "?\n- a\n- b\n:\n- c\n- d\n";
1417 let tree = parse_stream(input);
1418 let doc = first_document(&tree);
1419 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1420 let entries = block_map_entries(&map);
1421 assert_eq!(entries.len(), 1);
1422 let key = entry_key(&entries[0]);
1423 assert!(
1424 key.children()
1425 .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE),
1426 "explicit-key block sequence should be wrapped in YAML_BLOCK_SEQUENCE inside KEY",
1427 );
1428 let value = entry_value(&entries[0]);
1429 assert!(
1430 value
1431 .children()
1432 .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE),
1433 "value-side block sequence should remain wrapped",
1434 );
1435 assert_eq!(tree.text().to_string(), input);
1436 }
1437
1438 #[test]
1439 fn empty_key_shorthand_opens_entry_with_empty_key() {
1440 let input = ": value\n";
1444 let tree = parse_stream(input);
1445 let doc = first_document(&tree);
1446 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1447 let entries = block_map_entries(&map);
1448 assert_eq!(entries.len(), 1);
1449 let key = entry_key(&entries[0]);
1450 assert!(
1452 !key.children().any(|n| n.kind() == SyntaxKind::YAML_SCALAR),
1453 "empty-key shorthand has no scalar in KEY",
1454 );
1455 assert!(
1456 key.children_with_tokens().any(|el| el
1457 .as_token()
1458 .is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
1459 "empty-key KEY still owns the `:` token",
1460 );
1461 let value = entry_value(&entries[0]);
1462 assert!(
1463 value
1464 .children()
1465 .any(|n| n.kind() == SyntaxKind::YAML_SCALAR),
1466 "VALUE owns the `value` scalar",
1467 );
1468 assert_eq!(tree.text().to_string(), input);
1469 }
1470
1471 #[test]
1472 fn document_end_marker_lives_at_document_level_not_inside_block_map() {
1473 let input = "key: value\n...\n";
1477 let tree = parse_stream(input);
1478 let doc = first_document(&tree);
1479 let has_doc_end = doc.children_with_tokens().any(|el| {
1480 el.as_token()
1481 .is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_END)
1482 });
1483 assert!(
1484 has_doc_end,
1485 "DOCUMENT_END should be a direct child of YAML_DOCUMENT"
1486 );
1487 assert_eq!(tree.text().to_string(), input);
1488 }
1489
1490 fn flow_map_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
1491 parent
1492 .children()
1493 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
1494 }
1495
1496 fn flow_seq_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
1497 parent
1498 .children()
1499 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
1500 }
1501
1502 fn flow_map_entries(map: &SyntaxNode) -> Vec<SyntaxNode> {
1503 map.children()
1504 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
1505 .collect()
1506 }
1507
1508 fn flow_seq_items(seq: &SyntaxNode) -> Vec<SyntaxNode> {
1509 seq.children()
1510 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
1511 .collect()
1512 }
1513
1514 #[test]
1515 fn flow_sequence_wraps_each_item_in_flow_sequence_item() {
1516 let input = "[a, b, c]\n";
1517 let tree = parse_stream(input);
1518 let doc = first_document(&tree);
1519 let seq = flow_seq_under(&doc).expect("YAML_FLOW_SEQUENCE child");
1520 let items = flow_seq_items(&seq);
1521 assert_eq!(items.len(), 3);
1522 let bracket_count = seq
1525 .children_with_tokens()
1526 .filter(|el| {
1527 el.as_token().map(|t| t.text()) == Some("[")
1528 || el.as_token().map(|t| t.text()) == Some("]")
1529 })
1530 .count();
1531 assert_eq!(bracket_count, 2, "`[` and `]` at SEQUENCE level");
1532 assert_eq!(tree.text().to_string(), input);
1533 }
1534
1535 #[test]
1536 fn flow_mapping_wraps_each_entry_with_key_and_value() {
1537 let input = "{a: 1, b: 2}\n";
1538 let tree = parse_stream(input);
1539 let doc = first_document(&tree);
1540 let map = flow_map_under(&doc).expect("YAML_FLOW_MAP child");
1541 let entries = flow_map_entries(&map);
1542 assert_eq!(entries.len(), 2);
1543 for entry in &entries {
1544 let key = entry
1545 .children()
1546 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_KEY)
1547 .expect("entry has YAML_FLOW_MAP_KEY");
1548 assert!(
1549 key.children_with_tokens().any(|el| el
1550 .as_token()
1551 .is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
1552 "flow KEY owns trailing `:`",
1553 );
1554 let value = entry
1555 .children()
1556 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_VALUE)
1557 .expect("entry has YAML_FLOW_MAP_VALUE");
1558 assert!(
1559 value
1560 .children()
1561 .any(|n| n.kind() == SyntaxKind::YAML_SCALAR),
1562 "flow VALUE owns its scalar",
1563 );
1564 }
1565 assert_eq!(tree.text().to_string(), input);
1566 }
1567
1568 #[test]
1569 fn flow_sequence_inside_flow_sequence_nests_under_outer_item() {
1570 let input = "[[1, 2], [3, 4]]\n";
1571 let tree = parse_stream(input);
1572 let doc = first_document(&tree);
1573 let outer = flow_seq_under(&doc).expect("outer YAML_FLOW_SEQUENCE");
1574 let outer_items = flow_seq_items(&outer);
1575 assert_eq!(outer_items.len(), 2);
1576 for item in &outer_items {
1577 assert!(
1578 item.children()
1579 .any(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE),
1580 "outer item should contain a nested YAML_FLOW_SEQUENCE",
1581 );
1582 }
1583 assert_eq!(tree.text().to_string(), input);
1584 }
1585
1586 #[test]
1587 fn flow_mapping_inside_flow_sequence_nests_under_item() {
1588 let input = "[{a: 1}, {b: 2}]\n";
1589 let tree = parse_stream(input);
1590 let doc = first_document(&tree);
1591 let seq = flow_seq_under(&doc).expect("YAML_FLOW_SEQUENCE child");
1592 let items = flow_seq_items(&seq);
1593 assert_eq!(items.len(), 2);
1594 for item in &items {
1595 assert!(
1596 item.children()
1597 .any(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP),
1598 "each item should contain a nested YAML_FLOW_MAP",
1599 );
1600 }
1601 assert_eq!(tree.text().to_string(), input);
1602 }
1603
1604 #[test]
1605 fn flow_mapping_at_block_map_value_nests_under_block_map_value() {
1606 let input = "key: {a: 1, b: 2}\n";
1607 let tree = parse_stream(input);
1608 let doc = first_document(&tree);
1609 let block_map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
1610 let entries = block_map_entries(&block_map);
1611 assert_eq!(entries.len(), 1);
1612 let value = entry_value(&entries[0]);
1613 assert!(
1614 value
1615 .children()
1616 .any(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP),
1617 "flow map should be nested under outer block map's VALUE",
1618 );
1619 assert_eq!(tree.text().to_string(), input);
1620 }
1621
1622 #[test]
1623 fn directive_prelude_stays_inside_document_opened_by_marker() {
1624 let input = "%TAG !e! tag:example.com,2000:app/\n---\n!e!foo \"bar\"\n";
1629 let tree = parse_stream(input);
1630 assert_eq!(document_count(&tree), 1);
1631 let doc = first_document(&tree);
1632 let has_doc_start = doc.children_with_tokens().any(|el| {
1633 el.as_token()
1634 .is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_START)
1635 });
1636 assert!(has_doc_start, "the `---` should live inside the same doc");
1637 assert_eq!(tree.text().to_string(), input);
1638 }
1639
1640 #[test]
1641 fn explicit_key_without_value_emits_empty_value_for_shape_parity() {
1642 let input = "? a\n? b\n";
1646 let tree = parse_stream(input);
1647 let doc = first_document(&tree);
1648 let map = block_map_under(&doc).expect("YAML_BLOCK_MAP");
1649 let entries = block_map_entries(&map);
1650 assert_eq!(entries.len(), 2);
1651 for entry in &entries {
1652 assert!(
1653 entry
1654 .children()
1655 .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY),
1656 "ENTRY missing KEY child",
1657 );
1658 assert!(
1659 entry
1660 .children()
1661 .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE),
1662 "ENTRY missing VALUE child",
1663 );
1664 }
1665 }
1666}