1#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct WhitespaceError {
6 pub message: String,
8 pub range: std::ops::Range<usize>,
10 pub category: WhitespaceErrorCategory,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum WhitespaceErrorCategory {
17 TabIndentation,
19 LineTooLong,
21 MixedLineEndings,
23 InvalidIndentation,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
130#[repr(u16)]
131#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
132pub enum SyntaxKind {
133 ROOT = 0,
136 DOCUMENT,
138 SEQUENCE,
140 MAPPING,
142 SCALAR,
144 ALIAS,
146 TAGGED_NODE,
148 ERROR,
150
151 DASH,
154 PLUS,
156 COLON,
158 QUESTION,
160 LEFT_BRACKET,
162 RIGHT_BRACKET,
164 LEFT_BRACE,
166 RIGHT_BRACE,
168 COMMA,
170 PIPE,
172 GREATER,
174 AMPERSAND,
176 ASTERISK,
178 EXCLAMATION,
180 PERCENT,
182 AT,
184 BACKTICK,
186 QUOTE,
188 SINGLE_QUOTE,
190
191 DOC_START,
194 DOC_END,
196
197 KEY,
200 VALUE,
202 MAPPING_ENTRY,
204 SEQUENCE_ENTRY,
206
207 STRING,
210 UNTERMINATED_STRING,
212 INT,
214 FLOAT,
216 BOOL,
218 NULL,
220 TAG,
222 ANCHOR,
224 REFERENCE,
226 MERGE_KEY,
228 DIRECTIVE,
230
231 WHITESPACE,
234 NEWLINE,
236 INDENT,
238 COMMENT,
240
241 BOM,
244 EOF,
246}
247
248impl From<SyntaxKind> for rowan::SyntaxKind {
249 fn from(kind: SyntaxKind) -> Self {
250 Self(kind as u16)
251 }
252}
253
254fn plain_scalar_continues_past_whitespace(input: &str, ws_idx: usize, _flow_depth: u32) -> bool {
268 let rest = &input[ws_idx..];
269 let (offset, next) = match rest.char_indices().find(|(_, c)| *c != ' ' && *c != '\t') {
271 Some(v) => v,
272 None => return false,
273 };
274 match next {
275 '\n' | '\r' | '#' => false,
276 ',' | '[' | ']' | '{' | '}' => false,
277 ':' => {
278 let colon_pos = ws_idx + offset;
279 let after_colon = &input[colon_pos + 1..];
280 after_colon
281 .chars()
282 .next()
283 .is_some_and(|nc| !nc.is_whitespace())
284 }
285 c if is_yaml_special(c) => false,
286 _ => true,
287 }
288}
289
290fn read_scalar_from<'a>(
292 chars: &mut std::iter::Peekable<std::str::CharIndices<'a>>,
293 input: &'a str,
294 start_idx: usize,
295 exclude_chars: &str,
296) -> &'a str {
297 let mut end_idx = start_idx;
298 while let Some((idx, ch)) = chars.peek() {
299 if ch.is_whitespace() || is_yaml_special_except(*ch, exclude_chars) {
300 break;
301 }
302 end_idx = *idx + ch.len_utf8();
303 chars.next();
304 }
305 &input[start_idx..end_idx]
306}
307
308pub fn lex(input: &str) -> Vec<(SyntaxKind, &str)> {
310 let (tokens, _) = lex_with_validation(input);
311 tokens
312}
313
314pub struct ValidationConfig {
316 pub max_line_length: Option<usize>,
318 pub enforce_consistent_line_endings: bool,
320}
321
322impl Default for ValidationConfig {
323 fn default() -> Self {
324 Self {
325 max_line_length: Some(120), enforce_consistent_line_endings: true,
327 }
328 }
329}
330
331pub fn lex_with_validation(input: &str) -> (Vec<(SyntaxKind, &str)>, Vec<WhitespaceError>) {
333 lex_with_validation_config(input, &ValidationConfig::default())
334}
335
336pub fn lex_with_validation_config<'a>(
338 input: &'a str,
339 config: &ValidationConfig,
340) -> (Vec<(SyntaxKind, &'a str)>, Vec<WhitespaceError>) {
341 use SyntaxKind::*;
342
343 let mut tokens = Vec::with_capacity(input.len() / 8); let mut chars = input.char_indices().peekable();
345 let mut whitespace_errors = Vec::new();
346 let bytes = input.as_bytes();
347
348 let mut current_line_start = 0;
350 let mut detected_line_ending: Option<&str> = None;
351
352 let mut flow_depth: u32 = 0;
354
355 if let Some((0, '\u{FEFF}')) = chars.peek() {
358 chars.next(); tokens.push((BOM, "\u{FEFF}"));
360 }
361
362 while let Some((start_idx, ch)) = chars.next() {
363 let token_start = start_idx;
364
365 match ch {
366 '-' => {
368 if let Some((_, '-')) = chars.peek() {
369 chars.next(); if let Some((_, '-')) = chars.peek() {
371 chars.next(); tokens.push((DOC_START, &input[token_start..start_idx + 3]));
373 } else {
374 tokens.push((DASH, &input[token_start..start_idx + 1]));
376 tokens.push((DASH, &input[start_idx + 1..start_idx + 2]));
377 }
378 } else {
379 let line_start_pos = input[..token_start]
388 .rfind(['\n', '\r'])
389 .map(|pos| pos + 1)
390 .unwrap_or(0);
391 let before_dash = &input[line_start_pos..token_start];
392 let only_whitespace_before = before_dash.chars().all(|c| c == ' ' || c == '\t');
393
394 let after_value_indicator = tokens
397 .iter()
398 .rev()
399 .find(|(kind, _)| !matches!(kind, WHITESPACE | INDENT))
400 .is_some_and(|(kind, _)| matches!(kind, QUESTION | COLON));
401
402 let followed_by_whitespace_or_end = chars
404 .peek()
405 .map_or(true, |(_, next_ch)| next_ch.is_whitespace());
406
407 let is_sequence_marker = (only_whitespace_before || after_value_indicator)
408 && followed_by_whitespace_or_end;
409
410 if is_sequence_marker {
411 tokens.push((DASH, &input[token_start..start_idx + 1]));
412 } else {
413 let text = read_scalar_from(&mut chars, input, start_idx + 1, "-");
415 let full_text = &input[token_start..token_start + 1 + text.len()];
416 let token_kind = classify_scalar(full_text);
417 tokens.push((token_kind, full_text));
418 }
419 }
420 }
421 '+' => tokens.push((PLUS, &input[token_start..start_idx + 1])),
422 ':' => {
423 if flow_depth > 0 {
426 tokens.push((COLON, &input[token_start..start_idx + 1]));
428 } else if let Some((_, next_ch)) = chars.peek() {
429 if next_ch.is_whitespace() {
430 tokens.push((COLON, &input[token_start..start_idx + 1]));
432 } else {
433 let mut end_idx = start_idx + 1;
436 while let Some((idx, next_ch)) = chars.peek() {
437 if next_ch.is_whitespace() {
438 break;
439 }
440 if is_yaml_special_except(*next_ch, ":") {
442 break;
443 }
444 end_idx = *idx + next_ch.len_utf8();
445 chars.next();
446 }
447 let text = &input[token_start..end_idx];
448 tokens.push((classify_scalar(text), text));
449 }
450 } else {
451 tokens.push((COLON, &input[token_start..start_idx + 1]));
453 }
454 }
455 '?' => tokens.push((QUESTION, &input[token_start..start_idx + 1])),
456 '[' => {
457 flow_depth += 1;
458 tokens.push((LEFT_BRACKET, &input[token_start..start_idx + 1]));
459 }
460 ']' => {
461 flow_depth = flow_depth.saturating_sub(1);
462 tokens.push((RIGHT_BRACKET, &input[token_start..start_idx + 1]));
463 }
464 '{' => {
465 flow_depth += 1;
466 tokens.push((LEFT_BRACE, &input[token_start..start_idx + 1]));
467 }
468 '}' => {
469 flow_depth = flow_depth.saturating_sub(1);
470 tokens.push((RIGHT_BRACE, &input[token_start..start_idx + 1]));
471 }
472 ',' => tokens.push((COMMA, &input[token_start..start_idx + 1])),
473 '|' => tokens.push((PIPE, &input[token_start..start_idx + 1])),
474 '>' => tokens.push((GREATER, &input[token_start..start_idx + 1])),
475 '<' => {
476 if let Some((_, '<')) = chars.peek() {
478 chars.next(); tokens.push((MERGE_KEY, &input[token_start..start_idx + 2]));
480 } else {
481 let mut end_idx = start_idx + 1;
483 while let Some((idx, ch)) = chars.peek() {
484 if ch.is_whitespace() || is_yaml_special(*ch) {
485 break;
486 }
487 end_idx = *idx + ch.len_utf8();
488 chars.next();
489 }
490 let text = &input[token_start..end_idx];
491 let token_kind = classify_scalar(text);
492 tokens.push((token_kind, text));
493 }
494 }
495 '&' => {
496 let name = read_scalar_from(&mut chars, input, start_idx + 1, "");
498 if !name.is_empty() {
499 tokens.push((ANCHOR, &input[token_start..start_idx + 1 + name.len()]));
500 } else {
501 tokens.push((AMPERSAND, &input[token_start..start_idx + 1]));
502 }
503 }
504 '*' => {
505 let name = read_scalar_from(&mut chars, input, start_idx + 1, "");
507 if !name.is_empty() {
508 tokens.push((REFERENCE, &input[token_start..start_idx + 1 + name.len()]));
509 } else {
510 tokens.push((ASTERISK, &input[token_start..start_idx + 1]));
511 }
512 }
513 '"' => {
514 let mut end_idx = start_idx + 1;
516 let mut escaped = false;
517 let mut found_closing = false;
518
519 while let Some((idx, ch)) = chars.peek() {
520 let current_idx = *idx;
521 let current_ch = *ch;
522
523 if escaped {
524 escaped = false;
525 end_idx = current_idx + current_ch.len_utf8();
526 chars.next();
527 continue;
528 }
529
530 if current_ch == '\\' {
531 escaped = true;
532 end_idx = current_idx + current_ch.len_utf8();
533 chars.next();
534 } else if current_ch == '"' {
535 end_idx = current_idx + current_ch.len_utf8();
536 chars.next();
537 found_closing = true;
538 break;
539 } else {
540 end_idx = current_idx + current_ch.len_utf8();
541 chars.next();
542 }
543 }
544
545 if found_closing {
546 tokens.push((STRING, &input[token_start..end_idx]));
547 } else {
548 tokens.push((UNTERMINATED_STRING, &input[token_start..end_idx]));
550 }
551 }
552 '\'' => {
553 let mut end_idx = start_idx + 1;
555 let mut found_closing = false;
556
557 while let Some((idx, ch)) = chars.peek() {
558 let current_idx = *idx;
559 let current_ch = *ch;
560
561 if current_ch == '\'' {
562 end_idx = current_idx + current_ch.len_utf8();
564 chars.next();
565 if let Some((next_idx, '\'')) = chars.peek() {
566 end_idx = *next_idx + 1;
568 chars.next();
569 } else {
570 found_closing = true;
572 break;
573 }
574 } else {
575 end_idx = current_idx + current_ch.len_utf8();
576 chars.next();
577 }
578 }
579
580 if found_closing {
581 tokens.push((STRING, &input[token_start..end_idx]));
582 } else {
583 tokens.push((UNTERMINATED_STRING, &input[token_start..end_idx]));
585 }
586 }
587
588 '.' => {
590 if chars.peek() == Some(&(start_idx + 1, '.')) {
592 chars.next(); if chars.peek() == Some(&(start_idx + 2, '.')) {
594 chars.next(); tokens.push((DOC_END, &input[token_start..start_idx + 3]));
596 } else {
597 let rest = read_scalar_from(&mut chars, input, start_idx + 2, "");
599 let text = &input[token_start..start_idx + 2 + rest.len()];
600 let token_kind = classify_scalar(text);
601 tokens.push((token_kind, text));
602 }
603 } else {
604 let rest = read_scalar_from(&mut chars, input, start_idx + 1, "");
606 let text = &input[token_start..start_idx + 1 + rest.len()];
607 let token_kind = classify_scalar(text);
608 tokens.push((token_kind, text));
609 }
610 }
611
612 '#' => {
614 let mut end_idx = start_idx + 1;
615 while let Some((idx, ch)) = chars.peek() {
616 if *ch == '\n' || *ch == '\r' {
617 break;
618 }
619 end_idx = *idx + ch.len_utf8();
620 chars.next();
621 }
622 tokens.push((COMMENT, &input[token_start..end_idx]));
623 }
624
625 '!' => {
627 let mut end_idx = start_idx + 1;
629
630 if let Some((_, '!')) = chars.peek() {
632 chars.next(); end_idx = start_idx + 2;
634 }
635
636 while let Some((idx, ch)) = chars.peek() {
638 if ch.is_whitespace() || is_yaml_special(*ch) {
639 break;
640 }
641 end_idx = *idx + ch.len_utf8();
642 chars.next();
643 }
644
645 tokens.push((TAG, &input[token_start..end_idx]));
646 }
647
648 '%' => {
649 if flow_depth > 0 {
651 let mut end_idx = start_idx + 1;
653 while let Some((idx, next_ch)) = chars.peek() {
654 if next_ch.is_whitespace() {
655 break;
656 }
657 if is_yaml_special_except(*next_ch, "%") {
658 break;
659 }
660 end_idx = *idx + next_ch.len_utf8();
661 chars.next();
662 }
663 let text = &input[token_start..end_idx];
664 tokens.push((classify_scalar(text), text));
665 } else {
666 let mut end_idx = start_idx + 1;
668 while let Some((idx, ch)) = chars.peek() {
669 if *ch == '\n' || *ch == '\r' {
670 break;
671 }
672 end_idx = *idx + ch.len_utf8();
673 chars.next();
674 }
675 tokens.push((DIRECTIVE, &input[token_start..end_idx]));
676 }
677 }
678
679 '\n' => {
681 if let Some(max_len) = config.max_line_length {
683 let line_length = start_idx - current_line_start;
684 if line_length > max_len {
685 whitespace_errors.push(WhitespaceError {
686 message: format!(
687 "Line too long ({} > {} characters)",
688 line_length, max_len
689 ),
690 range: current_line_start..start_idx,
691 category: WhitespaceErrorCategory::LineTooLong,
692 });
693 }
694 }
695
696 let line_ending = "\n";
698 if config.enforce_consistent_line_endings {
699 if let Some(detected) = detected_line_ending {
700 if detected != line_ending {
701 whitespace_errors.push(WhitespaceError {
702 message: "Inconsistent line endings detected".to_string(),
703 range: token_start..start_idx + 1,
704 category: WhitespaceErrorCategory::MixedLineEndings,
705 });
706 }
707 } else {
708 detected_line_ending = Some(line_ending);
709 }
710 }
711
712 tokens.push((NEWLINE, &input[token_start..start_idx + 1]));
713 current_line_start = start_idx + 1;
714 }
715 '\r' => {
716 if let Some(max_len) = config.max_line_length {
718 let line_length = start_idx - current_line_start;
719 if line_length > max_len {
720 whitespace_errors.push(WhitespaceError {
721 message: format!(
722 "Line too long ({} > {} characters)",
723 line_length, max_len
724 ),
725 range: current_line_start..start_idx,
726 category: WhitespaceErrorCategory::LineTooLong,
727 });
728 }
729 }
730
731 let (line_ending, end_pos) = if let Some((_, '\n')) = chars.peek() {
732 chars.next();
733 ("\r\n", start_idx + 2)
734 } else {
735 ("\r", start_idx + 1)
736 };
737
738 if config.enforce_consistent_line_endings {
740 if let Some(detected) = detected_line_ending {
741 if detected != line_ending {
742 whitespace_errors.push(WhitespaceError {
743 message: "Inconsistent line endings detected".to_string(),
744 range: token_start..end_pos,
745 category: WhitespaceErrorCategory::MixedLineEndings,
746 });
747 }
748 } else {
749 detected_line_ending = Some(line_ending);
750 }
751 }
752
753 tokens.push((NEWLINE, &input[token_start..end_pos]));
754 current_line_start = end_pos;
755 }
756
757 ' ' | '\t' => {
759 let mut end_idx = start_idx + 1;
760 let mut has_tabs = ch == '\t';
761
762 while let Some((idx, ch)) = chars.peek() {
763 if *ch != ' ' && *ch != '\t' {
764 break;
765 }
766 if *ch == '\t' {
767 has_tabs = true;
768 }
769 end_idx = *idx + 1;
770 chars.next();
771 }
772
773 let is_indentation = token_start == 0
776 || (token_start > 0
777 && (bytes[token_start - 1] == b'\n' || bytes[token_start - 1] == b'\r'));
778
779 if is_indentation {
780 if has_tabs {
782 whitespace_errors.push(WhitespaceError {
783 message: "Tab character used for indentation (forbidden in YAML)"
784 .to_string(),
785 range: token_start..end_idx,
786 category: WhitespaceErrorCategory::TabIndentation,
787 });
788 }
789 tokens.push((INDENT, &input[token_start..end_idx]));
790 } else {
791 tokens.push((WHITESPACE, &input[token_start..end_idx]));
792 }
793 }
794
795 _ => {
797 let mut end_idx = start_idx + ch.len_utf8();
798
799 while let Some((idx, next_ch)) = chars.peek().copied() {
801 if next_ch == '\n' || next_ch == '\r' {
803 break;
804 }
805
806 if next_ch == ' ' || next_ch == '\t' {
812 if !plain_scalar_continues_past_whitespace(input, idx, flow_depth) {
813 break;
814 }
815 while let Some((wi, wc)) = chars.peek().copied() {
817 if wc != ' ' && wc != '\t' {
818 break;
819 }
820 end_idx = wi + wc.len_utf8();
821 chars.next();
822 }
823 continue;
824 }
825
826 if next_ch == ':' {
829 let next_idx = idx + next_ch.len_utf8();
831 if next_idx >= input.len() {
832 break;
834 } else if let Some(after) = input[next_idx..].chars().next() {
835 if after.is_whitespace() {
836 break;
838 }
839 }
840 end_idx = idx + next_ch.len_utf8();
842 chars.next();
843 continue;
844 }
845
846 if is_yaml_special_except(next_ch, "-:") {
848 if flow_depth == 0 && matches!(next_ch, '[' | ']' | '{' | '}' | ',') {
850 } else {
852 break;
853 }
854 }
855
856 if next_ch == '-' {
858 let line_start = input[..idx].rfind('\n').map(|p| p + 1).unwrap_or(0);
861 let before_hyphen = &input[line_start..idx];
862
863 if before_hyphen.chars().all(|c| c == ' ' || c == '\t') && idx == end_idx {
866 break;
867 }
868 }
869
870 end_idx = idx + next_ch.len_utf8();
871 chars.next();
872 }
873
874 let text = &input[token_start..end_idx];
875 tokens.push((classify_scalar(text), text));
876 }
877 }
878 }
879
880 if let Some(max_len) = config.max_line_length {
882 let final_line_length = input.len() - current_line_start;
883 if final_line_length > max_len && final_line_length > 0 {
884 whitespace_errors.push(WhitespaceError {
885 message: format!(
886 "Line too long ({} > {} characters)",
887 final_line_length, max_len
888 ),
889 range: current_line_start..input.len(),
890 category: WhitespaceErrorCategory::LineTooLong,
891 });
892 }
893 }
894
895 (tokens, whitespace_errors)
896}
897
898fn classify_scalar(text: &str) -> SyntaxKind {
900 use SyntaxKind::*;
901
902 match text {
904 "true" | "false" | "True" | "False" | "TRUE" | "FALSE" => return BOOL,
905 "null" | "Null" | "NULL" | "~" => return NULL,
906 _ => {}
907 }
908
909 if crate::scalar::ScalarValue::parse_integer(text).is_some() {
911 return INT;
912 }
913
914 match text {
919 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf" | "-.INF"
920 | ".nan" | ".NaN" | ".NAN" => return FLOAT,
921 "infinity" | "inf" | "Infinity" | "Inf" | "INFINITY" | "INF" | "-infinity" | "-inf"
924 | "-Infinity" | "-Inf" | "-INFINITY" | "-INF" | "+infinity" | "+inf" | "+Infinity"
925 | "+Inf" | "+INFINITY" | "+INF" | "nan" | "NaN" | "NAN" => return STRING,
926 _ => {}
927 }
928
929 if text.parse::<f64>().is_ok() {
931 return FLOAT;
932 }
933
934 STRING
936}
937
938const YAML_SPECIAL_CHARS: &str = ":+-?[]{},'|>&*!%\"#";
940
941fn is_yaml_special(ch: char) -> bool {
943 YAML_SPECIAL_CHARS.contains(ch)
944}
945
946fn is_yaml_special_except(ch: char, exclude: &str) -> bool {
948 YAML_SPECIAL_CHARS.contains(ch) && !exclude.contains(ch)
949}
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954
955 #[test]
956 fn test_simple_mapping() {
957 let input = "key: value";
958 let tokens = lex(input);
959
960 assert_eq!(tokens.len(), 4);
961 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
962 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
963 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
964 assert_eq!(tokens[3], (SyntaxKind::STRING, "value"));
965 }
966
967 #[test]
968 fn test_scalar_types() {
969 let tokens = lex("age: 42");
971 assert_eq!(tokens[0], (SyntaxKind::STRING, "age"));
972 assert_eq!(tokens[3], (SyntaxKind::INT, "42"));
973
974 let tokens = lex("pi: 3.14");
976 assert_eq!(tokens[0], (SyntaxKind::STRING, "pi"));
977 assert_eq!(tokens[3], (SyntaxKind::FLOAT, "3.14"));
978
979 let tokens = lex("enabled: true");
981 assert_eq!(tokens[0], (SyntaxKind::STRING, "enabled"));
982 assert_eq!(tokens[3], (SyntaxKind::BOOL, "true"));
983
984 let tokens = lex("disabled: false");
986 assert_eq!(tokens[3], (SyntaxKind::BOOL, "false"));
987
988 let tokens = lex("value: null");
990 assert_eq!(tokens[3], (SyntaxKind::NULL, "null"));
991
992 let tokens = lex("value: ~");
994 assert_eq!(tokens[3], (SyntaxKind::NULL, "~"));
995 }
996
997 #[test]
998 fn test_sequences() {
999 let input = "- item1\n- item2";
1000 let tokens = lex(input);
1001
1002 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1003 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1004 assert_eq!(tokens[2], (SyntaxKind::STRING, "item1"));
1005 assert_eq!(tokens[3], (SyntaxKind::NEWLINE, "\n"));
1006 assert_eq!(tokens[4], (SyntaxKind::DASH, "-"));
1007 assert_eq!(tokens[5], (SyntaxKind::WHITESPACE, " "));
1008 assert_eq!(tokens[6], (SyntaxKind::STRING, "item2"));
1009 }
1010
1011 #[test]
1012 fn test_hyphen_in_scalars() {
1013 let input = "Name: example-project";
1015 let tokens = lex(input);
1016
1017 println!("Hyphen test tokens:");
1018 for (i, (kind, text)) in tokens.iter().enumerate() {
1019 println!(" {}: {:?} = {:?}", i, kind, text);
1020 }
1021
1022 assert_eq!(tokens.len(), 4);
1024 assert_eq!(tokens[0], (SyntaxKind::STRING, "Name"));
1025 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1026 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1027 assert_eq!(tokens[3], (SyntaxKind::STRING, "example-project"));
1028 }
1029
1030 #[test]
1031 fn test_hyphen_sequence_vs_scalar() {
1032 let sequence_input = "- example-item";
1034 let tokens = lex(sequence_input);
1035
1036 println!("Sequence hyphen tokens:");
1037 for (i, (kind, text)) in tokens.iter().enumerate() {
1038 println!(" {}: {:?} = {:?}", i, kind, text);
1039 }
1040
1041 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1043 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1044 assert_eq!(tokens[2], (SyntaxKind::STRING, "example-item"));
1045
1046 let scalar_input = "package-name: my-awesome-package";
1048 let tokens = lex(scalar_input);
1049
1050 println!("Package hyphen tokens:");
1051 for (i, (kind, text)) in tokens.iter().enumerate() {
1052 println!(" {}: {:?} = {:?}", i, kind, text);
1053 }
1054
1055 assert_eq!(tokens.len(), 4);
1057 assert_eq!(tokens[0], (SyntaxKind::STRING, "package-name"));
1058 assert_eq!(tokens[3], (SyntaxKind::STRING, "my-awesome-package"));
1059 }
1060
1061 #[test]
1062 fn test_flow_style() {
1063 let tokens = lex("[1, 2, 3]");
1065 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACKET, "["));
1066 assert_eq!(tokens[1], (SyntaxKind::INT, "1"));
1067 assert_eq!(tokens[2], (SyntaxKind::COMMA, ","));
1068 assert_eq!(tokens[3], (SyntaxKind::WHITESPACE, " "));
1069 assert_eq!(tokens[4], (SyntaxKind::INT, "2"));
1070 assert_eq!(tokens[5], (SyntaxKind::COMMA, ","));
1071 assert_eq!(tokens[6], (SyntaxKind::WHITESPACE, " "));
1072 assert_eq!(tokens[7], (SyntaxKind::INT, "3"));
1073 assert_eq!(tokens[8], (SyntaxKind::RIGHT_BRACKET, "]"));
1074
1075 let tokens = lex("{a: 1, b: 2}");
1077 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACE, "{"));
1078 assert_eq!(tokens[1], (SyntaxKind::STRING, "a"));
1079 assert_eq!(tokens[2], (SyntaxKind::COLON, ":"));
1080 assert_eq!(tokens[3], (SyntaxKind::WHITESPACE, " "));
1081 assert_eq!(tokens[4], (SyntaxKind::INT, "1"));
1082 }
1083
1084 #[test]
1085 fn test_comments() {
1086 let input = "key: value # this is a comment\n# full line comment";
1087 let tokens = lex(input);
1088
1089 let comments: Vec<_> = tokens
1091 .iter()
1092 .filter(|(kind, _)| *kind == SyntaxKind::COMMENT)
1093 .collect();
1094
1095 assert_eq!(comments.len(), 2);
1096 assert_eq!(comments[0].1, "# this is a comment");
1097 assert_eq!(comments[1].1, "# full line comment");
1098 }
1099
1100 #[test]
1101 fn test_multiline_scalar() {
1102 let input = "key: value\n continued";
1103 let tokens = lex(input);
1104
1105 let indents: Vec<_> = tokens
1107 .iter()
1108 .filter(|(kind, _)| *kind == SyntaxKind::INDENT)
1109 .collect();
1110 assert_eq!(indents.len(), 1);
1111 assert_eq!(indents[0].1, " ");
1112 }
1113
1114 #[test]
1115 fn test_quoted_strings() {
1116 let input = r#"single: 'quoted'
1117double: "quoted""#;
1118 let tokens = lex(input);
1119
1120 let quoted_strings: Vec<_> = tokens
1122 .iter()
1123 .filter(|(kind, text)| {
1124 *kind == SyntaxKind::STRING && (text.starts_with('\'') || text.starts_with('"'))
1125 })
1126 .collect();
1127 assert_eq!(quoted_strings.len(), 2); let quoted_texts: Vec<&str> = {
1131 let mut v: Vec<&str> = quoted_strings.iter().map(|(_, t)| *t).collect();
1132 v.sort();
1133 v
1134 };
1135 assert_eq!(quoted_texts, ["\"quoted\"", "'quoted'"]);
1136 }
1137
1138 #[test]
1139 fn test_document_markers() {
1140 let input = "---\nkey: value\n...";
1141 let tokens = lex(input);
1142
1143 println!("Document tokens:");
1144 for (i, (kind, text)) in tokens.iter().enumerate() {
1145 println!(" {}: {:?} = {:?}", i, kind, text);
1146 }
1147
1148 let doc_start_count = tokens
1150 .iter()
1151 .filter(|(kind, _)| *kind == SyntaxKind::DOC_START)
1152 .count();
1153 let doc_end_count = tokens
1154 .iter()
1155 .filter(|(kind, _)| *kind == SyntaxKind::DOC_END)
1156 .count();
1157 assert_eq!(doc_start_count, 1);
1158 assert_eq!(doc_end_count, 1);
1159 }
1160
1161 #[test]
1162 fn test_empty_input() {
1163 let input = "";
1164 let tokens = lex(input);
1165 println!("Empty input tokens: {:?}", tokens);
1166 assert_eq!(tokens.len(), 0);
1167 }
1168
1169 #[test]
1170 fn test_anchors_and_aliases() {
1171 let input = "key: &anchor_name value";
1173 let tokens = lex(input);
1174 println!("Anchor tokens: {:?}", tokens);
1175
1176 let anchors: Vec<_> = tokens
1177 .iter()
1178 .filter(|(kind, _)| *kind == SyntaxKind::ANCHOR)
1179 .collect();
1180 assert_eq!(anchors.len(), 1);
1181 assert_eq!(anchors[0].1, "&anchor_name");
1182
1183 let input = "key: *reference_name";
1185 let tokens = lex(input);
1186 println!("Reference tokens: {:?}", tokens);
1187
1188 let references: Vec<_> = tokens
1189 .iter()
1190 .filter(|(kind, _)| *kind == SyntaxKind::REFERENCE)
1191 .collect();
1192 assert_eq!(references.len(), 1);
1193 assert_eq!(references[0].1, "*reference_name");
1194
1195 let input = "key: & *";
1197 let tokens = lex(input);
1198
1199 let ampersands: Vec<_> = tokens
1200 .iter()
1201 .filter(|(kind, _)| *kind == SyntaxKind::AMPERSAND)
1202 .collect();
1203 assert_eq!(ampersands.len(), 1);
1204
1205 let asterisks: Vec<_> = tokens
1206 .iter()
1207 .filter(|(kind, _)| *kind == SyntaxKind::ASTERISK)
1208 .collect();
1209 assert_eq!(asterisks.len(), 1);
1210 }
1211
1212 #[test]
1213 fn test_merge_key_token() {
1214 let input = "<<: *defaults";
1216 let tokens = lex(input);
1217
1218 let merge_keys: Vec<_> = tokens
1219 .iter()
1220 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
1221 .collect();
1222 assert_eq!(merge_keys.len(), 1);
1223 assert_eq!(merge_keys[0].1, "<<");
1224
1225 let input2 = "key: < value";
1227 let tokens2 = lex(input2);
1228
1229 let merge_keys2: Vec<_> = tokens2
1230 .iter()
1231 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
1232 .collect();
1233 assert_eq!(merge_keys2.len(), 0, "Single < should not be a merge key");
1234 }
1235
1236 #[test]
1237 fn test_plus_token() {
1238 let input = "key: |+ value";
1240 let tokens = lex(input);
1241
1242 let plus_tokens: Vec<_> = tokens
1243 .iter()
1244 .filter(|(kind, _)| *kind == SyntaxKind::PLUS)
1245 .collect();
1246 assert_eq!(plus_tokens.len(), 1);
1247 assert_eq!(plus_tokens[0].1, "+");
1248 }
1249
1250 #[test]
1251 fn test_block_scalar_indicators() {
1252 let input1 = "key: |+ content";
1254 let tokens1 = lex(input1);
1255
1256 assert!(tokens1
1257 .iter()
1258 .any(|(kind, text)| *kind == SyntaxKind::PIPE && *text == "|"));
1259 assert!(tokens1
1260 .iter()
1261 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
1262
1263 let input2 = "key: >- content";
1265 let tokens2 = lex(input2);
1266
1267 assert!(tokens2
1268 .iter()
1269 .any(|(kind, text)| *kind == SyntaxKind::GREATER && *text == ">"));
1270 assert!(tokens2
1271 .iter()
1272 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-"));
1273
1274 let input3 = "key: |2+ content";
1276 let tokens3 = lex(input3);
1277
1278 assert!(tokens3
1279 .iter()
1280 .any(|(kind, text)| *kind == SyntaxKind::PIPE && *text == "|"));
1281 assert!(tokens3
1282 .iter()
1283 .any(|(kind, text)| *kind == SyntaxKind::INT && *text == "2"));
1284 assert!(tokens3
1285 .iter()
1286 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
1287 }
1288
1289 #[test]
1290 fn test_special_characters_in_block_content() {
1291 let input = "line with - and + and : characters";
1292 let tokens = lex(input);
1293
1294 assert!(tokens
1298 .iter()
1299 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "line with"));
1300 assert!(tokens
1301 .iter()
1302 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-"));
1303
1304 assert!(tokens
1306 .iter()
1307 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
1308 assert!(tokens
1309 .iter()
1310 .any(|(kind, text)| *kind == SyntaxKind::COLON && *text == ":"));
1311
1312 let joined: String = tokens.iter().map(|(_, t)| *t).collect();
1314 assert_eq!(joined, input);
1315 }
1316
1317 #[test]
1318 fn test_token_recognition() {
1319 let input = "key: |2+ \n content with - and : and > chars\n more content";
1320 let tokens = lex(input);
1321
1322 println!("Comprehensive tokens:");
1324 for (i, (kind, text)) in tokens.iter().enumerate() {
1325 println!(" {}: {:?} = {:?}", i, kind, text);
1326 }
1327
1328 let count = |k: SyntaxKind| tokens.iter().filter(|(kind, _)| *kind == k).count();
1330 assert_eq!(count(SyntaxKind::COLON), 2);
1332 assert_eq!(count(SyntaxKind::PIPE), 1); assert_eq!(count(SyntaxKind::INT), 1); assert_eq!(count(SyntaxKind::PLUS), 1); assert_eq!(count(SyntaxKind::GREATER), 1); assert_eq!(count(SyntaxKind::NEWLINE), 2); assert_eq!(count(SyntaxKind::INDENT), 2); assert!(count(SyntaxKind::STRING) >= 1, "expected STRING tokens");
1340
1341 assert_eq!(
1343 tokens
1344 .iter()
1345 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
1346 .count(),
1347 1
1348 );
1349 }
1350
1351 #[test]
1352 fn test_dash_handling() {
1353 let input = "---\nkey: value";
1355 let tokens = lex(input);
1356 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
1357
1358 let input = "---";
1360 let tokens = lex(input);
1361 assert_eq!(tokens.len(), 1);
1362 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
1363
1364 let input = "--";
1366 let tokens = lex(input);
1367 assert_eq!(tokens.len(), 2);
1368 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1369 assert_eq!(tokens[1], (SyntaxKind::DASH, "-"));
1370
1371 let input = "----";
1373 let tokens = lex(input);
1374 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
1375 assert_eq!(tokens[1], (SyntaxKind::STRING, "-"));
1376 }
1377
1378 #[test]
1379 fn test_dash_in_different_scalar_contexts() {
1380 let input = "package-name: my-awesome-package-v2";
1382 let tokens = lex(input);
1383 assert_eq!(tokens[0], (SyntaxKind::STRING, "package-name"));
1384 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1385 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1386 assert_eq!(tokens[3], (SyntaxKind::STRING, "my-awesome-package-v2"));
1387
1388 let input = "id: 123e4567-e89b-12d3-a456-426614174000";
1390 let tokens = lex(input);
1391 assert_eq!(tokens[0], (SyntaxKind::STRING, "id"));
1392 assert_eq!(
1393 tokens[3],
1394 (SyntaxKind::STRING, "123e4567-e89b-12d3-a456-426614174000")
1395 );
1396
1397 let input = "args: --verbose --log-level=debug";
1399 let tokens = lex(input);
1400 assert_eq!(
1402 tokens
1403 .windows(3)
1404 .filter(|w| {
1405 w[0] == (SyntaxKind::DASH, "-")
1406 && w[1] == (SyntaxKind::DASH, "-")
1407 && w[2] == (SyntaxKind::STRING, "verbose")
1408 })
1409 .count(),
1410 1
1411 );
1412
1413 let input = "temperature: -40";
1415 let tokens = lex(input);
1416 assert_eq!(
1418 tokens
1419 .iter()
1420 .filter(|(kind, text)| *kind == SyntaxKind::INT && *text == "-40")
1421 .count(),
1422 1
1423 );
1424
1425 let input = "range: 1-10";
1427 let tokens = lex(input);
1428 assert_eq!(
1429 tokens
1430 .iter()
1431 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "1-10")
1432 .count(),
1433 1
1434 );
1435 }
1436
1437 #[test]
1438 fn test_sequence_markers_with_indentation() {
1439 let input = "- item1\n- item2";
1441 let tokens = lex(input);
1442 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1443 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1444 assert_eq!(tokens[2], (SyntaxKind::STRING, "item1"));
1445
1446 let input = " - item1\n - item2";
1448 let tokens = lex(input);
1449 assert_eq!(tokens[0], (SyntaxKind::INDENT, " "));
1450 assert_eq!(tokens[1], (SyntaxKind::DASH, "-"));
1451
1452 let input = "- item1\n - nested1\n - nested2\n- item2";
1454 let tokens = lex(input);
1455 let dash_tokens: Vec<_> = tokens
1456 .iter()
1457 .filter(|(kind, _)| *kind == SyntaxKind::DASH)
1458 .collect();
1459 assert_eq!(dash_tokens.len(), 4); let input = "- first-item\n- second-item";
1463 let tokens = lex(input);
1464 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1465 assert_eq!(tokens[2], (SyntaxKind::STRING, "first-item"));
1466 assert_eq!(tokens[4], (SyntaxKind::DASH, "-"));
1467 assert_eq!(tokens[6], (SyntaxKind::STRING, "second-item"));
1468 }
1469
1470 #[test]
1471 fn test_dash_after_colon() {
1472 let input = "key:-value";
1476 let tokens = lex(input);
1477 assert_eq!(tokens[0], (SyntaxKind::STRING, "key:-value"));
1478
1479 let input = "key: -value";
1481 let tokens = lex(input);
1482 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
1483 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1484 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1485 assert_eq!(tokens[3], (SyntaxKind::STRING, "-value"));
1486 }
1487
1488 #[test]
1489 fn test_yaml_spec_compliant_colon_handling() {
1490 let input = "http://example.com:8080/path";
1496 let tokens = lex(input);
1497 assert_eq!(tokens.len(), 1);
1498 assert_eq!(
1499 tokens[0],
1500 (SyntaxKind::STRING, "http://example.com:8080/path")
1501 );
1502
1503 let input = "2024:12:31:23:59:59";
1505 let tokens = lex(input);
1506 assert_eq!(tokens.len(), 1);
1507 assert_eq!(tokens[0], (SyntaxKind::STRING, "2024:12:31:23:59:59"));
1508
1509 let input = "key: value";
1511 let tokens = lex(input);
1512 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
1513 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1514 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1515 assert_eq!(tokens[3], (SyntaxKind::STRING, "value"));
1516
1517 let input = "key:value";
1519 let tokens = lex(input);
1520 assert_eq!(tokens.len(), 1);
1521 assert_eq!(tokens[0], (SyntaxKind::STRING, "key:value"));
1522
1523 let input = "a:b:c:d";
1525 let tokens = lex(input);
1526 assert_eq!(tokens.len(), 1);
1527 assert_eq!(tokens[0], (SyntaxKind::STRING, "a:b:c:d"));
1528 }
1529
1530 #[test]
1531 fn test_block_scalar_with_chomping() {
1532 let count_kind = |toks: &[(SyntaxKind, &str)], k: SyntaxKind| {
1534 toks.iter().filter(|(kind, _)| *kind == k).count()
1535 };
1536
1537 let input = "text: |-\n content";
1539 let tokens = lex(input);
1540 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
1541 assert_eq!(
1542 tokens
1543 .iter()
1544 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
1545 .count(),
1546 1
1547 );
1548
1549 let input = "text: |+\n content";
1551 let tokens = lex(input);
1552 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
1553 assert_eq!(count_kind(&tokens, SyntaxKind::PLUS), 1);
1554
1555 let input = "text: >-\n content";
1557 let tokens = lex(input);
1558 assert_eq!(count_kind(&tokens, SyntaxKind::GREATER), 1);
1559 assert_eq!(
1560 tokens
1561 .iter()
1562 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
1563 .count(),
1564 1
1565 );
1566
1567 let input = "text: |2-\n content";
1569 let tokens = lex(input);
1570 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
1571 let has_2_token = tokens.iter().any(|(kind, text)| {
1573 (*kind == SyntaxKind::STRING || *kind == SyntaxKind::INT) && text.contains("2")
1574 });
1575 assert!(has_2_token, "expected a token containing '2'");
1576 }
1577
1578 #[test]
1579 fn test_dash_edge_cases() {
1580 let input = "value-";
1582 let tokens = lex(input);
1583 assert_eq!(tokens[0], (SyntaxKind::STRING, "value-"));
1584
1585 let input = "-value";
1587 let tokens = lex(input);
1588 assert_eq!(tokens[0], (SyntaxKind::STRING, "-value"));
1589
1590 let input = "key: a---b";
1592 let tokens = lex(input);
1593 assert_eq!(
1594 tokens
1595 .iter()
1596 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "a---b")
1597 .count(),
1598 1
1599 );
1600
1601 let input = "key: value-\nnext: item";
1603 let tokens = lex(input);
1604 assert!(tokens
1605 .iter()
1606 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "value-"));
1607
1608 let input = "snake_case-with-dash_mix";
1610 let tokens = lex(input);
1611 assert_eq!(tokens[0], (SyntaxKind::STRING, "snake_case-with-dash_mix"));
1612 }
1613
1614 #[test]
1615 fn test_whitespace_validation_tab_indentation() {
1616 let input_with_tabs = "key: value\n\tindented_key: indented_value";
1618 let (tokens, errors) = lex_with_validation(input_with_tabs);
1619
1620 assert_eq!(errors.len(), 1);
1622 assert_eq!(errors[0].category, WhitespaceErrorCategory::TabIndentation);
1623 assert_eq!(
1624 errors[0].message,
1625 "Tab character used for indentation (forbidden in YAML)"
1626 );
1627
1628 assert!(tokens
1630 .iter()
1631 .any(|(kind, text)| *kind == SyntaxKind::INDENT && text.contains('\t')));
1632 }
1633
1634 #[test]
1635 fn test_whitespace_validation_line_endings() {
1636 let input_mixed = "line1\nline2\r\nline3\rline4";
1638 let config = ValidationConfig {
1639 enforce_consistent_line_endings: true,
1640 max_line_length: None,
1641 };
1642 let (tokens, errors) = lex_with_validation_config(input_mixed, &config);
1643
1644 assert!(errors
1646 .iter()
1647 .any(|e| e.category == WhitespaceErrorCategory::MixedLineEndings));
1648
1649 let newlines: Vec<_> = tokens
1651 .iter()
1652 .filter(|(kind, _)| *kind == SyntaxKind::NEWLINE)
1653 .collect();
1654 assert_eq!(newlines.len(), 3); assert_eq!(newlines[0].1, "\n");
1656 assert_eq!(newlines[1].1, "\r\n");
1657 assert_eq!(newlines[2].1, "\r");
1658 }
1659
1660 #[test]
1661 fn test_whitespace_validation_line_length() {
1662 let long_line = format!("key: {}", "a".repeat(150));
1664 let config = ValidationConfig {
1665 enforce_consistent_line_endings: false,
1666 max_line_length: Some(120),
1667 };
1668 let (_, errors) = lex_with_validation_config(&long_line, &config);
1669
1670 assert_eq!(errors.len(), 1);
1672 assert_eq!(errors[0].category, WhitespaceErrorCategory::LineTooLong);
1673 assert_eq!(errors[0].message, "Line too long (155 > 120 characters)");
1674 }
1675
1676 #[test]
1677 fn test_whitespace_validation_disabled() {
1678 let input_with_issues = "key: value\n\tindented: with_tabs\n";
1680 let config = ValidationConfig {
1681 enforce_consistent_line_endings: false,
1682 max_line_length: None,
1683 };
1684 let (tokens, errors) = lex_with_validation_config(input_with_issues, &config);
1685
1686 assert_eq!(errors.len(), 1);
1688 assert_eq!(errors[0].category, WhitespaceErrorCategory::TabIndentation);
1689
1690 assert!(!tokens.is_empty());
1692 }
1693
1694 #[test]
1695 fn test_dash_in_flow_collections() {
1696 let input = "[item-one, item-two]";
1698 let tokens = lex(input);
1699 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACKET, "["));
1700 assert_eq!(tokens[1], (SyntaxKind::STRING, "item-one"));
1701 assert_eq!(tokens[2], (SyntaxKind::COMMA, ","));
1702 assert_eq!(tokens[4], (SyntaxKind::STRING, "item-two"));
1703 assert_eq!(tokens[5], (SyntaxKind::RIGHT_BRACKET, "]"));
1704
1705 let input = "{kebab-key: kebab-value}";
1707 let tokens = lex(input);
1708 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACE, "{"));
1709 assert_eq!(tokens[1], (SyntaxKind::STRING, "kebab-key"));
1710 assert_eq!(tokens[2], (SyntaxKind::COLON, ":"));
1711 assert_eq!(tokens[4], (SyntaxKind::STRING, "kebab-value"));
1712 assert_eq!(tokens[5], (SyntaxKind::RIGHT_BRACE, "}"));
1713 }
1714
1715 #[test]
1716 fn test_dash_with_quotes() {
1717 let input = r#"key: "- not a sequence marker""#;
1719 let tokens = lex(input);
1720 assert_eq!(
1721 tokens
1722 .iter()
1723 .filter(|(kind, text)| {
1724 *kind == SyntaxKind::STRING && *text == "\"- not a sequence marker\""
1725 })
1726 .count(),
1727 1
1728 );
1729
1730 let input = r#"key: '- also not a sequence marker'"#;
1731 let tokens = lex(input);
1732 assert_eq!(
1733 tokens
1734 .iter()
1735 .filter(|(kind, text)| {
1736 *kind == SyntaxKind::STRING && *text == "'- also not a sequence marker'"
1737 })
1738 .count(),
1739 1
1740 );
1741 }
1742
1743 #[test]
1744 fn test_dash_in_multiline_values() {
1745 let input = "description: This is a multi-\n line value with dashes";
1750 let tokens = lex(input);
1751 assert!(tokens
1752 .iter()
1753 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "This is a multi-"));
1754
1755 let input = "text: value\n - but this is not a sequence";
1757 let tokens = lex(input);
1758 let indent_dash: Vec<_> = tokens
1760 .windows(2)
1761 .filter(|w| w[0].0 == SyntaxKind::INDENT && w[1].0 == SyntaxKind::DASH)
1762 .collect();
1763 assert_eq!(indent_dash.len(), 1);
1764 }
1765
1766 #[test]
1767 fn test_dash_special_yaml_values() {
1768 let input = "date: 2024-01-15";
1770 let tokens = lex(input);
1771 assert!(tokens
1772 .iter()
1773 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "2024-01-15"));
1774
1775 let input = "timestamp: 2024-01-15T10:30:00-05:00";
1777 let tokens = lex(input);
1778 assert!(tokens.iter().any(
1780 |(kind, text)| *kind == SyntaxKind::STRING && *text == "2024-01-15T10:30:00-05:00"
1781 ));
1782
1783 let input = "version: 1.0.0-beta.1";
1785 let tokens = lex(input);
1786 assert!(tokens
1787 .iter()
1788 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "1.0.0-beta.1"));
1789 }
1790
1791 #[test]
1792 fn test_flow_indicators_in_block_scalar() {
1793 let input = "key: unix:///Users/${metadata.username}/path";
1796 let tokens = lex(input);
1797 assert_eq!(tokens.len(), 4);
1798 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
1799 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1800 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1801 assert_eq!(
1802 tokens[3],
1803 (
1804 SyntaxKind::STRING,
1805 "unix:///Users/${metadata.username}/path"
1806 )
1807 );
1808 }
1809}