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 line_end = self.input[start_idx..]
1210 .find(['\n', '\r'])
1211 .map_or(self.input.len(), |i| start_idx + i);
1212 let end_idx = self.input[start_idx..line_end]
1213 .match_indices('#')
1214 .map(|(i, _)| start_idx + i)
1215 .find(|&i| is_hash_a_comment_start(self.input, i))
1216 .map_or(line_end, |i| {
1217 start_idx + self.input[start_idx..i].trim_end_matches([' ', '\t']).len()
1218 });
1219 while self.chars.next_if(|(idx, _)| *idx < end_idx).is_some() {}
1220 self.push(DIRECTIVE, start_idx, end_idx);
1221 }
1222 }
1223
1224 fn lex_line_break(&mut self, start_idx: usize, ch: char) {
1226 check_line_length(&mut self.errors, self.config, self.line_start, start_idx);
1227
1228 let (line_ending, end_pos) = if ch == '\n' {
1229 ("\n", start_idx + 1)
1230 } else if let Some((_, '\n')) = self.chars.peek() {
1231 self.chars.next();
1232 ("\r\n", start_idx + 2)
1233 } else {
1234 ("\r", start_idx + 1)
1235 };
1236
1237 note_line_ending(
1238 &mut self.errors,
1239 self.config,
1240 &mut self.detected_line_ending,
1241 line_ending,
1242 start_idx..end_pos,
1243 );
1244
1245 self.push(SyntaxKind::NEWLINE, start_idx, end_pos);
1246 self.line_start = end_pos;
1247 let in_body = self.block_scalar.is_some();
1248 track_block_scalar_body(
1249 &self.tokens,
1250 self.input,
1251 self.line_start,
1252 &mut self.block_scalar,
1253 );
1254 if in_body && self.block_scalar.is_none() {
1255 self.block_scalar_end = self.tokens.len();
1256 }
1257 }
1258
1259 fn lex_whitespace(&mut self, start_idx: usize, ch: char) {
1260 use SyntaxKind::*;
1261 let mut end_idx = start_idx + 1;
1262 let mut has_tabs = ch == '\t';
1263
1264 while let Some((idx, ch)) = self.chars.peek() {
1265 if *ch != ' ' && *ch != '\t' {
1266 break;
1267 }
1268 if *ch == '\t' {
1269 has_tabs = true;
1270 }
1271 end_idx = *idx + 1;
1272 self.chars.next();
1273 }
1274
1275 let is_indentation = start_idx == self.line_start;
1278
1279 if is_indentation {
1280 if has_tabs {
1282 self.errors.push(WhitespaceError {
1283 message: "Tab character used for indentation (forbidden in YAML)".to_string(),
1284 range: start_idx..end_idx,
1285 category: WhitespaceErrorCategory::TabIndentation,
1286 });
1287 }
1288 self.push(INDENT, start_idx, end_idx);
1289 } else {
1290 self.push(WHITESPACE, start_idx, end_idx);
1291 }
1292 }
1293
1294 fn lex_plain_scalar(&mut self, start_idx: usize, ch: char) {
1295 let after_header = ch.is_ascii_digit()
1298 && matches!(
1299 self.tokens.last(),
1300 Some((SyntaxKind::PIPE | SyntaxKind::GREATER, _))
1301 );
1302 let end = self.read_plain_scalar_body(start_idx + ch.len_utf8(), true, after_header);
1303 self.push_scalar(start_idx, end);
1304 }
1305
1306 fn finish(self) -> (Vec<(SyntaxKind, &'a str)>, Vec<WhitespaceError>) {
1307 let mut whitespace_errors = self.errors;
1308 if let Some(max_len) = self.config.max_line_length {
1310 let final_line_length = self.input.len() - self.line_start;
1311 if final_line_length > max_len && final_line_length > 0 {
1312 whitespace_errors.push(WhitespaceError {
1313 message: format!("Line too long ({final_line_length} > {max_len} characters)"),
1314 range: self.line_start..self.input.len(),
1315 category: WhitespaceErrorCategory::LineTooLong,
1316 });
1317 }
1318 }
1319 (self.tokens, whitespace_errors)
1320 }
1321}
1322
1323fn classify_scalar(text: &str) -> SyntaxKind {
1328 use crate::CoreScalarType;
1329 match crate::scalar::ScalarValue::classify_plain(text) {
1330 CoreScalarType::Null => SyntaxKind::NULL,
1331 CoreScalarType::Boolean => SyntaxKind::BOOL,
1332 CoreScalarType::Integer => SyntaxKind::INT,
1333 CoreScalarType::Float => SyntaxKind::FLOAT,
1334 CoreScalarType::String => SyntaxKind::STRING,
1335 }
1336}
1337
1338const YAML_SPECIAL_CHARS: &str = ":+-?[]{},'|>&*!%\"#";
1340
1341fn ends_document_marker(ch: char) -> bool {
1356 matches!(ch, ' ' | '\t' | '\n' | '\r')
1357}
1358
1359fn bom_starts_document(tokens: &[(SyntaxKind, &str)], input: &str, bom_idx: usize) -> bool {
1365 use SyntaxKind::*;
1366 let after_marker = tokens
1367 .iter()
1368 .rev()
1369 .find(|(kind, _)| !matches!(kind, WHITESPACE | INDENT | NEWLINE | COMMENT))
1370 .map_or(true, |(kind, _)| matches!(kind, DOC_START | DOC_END));
1371 let rest = &input[bom_idx + '\u{FEFF}'.len_utf8()..];
1372 let before_marker =
1373 rest.starts_with("---") && rest[3..].chars().next().map_or(true, ends_document_marker);
1374 after_marker || before_marker
1375}
1376
1377fn node_property_can_start(tokens: &[(SyntaxKind, &str)]) -> bool {
1378 for (kind, _) in tokens.iter().rev() {
1379 match kind {
1380 SyntaxKind::WHITESPACE | SyntaxKind::INDENT => continue,
1382 SyntaxKind::NEWLINE
1384 | SyntaxKind::COLON
1385 | SyntaxKind::DASH
1386 | SyntaxKind::QUESTION
1387 | SyntaxKind::COMMA
1388 | SyntaxKind::LEFT_BRACKET
1389 | SyntaxKind::LEFT_BRACE
1390 | SyntaxKind::DOC_START
1391 | SyntaxKind::BOM
1392 | SyntaxKind::TAG
1393 | SyntaxKind::ANCHOR
1394 | SyntaxKind::AMPERSAND
1397 | SyntaxKind::ASTERISK => return true,
1398 _ => return false,
1400 }
1401 }
1402 true
1404}
1405
1406fn line_is_blank(input: &str, line_start: usize) -> bool {
1408 input[line_start..]
1409 .split(['\n', '\r'])
1410 .next()
1411 .unwrap_or_default()
1412 .trim_matches([' ', '\t'])
1413 .is_empty()
1414}
1415
1416fn line_indent(input: &str, line_start: usize) -> usize {
1418 let line = &input[line_start..];
1419 line.len() - line.trim_start_matches([' ', '\t']).len()
1420}
1421
1422#[derive(Clone, Copy)]
1424struct BlockScalarBody {
1425 header_line_start: usize,
1427 header_token: usize,
1429 parent_column: usize,
1431 min_indent: usize,
1437 indent: Option<usize>,
1440}
1441
1442impl BlockScalarBody {
1443 fn new(tokens: &[(SyntaxKind, &str)], input: &str, line_start: usize) -> Self {
1446 let is_root = tokens
1449 .iter()
1450 .rev()
1451 .take_while(|(kind, _)| !matches!(kind, SyntaxKind::NEWLINE | SyntaxKind::BOM))
1452 .all(|(kind, _)| {
1453 matches!(
1454 kind,
1455 SyntaxKind::DOC_START
1456 | SyntaxKind::WHITESPACE
1457 | SyntaxKind::TAG
1458 | SyntaxKind::ANCHOR
1459 )
1460 });
1461 let parent_column = parent_column(tokens).unwrap_or(0);
1462 let min_indent = if is_root {
1463 0
1464 } else {
1465 line_indent(input, line_start).max(parent_column) + 1
1466 };
1467 Self {
1468 header_line_start: line_start,
1469 header_token: tokens.len(),
1470 parent_column,
1471 min_indent,
1472 indent: None,
1473 }
1474 }
1475}
1476
1477fn parent_column(tokens: &[(SyntaxKind, &str)]) -> Option<usize> {
1489 use SyntaxKind::*;
1490 for line in tokens.rsplit(|(kind, _)| matches!(kind, NEWLINE | BOM)) {
1491 let Some(last) = line.iter().rposition(|(kind, _)| {
1492 !matches!(
1493 kind,
1494 WHITESPACE
1495 | INDENT
1496 | COMMENT
1497 | TAG
1498 | ANCHOR
1499 | STRING
1500 | INT
1501 | FLOAT
1502 | BOOL
1503 | NULL
1504 | MERGE_KEY
1505 )
1506 }) else {
1507 continue;
1508 };
1509 let column = |index: usize| -> usize {
1510 line[..index]
1511 .iter()
1512 .map(|(_, text)| text.chars().count())
1513 .sum()
1514 };
1515 return match line[last].0 {
1516 DASH | QUESTION => Some(column(last)),
1517 COLON => {
1518 let key = line[..last]
1522 .iter()
1523 .position(|(kind, _)| {
1524 !matches!(kind, WHITESPACE | INDENT | DASH | QUESTION | COLON)
1525 })
1526 .unwrap_or(last);
1527 Some(column(key))
1528 }
1529 DOC_START | DOC_END => None,
1530 _ => Some(0),
1531 };
1532 }
1533 None
1534}
1535
1536pub(crate) fn block_scalar_header<'a>(
1544 tokens: impl IntoIterator<Item = (SyntaxKind, &'a str)>,
1545) -> (Option<usize>, usize) {
1546 let mut explicit_indent = None;
1547 let mut len = 0;
1548 for (kind, text) in tokens {
1549 match kind {
1550 SyntaxKind::INT => {
1551 let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
1559 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
1560 break;
1561 }
1562 explicit_indent = digits
1564 .chars()
1565 .next()
1566 .and_then(|c| c.to_digit(10))
1567 .filter(|d| *d > 0)
1568 .map(|d| d as usize);
1569 }
1570 SyntaxKind::STRING => {
1571 let mut chars = text.chars();
1577 let is_header = match (chars.next(), chars.next(), chars.next()) {
1578 (Some(a), None, _) => a == '+' || a == '-',
1579 (Some(a), Some(b), None) => {
1580 let digit = |c: char| c.is_ascii_digit() && c != '0';
1581 let chomp = |c: char| c == '+' || c == '-';
1582 (digit(a) && chomp(b)) || (chomp(a) && digit(b))
1583 }
1584 _ => false,
1585 };
1586 if !is_header {
1587 break;
1588 }
1589 explicit_indent = text
1590 .chars()
1591 .find_map(|c| c.to_digit(10))
1592 .filter(|d| *d > 0)
1593 .map(|d| d as usize);
1594 }
1595 SyntaxKind::PLUS | SyntaxKind::WHITESPACE => {}
1598 _ => break,
1599 }
1600 len += 1;
1601 }
1602 (explicit_indent, len)
1603}
1604
1605fn line_is_document_marker(input: &str, line_start: usize) -> bool {
1607 let line = &input[line_start..];
1608 (line.starts_with("---") || line.starts_with("..."))
1609 && line[3..].chars().next().map_or(true, ends_document_marker)
1610}
1611
1612fn track_block_scalar_body(
1622 tokens: &[(SyntaxKind, &str)],
1623 input: &str,
1624 line_start: usize,
1625 body: &mut Option<BlockScalarBody>,
1626) {
1627 let Some(current) = body else {
1628 return;
1629 };
1630 if current.indent.is_none() {
1634 if let (Some(indicator), _) =
1635 block_scalar_header(tokens[current.header_token + 1..].iter().copied())
1636 {
1637 current.indent = Some(current.parent_column + indicator);
1638 }
1639 }
1640 if line_is_blank(input, line_start) {
1641 return;
1642 }
1643 let indent = line_indent(input, line_start);
1644 if indent < current.indent.unwrap_or(current.min_indent)
1645 || line_is_document_marker(input, line_start)
1646 {
1647 *body = None;
1648 } else if current.indent.is_none() {
1649 let spaces = crate::column::space_indent(&input[line_start..]);
1652 current.indent = Some(if spaces >= current.min_indent {
1653 spaces
1654 } else {
1655 indent
1656 });
1657 }
1658}
1659
1660fn in_block_scalar_body(body: Option<BlockScalarBody>, line_start: usize) -> bool {
1669 body.is_some_and(|body| body.header_line_start != line_start)
1670}
1671
1672fn is_tag_char(ch: char) -> bool {
1680 if ch.is_whitespace() {
1681 return false;
1682 }
1683 !matches!(ch, ',' | '[' | ']' | '{' | '}' | '!')
1684}
1685
1686fn is_white_or_break(ch: char) -> bool {
1690 matches!(ch, ' ' | '\t' | '\n' | '\r')
1691}
1692
1693fn is_yaml_special(ch: char) -> bool {
1694 YAML_SPECIAL_CHARS.contains(ch)
1695}
1696
1697fn is_yaml_special_except(ch: char, exclude: &str) -> bool {
1699 YAML_SPECIAL_CHARS.contains(ch) && !exclude.contains(ch)
1700}
1701
1702pub(crate) fn is_printable(c: char) -> bool {
1705 matches!(c,
1706 '\t' | '\n' | '\r' | '\u{20}'..='\u{7e}' | '\u{85}' | '\u{a0}'..='\u{d7ff}'
1707 | '\u{e000}'..='\u{fffd}' | '\u{10000}'..)
1708 && c != '\u{feff}'
1709}
1710
1711pub(crate) fn is_valid_anchor_name(name: &str) -> bool {
1718 !name.is_empty() && name.chars().all(is_anchor_char)
1719}
1720
1721fn is_anchor_char(ch: char) -> bool {
1723 let ns_char = matches!(ch,
1725 '\u{21}'..='\u{7E}'
1726 | '\u{85}'
1727 | '\u{A0}'..='\u{D7FF}'
1728 | '\u{E000}'..='\u{FFFD}'
1729 | '\u{10000}'..='\u{10FFFF}')
1730 && ch != '\u{FEFF}';
1731 ns_char && !matches!(ch, ',' | '[' | ']' | '{' | '}')
1732}
1733
1734#[cfg(test)]
1735mod tests {
1736 use super::*;
1737
1738 #[test]
1739 fn test_is_valid_anchor_name() {
1740 assert!(is_valid_anchor_name("shared"));
1741 assert!(is_valid_anchor_name("shared_1"));
1742 assert!(is_valid_anchor_name("anchor.dotted"));
1743 assert!(is_valid_anchor_name("naïve"));
1744 for name in [
1746 "a:",
1747 "colon:name",
1748 "*star",
1749 "&",
1750 "-",
1751 "x?y",
1752 "#",
1753 "!t",
1754 "|>",
1755 "%'\"",
1756 "@`",
1757 "no\u{A0}break",
1758 "nel\u{85}",
1759 ] {
1760 assert!(is_valid_anchor_name(name), "{name:?}");
1761 }
1762
1763 for name in [
1764 "",
1765 "has space",
1766 "has\ttab",
1767 "has\nnewline",
1768 "has\rreturn",
1769 "comma,name",
1770 "bracket]name",
1771 "[",
1772 "{",
1773 "}",
1774 "bom\u{FEFF}",
1775 "bell\u{7}",
1776 "del\u{7F}",
1777 "c1\u{80}",
1778 ] {
1779 assert!(!is_valid_anchor_name(name), "{name:?}");
1780 }
1781 }
1782
1783 #[test]
1784 fn test_embedded_hyphens_stay_in_one_plain_scalar() {
1785 let input = format!("k: {}\n", "a-".repeat(200));
1786 let kinds: Vec<_> = lex(&input).into_iter().map(|(k, _)| k).collect();
1787 assert!(kinds.contains(&SyntaxKind::STRING) || kinds.contains(&SyntaxKind::SCALAR));
1788 let dash_count = kinds.iter().filter(|k| **k == SyntaxKind::DASH).count();
1789 assert_eq!(dash_count, 0);
1790 let seq = lex("- a\n- b\n");
1791 let seq_dashes = seq.iter().filter(|(k, _)| *k == SyntaxKind::DASH).count();
1792 assert_eq!(seq_dashes, 2);
1793 }
1794
1795 #[test]
1799 fn test_valid_anchor_names_round_trip_through_lexer() {
1800 for name in [
1801 "shared",
1802 "shared_1",
1803 "anchor.dotted",
1804 "naïve",
1805 "a:",
1806 "*star",
1807 "x#y",
1808 "no\u{A0}break",
1809 ] {
1810 let input = format!("*{name}");
1811 let tokens = lex(&input);
1812 assert_eq!(
1813 tokens,
1814 vec![(SyntaxKind::REFERENCE, input.as_str())],
1815 "name {name:?} did not round-trip as a single REFERENCE token"
1816 );
1817 }
1818 }
1819
1820 #[test]
1821 fn test_simple_mapping() {
1822 let input = "key: value";
1823 let tokens = lex(input);
1824
1825 assert_eq!(tokens.len(), 4);
1826 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
1827 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1828 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1829 assert_eq!(tokens[3], (SyntaxKind::STRING, "value"));
1830 }
1831
1832 #[test]
1833 fn test_scalar_types() {
1834 let tokens = lex("age: 42");
1836 assert_eq!(tokens[0], (SyntaxKind::STRING, "age"));
1837 assert_eq!(tokens[3], (SyntaxKind::INT, "42"));
1838
1839 let tokens = lex("pi: 3.14");
1841 assert_eq!(tokens[0], (SyntaxKind::STRING, "pi"));
1842 assert_eq!(tokens[3], (SyntaxKind::FLOAT, "3.14"));
1843
1844 let tokens = lex("enabled: true");
1846 assert_eq!(tokens[0], (SyntaxKind::STRING, "enabled"));
1847 assert_eq!(tokens[3], (SyntaxKind::BOOL, "true"));
1848
1849 let tokens = lex("disabled: false");
1851 assert_eq!(tokens[3], (SyntaxKind::BOOL, "false"));
1852
1853 let tokens = lex("value: null");
1855 assert_eq!(tokens[3], (SyntaxKind::NULL, "null"));
1856
1857 let tokens = lex("value: ~");
1859 assert_eq!(tokens[3], (SyntaxKind::NULL, "~"));
1860 }
1861
1862 #[test]
1863 fn test_sequences() {
1864 let input = "- item1\n- item2";
1865 let tokens = lex(input);
1866
1867 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1868 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1869 assert_eq!(tokens[2], (SyntaxKind::STRING, "item1"));
1870 assert_eq!(tokens[3], (SyntaxKind::NEWLINE, "\n"));
1871 assert_eq!(tokens[4], (SyntaxKind::DASH, "-"));
1872 assert_eq!(tokens[5], (SyntaxKind::WHITESPACE, " "));
1873 assert_eq!(tokens[6], (SyntaxKind::STRING, "item2"));
1874 }
1875
1876 #[test]
1877 fn test_hyphen_in_scalars() {
1878 let input = "Name: example-project";
1880 let tokens = lex(input);
1881
1882 println!("Hyphen test tokens:");
1883 for (i, (kind, text)) in tokens.iter().enumerate() {
1884 println!(" {}: {:?} = {:?}", i, kind, text);
1885 }
1886
1887 assert_eq!(tokens.len(), 4);
1889 assert_eq!(tokens[0], (SyntaxKind::STRING, "Name"));
1890 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
1891 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
1892 assert_eq!(tokens[3], (SyntaxKind::STRING, "example-project"));
1893 }
1894
1895 #[test]
1896 fn test_hyphen_sequence_vs_scalar() {
1897 let sequence_input = "- example-item";
1899 let tokens = lex(sequence_input);
1900
1901 println!("Sequence hyphen tokens:");
1902 for (i, (kind, text)) in tokens.iter().enumerate() {
1903 println!(" {}: {:?} = {:?}", i, kind, text);
1904 }
1905
1906 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
1908 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
1909 assert_eq!(tokens[2], (SyntaxKind::STRING, "example-item"));
1910
1911 let scalar_input = "package-name: my-awesome-package";
1913 let tokens = lex(scalar_input);
1914
1915 println!("Package hyphen tokens:");
1916 for (i, (kind, text)) in tokens.iter().enumerate() {
1917 println!(" {}: {:?} = {:?}", i, kind, text);
1918 }
1919
1920 assert_eq!(tokens.len(), 4);
1922 assert_eq!(tokens[0], (SyntaxKind::STRING, "package-name"));
1923 assert_eq!(tokens[3], (SyntaxKind::STRING, "my-awesome-package"));
1924 }
1925
1926 #[test]
1927 fn test_flow_style() {
1928 let tokens = lex("[1, 2, 3]");
1930 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACKET, "["));
1931 assert_eq!(tokens[1], (SyntaxKind::INT, "1"));
1932 assert_eq!(tokens[2], (SyntaxKind::COMMA, ","));
1933 assert_eq!(tokens[3], (SyntaxKind::WHITESPACE, " "));
1934 assert_eq!(tokens[4], (SyntaxKind::INT, "2"));
1935 assert_eq!(tokens[5], (SyntaxKind::COMMA, ","));
1936 assert_eq!(tokens[6], (SyntaxKind::WHITESPACE, " "));
1937 assert_eq!(tokens[7], (SyntaxKind::INT, "3"));
1938 assert_eq!(tokens[8], (SyntaxKind::RIGHT_BRACKET, "]"));
1939
1940 let tokens = lex("{a: 1, b: 2}");
1942 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACE, "{"));
1943 assert_eq!(tokens[1], (SyntaxKind::STRING, "a"));
1944 assert_eq!(tokens[2], (SyntaxKind::COLON, ":"));
1945 assert_eq!(tokens[3], (SyntaxKind::WHITESPACE, " "));
1946 assert_eq!(tokens[4], (SyntaxKind::INT, "1"));
1947 }
1948
1949 #[test]
1950 fn test_comments() {
1951 let input = "key: value # this is a comment\n# full line comment";
1952 let tokens = lex(input);
1953
1954 let comments: Vec<_> = tokens
1956 .iter()
1957 .filter(|(kind, _)| *kind == SyntaxKind::COMMENT)
1958 .collect();
1959
1960 assert_eq!(comments.len(), 2);
1961 assert_eq!(comments[0].1, "# this is a comment");
1962 assert_eq!(comments[1].1, "# full line comment");
1963 }
1964
1965 #[test]
1966 fn test_multiline_scalar() {
1967 let input = "key: value\n continued";
1968 let tokens = lex(input);
1969
1970 let indents: Vec<_> = tokens
1972 .iter()
1973 .filter(|(kind, _)| *kind == SyntaxKind::INDENT)
1974 .collect();
1975 assert_eq!(indents.len(), 1);
1976 assert_eq!(indents[0].1, " ");
1977 }
1978
1979 #[test]
1980 fn test_quoted_strings() {
1981 let input = r#"single: 'quoted'
1982double: "quoted""#;
1983 let tokens = lex(input);
1984
1985 let quoted_strings: Vec<_> = tokens
1987 .iter()
1988 .filter(|(kind, text)| {
1989 *kind == SyntaxKind::STRING && (text.starts_with('\'') || text.starts_with('"'))
1990 })
1991 .collect();
1992 assert_eq!(quoted_strings.len(), 2); let quoted_texts: Vec<&str> = {
1996 let mut v: Vec<&str> = quoted_strings.iter().map(|(_, t)| *t).collect();
1997 v.sort();
1998 v
1999 };
2000 assert_eq!(quoted_texts, ["\"quoted\"", "'quoted'"]);
2001 }
2002
2003 #[test]
2004 fn test_document_markers() {
2005 let input = "---\nkey: value\n...";
2006 let tokens = lex(input);
2007
2008 println!("Document tokens:");
2009 for (i, (kind, text)) in tokens.iter().enumerate() {
2010 println!(" {}: {:?} = {:?}", i, kind, text);
2011 }
2012
2013 let doc_start_count = tokens
2015 .iter()
2016 .filter(|(kind, _)| *kind == SyntaxKind::DOC_START)
2017 .count();
2018 let doc_end_count = tokens
2019 .iter()
2020 .filter(|(kind, _)| *kind == SyntaxKind::DOC_END)
2021 .count();
2022 assert_eq!(doc_start_count, 1);
2023 assert_eq!(doc_end_count, 1);
2024 }
2025
2026 #[test]
2027 fn test_empty_input() {
2028 let input = "";
2029 let tokens = lex(input);
2030 println!("Empty input tokens: {:?}", tokens);
2031 assert_eq!(tokens.len(), 0);
2032 }
2033
2034 #[test]
2035 fn test_anchors_and_aliases() {
2036 let input = "key: &anchor_name value";
2038 let tokens = lex(input);
2039 println!("Anchor tokens: {:?}", tokens);
2040
2041 let anchors: Vec<_> = tokens
2042 .iter()
2043 .filter(|(kind, _)| *kind == SyntaxKind::ANCHOR)
2044 .collect();
2045 assert_eq!(anchors.len(), 1);
2046 assert_eq!(anchors[0].1, "&anchor_name");
2047
2048 let input = "key: *reference_name";
2050 let tokens = lex(input);
2051 println!("Reference tokens: {:?}", tokens);
2052
2053 let references: Vec<_> = tokens
2054 .iter()
2055 .filter(|(kind, _)| *kind == SyntaxKind::REFERENCE)
2056 .collect();
2057 assert_eq!(references.len(), 1);
2058 assert_eq!(references[0].1, "*reference_name");
2059
2060 let input = "key: & *";
2062 let tokens = lex(input);
2063
2064 let ampersands: Vec<_> = tokens
2065 .iter()
2066 .filter(|(kind, _)| *kind == SyntaxKind::AMPERSAND)
2067 .collect();
2068 assert_eq!(ampersands.len(), 1);
2069
2070 let asterisks: Vec<_> = tokens
2071 .iter()
2072 .filter(|(kind, _)| *kind == SyntaxKind::ASTERISK)
2073 .collect();
2074 assert_eq!(asterisks.len(), 1);
2075 }
2076
2077 #[test]
2078 fn test_indicators_mid_plain_scalar_are_scalar_content() {
2079 for input in [
2086 "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",
2087 "k: a *b", "- a !b",
2088 ] {
2089 let tokens = lex(input);
2090 let bad: Vec<_> = tokens
2091 .iter()
2092 .filter(|(kind, _)| {
2093 matches!(
2094 kind,
2095 SyntaxKind::ANCHOR
2096 | SyntaxKind::REFERENCE
2097 | SyntaxKind::TAG
2098 | SyntaxKind::QUESTION
2099 )
2100 })
2101 .collect();
2102 assert!(
2103 bad.is_empty(),
2104 "{:?} produced structural tokens mid-scalar: {:?}",
2105 input,
2106 bad
2107 );
2108 }
2109 let tokens = lex("a: &real 1");
2111 assert!(
2112 tokens.iter().any(|(k, _)| *k == SyntaxKind::ANCHOR),
2113 "real anchor should still be recognized: {:?}",
2114 tokens
2115 );
2116 }
2117
2118 #[test]
2119 fn test_merge_key_token() {
2120 let input = "<<: *defaults";
2122 let tokens = lex(input);
2123
2124 let merge_keys: Vec<_> = tokens
2125 .iter()
2126 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2127 .collect();
2128 assert_eq!(merge_keys.len(), 1);
2129 assert_eq!(merge_keys[0].1, "<<");
2130
2131 let input2 = "key: < value";
2133 let tokens2 = lex(input2);
2134
2135 let merge_keys2: Vec<_> = tokens2
2136 .iter()
2137 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2138 .collect();
2139 assert_eq!(merge_keys2.len(), 0, "Single < should not be a merge key");
2140 }
2141
2142 #[test]
2143 fn test_angle_prefix_is_plain_scalar_not_merge_key() {
2144 let tokens = lex("<<foo: 1");
2145 let merge_keys: Vec<_> = tokens
2146 .iter()
2147 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2148 .collect();
2149 assert_eq!(
2150 merge_keys.len(),
2151 0,
2152 "<<foo should be a plain key, got {:?}",
2153 tokens
2154 );
2155 assert!(
2156 tokens
2157 .iter()
2158 .any(|(kind, text)| *kind != SyntaxKind::MERGE_KEY && *text == "<<foo"),
2159 "expected <<foo scalar, got {:?}",
2160 tokens
2161 );
2162
2163 let tokens = lex("{<<foo: 1, more: x}");
2164 assert_eq!(
2165 tokens
2166 .iter()
2167 .filter(|(kind, _)| *kind == SyntaxKind::MERGE_KEY)
2168 .count(),
2169 0,
2170 "flow <<foo should not be MERGE_KEY: {:?}",
2171 tokens
2172 );
2173
2174 let tokens = lex("<<: *defaults");
2175 assert_eq!(
2176 tokens
2177 .iter()
2178 .filter(|(kind, text)| *kind == SyntaxKind::MERGE_KEY && *text == "<<")
2179 .count(),
2180 1
2181 );
2182
2183 use crate::YamlFile;
2184 use std::str::FromStr;
2185 let file = YamlFile::from_str("<<foo: 1\nbar: 2\n").unwrap();
2186 let mapping = file.document().unwrap().as_mapping().unwrap();
2187 assert_eq!(mapping.keys().count(), 2);
2188 assert_eq!(
2189 mapping
2190 .get("<<foo")
2191 .unwrap()
2192 .as_scalar()
2193 .unwrap()
2194 .as_string(),
2195 "1"
2196 );
2197 }
2198
2199 #[test]
2203 fn test_merge_key_is_a_whole_plain_scalar() {
2204 use SyntaxKind::*;
2205 assert_eq!(
2206 lex("<<#: 1"),
2207 [(STRING, "<<#"), (COLON, ":"), (WHITESPACE, " "), (INT, "1")]
2208 );
2209 assert_eq!(
2210 lex("<<:x: 1"),
2211 [
2212 (STRING, "<<:x"),
2213 (COLON, ":"),
2214 (WHITESPACE, " "),
2215 (INT, "1")
2216 ]
2217 );
2218 assert_eq!(
2219 lex("<< x: 1"),
2220 [
2221 (STRING, "<< x"),
2222 (COLON, ":"),
2223 (WHITESPACE, " "),
2224 (INT, "1")
2225 ]
2226 );
2227 assert_eq!(
2228 lex("<<]: 1"),
2229 [(STRING, "<<]"), (COLON, ":"), (WHITESPACE, " "), (INT, "1")]
2230 );
2231 assert_eq!(
2232 lex("<< #c\n"),
2233 [
2234 (MERGE_KEY, "<<"),
2235 (WHITESPACE, " "),
2236 (COMMENT, "#c"),
2237 (NEWLINE, "\n")
2238 ]
2239 );
2240 assert_eq!(
2241 lex("{<<,<<: 1}"),
2242 [
2243 (LEFT_BRACE, "{"),
2244 (MERGE_KEY, "<<"),
2245 (COMMA, ","),
2246 (MERGE_KEY, "<<"),
2247 (COLON, ":"),
2248 (WHITESPACE, " "),
2249 (INT, "1"),
2250 (RIGHT_BRACE, "}")
2251 ]
2252 );
2253 }
2254
2255 #[test]
2256 fn test_angle_prefixed_scalar_uses_plain_scalar_rules() {
2257 use crate::YamlFile;
2261 use std::str::FromStr;
2262
2263 for key in [
2264 "<foo-bar",
2265 "<<foo-bar",
2266 "<foo!x",
2267 "<<foo!x",
2268 "<foo&x",
2269 "<<foo&x",
2270 "<foo:x",
2271 "<<foo:x",
2272 ] {
2273 let src = format!("{}: 1\n", key);
2274 let tokens = lex(&src);
2275 assert_eq!(
2276 tokens
2277 .iter()
2278 .filter(|(kind, _)| *kind != SyntaxKind::WHITESPACE)
2279 .map(|(_, text)| *text)
2280 .collect::<Vec<_>>(),
2281 vec![key, ":", "1", "\n"],
2282 "unexpected tokens for {:?}",
2283 src
2284 );
2285
2286 let file = YamlFile::from_str(&src).unwrap();
2287 assert_eq!(file.to_string(), src);
2288 let mapping = file.document().unwrap().as_mapping().unwrap();
2289 assert_eq!(
2290 mapping.get(key).unwrap().as_scalar().unwrap().as_string(),
2291 "1"
2292 );
2293 }
2294 }
2295
2296 #[test]
2297 fn test_plus_token() {
2298 let input = "key: |+ value";
2300 let tokens = lex(input);
2301
2302 let plus_tokens: Vec<_> = tokens
2303 .iter()
2304 .filter(|(kind, _)| *kind == SyntaxKind::PLUS)
2305 .collect();
2306 assert_eq!(plus_tokens.len(), 1);
2307 assert_eq!(plus_tokens[0].1, "+");
2308 }
2309
2310 #[test]
2313 fn test_plus_tokens() {
2314 use SyntaxKind::*;
2315 let cases: Vec<(&str, Vec<(SyntaxKind, &str)>)> = vec![
2316 (
2317 "+ x: v\n",
2318 vec![
2319 (STRING, "+ x"),
2320 (COLON, ":"),
2321 (WHITESPACE, " "),
2322 (STRING, "v"),
2323 (NEWLINE, "\n"),
2324 ],
2325 ),
2326 (
2327 "+\tx: v\n",
2328 vec![
2329 (STRING, "+\tx"),
2330 (COLON, ":"),
2331 (WHITESPACE, " "),
2332 (STRING, "v"),
2333 (NEWLINE, "\n"),
2334 ],
2335 ),
2336 (
2337 "+ \n",
2338 vec![(STRING, "+"), (WHITESPACE, " "), (NEWLINE, "\n")],
2339 ),
2340 (
2341 "+ #c\n",
2342 vec![
2343 (STRING, "+"),
2344 (WHITESPACE, " "),
2345 (COMMENT, "#c"),
2346 (NEWLINE, "\n"),
2347 ],
2348 ),
2349 ("+[b]\n", vec![(STRING, "+[b]"), (NEWLINE, "\n")]),
2350 (
2351 "[+[b]]\n",
2352 vec![
2353 (LEFT_BRACKET, "["),
2354 (STRING, "+"),
2355 (LEFT_BRACKET, "["),
2356 (STRING, "b"),
2357 (RIGHT_BRACKET, "]"),
2358 (RIGHT_BRACKET, "]"),
2359 (NEWLINE, "\n"),
2360 ],
2361 ),
2362 ("+'a'\n", vec![(STRING, "+'a'"), (NEWLINE, "\n")]),
2363 ("+?\n", vec![(STRING, "+?"), (NEWLINE, "\n")]),
2364 ("+|\n", vec![(STRING, "+|"), (NEWLINE, "\n")]),
2365 (
2366 "+#: v\n",
2367 vec![
2368 (STRING, "+#"),
2369 (COLON, ":"),
2370 (WHITESPACE, " "),
2371 (STRING, "v"),
2372 (NEWLINE, "\n"),
2373 ],
2374 ),
2375 (
2376 "+ #: v\n",
2377 vec![
2378 (STRING, "+"),
2379 (WHITESPACE, " "),
2380 (COMMENT, "#: v"),
2381 (NEWLINE, "\n"),
2382 ],
2383 ),
2384 (
2385 "+: v\n",
2386 vec![
2387 (STRING, "+"),
2388 (COLON, ":"),
2389 (WHITESPACE, " "),
2390 (STRING, "v"),
2391 (NEWLINE, "\n"),
2392 ],
2393 ),
2394 (
2395 "+ : v\n",
2396 vec![
2397 (STRING, "+"),
2398 (WHITESPACE, " "),
2399 (COLON, ":"),
2400 (WHITESPACE, " "),
2401 (STRING, "v"),
2402 (NEWLINE, "\n"),
2403 ],
2404 ),
2405 ("+:x\n", vec![(STRING, "+:x"), (NEWLINE, "\n")]),
2406 (
2407 "+\nx\n",
2408 vec![
2409 (STRING, "+"),
2410 (NEWLINE, "\n"),
2411 (STRING, "x"),
2412 (NEWLINE, "\n"),
2413 ],
2414 ),
2415 (
2416 "+\r\nx\n",
2417 vec![
2418 (STRING, "+"),
2419 (NEWLINE, "\r\n"),
2420 (STRING, "x"),
2421 (NEWLINE, "\n"),
2422 ],
2423 ),
2424 (
2425 "- |+\n x\n",
2426 vec![
2427 (DASH, "-"),
2428 (WHITESPACE, " "),
2429 (PIPE, "|"),
2430 (PLUS, "+"),
2431 (NEWLINE, "\n"),
2432 (INDENT, " "),
2433 (STRING, "x"),
2434 (NEWLINE, "\n"),
2435 ],
2436 ),
2437 (
2438 "/|+: v\n",
2439 vec![
2440 (STRING, "/|+"),
2441 (COLON, ":"),
2442 (WHITESPACE, " "),
2443 (STRING, "v"),
2444 (NEWLINE, "\n"),
2445 ],
2446 ),
2447 ("i:|+\n", vec![(STRING, "i:|+"), (NEWLINE, "\n")]),
2448 ("+.INF\n", vec![(FLOAT, "+.INF"), (NEWLINE, "\n")]),
2449 (
2450 "{+, a}\n",
2451 vec![
2452 (LEFT_BRACE, "{"),
2453 (STRING, "+"),
2454 (COMMA, ","),
2455 (WHITESPACE, " "),
2456 (STRING, "a"),
2457 (RIGHT_BRACE, "}"),
2458 (NEWLINE, "\n"),
2459 ],
2460 ),
2461 ("+,\n", vec![(STRING, "+,"), (NEWLINE, "\n")]),
2462 (
2463 "a: 1+2\n",
2464 vec![
2465 (STRING, "a"),
2466 (COLON, ":"),
2467 (WHITESPACE, " "),
2468 (STRING, "1+2"),
2469 (NEWLINE, "\n"),
2470 ],
2471 ),
2472 ("i:|2+\n", vec![(STRING, "i:|2+"), (NEWLINE, "\n")]),
2473 (
2474 "x: a |2+\n",
2475 vec![
2476 (STRING, "x"),
2477 (COLON, ":"),
2478 (WHITESPACE, " "),
2479 (STRING, "a |2+"),
2480 (NEWLINE, "\n"),
2481 ],
2482 ),
2483 (
2484 "k: |2+\n",
2485 vec![
2486 (STRING, "k"),
2487 (COLON, ":"),
2488 (WHITESPACE, " "),
2489 (PIPE, "|"),
2490 (INT, "2"),
2491 (PLUS, "+"),
2492 (NEWLINE, "\n"),
2493 ],
2494 ),
2495 (
2496 "- |2+\n",
2497 vec![
2498 (DASH, "-"),
2499 (WHITESPACE, " "),
2500 (PIPE, "|"),
2501 (INT, "2"),
2502 (PLUS, "+"),
2503 (NEWLINE, "\n"),
2504 ],
2505 ),
2506 ];
2507 for (input, expected) in cases {
2508 assert_eq!(lex(input), expected, "lexing {input:?}");
2509 }
2510 }
2511
2512 #[test]
2515 fn test_chomping_plus_after_properties() {
2516 use SyntaxKind::*;
2517 for (input, before) in [
2518 ("? |2+\n", vec![(QUESTION, "?")]),
2519 ("!t |2+\n", vec![(TAG, "!t")]),
2520 ("&a |2+\n", vec![(ANCHOR, "&a")]),
2521 ] {
2522 let mut expected = before;
2523 expected.extend([
2524 (WHITESPACE, " "),
2525 (PIPE, "|"),
2526 (INT, "2"),
2527 (PLUS, "+"),
2528 (NEWLINE, "\n"),
2529 ]);
2530 assert_eq!(lex(input), expected, "lexing {input:?}");
2531 }
2532 }
2533
2534 #[test]
2535 fn test_block_scalar_indicators() {
2536 let input1 = "key: |+ content";
2538 let tokens1 = lex(input1);
2539
2540 assert!(tokens1
2541 .iter()
2542 .any(|(kind, text)| *kind == SyntaxKind::PIPE && *text == "|"));
2543 assert!(tokens1
2544 .iter()
2545 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
2546
2547 let input2 = "key: >- content";
2549 let tokens2 = lex(input2);
2550
2551 assert!(tokens2
2552 .iter()
2553 .any(|(kind, text)| *kind == SyntaxKind::GREATER && *text == ">"));
2554 assert!(tokens2
2555 .iter()
2556 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-"));
2557
2558 let input3 = "key: |2+ content";
2560 let tokens3 = lex(input3);
2561
2562 assert!(tokens3
2563 .iter()
2564 .any(|(kind, text)| *kind == SyntaxKind::PIPE && *text == "|"));
2565 assert!(tokens3
2566 .iter()
2567 .any(|(kind, text)| *kind == SyntaxKind::INT && *text == "2"));
2568 assert!(tokens3
2569 .iter()
2570 .any(|(kind, text)| *kind == SyntaxKind::PLUS && *text == "+"));
2571 }
2572
2573 #[test]
2574 fn test_special_characters_in_block_content() {
2575 let input = "line with - and + and : characters";
2576 let tokens = lex(input);
2577
2578 assert!(tokens
2582 .iter()
2583 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "line with - and + and"));
2584 assert!(!tokens.iter().any(|(kind, _)| *kind == SyntaxKind::PLUS));
2585
2586 assert!(tokens
2588 .iter()
2589 .any(|(kind, text)| *kind == SyntaxKind::COLON && *text == ":"));
2590
2591 let joined: String = tokens.iter().map(|(_, t)| *t).collect();
2593 assert_eq!(joined, input);
2594 }
2595
2596 #[test]
2597 fn test_token_recognition() {
2598 let input = "key: |2+ \n content with - and : and > chars\n more content";
2599 let tokens = lex(input);
2600
2601 println!("Tokens:");
2603 for (i, (kind, text)) in tokens.iter().enumerate() {
2604 println!(" {}: {:?} = {:?}", i, kind, text);
2605 }
2606
2607 let count = |k: SyntaxKind| tokens.iter().filter(|(kind, _)| *kind == k).count();
2609 assert_eq!(count(SyntaxKind::COLON), 2);
2611 assert_eq!(count(SyntaxKind::PIPE), 1); assert_eq!(count(SyntaxKind::INT), 1); assert_eq!(count(SyntaxKind::PLUS), 1); assert_eq!(count(SyntaxKind::GREATER), 0);
2617 assert_eq!(count(SyntaxKind::NEWLINE), 2); assert_eq!(count(SyntaxKind::INDENT), 2); assert!(count(SyntaxKind::STRING) >= 1, "expected STRING tokens");
2621
2622 assert_eq!(
2623 tokens
2624 .iter()
2625 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "content with - and")
2626 .count(),
2627 1
2628 );
2629 }
2630
2631 #[test]
2632 fn test_dash_handling() {
2633 let input = "---\nkey: value";
2635 let tokens = lex(input);
2636 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
2637
2638 let input = "---";
2640 let tokens = lex(input);
2641 assert_eq!(tokens.len(), 1);
2642 assert_eq!(tokens[0], (SyntaxKind::DOC_START, "---"));
2643
2644 let input = "--";
2646 let tokens = lex(input);
2647 assert_eq!(tokens, vec![(SyntaxKind::STRING, "--")]);
2648
2649 let input = "----";
2651 let tokens = lex(input);
2652 assert_eq!(tokens, vec![(SyntaxKind::STRING, "----")]);
2653 }
2654
2655 #[test]
2656 fn test_dash_in_different_scalar_contexts() {
2657 let input = "package-name: my-awesome-package-v2";
2659 let tokens = lex(input);
2660 assert_eq!(tokens[0], (SyntaxKind::STRING, "package-name"));
2661 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
2662 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
2663 assert_eq!(tokens[3], (SyntaxKind::STRING, "my-awesome-package-v2"));
2664
2665 let input = "id: 123e4567-e89b-12d3-a456-426614174000";
2667 let tokens = lex(input);
2668 assert_eq!(tokens[0], (SyntaxKind::STRING, "id"));
2669 assert_eq!(
2670 tokens[3],
2671 (SyntaxKind::STRING, "123e4567-e89b-12d3-a456-426614174000")
2672 );
2673
2674 let input = "args: --verbose --log-level=debug";
2678 let tokens = lex(input);
2679 assert_eq!(
2680 tokens[3],
2681 (SyntaxKind::STRING, "--verbose --log-level=debug")
2682 );
2683
2684 let input = "temperature: -40";
2686 let tokens = lex(input);
2687 assert_eq!(
2689 tokens
2690 .iter()
2691 .filter(|(kind, text)| *kind == SyntaxKind::INT && *text == "-40")
2692 .count(),
2693 1
2694 );
2695
2696 let input = "range: 1-10";
2698 let tokens = lex(input);
2699 assert_eq!(
2700 tokens
2701 .iter()
2702 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "1-10")
2703 .count(),
2704 1
2705 );
2706 }
2707
2708 #[test]
2709 fn test_sequence_markers_with_indentation() {
2710 let input = "- item1\n- item2";
2712 let tokens = lex(input);
2713 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
2714 assert_eq!(tokens[1], (SyntaxKind::WHITESPACE, " "));
2715 assert_eq!(tokens[2], (SyntaxKind::STRING, "item1"));
2716
2717 let input = " - item1\n - item2";
2719 let tokens = lex(input);
2720 assert_eq!(tokens[0], (SyntaxKind::INDENT, " "));
2721 assert_eq!(tokens[1], (SyntaxKind::DASH, "-"));
2722
2723 let input = "- item1\n - nested1\n - nested2\n- item2";
2726 let tokens = lex(input);
2727 let dash_tokens: Vec<_> = tokens
2728 .iter()
2729 .filter(|(kind, _)| *kind == SyntaxKind::DASH)
2730 .collect();
2731 assert_eq!(dash_tokens.len(), 2);
2732
2733 let input = "- first-item\n- second-item";
2735 let tokens = lex(input);
2736 assert_eq!(tokens[0], (SyntaxKind::DASH, "-"));
2737 assert_eq!(tokens[2], (SyntaxKind::STRING, "first-item"));
2738 assert_eq!(tokens[4], (SyntaxKind::DASH, "-"));
2739 assert_eq!(tokens[6], (SyntaxKind::STRING, "second-item"));
2740 }
2741
2742 #[test]
2743 fn test_dash_after_colon() {
2744 let input = "key:-value";
2748 let tokens = lex(input);
2749 assert_eq!(tokens[0], (SyntaxKind::STRING, "key:-value"));
2750
2751 let input = "key: -value";
2753 let tokens = lex(input);
2754 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
2755 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
2756 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
2757 assert_eq!(tokens[3], (SyntaxKind::STRING, "-value"));
2758 }
2759
2760 #[test]
2761 fn test_yaml_spec_compliant_colon_handling() {
2762 let input = "http://example.com:8080/path";
2768 let tokens = lex(input);
2769 assert_eq!(tokens.len(), 1);
2770 assert_eq!(
2771 tokens[0],
2772 (SyntaxKind::STRING, "http://example.com:8080/path")
2773 );
2774
2775 let input = "2024:12:31:23:59:59";
2777 let tokens = lex(input);
2778 assert_eq!(tokens.len(), 1);
2779 assert_eq!(tokens[0], (SyntaxKind::STRING, "2024:12:31:23:59:59"));
2780
2781 let input = "key: value";
2783 let tokens = lex(input);
2784 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
2785 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
2786 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
2787 assert_eq!(tokens[3], (SyntaxKind::STRING, "value"));
2788
2789 let input = "key:value";
2791 let tokens = lex(input);
2792 assert_eq!(tokens.len(), 1);
2793 assert_eq!(tokens[0], (SyntaxKind::STRING, "key:value"));
2794
2795 let input = "a:b:c:d";
2797 let tokens = lex(input);
2798 assert_eq!(tokens.len(), 1);
2799 assert_eq!(tokens[0], (SyntaxKind::STRING, "a:b:c:d"));
2800 }
2801
2802 #[test]
2803 fn test_block_scalar_with_chomping() {
2804 let count_kind = |toks: &[(SyntaxKind, &str)], k: SyntaxKind| {
2806 toks.iter().filter(|(kind, _)| *kind == k).count()
2807 };
2808
2809 let input = "text: |-\n content";
2811 let tokens = lex(input);
2812 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
2813 assert_eq!(
2814 tokens
2815 .iter()
2816 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
2817 .count(),
2818 1
2819 );
2820
2821 let input = "text: |+\n content";
2823 let tokens = lex(input);
2824 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
2825 assert_eq!(count_kind(&tokens, SyntaxKind::PLUS), 1);
2826
2827 let input = "text: >-\n content";
2829 let tokens = lex(input);
2830 assert_eq!(count_kind(&tokens, SyntaxKind::GREATER), 1);
2831 assert_eq!(
2832 tokens
2833 .iter()
2834 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "-")
2835 .count(),
2836 1
2837 );
2838
2839 let input = "text: |2-\n content";
2841 let tokens = lex(input);
2842 assert_eq!(count_kind(&tokens, SyntaxKind::PIPE), 1);
2843 let has_2_token = tokens.iter().any(|(kind, text)| {
2845 (*kind == SyntaxKind::STRING || *kind == SyntaxKind::INT) && text.contains("2")
2846 });
2847 assert!(has_2_token, "expected a token containing '2'");
2848 }
2849
2850 #[test]
2851 fn test_dash_edge_cases() {
2852 let input = "value-";
2854 let tokens = lex(input);
2855 assert_eq!(tokens[0], (SyntaxKind::STRING, "value-"));
2856
2857 let input = "-value";
2859 let tokens = lex(input);
2860 assert_eq!(tokens[0], (SyntaxKind::STRING, "-value"));
2861
2862 let input = "key: a---b";
2864 let tokens = lex(input);
2865 assert_eq!(
2866 tokens
2867 .iter()
2868 .filter(|(kind, text)| *kind == SyntaxKind::STRING && *text == "a---b")
2869 .count(),
2870 1
2871 );
2872
2873 let input = "key: value-\nnext: item";
2875 let tokens = lex(input);
2876 assert!(tokens
2877 .iter()
2878 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "value-"));
2879
2880 let input = "snake_case-with-dash_mix";
2882 let tokens = lex(input);
2883 assert_eq!(tokens[0], (SyntaxKind::STRING, "snake_case-with-dash_mix"));
2884 }
2885
2886 #[test]
2887 fn test_whitespace_validation_tab_indentation() {
2888 let input_with_tabs = "key: value\n\tindented_key: indented_value";
2890 let (tokens, errors) = lex_with_validation(input_with_tabs);
2891
2892 assert_eq!(errors.len(), 1);
2894 assert_eq!(errors[0].category, WhitespaceErrorCategory::TabIndentation);
2895 assert_eq!(
2896 errors[0].message,
2897 "Tab character used for indentation (forbidden in YAML)"
2898 );
2899
2900 assert!(tokens
2902 .iter()
2903 .any(|(kind, text)| *kind == SyntaxKind::INDENT && text.contains('\t')));
2904 }
2905
2906 #[test]
2907 fn test_whitespace_validation_line_endings() {
2908 let input_mixed = "line1\nline2\r\nline3\rline4";
2910 let config = ValidationConfig {
2911 enforce_consistent_line_endings: true,
2912 max_line_length: None,
2913 };
2914 let (tokens, errors) = lex_with_validation_config(input_mixed, &config);
2915
2916 assert!(errors
2918 .iter()
2919 .any(|e| e.category == WhitespaceErrorCategory::MixedLineEndings));
2920
2921 let newlines: Vec<_> = tokens
2923 .iter()
2924 .filter(|(kind, _)| *kind == SyntaxKind::NEWLINE)
2925 .collect();
2926 assert_eq!(newlines.len(), 3); assert_eq!(newlines[0].1, "\n");
2928 assert_eq!(newlines[1].1, "\r\n");
2929 assert_eq!(newlines[2].1, "\r");
2930 }
2931
2932 #[test]
2933 fn test_whitespace_validation_line_length() {
2934 let long_line = format!("key: {}", "a".repeat(150));
2936 let config = ValidationConfig {
2937 enforce_consistent_line_endings: false,
2938 max_line_length: Some(120),
2939 };
2940 let (_, errors) = lex_with_validation_config(&long_line, &config);
2941
2942 assert_eq!(errors.len(), 1);
2944 assert_eq!(errors[0].category, WhitespaceErrorCategory::LineTooLong);
2945 assert_eq!(errors[0].message, "Line too long (155 > 120 characters)");
2946 }
2947
2948 #[test]
2949 fn test_whitespace_validation_disabled() {
2950 let input_with_issues = "key: value\n\tindented: with_tabs\n";
2952 let config = ValidationConfig {
2953 enforce_consistent_line_endings: false,
2954 max_line_length: None,
2955 };
2956 let (tokens, errors) = lex_with_validation_config(input_with_issues, &config);
2957
2958 assert_eq!(errors.len(), 1);
2960 assert_eq!(errors[0].category, WhitespaceErrorCategory::TabIndentation);
2961
2962 assert!(!tokens.is_empty());
2964 }
2965
2966 #[test]
2967 fn test_dash_in_flow_collections() {
2968 let input = "[item-one, item-two]";
2970 let tokens = lex(input);
2971 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACKET, "["));
2972 assert_eq!(tokens[1], (SyntaxKind::STRING, "item-one"));
2973 assert_eq!(tokens[2], (SyntaxKind::COMMA, ","));
2974 assert_eq!(tokens[4], (SyntaxKind::STRING, "item-two"));
2975 assert_eq!(tokens[5], (SyntaxKind::RIGHT_BRACKET, "]"));
2976
2977 let input = "{kebab-key: kebab-value}";
2979 let tokens = lex(input);
2980 assert_eq!(tokens[0], (SyntaxKind::LEFT_BRACE, "{"));
2981 assert_eq!(tokens[1], (SyntaxKind::STRING, "kebab-key"));
2982 assert_eq!(tokens[2], (SyntaxKind::COLON, ":"));
2983 assert_eq!(tokens[4], (SyntaxKind::STRING, "kebab-value"));
2984 assert_eq!(tokens[5], (SyntaxKind::RIGHT_BRACE, "}"));
2985 }
2986
2987 #[test]
2988 fn test_dash_with_quotes() {
2989 let input = r#"key: "- not a sequence marker""#;
2991 let tokens = lex(input);
2992 assert_eq!(
2993 tokens
2994 .iter()
2995 .filter(|(kind, text)| {
2996 *kind == SyntaxKind::STRING && *text == "\"- not a sequence marker\""
2997 })
2998 .count(),
2999 1
3000 );
3001
3002 let input = r#"key: '- also not a sequence marker'"#;
3003 let tokens = lex(input);
3004 assert_eq!(
3005 tokens
3006 .iter()
3007 .filter(|(kind, text)| {
3008 *kind == SyntaxKind::STRING && *text == "'- also not a sequence marker'"
3009 })
3010 .count(),
3011 1
3012 );
3013 }
3014
3015 #[test]
3016 fn test_dash_in_multiline_values() {
3017 let input = "description: This is a multi-\n line value with dashes";
3022 let tokens = lex(input);
3023 assert!(tokens
3024 .iter()
3025 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "This is a multi-"));
3026
3027 let input = "text: value\n - but this is not a sequence";
3029 let tokens = lex(input);
3030 assert_eq!(
3031 &tokens[tokens.len() - 2..],
3032 [
3033 (SyntaxKind::INDENT, " "),
3034 (SyntaxKind::STRING, "- but this is not a sequence"),
3035 ]
3036 );
3037 }
3038
3039 #[test]
3040 fn test_dash_special_yaml_values() {
3041 let input = "date: 2024-01-15";
3043 let tokens = lex(input);
3044 assert!(tokens
3045 .iter()
3046 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "2024-01-15"));
3047
3048 let input = "timestamp: 2024-01-15T10:30:00-05:00";
3050 let tokens = lex(input);
3051 assert!(tokens.iter().any(
3053 |(kind, text)| *kind == SyntaxKind::STRING && *text == "2024-01-15T10:30:00-05:00"
3054 ));
3055
3056 let input = "version: 1.0.0-beta.1";
3058 let tokens = lex(input);
3059 assert!(tokens
3060 .iter()
3061 .any(|(kind, text)| *kind == SyntaxKind::STRING && *text == "1.0.0-beta.1"));
3062 }
3063
3064 #[test]
3065 fn test_block_scalar_indentation_indicator_sets_body_column() {
3066 use SyntaxKind::*;
3067 assert_eq!(
3070 lex("- |2\n x\n y # z\n [a\n"),
3071 vec![
3072 (DASH, "-"),
3073 (WHITESPACE, " "),
3074 (PIPE, "|"),
3075 (INT, "2"),
3076 (NEWLINE, "\n"),
3077 (INDENT, " "),
3078 (STRING, "x"),
3079 (NEWLINE, "\n"),
3080 (INDENT, " "),
3081 (STRING, "y"),
3082 (WHITESPACE, " "),
3083 (STRING, "# z"),
3084 (NEWLINE, "\n"),
3085 (INDENT, " "),
3086 (STRING, "[a"),
3087 (NEWLINE, "\n"),
3088 ]
3089 );
3090 assert_eq!(
3091 lex("- |2\r\n x\r\n y # z\r\n [a\r\n"),
3092 vec![
3093 (DASH, "-"),
3094 (WHITESPACE, " "),
3095 (PIPE, "|"),
3096 (INT, "2"),
3097 (NEWLINE, "\r\n"),
3098 (INDENT, " "),
3099 (STRING, "x"),
3100 (NEWLINE, "\r\n"),
3101 (INDENT, " "),
3102 (STRING, "y"),
3103 (WHITESPACE, " "),
3104 (STRING, "# z"),
3105 (NEWLINE, "\r\n"),
3106 (INDENT, " "),
3107 (STRING, "[a"),
3108 (NEWLINE, "\r\n"),
3109 ]
3110 );
3111 }
3112
3113 #[test]
3114 fn test_block_scalar_indentation_indicator_counts_from_parent() {
3115 use SyntaxKind::*;
3116 assert_eq!(
3119 lex("- k: >-2\n x\n [a\n"),
3120 vec![
3121 (DASH, "-"),
3122 (WHITESPACE, " "),
3123 (STRING, "k"),
3124 (COLON, ":"),
3125 (WHITESPACE, " "),
3126 (GREATER, ">"),
3127 (INT, "-2"),
3128 (NEWLINE, "\n"),
3129 (INDENT, " "),
3130 (STRING, "x"),
3131 (NEWLINE, "\n"),
3132 (INDENT, " "),
3133 (STRING, "[a"),
3134 (NEWLINE, "\n"),
3135 ]
3136 );
3137 assert_eq!(
3139 lex("k:\n |1\n x\n # z\n"),
3140 vec![
3141 (STRING, "k"),
3142 (COLON, ":"),
3143 (NEWLINE, "\n"),
3144 (INDENT, " "),
3145 (PIPE, "|"),
3146 (INT, "1"),
3147 (NEWLINE, "\n"),
3148 (INDENT, " "),
3149 (STRING, "x"),
3150 (NEWLINE, "\n"),
3151 (INDENT, " "),
3152 (STRING, "# z"),
3153 (NEWLINE, "\n"),
3154 ]
3155 );
3156 assert_eq!(
3158 lex("- k: |1\n x\n j: [a]\n"),
3159 vec![
3160 (DASH, "-"),
3161 (WHITESPACE, " "),
3162 (STRING, "k"),
3163 (COLON, ":"),
3164 (WHITESPACE, " "),
3165 (PIPE, "|"),
3166 (INT, "1"),
3167 (NEWLINE, "\n"),
3168 (INDENT, " "),
3169 (STRING, "x"),
3170 (NEWLINE, "\n"),
3171 (INDENT, " "),
3172 (STRING, "j"),
3173 (COLON, ":"),
3174 (WHITESPACE, " "),
3175 (LEFT_BRACKET, "["),
3176 (STRING, "a"),
3177 (RIGHT_BRACKET, "]"),
3178 (NEWLINE, "\n"),
3179 ]
3180 );
3181 }
3182
3183 #[test]
3184 fn test_block_scalar_indentation_indicator_in_explicit_value() {
3185 use SyntaxKind::*;
3186 let head = [
3189 (STRING, "x"),
3190 (COLON, ":"),
3191 (NEWLINE, "\n"),
3192 (INDENT, " "),
3193 (QUESTION, "?"),
3194 (WHITESPACE, " "),
3195 (STRING, "a"),
3196 (NEWLINE, "\n"),
3197 (INDENT, " "),
3198 (COLON, ":"),
3199 ];
3200 let tail = [
3201 (INDENT, " "),
3202 (STRING, "x"),
3203 (NEWLINE, "\n"),
3204 (INDENT, " "),
3205 (LEFT_BRACKET, "["),
3206 (STRING, "a"),
3207 (RIGHT_BRACKET, "]"),
3208 (NEWLINE, "\n"),
3209 ];
3210 assert_eq!(
3211 lex("x:\n ? a\n : |1\n x\n [a]\n"),
3212 [
3213 &head[..],
3214 &[(WHITESPACE, " "), (PIPE, "|"), (INT, "1"), (NEWLINE, "\n")],
3215 &tail,
3216 ]
3217 .concat()
3218 );
3219 assert_eq!(
3220 lex("x:\n ? a\n :\n |1\n x\n [a]\n"),
3221 [
3222 &head[..],
3223 &[
3224 (NEWLINE, "\n"),
3225 (INDENT, " "),
3226 (PIPE, "|"),
3227 (INT, "1"),
3228 (NEWLINE, "\n"),
3229 ],
3230 &tail,
3231 ]
3232 .concat()
3233 );
3234 }
3235
3236 #[test]
3237 fn test_block_scalar_indentation_indicator_in_compact_explicit_value() {
3238 use SyntaxKind::*;
3239 let head = [
3242 (QUESTION, "?"),
3243 (WHITESPACE, " "),
3244 (STRING, "a"),
3245 (NEWLINE, "\n"),
3246 (COLON, ":"),
3247 (WHITESPACE, " "),
3248 (STRING, "b"),
3249 (COLON, ":"),
3250 ];
3251 let tail = [
3252 (INDENT, " "),
3253 (STRING, "x"),
3254 (NEWLINE, "\n"),
3255 (INDENT, " "),
3256 (STRING, "c"),
3257 (COLON, ":"),
3258 (WHITESPACE, " "),
3259 (LEFT_BRACKET, "["),
3260 (STRING, "d"),
3261 (RIGHT_BRACKET, "]"),
3262 (NEWLINE, "\n"),
3263 ];
3264 assert_eq!(
3265 lex("? a\n: b: |1\n x\n c: [d]\n"),
3266 [
3267 &head[..],
3268 &[(WHITESPACE, " "), (PIPE, "|"), (INT, "1"), (NEWLINE, "\n")],
3269 &tail,
3270 ]
3271 .concat()
3272 );
3273 assert_eq!(
3274 lex("? a\n: b:\n |1\n x\n c: [d]\n"),
3275 [
3276 &head[..],
3277 &[
3278 (NEWLINE, "\n"),
3279 (INDENT, " "),
3280 (PIPE, "|"),
3281 (INT, "1"),
3282 (NEWLINE, "\n"),
3283 ],
3284 &tail,
3285 ]
3286 .concat()
3287 );
3288 assert_eq!(
3290 lex("- ? a\n : b: |1\n x\n c: [d]\n"),
3291 vec![
3292 (DASH, "-"),
3293 (WHITESPACE, " "),
3294 (QUESTION, "?"),
3295 (WHITESPACE, " "),
3296 (STRING, "a"),
3297 (NEWLINE, "\n"),
3298 (INDENT, " "),
3299 (COLON, ":"),
3300 (WHITESPACE, " "),
3301 (STRING, "b"),
3302 (COLON, ":"),
3303 (WHITESPACE, " "),
3304 (PIPE, "|"),
3305 (INT, "1"),
3306 (NEWLINE, "\n"),
3307 (INDENT, " "),
3308 (STRING, "x"),
3309 (NEWLINE, "\n"),
3310 (INDENT, " "),
3311 (STRING, "c"),
3312 (COLON, ":"),
3313 (WHITESPACE, " "),
3314 (LEFT_BRACKET, "["),
3315 (STRING, "d"),
3316 (RIGHT_BRACKET, "]"),
3317 (NEWLINE, "\n"),
3318 ]
3319 );
3320 }
3321
3322 #[test]
3323 fn test_flow_indicators_in_block_scalar() {
3324 let input = "key: unix:///Users/${metadata.username}/path";
3327 let tokens = lex(input);
3328 assert_eq!(tokens.len(), 4);
3329 assert_eq!(tokens[0], (SyntaxKind::STRING, "key"));
3330 assert_eq!(tokens[1], (SyntaxKind::COLON, ":"));
3331 assert_eq!(tokens[2], (SyntaxKind::WHITESPACE, " "));
3332 assert_eq!(
3333 tokens[3],
3334 (
3335 SyntaxKind::STRING,
3336 "unix:///Users/${metadata.username}/path"
3337 )
3338 );
3339 }
3340
3341 #[test]
3342 fn test_block_scalar_body_tab_is_not_indentation() {
3343 use SyntaxKind::*;
3346 assert_eq!(
3347 lex("- |-\n \t[\n ]\"\n- b\n"),
3348 vec![
3349 (DASH, "-"),
3350 (WHITESPACE, " "),
3351 (PIPE, "|"),
3352 (STRING, "-"),
3353 (NEWLINE, "\n"),
3354 (INDENT, " \t"),
3355 (STRING, "["),
3356 (NEWLINE, "\n"),
3357 (INDENT, " "),
3358 (STRING, "]\""),
3359 (NEWLINE, "\n"),
3360 (DASH, "-"),
3361 (WHITESPACE, " "),
3362 (STRING, "b"),
3363 (NEWLINE, "\n"),
3364 ]
3365 );
3366 }
3367
3368 #[test]
3369 fn test_unterminated_verbatim_tag_ends_at_line_break() {
3370 use SyntaxKind::*;
3371 for line_break in ["\n", "\r\n", "\r"] {
3372 let input = format!("!<x{line_break}a");
3373 assert_eq!(
3374 lex(&input),
3375 vec![(TAG, "!<x"), (NEWLINE, line_break), (STRING, "a")]
3376 );
3377 }
3378 }
3379}