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 {
263 let rest = &input[ws_idx..];
264 let (offset, next) = match rest.char_indices().find(|(_, c)| *c != ' ' && *c != '\t') {
266 Some(v) => v,
267 None => return false,
268 };
269 match next {
270 '\n' | '\r' | '#' => false,
271 ',' | '[' | ']' | '{' | '}' if flow_depth > 0 => false,
272 ':' => !is_colon_a_mapping_indicator(input, ws_idx + offset, flow_depth),
273 '-' | '+' | '?' | '|' | '>' | '&' | '*' | '!' | '%' | '\'' | '"' | ',' | '[' | ']'
278 | '{' | '}' => true,
279 c if is_yaml_special(c) => false,
280 _ => true,
281 }
282}
283
284fn read_anchor_name_from<'a>(
292 chars: &mut std::iter::Peekable<std::str::CharIndices<'a>>,
293 input: &'a str,
294 start_idx: usize,
295) -> &'a str {
296 let mut end_idx = start_idx;
297 while let Some((idx, ch)) = chars.peek().copied() {
298 if is_white_or_break(ch) || matches!(ch, ',' | '[' | ']' | '{' | '}') {
299 break;
300 }
301 end_idx = idx + ch.len_utf8();
302 chars.next();
303 }
304 &input[start_idx..end_idx]
305}
306
307fn is_colon_a_mapping_indicator(input: &str, colon_idx: usize, flow_depth: u32) -> bool {
323 if flow_depth > 0
328 && input[..colon_idx]
329 .chars()
330 .next_back()
331 .is_some_and(|ch| matches!(ch, '}' | ']' | '"' | '\''))
332 {
333 return true;
334 }
335 is_colon_followed_by_separator(input, colon_idx, flow_depth)
336}
337
338fn is_colon_followed_by_separator(input: &str, colon_idx: usize, flow_depth: u32) -> bool {
342 let after = input[colon_idx + 1..].chars().next();
343 match after {
344 None => true,
345 Some(ch) if is_white_or_break(ch) => true,
346 Some(ch) if flow_depth > 0 && matches!(ch, ',' | ']' | '}') => true,
347 _ => false,
348 }
349}
350
351fn is_hash_a_comment_start(input: &str, hash_idx: usize) -> bool {
359 hash_idx == 0
360 || input[..hash_idx]
361 .chars()
362 .next_back()
363 .is_some_and(is_white_or_break)
364}
365
366fn scan_double_quoted(
371 chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>,
372 start_idx: usize,
373) -> (usize, bool) {
374 let mut end_idx = start_idx + 1;
375 let mut escaped = false;
376
377 while let Some((idx, ch)) = chars.peek() {
378 let (current_idx, current_ch) = (*idx, *ch);
379 end_idx = current_idx + current_ch.len_utf8();
380 chars.next();
381
382 if escaped {
383 escaped = false;
384 } else if current_ch == '\\' {
385 escaped = true;
386 } else if current_ch == '"' {
387 return (end_idx, true);
388 }
389 }
390 (end_idx, false)
391}
392
393fn scan_single_quoted(
398 chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>,
399 start_idx: usize,
400) -> (usize, bool) {
401 let mut end_idx = start_idx + 1;
402
403 while let Some((idx, ch)) = chars.peek() {
404 let (current_idx, current_ch) = (*idx, *ch);
405 end_idx = current_idx + current_ch.len_utf8();
406 chars.next();
407
408 if current_ch == '\'' {
409 if let Some((next_idx, '\'')) = chars.peek() {
410 end_idx = *next_idx + 1;
412 chars.next();
413 } else {
414 return (end_idx, true);
415 }
416 }
417 }
418 (end_idx, false)
419}
420
421fn flow_collection_is_implicit_key(input: &str, open_idx: usize) -> bool {
425 let line = input[open_idx..]
426 .split(['\n', '\r'])
427 .next()
428 .unwrap_or_default();
429 let mut chars = line.char_indices().peekable();
430 let mut closers = Vec::new();
431 let mut prev = ' ';
432 while let Some((idx, ch)) = chars.next() {
433 match ch {
434 '[' => closers.push(']'),
435 '{' => closers.push('}'),
436 ']' | '}' => {
437 if closers.pop() != Some(ch) {
438 return false;
439 }
440 if closers.is_empty() {
441 let rest = &line[idx + 1..];
444 let comment = [" #", "\t#"].iter().filter_map(|c| rest.find(c)).min();
445 return rest[..comment.unwrap_or(rest.len())]
446 .match_indices(':')
447 .any(|(i, _)| is_colon_a_mapping_indicator(line, idx + 1 + i, 0));
448 }
449 }
450 '"' | '\'' if matches!(prev, ' ' | '\t' | '[' | '{' | ',' | ':') => {
452 let (_, closed) = if ch == '"' {
453 scan_double_quoted(&mut chars, idx)
454 } else {
455 scan_single_quoted(&mut chars, idx)
456 };
457 if !closed {
458 return false;
459 }
460 }
461 '#' if matches!(prev, ' ' | '\t') => return false,
462 _ => {}
463 }
464 prev = ch;
465 }
466 false
467}
468
469fn is_chomping_indicator(line_prefix: &str) -> bool {
473 line_prefix
474 .bytes()
475 .rev()
476 .find(|b| !b.is_ascii_digit())
477 .is_some_and(|b| b == b'|' || b == b'>')
478}
479
480pub fn lex(input: &str) -> Vec<(SyntaxKind, &str)> {
482 let (tokens, _) = lex_with_validation(input);
483 tokens
484}
485
486pub struct ValidationConfig {
488 pub max_line_length: Option<usize>,
490 pub enforce_consistent_line_endings: bool,
492}
493
494impl Default for ValidationConfig {
495 fn default() -> Self {
496 Self {
497 max_line_length: Some(120), enforce_consistent_line_endings: true,
499 }
500 }
501}
502
503pub fn lex_with_validation(input: &str) -> (Vec<(SyntaxKind, &str)>, Vec<WhitespaceError>) {
505 lex_with_validation_config(input, &ValidationConfig::default())
506}
507
508fn check_line_length(
511 errors: &mut Vec<WhitespaceError>,
512 config: &ValidationConfig,
513 line_start: usize,
514 end: usize,
515) {
516 let Some(max_len) = config.max_line_length else {
517 return;
518 };
519 let line_length = end - line_start;
520 if line_length > max_len {
521 errors.push(WhitespaceError {
522 message: format!("Line too long ({line_length} > {max_len} characters)"),
523 range: line_start..end,
524 category: WhitespaceErrorCategory::LineTooLong,
525 });
526 }
527}
528
529fn note_line_ending<'a>(
532 errors: &mut Vec<WhitespaceError>,
533 config: &ValidationConfig,
534 detected: &mut Option<&'a str>,
535 line_ending: &'a str,
536 range: std::ops::Range<usize>,
537) {
538 if !config.enforce_consistent_line_endings {
539 return;
540 }
541 match *detected {
542 Some(seen) if seen != line_ending => errors.push(WhitespaceError {
543 message: "Inconsistent line endings detected".to_string(),
544 range,
545 category: WhitespaceErrorCategory::MixedLineEndings,
546 }),
547 Some(_) => {}
548 None => *detected = Some(line_ending),
549 }
550}
551
552pub fn lex_with_validation_config<'a>(
554 input: &'a str,
555 config: &ValidationConfig,
556) -> (Vec<(SyntaxKind, &'a str)>, Vec<WhitespaceError>) {
557 let mut state = LexState::new(input, config);
558 while let Some((start_idx, ch)) = state.chars.next() {
559 state.lex_char(start_idx, ch);
560 }
561 state.finish()
562}
563
564struct LexState<'a, 'c> {
566 input: &'a str,
567 config: &'c ValidationConfig,
568 chars: std::iter::Peekable<std::str::CharIndices<'a>>,
569 tokens: Vec<(SyntaxKind, &'a str)>,
570 errors: Vec<WhitespaceError>,
571 line_start: usize,
573 detected_line_ending: Option<&'a str>,
574 flow_depth: u32,
576 block_scalar: Option<BlockScalarBody>,
581 block_scalar_end: usize,
584}
585
586impl<'a, 'c> LexState<'a, 'c> {
587 fn new(input: &'a str, config: &'c ValidationConfig) -> Self {
588 Self {
589 input,
590 config,
591 chars: input.char_indices().peekable(),
592 tokens: Vec::with_capacity(input.len() / 8), errors: Vec::new(),
594 line_start: 0,
595 detected_line_ending: None,
596 flow_depth: 0,
597 block_scalar: None,
598 block_scalar_end: 0,
599 }
600 }
601
602 fn push(&mut self, kind: SyntaxKind, start: usize, end: usize) {
604 self.tokens.push((kind, &self.input[start..end]));
605 }
606
607 fn push_scalar(&mut self, start: usize, end: usize) {
609 let text = &self.input[start..end];
610 self.tokens.push((classify_scalar(text), text));
611 }
612
613 fn push_quoted_string(&mut self, start: usize, end: usize, found_closing: bool) {
616 let kind = if found_closing {
617 SyntaxKind::STRING
618 } else {
619 SyntaxKind::UNTERMINATED_STRING
620 };
621 self.push(kind, start, end);
622 }
623
624 fn push_plain_scalar(&mut self, start: usize, body_start: usize, absorb_spaces: bool) {
630 let end = self.read_plain_scalar_body(body_start, absorb_spaces, false);
631 self.push_scalar(start, end);
632 }
633
634 fn read_plain_scalar_body(
648 &mut self,
649 start_idx: usize,
650 absorb_spaces: bool,
651 stop_at_chomping_plus: bool,
652 ) -> usize {
653 let input = self.input;
654 let flow_depth = self.flow_depth;
655 let mut end_idx = start_idx;
656 while let Some((idx, ch)) = self.chars.peek().copied() {
657 if absorb_spaces && (ch == ' ' || ch == '\t') {
662 if !plain_scalar_continues_past_whitespace(input, idx, flow_depth) {
663 break;
664 }
665 while let Some((wi, wc)) = self.chars.peek().copied() {
666 if wc != ' ' && wc != '\t' {
667 break;
668 }
669 end_idx = wi + wc.len_utf8();
670 self.chars.next();
671 }
672 continue;
673 }
674 if is_white_or_break(ch) {
675 break;
676 }
677 if ch == ':' && is_colon_followed_by_separator(input, idx, flow_depth) {
680 break;
681 }
682 if ch == '#' && is_hash_a_comment_start(input, idx) {
683 break;
684 }
685 if stop_at_chomping_plus
686 && ch == '+'
687 && input[start_idx..idx].bytes().all(|b| b.is_ascii_digit())
688 {
689 break;
690 }
691 if is_yaml_special_except(ch, "-+:#'\"?|>&*!%")
698 && !(flow_depth == 0 && matches!(ch, ',' | '[' | ']' | '{' | '}'))
699 {
700 break;
701 }
702 end_idx = idx + ch.len_utf8();
703 self.chars.next();
704 }
705 end_idx
706 }
707
708 fn node_can_start(&self) -> bool {
714 !in_block_scalar_body(self.block_scalar, self.line_start)
715 && node_property_can_start(&self.tokens)
716 }
717
718 fn property_can_start(&self, idx: usize) -> bool {
723 self.node_can_start() && !self.opens_plain_scalar_continuation_line(idx)
724 }
725
726 fn block_scalar_header_can_start(&self, idx: usize) -> bool {
728 self.flow_depth == 0
729 && self.node_can_start()
730 && !self.opens_plain_scalar_continuation_line(idx)
731 }
732
733 fn opens_plain_scalar_continuation_line(&self, idx: usize) -> bool {
741 use SyntaxKind::*;
742 self.input[self.line_start..idx]
743 .chars()
744 .all(|c| c == ' ' || c == '\t')
745 && (self.flow_depth > 0
746 || parent_column(&self.tokens)
747 .map_or(true, |column| idx - self.line_start > column))
748 && self
749 .tokens
750 .iter()
751 .enumerate()
752 .rev()
753 .find(|(_, (kind, _))| !matches!(kind, WHITESPACE | INDENT | NEWLINE))
754 .is_some_and(|(i, (kind, _))| {
755 i >= self.block_scalar_end
756 && matches!(kind, STRING | INT | FLOAT | BOOL | NULL | MERGE_KEY)
757 })
758 }
759
760 fn lex_char(&mut self, start_idx: usize, ch: char) {
762 use SyntaxKind::*;
763
764 match ch {
765 '\u{FEFF}'
768 if self.flow_depth == 0
769 && start_idx == self.line_start
770 && bom_starts_document(&self.tokens, self.input, start_idx) =>
771 {
772 self.push(BOM, start_idx, start_idx + ch.len_utf8());
773 self.line_start = start_idx + ch.len_utf8();
774 }
775 '-' => self.lex_dash(start_idx),
777 '+' => self.lex_plus(start_idx),
778 ':' => self.lex_colon(start_idx),
779 '?' if self.property_can_start(start_idx)
794 && self.chars.peek().map_or(true, |(_, next)| {
795 is_white_or_break(*next)
796 || (self.flow_depth > 0 && matches!(next, ',' | '[' | ']' | '{' | '}'))
797 }) =>
798 {
799 self.push(QUESTION, start_idx, start_idx + 1)
800 }
801 '[' | ']' | '{' | '}' | ',' | '#'
811 if in_block_scalar_body(self.block_scalar, self.line_start) =>
812 {
813 self.push_plain_scalar(start_idx, start_idx + 1, true);
814 }
815 '[' | '{' | ']' | '}'
821 if self.flow_depth == 0
822 && self.opens_plain_scalar_continuation_line(start_idx)
823 && !(matches!(ch, '[' | '{')
824 && flow_collection_is_implicit_key(self.input, start_idx)) =>
825 {
826 self.push_plain_scalar(start_idx, start_idx + 1, true);
827 }
828 '[' => {
829 self.flow_depth += 1;
830 self.push(LEFT_BRACKET, start_idx, start_idx + 1);
831 }
832 ']' => {
833 self.flow_depth = self.flow_depth.saturating_sub(1);
834 self.push(RIGHT_BRACKET, start_idx, start_idx + 1);
835 }
836 '{' => {
837 self.flow_depth += 1;
838 self.push(LEFT_BRACE, start_idx, start_idx + 1);
839 }
840 '}' => {
841 self.flow_depth = self.flow_depth.saturating_sub(1);
842 self.push(RIGHT_BRACE, start_idx, start_idx + 1);
843 }
844 ',' if self.flow_depth > 0 => self
849 .tokens
850 .push((COMMA, &self.input[start_idx..start_idx + 1])),
851 '|' if self.block_scalar_header_can_start(start_idx) => {
864 self.block_scalar = Some(BlockScalarBody::new(
865 &self.tokens,
866 self.input,
867 self.line_start,
868 ));
869 self.push(PIPE, start_idx, start_idx + 1)
870 }
871 '>' if self.block_scalar_header_can_start(start_idx) => {
872 self.block_scalar = Some(BlockScalarBody::new(
873 &self.tokens,
874 self.input,
875 self.line_start,
876 ));
877 self.push(GREATER, start_idx, start_idx + 1)
878 }
879 '<' => {
883 self.lex_plain_scalar(start_idx, ch);
884 if let Some(token) = self.tokens.last_mut().filter(|t| t.1 == "<<") {
885 token.0 = MERGE_KEY;
886 }
887 }
888 '&' if self.property_can_start(start_idx) => {
891 let name = read_anchor_name_from(&mut self.chars, self.input, start_idx + 1);
893 if !name.is_empty() {
894 self.push(ANCHOR, start_idx, start_idx + 1 + name.len());
895 } else {
896 self.push(AMPERSAND, start_idx, start_idx + 1);
897 }
898 }
899 '*' if self.property_can_start(start_idx) => {
900 let name = read_anchor_name_from(&mut self.chars, self.input, start_idx + 1);
902 if !name.is_empty() {
903 self.push(REFERENCE, start_idx, start_idx + 1 + name.len());
904 } else {
905 self.push(ASTERISK, start_idx, start_idx + 1);
906 }
907 }
908 '"' | '\''
914 if in_block_scalar_body(self.block_scalar, self.line_start)
915 || self.opens_plain_scalar_continuation_line(start_idx) =>
916 {
917 self.push_plain_scalar(start_idx, start_idx + 1, true);
918 }
919 '"' => {
920 let (end_idx, found_closing) = scan_double_quoted(&mut self.chars, start_idx);
921 self.push_quoted_string(start_idx, end_idx, found_closing);
922 }
923 '\'' => {
924 let (end_idx, found_closing) = scan_single_quoted(&mut self.chars, start_idx);
925 self.push_quoted_string(start_idx, end_idx, found_closing);
926 }
927
928 '.' => self.lex_dot(start_idx),
930
931 '#' => self.lex_comment(start_idx),
948
949 '!' if self.property_can_start(start_idx) => self.lex_tag(start_idx),
955
956 '%' => self.lex_percent(start_idx),
957
958 '\n' | '\r' => self.lex_line_break(start_idx, ch),
960
961 ' ' | '\t' => self.lex_whitespace(start_idx, ch),
963
964 _ => self.lex_plain_scalar(start_idx, ch),
966 }
967 }
968
969 fn lex_dash(&mut self, start_idx: usize) {
970 use SyntaxKind::*;
971 if self.flow_depth == 0
972 && start_idx == self.line_start
973 && self.input[start_idx..].starts_with("---")
974 && self.input[start_idx + 3..]
975 .chars()
976 .next()
977 .map_or(true, ends_document_marker)
978 {
979 self.chars.next();
980 self.chars.next();
981 self.push(DOC_START, start_idx, start_idx + 3);
982 } else {
983 let line_start_pos = self.line_start;
991 let before_dash = &self.input[line_start_pos..start_idx];
992 let only_whitespace_before = before_dash.chars().all(|c| c == ' ' || c == '\t');
993
994 let after_value_indicator = self
1001 .tokens
1002 .iter()
1003 .rev()
1004 .find(|(kind, _)| !matches!(kind, WHITESPACE | INDENT))
1005 .is_some_and(|(kind, _)| matches!(kind, QUESTION | COLON | DASH | DOC_START));
1006
1007 let followed_by_whitespace_or_end = self
1009 .chars
1010 .peek()
1011 .map_or(true, |(_, next_ch)| is_white_or_break(*next_ch));
1012
1013 let is_sequence_marker = ((only_whitespace_before
1016 && !self.opens_plain_scalar_continuation_line(start_idx))
1017 || after_value_indicator)
1018 && followed_by_whitespace_or_end;
1019
1020 if is_sequence_marker {
1021 self.push(DASH, start_idx, start_idx + 1);
1022 } else {
1023 self.push_plain_scalar(
1032 start_idx,
1033 start_idx + 1,
1034 !is_chomping_indicator(&self.input[self.line_start..start_idx]),
1035 );
1036 }
1037 }
1038 }
1039
1040 fn lex_plus(&mut self, start_idx: usize) {
1041 if is_chomping_indicator(&self.input[self.line_start..start_idx]) {
1047 self.push(SyntaxKind::PLUS, start_idx, start_idx + 1);
1048 } else {
1049 self.push_plain_scalar(start_idx, start_idx + 1, true);
1050 }
1051 }
1052
1053 fn lex_colon(&mut self, start_idx: usize) {
1054 use SyntaxKind::*;
1055 let preceded_by_json_key = self.flow_depth > 0
1066 && self
1067 .tokens
1068 .iter()
1069 .rev()
1070 .find(|(k, _)| !matches!(k, WHITESPACE | INDENT | NEWLINE | COMMENT))
1071 .is_some_and(|(k, text)| {
1072 matches!(k, RIGHT_BRACKET | RIGHT_BRACE)
1073 || (*k == STRING && text.starts_with(['"', '\'']))
1074 });
1075 if preceded_by_json_key
1076 || is_colon_a_mapping_indicator(self.input, start_idx, self.flow_depth)
1077 {
1078 self.push(COLON, start_idx, start_idx + 1);
1079 } else {
1080 self.push_plain_scalar(start_idx, start_idx + 1, true);
1086 }
1087 }
1088
1089 fn lex_dot(&mut self, start_idx: usize) {
1090 use SyntaxKind::*;
1091 let is_doc_end = self.flow_depth == 0
1097 && start_idx == self.line_start
1098 && self.input[start_idx..].starts_with("...")
1099 && self.input[start_idx + 3..]
1100 .chars()
1101 .next()
1102 .map_or(true, ends_document_marker);
1103 if is_doc_end && self.chars.peek() == Some(&(start_idx + 1, '.')) {
1104 self.chars.next(); if self.chars.peek() == Some(&(start_idx + 2, '.')) {
1106 self.chars.next(); self.push(DOC_END, start_idx, start_idx + 3);
1108 } else {
1109 self.push_plain_scalar(start_idx, start_idx + 2, true);
1112 }
1113 } else {
1114 self.push_plain_scalar(start_idx, start_idx + 1, true);
1116 }
1117 }
1118
1119 fn lex_comment(&mut self, start_idx: usize) {
1120 use SyntaxKind::*;
1121 let mut end_idx = start_idx + 1;
1122 while let Some((idx, ch)) = self.chars.peek() {
1123 if *ch == '\n' || *ch == '\r' {
1124 break;
1125 }
1126 end_idx = *idx + ch.len_utf8();
1127 self.chars.next();
1128 }
1129 self.push(COMMENT, start_idx, end_idx);
1130 }
1131
1132 fn lex_tag(&mut self, start_idx: usize) {
1133 use SyntaxKind::*;
1134 let mut end_idx = start_idx + 1;
1136
1137 if let Some((_, '<')) = self.chars.peek() {
1140 self.chars.next(); end_idx = start_idx + 2;
1142 while let Some((idx, ch)) = self.chars.peek() {
1146 if matches!(ch, '\n' | '\r') {
1147 break;
1148 }
1149 end_idx = *idx + ch.len_utf8();
1150 let closing = *ch == '>';
1151 self.chars.next();
1152 if closing {
1153 break;
1154 }
1155 }
1156 self.push(TAG, start_idx, end_idx);
1157 return;
1158 }
1159
1160 if let Some((_, '!')) = self.chars.peek() {
1162 self.chars.next(); end_idx = start_idx + 2;
1164 } else {
1165 let word = self.input[start_idx + 1..]
1168 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '-'))
1169 .map_or(self.input.len(), |i| start_idx + 1 + i);
1170 if word > start_idx + 1 && self.input[word..].starts_with('!') {
1171 while self.chars.peek().is_some_and(|(idx, _)| *idx <= word) {
1172 self.chars.next();
1173 }
1174 end_idx = word + 1;
1175 }
1176 }
1177
1178 while let Some((idx, ch)) = self.chars.peek() {
1180 if !is_tag_char(*ch) {
1181 break;
1182 }
1183 end_idx = *idx + ch.len_utf8();
1184 self.chars.next();
1185 }
1186
1187 self.push(TAG, start_idx, end_idx);
1188 }
1189
1190 fn lex_percent(&mut self, start_idx: usize) {
1191 use SyntaxKind::*;
1192 if self.flow_depth > 0
1196 || start_idx != self.line_start
1197 || in_block_scalar_body(self.block_scalar, self.line_start)
1198 {
1199 self.push_plain_scalar(start_idx, start_idx + 1, true);
1205 } else {
1206 let mut end_idx = start_idx + 1;
1208 while let Some((idx, ch)) = self.chars.peek() {
1209 if *ch == '\n' || *ch == '\r' {
1210 break;
1211 }
1212 end_idx = *idx + ch.len_utf8();
1213 self.chars.next();
1214 }
1215 self.push(DIRECTIVE, start_idx, end_idx);
1216 }
1217 }
1218
1219 fn lex_line_break(&mut self, start_idx: usize, ch: char) {
1221 check_line_length(&mut self.errors, self.config, self.line_start, start_idx);
1222
1223 let (line_ending, end_pos) = if ch == '\n' {
1224 ("\n", start_idx + 1)
1225 } else if let Some((_, '\n')) = self.chars.peek() {
1226 self.chars.next();
1227 ("\r\n", start_idx + 2)
1228 } else {
1229 ("\r", start_idx + 1)
1230 };
1231
1232 note_line_ending(
1233 &mut self.errors,
1234 self.config,
1235 &mut self.detected_line_ending,
1236 line_ending,
1237 start_idx..end_pos,
1238 );
1239
1240 self.push(SyntaxKind::NEWLINE, start_idx, end_pos);
1241 self.line_start = end_pos;
1242 let in_body = self.block_scalar.is_some();
1243 track_block_scalar_body(
1244 &self.tokens,
1245 self.input,
1246 self.line_start,
1247 &mut self.block_scalar,
1248 );
1249 if in_body && self.block_scalar.is_none() {
1250 self.block_scalar_end = self.tokens.len();
1251 }
1252 }
1253
1254 fn lex_whitespace(&mut self, start_idx: usize, ch: char) {
1255 use SyntaxKind::*;
1256 let mut end_idx = start_idx + 1;
1257 let mut has_tabs = ch == '\t';
1258
1259 while let Some((idx, ch)) = self.chars.peek() {
1260 if *ch != ' ' && *ch != '\t' {
1261 break;
1262 }
1263 if *ch == '\t' {
1264 has_tabs = true;
1265 }
1266 end_idx = *idx + 1;
1267 self.chars.next();
1268 }
1269
1270 let is_indentation = start_idx == self.line_start;
1273
1274 if is_indentation {
1275 if has_tabs {
1277 self.errors.push(WhitespaceError {
1278 message: "Tab character used for indentation (forbidden in YAML)".to_string(),
1279 range: start_idx..end_idx,
1280 category: WhitespaceErrorCategory::TabIndentation,
1281 });
1282 }
1283 self.push(INDENT, start_idx, end_idx);
1284 } else {
1285 self.push(WHITESPACE, start_idx, end_idx);
1286 }
1287 }
1288
1289 fn lex_plain_scalar(&mut self, start_idx: usize, ch: char) {
1290 let after_header = ch.is_ascii_digit()
1293 && matches!(
1294 self.tokens.last(),
1295 Some((SyntaxKind::PIPE | SyntaxKind::GREATER, _))
1296 );
1297 let end = self.read_plain_scalar_body(start_idx + ch.len_utf8(), true, after_header);
1298 self.push_scalar(start_idx, end);
1299 }
1300
1301 fn finish(self) -> (Vec<(SyntaxKind, &'a str)>, Vec<WhitespaceError>) {
1302 let mut whitespace_errors = self.errors;
1303 if let Some(max_len) = self.config.max_line_length {
1305 let final_line_length = self.input.len() - self.line_start;
1306 if final_line_length > max_len && final_line_length > 0 {
1307 whitespace_errors.push(WhitespaceError {
1308 message: format!("Line too long ({final_line_length} > {max_len} characters)"),
1309 range: self.line_start..self.input.len(),
1310 category: WhitespaceErrorCategory::LineTooLong,
1311 });
1312 }
1313 }
1314 (self.tokens, whitespace_errors)
1315 }
1316}
1317
1318fn classify_scalar(text: &str) -> SyntaxKind {
1323 use crate::CoreScalarType;
1324 match crate::scalar::ScalarValue::classify_plain(text) {
1325 CoreScalarType::Null => SyntaxKind::NULL,
1326 CoreScalarType::Boolean => SyntaxKind::BOOL,
1327 CoreScalarType::Integer => SyntaxKind::INT,
1328 CoreScalarType::Float => SyntaxKind::FLOAT,
1329 CoreScalarType::String => SyntaxKind::STRING,
1330 }
1331}
1332
1333const YAML_SPECIAL_CHARS: &str = ":+-?[]{},'|>&*!%\"#";
1335
1336fn ends_document_marker(ch: char) -> bool {
1351 matches!(ch, ' ' | '\t' | '\n' | '\r')
1352}
1353
1354fn bom_starts_document(tokens: &[(SyntaxKind, &str)], input: &str, bom_idx: usize) -> bool {
1360 use SyntaxKind::*;
1361 let after_marker = tokens
1362 .iter()
1363 .rev()
1364 .find(|(kind, _)| !matches!(kind, WHITESPACE | INDENT | NEWLINE | COMMENT))
1365 .map_or(true, |(kind, _)| matches!(kind, DOC_START | DOC_END));
1366 let rest = &input[bom_idx + '\u{FEFF}'.len_utf8()..];
1367 let before_marker =
1368 rest.starts_with("---") && rest[3..].chars().next().map_or(true, ends_document_marker);
1369 after_marker || before_marker
1370}
1371
1372fn node_property_can_start(tokens: &[(SyntaxKind, &str)]) -> bool {
1373 for (kind, _) in tokens.iter().rev() {
1374 match kind {
1375 SyntaxKind::WHITESPACE | SyntaxKind::INDENT => continue,
1377 SyntaxKind::NEWLINE
1379 | SyntaxKind::COLON
1380 | SyntaxKind::DASH
1381 | SyntaxKind::QUESTION
1382 | SyntaxKind::COMMA
1383 | SyntaxKind::LEFT_BRACKET
1384 | SyntaxKind::LEFT_BRACE
1385 | SyntaxKind::DOC_START
1386 | SyntaxKind::BOM
1387 | SyntaxKind::TAG
1388 | SyntaxKind::ANCHOR
1389 | SyntaxKind::AMPERSAND
1392 | SyntaxKind::ASTERISK => return true,
1393 _ => return false,
1395 }
1396 }
1397 true
1399}
1400
1401fn line_is_blank(input: &str, line_start: usize) -> bool {
1403 input[line_start..]
1404 .split(['\n', '\r'])
1405 .next()
1406 .unwrap_or_default()
1407 .trim_matches([' ', '\t'])
1408 .is_empty()
1409}
1410
1411fn line_indent(input: &str, line_start: usize) -> usize {
1413 let line = &input[line_start..];
1414 line.len() - line.trim_start_matches([' ', '\t']).len()
1415}
1416
1417#[derive(Clone, Copy)]
1419struct BlockScalarBody {
1420 header_line_start: usize,
1422 header_token: usize,
1424 parent_column: usize,
1426 min_indent: usize,
1432 indent: Option<usize>,
1435}
1436
1437impl BlockScalarBody {
1438 fn new(tokens: &[(SyntaxKind, &str)], input: &str, line_start: usize) -> Self {
1441 let is_root = tokens
1444 .iter()
1445 .rev()
1446 .take_while(|(kind, _)| !matches!(kind, SyntaxKind::NEWLINE | SyntaxKind::BOM))
1447 .all(|(kind, _)| {
1448 matches!(
1449 kind,
1450 SyntaxKind::DOC_START
1451 | SyntaxKind::WHITESPACE
1452 | SyntaxKind::TAG
1453 | SyntaxKind::ANCHOR
1454 )
1455 });
1456 let parent_column = parent_column(tokens).unwrap_or(0);
1457 let min_indent = if is_root {
1458 0
1459 } else {
1460 line_indent(input, line_start).max(parent_column) + 1
1461 };
1462 Self {
1463 header_line_start: line_start,
1464 header_token: tokens.len(),
1465 parent_column,
1466 min_indent,
1467 indent: None,
1468 }
1469 }
1470}
1471
1472fn parent_column(tokens: &[(SyntaxKind, &str)]) -> Option<usize> {
1484 use SyntaxKind::*;
1485 for line in tokens.rsplit(|(kind, _)| matches!(kind, NEWLINE | BOM)) {
1486 let Some(last) = line.iter().rposition(|(kind, _)| {
1487 !matches!(
1488 kind,
1489 WHITESPACE
1490 | INDENT
1491 | COMMENT
1492 | TAG
1493 | ANCHOR
1494 | STRING
1495 | INT
1496 | FLOAT
1497 | BOOL
1498 | NULL
1499 | MERGE_KEY
1500 )
1501 }) else {
1502 continue;
1503 };
1504 let column = |index: usize| -> usize {
1505 line[..index]
1506 .iter()
1507 .map(|(_, text)| text.chars().count())
1508 .sum()
1509 };
1510 return match line[last].0 {
1511 DASH | QUESTION => Some(column(last)),
1512 COLON => {
1513 let key = line[..last]
1517 .iter()
1518 .position(|(kind, _)| {
1519 !matches!(kind, WHITESPACE | INDENT | DASH | QUESTION | COLON)
1520 })
1521 .unwrap_or(last);
1522 Some(column(key))
1523 }
1524 DOC_START | DOC_END => None,
1525 _ => Some(0),
1526 };
1527 }
1528 None
1529}
1530
1531pub(crate) fn block_scalar_header<'a>(
1539 tokens: impl IntoIterator<Item = (SyntaxKind, &'a str)>,
1540) -> (Option<usize>, usize) {
1541 let mut explicit_indent = None;
1542 let mut len = 0;
1543 for (kind, text) in tokens {
1544 match kind {
1545 SyntaxKind::INT => {
1546 let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
1554 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
1555 break;
1556 }
1557 explicit_indent = digits
1559 .chars()
1560 .next()
1561 .and_then(|c| c.to_digit(10))
1562 .filter(|d| *d > 0)
1563 .map(|d| d as usize);
1564 }
1565 SyntaxKind::STRING => {
1566 let mut chars = text.chars();
1572 let is_header = match (chars.next(), chars.next(), chars.next()) {
1573 (Some(a), None, _) => a == '+' || a == '-',
1574 (Some(a), Some(b), None) => {
1575 let digit = |c: char| c.is_ascii_digit() && c != '0';
1576 let chomp = |c: char| c == '+' || c == '-';
1577 (digit(a) && chomp(b)) || (chomp(a) && digit(b))
1578 }
1579 _ => false,
1580 };
1581 if !is_header {
1582 break;
1583 }
1584 explicit_indent = text
1585 .chars()
1586 .find_map(|c| c.to_digit(10))
1587 .filter(|d| *d > 0)
1588 .map(|d| d as usize);
1589 }
1590 SyntaxKind::PLUS | SyntaxKind::WHITESPACE => {}
1593 _ => break,
1594 }
1595 len += 1;
1596 }
1597 (explicit_indent, len)
1598}
1599
1600fn line_is_document_marker(input: &str, line_start: usize) -> bool {
1602 let line = &input[line_start..];
1603 (line.starts_with("---") || line.starts_with("..."))
1604 && line[3..].chars().next().map_or(true, ends_document_marker)
1605}
1606
1607fn track_block_scalar_body(
1617 tokens: &[(SyntaxKind, &str)],
1618 input: &str,
1619 line_start: usize,
1620 body: &mut Option<BlockScalarBody>,
1621) {
1622 let Some(current) = body else {
1623 return;
1624 };
1625 if current.indent.is_none() {
1629 if let (Some(indicator), _) =
1630 block_scalar_header(tokens[current.header_token + 1..].iter().copied())
1631 {
1632 current.indent = Some(current.parent_column + indicator);
1633 }
1634 }
1635 if line_is_blank(input, line_start) {
1636 return;
1637 }
1638 let indent = line_indent(input, line_start);
1639 if indent < current.indent.unwrap_or(current.min_indent)
1640 || line_is_document_marker(input, line_start)
1641 {
1642 *body = None;
1643 } else if current.indent.is_none() {
1644 let spaces = crate::column::space_indent(&input[line_start..]);
1647 current.indent = Some(if spaces >= current.min_indent {
1648 spaces
1649 } else {
1650 indent
1651 });
1652 }
1653}
1654
1655fn in_block_scalar_body(body: Option<BlockScalarBody>, line_start: usize) -> bool {
1664 body.is_some_and(|body| body.header_line_start != line_start)
1665}
1666
1667fn is_tag_char(ch: char) -> bool {
1675 if ch.is_whitespace() {
1676 return false;
1677 }
1678 !matches!(ch, ',' | '[' | ']' | '{' | '}' | '!')
1679}
1680
1681fn is_white_or_break(ch: char) -> bool {
1685 matches!(ch, ' ' | '\t' | '\n' | '\r')
1686}
1687
1688fn is_yaml_special(ch: char) -> bool {
1689 YAML_SPECIAL_CHARS.contains(ch)
1690}
1691
1692fn is_yaml_special_except(ch: char, exclude: &str) -> bool {
1694 YAML_SPECIAL_CHARS.contains(ch) && !exclude.contains(ch)
1695}
1696
1697pub(crate) fn is_valid_anchor_name(name: &str) -> bool {
1704 !name.is_empty() && name.chars().all(is_anchor_char)
1705}
1706
1707fn is_anchor_char(ch: char) -> bool {
1709 let ns_char = matches!(ch,
1711 '\u{21}'..='\u{7E}'
1712 | '\u{85}'
1713 | '\u{A0}'..='\u{D7FF}'
1714 | '\u{E000}'..='\u{FFFD}'
1715 | '\u{10000}'..='\u{10FFFF}')
1716 && ch != '\u{FEFF}';
1717 ns_char && !matches!(ch, ',' | '[' | ']' | '{' | '}')
1718}
1719
1720#[cfg(test)]
1721mod tests {
1722 use super::*;
1723
1724 #[test]
1725 fn test_is_valid_anchor_name() {
1726 assert!(is_valid_anchor_name("shared"));
1727 assert!(is_valid_anchor_name("shared_1"));
1728 assert!(is_valid_anchor_name("anchor.dotted"));
1729 assert!(is_valid_anchor_name("naïve"));
1730 for name in [
1732 "a:",
1733 "colon:name",
1734 "*star",
1735 "&",
1736 "-",
1737 "x?y",
1738 "#",
1739 "!t",
1740 "|>",
1741 "%'\"",
1742 "@`",
1743 "no\u{A0}break",
1744 "nel\u{85}",
1745 ] {
1746 assert!(is_valid_anchor_name(name), "{name:?}");
1747 }
1748
1749 for name in [
1750 "",
1751 "has space",
1752 "has\ttab",
1753 "has\nnewline",
1754 "has\rreturn",
1755 "comma,name",
1756 "bracket]name",
1757 "[",
1758 "{",
1759 "}",
1760 "bom\u{FEFF}",
1761 "bell\u{7}",
1762 "del\u{7F}",
1763 "c1\u{80}",
1764 ] {
1765 assert!(!is_valid_anchor_name(name), "{name:?}");
1766 }
1767 }
1768
1769 #[test]
1770 fn test_embedded_hyphens_stay_in_one_plain_scalar() {
1771 let input = format!("k: {}\n", "a-".repeat(200));
1772 let kinds: Vec<_> = lex(&input).into_iter().map(|(k, _)| k).collect();
1773 assert!(kinds.contains(&SyntaxKind::STRING) || kinds.contains(&SyntaxKind::SCALAR));
1774 let dash_count = kinds.iter().filter(|k| **k == SyntaxKind::DASH).count();
1775 assert_eq!(dash_count, 0);
1776 let seq = lex("- a\n- b\n");
1777 let seq_dashes = seq.iter().filter(|(k, _)| *k == SyntaxKind::DASH).count();
1778 assert_eq!(seq_dashes, 2);
1779 }
1780
1781 #[test]
1785 fn test_valid_anchor_names_round_trip_through_lexer() {
1786 for name in [
1787 "shared",
1788 "shared_1",
1789 "anchor.dotted",
1790 "naïve",
1791 "a:",
1792 "*star",
1793 "x#y",
1794 "no\u{A0}break",
1795 ] {
1796 let input = format!("*{name}");
1797 let tokens = lex(&input);
1798 assert_eq!(
1799 tokens,
1800 vec![(SyntaxKind::REFERENCE, input.as_str())],
1801 "name {name:?} did not round-trip as a single REFERENCE token"
1802 );
1803 }
1804 }
1805
1806 #[test]
1807 fn test_simple_mapping() {
1808 let input = "key: value";
1809 let tokens = lex(input);
1810
1811 assert_eq!(tokens.len(), 4);
1812 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
1813 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1814 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1815 assert_eq!(tokens[3], (SyntaxKind::STRING, "value"));
1816 }
1817
1818 #[test]
1819 fn test_scalar_types() {
1820 let tokens = lex("age: 42");
1822 assert_eq!(tokens[0], (SyntaxKind::STRING, "age"));
1823 assert_eq!(tokens[3], (SyntaxKind::INT, "42"));
1824
1825 let tokens = lex("pi: 3.14");
1827 assert_eq!(tokens[0], (SyntaxKind::STRING, "pi"));
1828 assert_eq!(tokens[3], (SyntaxKind::FLOAT, "3.14"));
1829
1830 let tokens = lex("enabled: true");
1832 assert_eq!(tokens[0], (SyntaxKind::STRING, "enabled"));
1833 assert_eq!(tokens[3], (SyntaxKind::BOOL, "true"));
1834
1835 let tokens = lex("disabled: false");
1837 assert_eq!(tokens[3], (SyntaxKind::BOOL, "false"));
1838
1839 let tokens = lex("value: null");
1841 assert_eq!(tokens[3], (SyntaxKind::NULL, "null"));
1842
1843 let tokens = lex("value: ~");
1845 assert_eq!(tokens[3], (SyntaxKind::NULL, "~"));
1846 }
1847
1848 #[test]
1849 fn test_sequences() {
1850 let input = "- item1\n- item2";
1851 let tokens = lex(input);
1852
1853 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1854 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1855 assert_eq!(tokens[2], (SyntaxKind::STRING, "item1"));
1856 assert_eq!(tokens[3], (SyntaxKind::NEWLINE, "\n"));
1857 assert_eq!(tokens[4], (SyntaxKind::DASH, "-"));
1858 assert_eq!(tokens[5], (SyntaxKind::WHITESPACE, " "));
1859 assert_eq!(tokens[6], (SyntaxKind::STRING, "item2"));
1860 }
1861
1862 #[test]
1863 fn test_hyphen_in_scalars() {
1864 let input = "Name: example-project";
1866 let tokens = lex(input);
1867
1868 println!("Hyphen test tokens:");
1869 for (i, (kind, text)) in tokens.iter().enumerate() {
1870 println!(" {}: {:?} = {:?}", i, kind, text);
1871 }
1872
1873 assert_eq!(tokens.len(), 4);
1875 assert_eq!(tokens[0], (SyntaxKind::STRING, "Name"));
1876 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1877 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1878 assert_eq!(tokens[3], (SyntaxKind::STRING, "example-project"));
1879 }
1880
1881 #[test]
1882 fn test_hyphen_sequence_vs_scalar() {
1883 let sequence_input = "- example-item";
1885 let tokens = lex(sequence_input);
1886
1887 println!("Sequence hyphen tokens:");
1888 for (i, (kind, text)) in tokens.iter().enumerate() {
1889 println!(" {}: {:?} = {:?}", i, kind, text);
1890 }
1891
1892 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1894 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1895 assert_eq!(tokens[2], (SyntaxKind::STRING, "example-item"));
1896
1897 let scalar_input = "package-name: my-awesome-package";
1899 let tokens = lex(scalar_input);
1900
1901 println!("Package hyphen tokens:");
1902 for (i, (kind, text)) in tokens.iter().enumerate() {
1903 println!(" {}: {:?} = {:?}", i, kind, text);
1904 }
1905
1906 assert_eq!(tokens.len(), 4);
1908 assert_eq!(tokens[0], (SyntaxKind::STRING, "package-name"));
1909 assert_eq!(tokens[3], (SyntaxKind::STRING, "my-awesome-package"));
1910 }
1911
1912 #[test]
1913 fn test_flow_style() {
1914 let tokens = lex("[1, 2, 3]");
1916 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACKET, "["));
1917 assert_eq!(tokens[1], (SyntaxKind::INT, "1"));
1918 assert_eq!(tokens[2], (SyntaxKind::COMMA, ","));
1919 assert_eq!(tokens[3], (SyntaxKind::WHITESPACE, " "));
1920 assert_eq!(tokens[4], (SyntaxKind::INT, "2"));
1921 assert_eq!(tokens[5], (SyntaxKind::COMMA, ","));
1922 assert_eq!(tokens[6], (SyntaxKind::WHITESPACE, " "));
1923 assert_eq!(tokens[7], (SyntaxKind::INT, "3"));
1924 assert_eq!(tokens[8], (SyntaxKind::RIGHT_BRACKET, "]"));
1925
1926 let tokens = lex("{a: 1, b: 2}");
1928 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACE, "{"));
1929 assert_eq!(tokens[1], (SyntaxKind::STRING, "a"));
1930 assert_eq!(tokens[2], (SyntaxKind::COLON, ":"));
1931 assert_eq!(tokens[3], (SyntaxKind::WHITESPACE, " "));
1932 assert_eq!(tokens[4], (SyntaxKind::INT, "1"));
1933 }
1934
1935 #[test]
1936 fn test_comments() {
1937 let input = "key: value # this is a comment\n# full line comment";
1938 let tokens = lex(input);
1939
1940 let comments: Vec<_> = tokens
1942 .iter()
1943 .filter(|(kind, _)| *kind == SyntaxKind::COMMENT)
1944 .collect();
1945
1946 assert_eq!(comments.len(), 2);
1947 assert_eq!(comments[0].1, "# this is a comment");
1948 assert_eq!(comments[1].1, "# full line comment");
1949 }
1950
1951 #[test]
1952 fn test_multiline_scalar() {
1953 let input = "key: value\n continued";
1954 let tokens = lex(input);
1955
1956 let indents: Vec<_> = tokens
1958 .iter()
1959 .filter(|(kind, _)| *kind == SyntaxKind::INDENT)
1960 .collect();
1961 assert_eq!(indents.len(), 1);
1962 assert_eq!(indents[0].1, " ");
1963 }
1964
1965 #[test]
1966 fn test_quoted_strings() {
1967 let input = r#"single: 'quoted'
1968double: "quoted""#;
1969 let tokens = lex(input);
1970
1971 let quoted_strings: Vec<_> = tokens
1973 .iter()
1974 .filter(|(kind, text)| {
1975 *kind == SyntaxKind::STRING && (text.starts_with('\'') || text.starts_with('"'))
1976 })
1977 .collect();
1978 assert_eq!(quoted_strings.len(), 2); let quoted_texts: Vec<&str> = {
1982 let mut v: Vec<&str> = quoted_strings.iter().map(|(_, t)| *t).collect();
1983 v.sort();
1984 v
1985 };
1986 assert_eq!(quoted_texts, ["\"quoted\"", "'quoted'"]);
1987 }
1988
1989 #[test]
1990 fn test_document_markers() {
1991 let input = "---\nkey: value\n...";
1992 let tokens = lex(input);
1993
1994 println!("Document tokens:");
1995 for (i, (kind, text)) in tokens.iter().enumerate() {
1996 println!(" {}: {:?} = {:?}", i, kind, text);
1997 }
1998
1999 let doc_start_count = tokens
2001 .iter()
2002 .filter(|(kind, _)| *kind == SyntaxKind::DOC_START)
2003 .count();
2004 let doc_end_count = tokens
2005 .iter()
2006 .filter(|(kind, _)| *kind == SyntaxKind::DOC_END)
2007 .count();
2008 assert_eq!(doc_start_count, 1);
2009 assert_eq!(doc_end_count, 1);
2010 }
2011
2012 #[test]
2013 fn test_empty_input() {
2014 let input = "";
2015 let tokens = lex(input);
2016 println!("Empty input tokens: {:?}", tokens);
2017 assert_eq!(tokens.len(), 0);
2018 }
2019
2020 #[test]
2021 fn test_anchors_and_aliases() {
2022 let input = "key: &anchor_name value";
2024 let tokens = lex(input);
2025 println!("Anchor tokens: {:?}", tokens);
2026
2027 let anchors: Vec<_> = tokens
2028 .iter()
2029 .filter(|(kind, _)| *kind == SyntaxKind::ANCHOR)
2030 .collect();
2031 assert_eq!(anchors.len(), 1);
2032 assert_eq!(anchors[0].1, "&anchor_name");
2033
2034 let input = "key: *reference_name";
2036 let tokens = lex(input);
2037 println!("Reference tokens: {:?}", tokens);
2038
2039 let references: Vec<_> = tokens
2040 .iter()
2041 .filter(|(kind, _)| *kind == SyntaxKind::REFERENCE)
2042 .collect();
2043 assert_eq!(references.len(), 1);
2044 assert_eq!(references[0].1, "*reference_name");
2045
2046 let input = "key: & *";
2048 let tokens = lex(input);
2049
2050 let ampersands: Vec<_> = tokens
2051 .iter()
2052 .filter(|(kind, _)| *kind == SyntaxKind::AMPERSAND)
2053 .collect();
2054 assert_eq!(ampersands.len(), 1);
2055
2056 let asterisks: Vec<_> = tokens
2057 .iter()
2058 .filter(|(kind, _)| *kind == SyntaxKind::ASTERISK)
2059 .collect();
2060 assert_eq!(asterisks.len(), 1);
2061 }
2062
2063 #[test]
2064 fn test_indicators_mid_plain_scalar_are_scalar_content() {
2065 for input in [
2072 "k: 3.1&1", "k: a&b", "k: a*b", "k: a!b", "k: a?b", "k: a !b", "k: a !!b", "k: a &b",
2073 "k: a *b", "- a !b",
2074 ] {
2075 let tokens = lex(input);
2076 let bad: Vec<_> = tokens
2077 .iter()
2078 .filter(|(kind, _)| {
2079 matches!(
2080 kind,
2081 SyntaxKind::ANCHOR
2082 | SyntaxKind::REFERENCE
2083 | SyntaxKind::TAG
2084 | SyntaxKind::QUESTION
2085 )
2086 })
2087 .collect();
2088 assert!(
2089 bad.is_empty(),
2090 "{:?} produced structural tokens mid-scalar: {:?}",
2091 input,
2092 bad
2093 );
2094 }
2095 let tokens = lex("a: &real 1");
2097 assert!(
2098 tokens.iter().any(|(k, _)| *k == SyntaxKind::ANCHOR),
2099 "real anchor should still be recognized: {:?}",
2100 tokens
2101 );
2102 }
2103
2104 #[test]
2105 fn test_merge_key_token() {
2106 let input = "<<: *defaults";
2108 let tokens = lex(input);
2109
2110 let merge_keys: Vec<_> = tokens
2111 .iter()
2112 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2113 .collect();
2114 assert_eq!(merge_keys.len(), 1);
2115 assert_eq!(merge_keys[0].1, "<<");
2116
2117 let input2 = "key: < value";
2119 let tokens2 = lex(input2);
2120
2121 let merge_keys2: Vec<_> = tokens2
2122 .iter()
2123 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2124 .collect();
2125 assert_eq!(merge_keys2.len(), 0, "Single < should not be a merge key");
2126 }
2127
2128 #[test]
2129 fn test_angle_prefix_is_plain_scalar_not_merge_key() {
2130 let tokens = lex("<<foo: 1");
2131 let merge_keys: Vec<_> = tokens
2132 .iter()
2133 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2134 .collect();
2135 assert_eq!(
2136 merge_keys.len(),
2137 0,
2138 "<<foo should be a plain key, got {:?}",
2139 tokens
2140 );
2141 assert!(
2142 tokens
2143 .iter()
2144 .any(|(kind, text)| *kind != SyntaxKind::MERGE_KEY && *text == "<<foo"),
2145 "expected <<foo scalar, got {:?}",
2146 tokens
2147 );
2148
2149 let tokens = lex("{<<foo: 1, more: x}");
2150 assert_eq!(
2151 tokens
2152 .iter()
2153 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2154 .count(),
2155 0,
2156 "flow <<foo should not be MERGE_KEY: {:?}",
2157 tokens
2158 );
2159
2160 let tokens = lex("<<: *defaults");
2161 assert_eq!(
2162 tokens
2163 .iter()
2164 .filter(|(kind, text)| *kind == SyntaxKind::MERGE_KEY && *text == "<<")
2165 .count(),
2166 1
2167 );
2168
2169 use crate::YamlFile;
2170 use std::str::FromStr;
2171 let file = YamlFile::from_str("<<foo: 1\nbar: 2\n").unwrap();
2172 let mapping = file.document().unwrap().as_mapping().unwrap();
2173 assert_eq!(mapping.keys().count(), 2);
2174 assert_eq!(
2175 mapping
2176 .get("<<foo")
2177 .unwrap()
2178 .as_scalar()
2179 .unwrap()
2180 .as_string(),
2181 "1"
2182 );
2183 }
2184
2185 #[test]
2189 fn test_merge_key_is_a_whole_plain_scalar() {
2190 use SyntaxKind::*;
2191 assert_eq!(
2192 lex("<<#: 1"),
2193 [(STRING, "<<#"), (COLON, ":"), (WHITESPACE, " "), (INT, "1")]
2194 );
2195 assert_eq!(
2196 lex("<<:x: 1"),
2197 [
2198 (STRING, "<<:x"),
2199 (COLON, ":"),
2200 (WHITESPACE, " "),
2201 (INT, "1")
2202 ]
2203 );
2204 assert_eq!(
2205 lex("<< x: 1"),
2206 [
2207 (STRING, "<< x"),
2208 (COLON, ":"),
2209 (WHITESPACE, " "),
2210 (INT, "1")
2211 ]
2212 );
2213 assert_eq!(
2214 lex("<<]: 1"),
2215 [(STRING, "<<]"), (COLON, ":"), (WHITESPACE, " "), (INT, "1")]
2216 );
2217 assert_eq!(
2218 lex("<< #c\n"),
2219 [
2220 (MERGE_KEY, "<<"),
2221 (WHITESPACE, " "),
2222 (COMMENT, "#c"),
2223 (NEWLINE, "\n")
2224 ]
2225 );
2226 assert_eq!(
2227 lex("{<<,<<: 1}"),
2228 [
2229 (LEFT_BRACE, "{"),
2230 (MERGE_KEY, "<<"),
2231 (COMMA, ","),
2232 (MERGE_KEY, "<<"),
2233 (COLON, ":"),
2234 (WHITESPACE, " "),
2235 (INT, "1"),
2236 (RIGHT_BRACE, "}")
2237 ]
2238 );
2239 }
2240
2241 #[test]
2242 fn test_angle_prefixed_scalar_uses_plain_scalar_rules() {
2243 use crate::YamlFile;
2247 use std::str::FromStr;
2248
2249 for key in [
2250 "<foo-bar",
2251 "<<foo-bar",
2252 "<foo!x",
2253 "<<foo!x",
2254 "<foo&x",
2255 "<<foo&x",
2256 "<foo:x",
2257 "<<foo:x",
2258 ] {
2259 let src = format!("{}: 1\n", key);
2260 let tokens = lex(&src);
2261 assert_eq!(
2262 tokens
2263 .iter()
2264 .filter(|(kind, _)| *kind != SyntaxKind::WHITESPACE)
2265 .map(|(_, text)| *text)
2266 .collect::<Vec<_>>(),
2267 vec![key, ":", "1", "\n"],
2268 "unexpected tokens for {:?}",
2269 src
2270 );
2271
2272 let file = YamlFile::from_str(&src).unwrap();
2273 assert_eq!(file.to_string(), src);
2274 let mapping = file.document().unwrap().as_mapping().unwrap();
2275 assert_eq!(
2276 mapping.get(key).unwrap().as_scalar().unwrap().as_string(),
2277 "1"
2278 );
2279 }
2280 }
2281
2282 #[test]
2283 fn test_plus_token() {
2284 let input = "key: |+ value";
2286 let tokens = lex(input);
2287
2288 let plus_tokens: Vec<_> = tokens
2289 .iter()
2290 .filter(|(kind, _)| *kind == SyntaxKind::PLUS)
2291 .collect();
2292 assert_eq!(plus_tokens.len(), 1);
2293 assert_eq!(plus_tokens[0].1, "+");
2294 }
2295
2296 #[test]
2299 fn test_plus_tokens() {
2300 use SyntaxKind::*;
2301 let cases: Vec<(&str, Vec<(SyntaxKind, &str)>)> = vec![
2302 (
2303 "+ x: v\n",
2304 vec![
2305 (STRING, "+ x"),
2306 (COLON, ":"),
2307 (WHITESPACE, " "),
2308 (STRING, "v"),
2309 (NEWLINE, "\n"),
2310 ],
2311 ),
2312 (
2313 "+\tx: v\n",
2314 vec![
2315 (STRING, "+\tx"),
2316 (COLON, ":"),
2317 (WHITESPACE, " "),
2318 (STRING, "v"),
2319 (NEWLINE, "\n"),
2320 ],
2321 ),
2322 (
2323 "+ \n",
2324 vec![(STRING, "+"), (WHITESPACE, " "), (NEWLINE, "\n")],
2325 ),
2326 (
2327 "+ #c\n",
2328 vec![
2329 (STRING, "+"),
2330 (WHITESPACE, " "),
2331 (COMMENT, "#c"),
2332 (NEWLINE, "\n"),
2333 ],
2334 ),
2335 ("+[b]\n", vec![(STRING, "+[b]"), (NEWLINE, "\n")]),
2336 (
2337 "[+[b]]\n",
2338 vec![
2339 (LEFT_BRACKET, "["),
2340 (STRING, "+"),
2341 (LEFT_BRACKET, "["),
2342 (STRING, "b"),
2343 (RIGHT_BRACKET, "]"),
2344 (RIGHT_BRACKET, "]"),
2345 (NEWLINE, "\n"),
2346 ],
2347 ),
2348 ("+'a'\n", vec![(STRING, "+'a'"), (NEWLINE, "\n")]),
2349 ("+?\n", vec![(STRING, "+?"), (NEWLINE, "\n")]),
2350 ("+|\n", vec![(STRING, "+|"), (NEWLINE, "\n")]),
2351 (
2352 "+#: v\n",
2353 vec![
2354 (STRING, "+#"),
2355 (COLON, ":"),
2356 (WHITESPACE, " "),
2357 (STRING, "v"),
2358 (NEWLINE, "\n"),
2359 ],
2360 ),
2361 (
2362 "+ #: v\n",
2363 vec![
2364 (STRING, "+"),
2365 (WHITESPACE, " "),
2366 (COMMENT, "#: v"),
2367 (NEWLINE, "\n"),
2368 ],
2369 ),
2370 (
2371 "+: v\n",
2372 vec![
2373 (STRING, "+"),
2374 (COLON, ":"),
2375 (WHITESPACE, " "),
2376 (STRING, "v"),
2377 (NEWLINE, "\n"),
2378 ],
2379 ),
2380 (
2381 "+ : v\n",
2382 vec![
2383 (STRING, "+"),
2384 (WHITESPACE, " "),
2385 (COLON, ":"),
2386 (WHITESPACE, " "),
2387 (STRING, "v"),
2388 (NEWLINE, "\n"),
2389 ],
2390 ),
2391 ("+:x\n", vec![(STRING, "+:x"), (NEWLINE, "\n")]),
2392 (
2393 "+\nx\n",
2394 vec![
2395 (STRING, "+"),
2396 (NEWLINE, "\n"),
2397 (STRING, "x"),
2398 (NEWLINE, "\n"),
2399 ],
2400 ),
2401 (
2402 "+\r\nx\n",
2403 vec![
2404 (STRING, "+"),
2405 (NEWLINE, "\r\n"),
2406 (STRING, "x"),
2407 (NEWLINE, "\n"),
2408 ],
2409 ),
2410 (
2411 "- |+\n x\n",
2412 vec![
2413 (DASH, "-"),
2414 (WHITESPACE, " "),
2415 (PIPE, "|"),
2416 (PLUS, "+"),
2417 (NEWLINE, "\n"),
2418 (INDENT, " "),
2419 (STRING, "x"),
2420 (NEWLINE, "\n"),
2421 ],
2422 ),
2423 (
2424 "/|+: v\n",
2425 vec![
2426 (STRING, "/|+"),
2427 (COLON, ":"),
2428 (WHITESPACE, " "),
2429 (STRING, "v"),
2430 (NEWLINE, "\n"),
2431 ],
2432 ),
2433 ("i:|+\n", vec![(STRING, "i:|+"), (NEWLINE, "\n")]),
2434 ("+.INF\n", vec![(FLOAT, "+.INF"), (NEWLINE, "\n")]),
2435 (
2436 "{+, a}\n",
2437 vec![
2438 (LEFT_BRACE, "{"),
2439 (STRING, "+"),
2440 (COMMA, ","),
2441 (WHITESPACE, " "),
2442 (STRING, "a"),
2443 (RIGHT_BRACE, "}"),
2444 (NEWLINE, "\n"),
2445 ],
2446 ),
2447 ("+,\n", vec![(STRING, "+,"), (NEWLINE, "\n")]),
2448 (
2449 "a: 1+2\n",
2450 vec![
2451 (STRING, "a"),
2452 (COLON, ":"),
2453 (WHITESPACE, " "),
2454 (STRING, "1+2"),
2455 (NEWLINE, "\n"),
2456 ],
2457 ),
2458 ("i:|2+\n", vec![(STRING, "i:|2+"), (NEWLINE, "\n")]),
2459 (
2460 "x: a |2+\n",
2461 vec![
2462 (STRING, "x"),
2463 (COLON, ":"),
2464 (WHITESPACE, " "),
2465 (STRING, "a |2+"),
2466 (NEWLINE, "\n"),
2467 ],
2468 ),
2469 (
2470 "k: |2+\n",
2471 vec![
2472 (STRING, "k"),
2473 (COLON, ":"),
2474 (WHITESPACE, " "),
2475 (PIPE, "|"),
2476 (INT, "2"),
2477 (PLUS, "+"),
2478 (NEWLINE, "\n"),
2479 ],
2480 ),
2481 (
2482 "- |2+\n",
2483 vec![
2484 (DASH, "-"),
2485 (WHITESPACE, " "),
2486 (PIPE, "|"),
2487 (INT, "2"),
2488 (PLUS, "+"),
2489 (NEWLINE, "\n"),
2490 ],
2491 ),
2492 ];
2493 for (input, expected) in cases {
2494 assert_eq!(lex(input), expected, "lexing {input:?}");
2495 }
2496 }
2497
2498 #[test]
2501 fn test_chomping_plus_after_properties() {
2502 use SyntaxKind::*;
2503 for (input, before) in [
2504 ("? |2+\n", vec![(QUESTION, "?")]),
2505 ("!t |2+\n", vec![(TAG, "!t")]),
2506 ("&a |2+\n", vec![(ANCHOR, "&a")]),
2507 ] {
2508 let mut expected = before;
2509 expected.extend([
2510 (WHITESPACE, " "),
2511 (PIPE, "|"),
2512 (INT, "2"),
2513 (PLUS, "+"),
2514 (NEWLINE, "\n"),
2515 ]);
2516 assert_eq!(lex(input), expected, "lexing {input:?}");
2517 }
2518 }
2519
2520 #[test]
2521 fn test_block_scalar_indicators() {
2522 let input1 = "key: |+ content";
2524 let tokens1 = lex(input1);
2525
2526 assert!(tokens1
2527 .iter()
2528 .any(|(kind, text)| *kind == SyntaxKind::PIPE && *text == "|"));
2529 assert!(tokens1
2530 .iter()
2531 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
2532
2533 let input2 = "key: >- content";
2535 let tokens2 = lex(input2);
2536
2537 assert!(tokens2
2538 .iter()
2539 .any(|(kind, text)| *kind == SyntaxKind::GREATER && *text == ">"));
2540 assert!(tokens2
2541 .iter()
2542 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-"));
2543
2544 let input3 = "key: |2+ content";
2546 let tokens3 = lex(input3);
2547
2548 assert!(tokens3
2549 .iter()
2550 .any(|(kind, text)| *kind == SyntaxKind::PIPE && *text == "|"));
2551 assert!(tokens3
2552 .iter()
2553 .any(|(kind, text)| *kind == SyntaxKind::INT && *text == "2"));
2554 assert!(tokens3
2555 .iter()
2556 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
2557 }
2558
2559 #[test]
2560 fn test_special_characters_in_block_content() {
2561 let input = "line with - and + and : characters";
2562 let tokens = lex(input);
2563
2564 assert!(tokens
2568 .iter()
2569 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "line with - and + and"));
2570 assert!(!tokens.iter().any(|(kind, _)| *kind == SyntaxKind::PLUS));
2571
2572 assert!(tokens
2574 .iter()
2575 .any(|(kind, text)| *kind == SyntaxKind::COLON && *text == ":"));
2576
2577 let joined: String = tokens.iter().map(|(_, t)| *t).collect();
2579 assert_eq!(joined, input);
2580 }
2581
2582 #[test]
2583 fn test_token_recognition() {
2584 let input = "key: |2+ \n content with - and : and > chars\n more content";
2585 let tokens = lex(input);
2586
2587 println!("Tokens:");
2589 for (i, (kind, text)) in tokens.iter().enumerate() {
2590 println!(" {}: {:?} = {:?}", i, kind, text);
2591 }
2592
2593 let count = |k: SyntaxKind| tokens.iter().filter(|(kind, _)| *kind == k).count();
2595 assert_eq!(count(SyntaxKind::COLON), 2);
2597 assert_eq!(count(SyntaxKind::PIPE), 1); assert_eq!(count(SyntaxKind::INT), 1); assert_eq!(count(SyntaxKind::PLUS), 1); assert_eq!(count(SyntaxKind::GREATER), 0);
2603 assert_eq!(count(SyntaxKind::NEWLINE), 2); assert_eq!(count(SyntaxKind::INDENT), 2); assert!(count(SyntaxKind::STRING) >= 1, "expected STRING tokens");
2607
2608 assert_eq!(
2609 tokens
2610 .iter()
2611 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "content with - and")
2612 .count(),
2613 1
2614 );
2615 }
2616
2617 #[test]
2618 fn test_dash_handling() {
2619 let input = "---\nkey: value";
2621 let tokens = lex(input);
2622 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
2623
2624 let input = "---";
2626 let tokens = lex(input);
2627 assert_eq!(tokens.len(), 1);
2628 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
2629
2630 let input = "--";
2632 let tokens = lex(input);
2633 assert_eq!(tokens, vec![(SyntaxKind::STRING, "--")]);
2634
2635 let input = "----";
2637 let tokens = lex(input);
2638 assert_eq!(tokens, vec![(SyntaxKind::STRING, "----")]);
2639 }
2640
2641 #[test]
2642 fn test_dash_in_different_scalar_contexts() {
2643 let input = "package-name: my-awesome-package-v2";
2645 let tokens = lex(input);
2646 assert_eq!(tokens[0], (SyntaxKind::STRING, "package-name"));
2647 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
2648 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
2649 assert_eq!(tokens[3], (SyntaxKind::STRING, "my-awesome-package-v2"));
2650
2651 let input = "id: 123e4567-e89b-12d3-a456-426614174000";
2653 let tokens = lex(input);
2654 assert_eq!(tokens[0], (SyntaxKind::STRING, "id"));
2655 assert_eq!(
2656 tokens[3],
2657 (SyntaxKind::STRING, "123e4567-e89b-12d3-a456-426614174000")
2658 );
2659
2660 let input = "args: --verbose --log-level=debug";
2664 let tokens = lex(input);
2665 assert_eq!(
2666 tokens[3],
2667 (SyntaxKind::STRING, "--verbose --log-level=debug")
2668 );
2669
2670 let input = "temperature: -40";
2672 let tokens = lex(input);
2673 assert_eq!(
2675 tokens
2676 .iter()
2677 .filter(|(kind, text)| *kind == SyntaxKind::INT && *text == "-40")
2678 .count(),
2679 1
2680 );
2681
2682 let input = "range: 1-10";
2684 let tokens = lex(input);
2685 assert_eq!(
2686 tokens
2687 .iter()
2688 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "1-10")
2689 .count(),
2690 1
2691 );
2692 }
2693
2694 #[test]
2695 fn test_sequence_markers_with_indentation() {
2696 let input = "- item1\n- item2";
2698 let tokens = lex(input);
2699 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
2700 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
2701 assert_eq!(tokens[2], (SyntaxKind::STRING, "item1"));
2702
2703 let input = " - item1\n - item2";
2705 let tokens = lex(input);
2706 assert_eq!(tokens[0], (SyntaxKind::INDENT, " "));
2707 assert_eq!(tokens[1], (SyntaxKind::DASH, "-"));
2708
2709 let input = "- item1\n - nested1\n - nested2\n- item2";
2712 let tokens = lex(input);
2713 let dash_tokens: Vec<_> = tokens
2714 .iter()
2715 .filter(|(kind, _)| *kind == SyntaxKind::DASH)
2716 .collect();
2717 assert_eq!(dash_tokens.len(), 2);
2718
2719 let input = "- first-item\n- second-item";
2721 let tokens = lex(input);
2722 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
2723 assert_eq!(tokens[2], (SyntaxKind::STRING, "first-item"));
2724 assert_eq!(tokens[4], (SyntaxKind::DASH, "-"));
2725 assert_eq!(tokens[6], (SyntaxKind::STRING, "second-item"));
2726 }
2727
2728 #[test]
2729 fn test_dash_after_colon() {
2730 let input = "key:-value";
2734 let tokens = lex(input);
2735 assert_eq!(tokens[0], (SyntaxKind::STRING, "key:-value"));
2736
2737 let input = "key: -value";
2739 let tokens = lex(input);
2740 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
2741 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
2742 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
2743 assert_eq!(tokens[3], (SyntaxKind::STRING, "-value"));
2744 }
2745
2746 #[test]
2747 fn test_yaml_spec_compliant_colon_handling() {
2748 let input = "http://example.com:8080/path";
2754 let tokens = lex(input);
2755 assert_eq!(tokens.len(), 1);
2756 assert_eq!(
2757 tokens[0],
2758 (SyntaxKind::STRING, "http://example.com:8080/path")
2759 );
2760
2761 let input = "2024:12:31:23:59:59";
2763 let tokens = lex(input);
2764 assert_eq!(tokens.len(), 1);
2765 assert_eq!(tokens[0], (SyntaxKind::STRING, "2024:12:31:23:59:59"));
2766
2767 let input = "key: value";
2769 let tokens = lex(input);
2770 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
2771 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
2772 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
2773 assert_eq!(tokens[3], (SyntaxKind::STRING, "value"));
2774
2775 let input = "key:value";
2777 let tokens = lex(input);
2778 assert_eq!(tokens.len(), 1);
2779 assert_eq!(tokens[0], (SyntaxKind::STRING, "key:value"));
2780
2781 let input = "a:b:c:d";
2783 let tokens = lex(input);
2784 assert_eq!(tokens.len(), 1);
2785 assert_eq!(tokens[0], (SyntaxKind::STRING, "a:b:c:d"));
2786 }
2787
2788 #[test]
2789 fn test_block_scalar_with_chomping() {
2790 let count_kind = |toks: &[(SyntaxKind, &str)], k: SyntaxKind| {
2792 toks.iter().filter(|(kind, _)| *kind == k).count()
2793 };
2794
2795 let input = "text: |-\n content";
2797 let tokens = lex(input);
2798 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
2799 assert_eq!(
2800 tokens
2801 .iter()
2802 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
2803 .count(),
2804 1
2805 );
2806
2807 let input = "text: |+\n content";
2809 let tokens = lex(input);
2810 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
2811 assert_eq!(count_kind(&tokens, SyntaxKind::PLUS), 1);
2812
2813 let input = "text: >-\n content";
2815 let tokens = lex(input);
2816 assert_eq!(count_kind(&tokens, SyntaxKind::GREATER), 1);
2817 assert_eq!(
2818 tokens
2819 .iter()
2820 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
2821 .count(),
2822 1
2823 );
2824
2825 let input = "text: |2-\n content";
2827 let tokens = lex(input);
2828 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
2829 let has_2_token = tokens.iter().any(|(kind, text)| {
2831 (*kind == SyntaxKind::STRING || *kind == SyntaxKind::INT) && text.contains("2")
2832 });
2833 assert!(has_2_token, "expected a token containing '2'");
2834 }
2835
2836 #[test]
2837 fn test_dash_edge_cases() {
2838 let input = "value-";
2840 let tokens = lex(input);
2841 assert_eq!(tokens[0], (SyntaxKind::STRING, "value-"));
2842
2843 let input = "-value";
2845 let tokens = lex(input);
2846 assert_eq!(tokens[0], (SyntaxKind::STRING, "-value"));
2847
2848 let input = "key: a---b";
2850 let tokens = lex(input);
2851 assert_eq!(
2852 tokens
2853 .iter()
2854 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "a---b")
2855 .count(),
2856 1
2857 );
2858
2859 let input = "key: value-\nnext: item";
2861 let tokens = lex(input);
2862 assert!(tokens
2863 .iter()
2864 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "value-"));
2865
2866 let input = "snake_case-with-dash_mix";
2868 let tokens = lex(input);
2869 assert_eq!(tokens[0], (SyntaxKind::STRING, "snake_case-with-dash_mix"));
2870 }
2871
2872 #[test]
2873 fn test_whitespace_validation_tab_indentation() {
2874 let input_with_tabs = "key: value\n\tindented_key: indented_value";
2876 let (tokens, errors) = lex_with_validation(input_with_tabs);
2877
2878 assert_eq!(errors.len(), 1);
2880 assert_eq!(errors[0].category, WhitespaceErrorCategory::TabIndentation);
2881 assert_eq!(
2882 errors[0].message,
2883 "Tab character used for indentation (forbidden in YAML)"
2884 );
2885
2886 assert!(tokens
2888 .iter()
2889 .any(|(kind, text)| *kind == SyntaxKind::INDENT && text.contains('\t')));
2890 }
2891
2892 #[test]
2893 fn test_whitespace_validation_line_endings() {
2894 let input_mixed = "line1\nline2\r\nline3\rline4";
2896 let config = ValidationConfig {
2897 enforce_consistent_line_endings: true,
2898 max_line_length: None,
2899 };
2900 let (tokens, errors) = lex_with_validation_config(input_mixed, &config);
2901
2902 assert!(errors
2904 .iter()
2905 .any(|e| e.category == WhitespaceErrorCategory::MixedLineEndings));
2906
2907 let newlines: Vec<_> = tokens
2909 .iter()
2910 .filter(|(kind, _)| *kind == SyntaxKind::NEWLINE)
2911 .collect();
2912 assert_eq!(newlines.len(), 3); assert_eq!(newlines[0].1, "\n");
2914 assert_eq!(newlines[1].1, "\r\n");
2915 assert_eq!(newlines[2].1, "\r");
2916 }
2917
2918 #[test]
2919 fn test_whitespace_validation_line_length() {
2920 let long_line = format!("key: {}", "a".repeat(150));
2922 let config = ValidationConfig {
2923 enforce_consistent_line_endings: false,
2924 max_line_length: Some(120),
2925 };
2926 let (_, errors) = lex_with_validation_config(&long_line, &config);
2927
2928 assert_eq!(errors.len(), 1);
2930 assert_eq!(errors[0].category, WhitespaceErrorCategory::LineTooLong);
2931 assert_eq!(errors[0].message, "Line too long (155 > 120 characters)");
2932 }
2933
2934 #[test]
2935 fn test_whitespace_validation_disabled() {
2936 let input_with_issues = "key: value\n\tindented: with_tabs\n";
2938 let config = ValidationConfig {
2939 enforce_consistent_line_endings: false,
2940 max_line_length: None,
2941 };
2942 let (tokens, errors) = lex_with_validation_config(input_with_issues, &config);
2943
2944 assert_eq!(errors.len(), 1);
2946 assert_eq!(errors[0].category, WhitespaceErrorCategory::TabIndentation);
2947
2948 assert!(!tokens.is_empty());
2950 }
2951
2952 #[test]
2953 fn test_dash_in_flow_collections() {
2954 let input = "[item-one, item-two]";
2956 let tokens = lex(input);
2957 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACKET, "["));
2958 assert_eq!(tokens[1], (SyntaxKind::STRING, "item-one"));
2959 assert_eq!(tokens[2], (SyntaxKind::COMMA, ","));
2960 assert_eq!(tokens[4], (SyntaxKind::STRING, "item-two"));
2961 assert_eq!(tokens[5], (SyntaxKind::RIGHT_BRACKET, "]"));
2962
2963 let input = "{kebab-key: kebab-value}";
2965 let tokens = lex(input);
2966 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACE, "{"));
2967 assert_eq!(tokens[1], (SyntaxKind::STRING, "kebab-key"));
2968 assert_eq!(tokens[2], (SyntaxKind::COLON, ":"));
2969 assert_eq!(tokens[4], (SyntaxKind::STRING, "kebab-value"));
2970 assert_eq!(tokens[5], (SyntaxKind::RIGHT_BRACE, "}"));
2971 }
2972
2973 #[test]
2974 fn test_dash_with_quotes() {
2975 let input = r#"key: "- not a sequence marker""#;
2977 let tokens = lex(input);
2978 assert_eq!(
2979 tokens
2980 .iter()
2981 .filter(|(kind, text)| {
2982 *kind == SyntaxKind::STRING && *text == "\"- not a sequence marker\""
2983 })
2984 .count(),
2985 1
2986 );
2987
2988 let input = r#"key: '- also not a sequence marker'"#;
2989 let tokens = lex(input);
2990 assert_eq!(
2991 tokens
2992 .iter()
2993 .filter(|(kind, text)| {
2994 *kind == SyntaxKind::STRING && *text == "'- also not a sequence marker'"
2995 })
2996 .count(),
2997 1
2998 );
2999 }
3000
3001 #[test]
3002 fn test_dash_in_multiline_values() {
3003 let input = "description: This is a multi-\n line value with dashes";
3008 let tokens = lex(input);
3009 assert!(tokens
3010 .iter()
3011 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "This is a multi-"));
3012
3013 let input = "text: value\n - but this is not a sequence";
3015 let tokens = lex(input);
3016 assert_eq!(
3017 &tokens[tokens.len() - 2..],
3018 [
3019 (SyntaxKind::INDENT, " "),
3020 (SyntaxKind::STRING, "- but this is not a sequence"),
3021 ]
3022 );
3023 }
3024
3025 #[test]
3026 fn test_dash_special_yaml_values() {
3027 let input = "date: 2024-01-15";
3029 let tokens = lex(input);
3030 assert!(tokens
3031 .iter()
3032 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "2024-01-15"));
3033
3034 let input = "timestamp: 2024-01-15T10:30:00-05:00";
3036 let tokens = lex(input);
3037 assert!(tokens.iter().any(
3039 |(kind, text)| *kind == SyntaxKind::STRING && *text == "2024-01-15T10:30:00-05:00"
3040 ));
3041
3042 let input = "version: 1.0.0-beta.1";
3044 let tokens = lex(input);
3045 assert!(tokens
3046 .iter()
3047 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "1.0.0-beta.1"));
3048 }
3049
3050 #[test]
3051 fn test_block_scalar_indentation_indicator_sets_body_column() {
3052 use SyntaxKind::*;
3053 assert_eq!(
3056 lex("- |2\n x\n y # z\n [a\n"),
3057 vec![
3058 (DASH, "-"),
3059 (WHITESPACE, " "),
3060 (PIPE, "|"),
3061 (INT, "2"),
3062 (NEWLINE, "\n"),
3063 (INDENT, " "),
3064 (STRING, "x"),
3065 (NEWLINE, "\n"),
3066 (INDENT, " "),
3067 (STRING, "y"),
3068 (WHITESPACE, " "),
3069 (STRING, "# z"),
3070 (NEWLINE, "\n"),
3071 (INDENT, " "),
3072 (STRING, "[a"),
3073 (NEWLINE, "\n"),
3074 ]
3075 );
3076 assert_eq!(
3077 lex("- |2\r\n x\r\n y # z\r\n [a\r\n"),
3078 vec![
3079 (DASH, "-"),
3080 (WHITESPACE, " "),
3081 (PIPE, "|"),
3082 (INT, "2"),
3083 (NEWLINE, "\r\n"),
3084 (INDENT, " "),
3085 (STRING, "x"),
3086 (NEWLINE, "\r\n"),
3087 (INDENT, " "),
3088 (STRING, "y"),
3089 (WHITESPACE, " "),
3090 (STRING, "# z"),
3091 (NEWLINE, "\r\n"),
3092 (INDENT, " "),
3093 (STRING, "[a"),
3094 (NEWLINE, "\r\n"),
3095 ]
3096 );
3097 }
3098
3099 #[test]
3100 fn test_block_scalar_indentation_indicator_counts_from_parent() {
3101 use SyntaxKind::*;
3102 assert_eq!(
3105 lex("- k: >-2\n x\n [a\n"),
3106 vec![
3107 (DASH, "-"),
3108 (WHITESPACE, " "),
3109 (STRING, "k"),
3110 (COLON, ":"),
3111 (WHITESPACE, " "),
3112 (GREATER, ">"),
3113 (INT, "-2"),
3114 (NEWLINE, "\n"),
3115 (INDENT, " "),
3116 (STRING, "x"),
3117 (NEWLINE, "\n"),
3118 (INDENT, " "),
3119 (STRING, "[a"),
3120 (NEWLINE, "\n"),
3121 ]
3122 );
3123 assert_eq!(
3125 lex("k:\n |1\n x\n # z\n"),
3126 vec![
3127 (STRING, "k"),
3128 (COLON, ":"),
3129 (NEWLINE, "\n"),
3130 (INDENT, " "),
3131 (PIPE, "|"),
3132 (INT, "1"),
3133 (NEWLINE, "\n"),
3134 (INDENT, " "),
3135 (STRING, "x"),
3136 (NEWLINE, "\n"),
3137 (INDENT, " "),
3138 (STRING, "# z"),
3139 (NEWLINE, "\n"),
3140 ]
3141 );
3142 assert_eq!(
3144 lex("- k: |1\n x\n j: [a]\n"),
3145 vec![
3146 (DASH, "-"),
3147 (WHITESPACE, " "),
3148 (STRING, "k"),
3149 (COLON, ":"),
3150 (WHITESPACE, " "),
3151 (PIPE, "|"),
3152 (INT, "1"),
3153 (NEWLINE, "\n"),
3154 (INDENT, " "),
3155 (STRING, "x"),
3156 (NEWLINE, "\n"),
3157 (INDENT, " "),
3158 (STRING, "j"),
3159 (COLON, ":"),
3160 (WHITESPACE, " "),
3161 (LEFT_BRACKET, "["),
3162 (STRING, "a"),
3163 (RIGHT_BRACKET, "]"),
3164 (NEWLINE, "\n"),
3165 ]
3166 );
3167 }
3168
3169 #[test]
3170 fn test_block_scalar_indentation_indicator_in_explicit_value() {
3171 use SyntaxKind::*;
3172 let head = [
3175 (STRING, "x"),
3176 (COLON, ":"),
3177 (NEWLINE, "\n"),
3178 (INDENT, " "),
3179 (QUESTION, "?"),
3180 (WHITESPACE, " "),
3181 (STRING, "a"),
3182 (NEWLINE, "\n"),
3183 (INDENT, " "),
3184 (COLON, ":"),
3185 ];
3186 let tail = [
3187 (INDENT, " "),
3188 (STRING, "x"),
3189 (NEWLINE, "\n"),
3190 (INDENT, " "),
3191 (LEFT_BRACKET, "["),
3192 (STRING, "a"),
3193 (RIGHT_BRACKET, "]"),
3194 (NEWLINE, "\n"),
3195 ];
3196 assert_eq!(
3197 lex("x:\n ? a\n : |1\n x\n [a]\n"),
3198 [
3199 &head[..],
3200 &[(WHITESPACE, " "), (PIPE, "|"), (INT, "1"), (NEWLINE, "\n")],
3201 &tail,
3202 ]
3203 .concat()
3204 );
3205 assert_eq!(
3206 lex("x:\n ? a\n :\n |1\n x\n [a]\n"),
3207 [
3208 &head[..],
3209 &[
3210 (NEWLINE, "\n"),
3211 (INDENT, " "),
3212 (PIPE, "|"),
3213 (INT, "1"),
3214 (NEWLINE, "\n"),
3215 ],
3216 &tail,
3217 ]
3218 .concat()
3219 );
3220 }
3221
3222 #[test]
3223 fn test_block_scalar_indentation_indicator_in_compact_explicit_value() {
3224 use SyntaxKind::*;
3225 let head = [
3228 (QUESTION, "?"),
3229 (WHITESPACE, " "),
3230 (STRING, "a"),
3231 (NEWLINE, "\n"),
3232 (COLON, ":"),
3233 (WHITESPACE, " "),
3234 (STRING, "b"),
3235 (COLON, ":"),
3236 ];
3237 let tail = [
3238 (INDENT, " "),
3239 (STRING, "x"),
3240 (NEWLINE, "\n"),
3241 (INDENT, " "),
3242 (STRING, "c"),
3243 (COLON, ":"),
3244 (WHITESPACE, " "),
3245 (LEFT_BRACKET, "["),
3246 (STRING, "d"),
3247 (RIGHT_BRACKET, "]"),
3248 (NEWLINE, "\n"),
3249 ];
3250 assert_eq!(
3251 lex("? a\n: b: |1\n x\n c: [d]\n"),
3252 [
3253 &head[..],
3254 &[(WHITESPACE, " "), (PIPE, "|"), (INT, "1"), (NEWLINE, "\n")],
3255 &tail,
3256 ]
3257 .concat()
3258 );
3259 assert_eq!(
3260 lex("? a\n: b:\n |1\n x\n c: [d]\n"),
3261 [
3262 &head[..],
3263 &[
3264 (NEWLINE, "\n"),
3265 (INDENT, " "),
3266 (PIPE, "|"),
3267 (INT, "1"),
3268 (NEWLINE, "\n"),
3269 ],
3270 &tail,
3271 ]
3272 .concat()
3273 );
3274 assert_eq!(
3276 lex("- ? a\n : b: |1\n x\n c: [d]\n"),
3277 vec![
3278 (DASH, "-"),
3279 (WHITESPACE, " "),
3280 (QUESTION, "?"),
3281 (WHITESPACE, " "),
3282 (STRING, "a"),
3283 (NEWLINE, "\n"),
3284 (INDENT, " "),
3285 (COLON, ":"),
3286 (WHITESPACE, " "),
3287 (STRING, "b"),
3288 (COLON, ":"),
3289 (WHITESPACE, " "),
3290 (PIPE, "|"),
3291 (INT, "1"),
3292 (NEWLINE, "\n"),
3293 (INDENT, " "),
3294 (STRING, "x"),
3295 (NEWLINE, "\n"),
3296 (INDENT, " "),
3297 (STRING, "c"),
3298 (COLON, ":"),
3299 (WHITESPACE, " "),
3300 (LEFT_BRACKET, "["),
3301 (STRING, "d"),
3302 (RIGHT_BRACKET, "]"),
3303 (NEWLINE, "\n"),
3304 ]
3305 );
3306 }
3307
3308 #[test]
3309 fn test_flow_indicators_in_block_scalar() {
3310 let input = "key: unix:///Users/${metadata.username}/path";
3313 let tokens = lex(input);
3314 assert_eq!(tokens.len(), 4);
3315 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
3316 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
3317 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
3318 assert_eq!(
3319 tokens[3],
3320 (
3321 SyntaxKind::STRING,
3322 "unix:///Users/${metadata.username}/path"
3323 )
3324 );
3325 }
3326
3327 #[test]
3328 fn test_block_scalar_body_tab_is_not_indentation() {
3329 use SyntaxKind::*;
3332 assert_eq!(
3333 lex("- |-\n \t[\n ]\"\n- b\n"),
3334 vec![
3335 (DASH, "-"),
3336 (WHITESPACE, " "),
3337 (PIPE, "|"),
3338 (STRING, "-"),
3339 (NEWLINE, "\n"),
3340 (INDENT, " \t"),
3341 (STRING, "["),
3342 (NEWLINE, "\n"),
3343 (INDENT, " "),
3344 (STRING, "]\""),
3345 (NEWLINE, "\n"),
3346 (DASH, "-"),
3347 (WHITESPACE, " "),
3348 (STRING, "b"),
3349 (NEWLINE, "\n"),
3350 ]
3351 );
3352 }
3353
3354 #[test]
3355 fn test_unterminated_verbatim_tag_ends_at_line_break() {
3356 use SyntaxKind::*;
3357 for line_break in ["\n", "\r\n", "\r"] {
3358 let input = format!("!<x{line_break}a");
3359 assert_eq!(
3360 lex(&input),
3361 vec![(TAG, "!<x"), (NEWLINE, line_break), (STRING, "a")]
3362 );
3363 }
3364 }
3365}