1use super::{Mapping, Value};
5use std::fmt;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct YamlError {
10 pub line: usize,
12 pub message: String,
14}
15
16impl fmt::Display for YamlError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 if self.line > 0 {
19 write!(f, "YAML error at line {}: {}", self.line, self.message)
20 } else {
21 write!(f, "YAML error: {}", self.message)
22 }
23 }
24}
25
26impl std::error::Error for YamlError {}
27
28pub fn parse(text: &str) -> Result<Value, YamlError> {
33 let lines: Vec<String> = text.lines().map(std::string::ToString::to_string).collect();
34 let mut p = Parser { lines, pos: 0 };
35 p.skip_blank_and_comments();
36 if p.pos >= p.lines.len() {
37 return Ok(Value::Null);
38 }
39 let base = p.current_indent()?;
40 let value = p.parse_node(base)?;
41 p.skip_blank_and_comments();
42 if p.pos < p.lines.len() {
43 return Err(p.err("unexpected trailing content"));
44 }
45 Ok(value)
46}
47
48struct Parser {
49 lines: Vec<String>,
50 pos: usize,
51}
52
53impl Parser {
54 fn err(&self, msg: impl Into<String>) -> YamlError {
55 YamlError {
56 line: self.pos + 1,
57 message: msg.into(),
58 }
59 }
60
61 fn is_blank_or_comment(line: &str) -> bool {
62 let t = line.trim_start();
63 t.is_empty() || t.starts_with('#')
64 }
65
66 fn skip_blank_and_comments(&mut self) {
67 while self.pos < self.lines.len() && Self::is_blank_or_comment(&self.lines[self.pos]) {
68 self.pos += 1;
69 }
70 }
71
72 fn current_indent(&self) -> Result<usize, YamlError> {
75 indent_of(&self.lines[self.pos]).ok_or_else(|| self.err("tab character in indentation"))
76 }
77
78 fn parse_node(&mut self, indent: usize) -> Result<Value, YamlError> {
80 let line = &self.lines[self.pos];
81 let content = &line[indent.min(line.len())..];
82 let trimmed = content.trim_start();
83
84 if trimmed == "-" || trimmed.starts_with("- ") {
85 self.parse_sequence(indent)
86 } else if split_key_value(trimmed).is_some() {
87 self.parse_mapping(indent)
88 } else {
89 let first = trimmed.to_string();
92 let entry_line = self.pos;
93 self.pos += 1;
94 self.read_scalar(&first, indent, entry_line)
95 }
96 }
97
98 fn parse_mapping(&mut self, indent: usize) -> Result<Value, YamlError> {
99 let mut map = Mapping::new();
100 loop {
101 self.skip_blank_and_comments();
102 if self.pos >= self.lines.len() {
103 break;
104 }
105 let ind = self.current_indent()?;
106 if ind < indent {
107 break;
108 }
109 if ind > indent {
110 return Err(self.err("unexpected indentation in mapping"));
111 }
112 let line = self.lines[self.pos].clone();
113 let content = line[indent..].to_string();
114 let trimmed = content.trim_start();
115 if trimmed == "-" || trimmed.starts_with("- ") {
116 break; }
118 let (key_str, rest) = split_key_value(trimmed)
119 .ok_or_else(|| self.err("expected 'key: value' mapping entry"))?;
120 let key = parse_scalar(&key_str, self.pos)?;
121 let entry_line = self.pos;
122 self.pos += 1;
123
124 let value = match rest {
125 Some(r) if r.starts_with('|') || r.starts_with('>') => {
126 self.parse_block_scalar(indent, &r)?
127 }
128 Some(r) => self.read_scalar(&r, indent + 1, entry_line)?,
129 None => {
130 self.parse_nested(indent)?
132 }
133 };
134 map.push_raw(key, value);
135 }
136 Ok(Value::Mapping(map))
137 }
138
139 fn parse_sequence(&mut self, indent: usize) -> Result<Value, YamlError> {
140 let mut seq = Vec::new();
141 loop {
142 self.skip_blank_and_comments();
143 if self.pos >= self.lines.len() {
144 break;
145 }
146 let ind = self.current_indent()?;
147 if ind < indent {
148 break;
149 }
150 if ind > indent {
151 return Err(self.err("unexpected indentation in sequence"));
152 }
153 let line = self.lines[self.pos].clone();
154 let content = &line[indent..];
155 if !(content == "-" || content.starts_with("- ")) {
156 break;
157 }
158 let dash_rest = &content[1..]; let item_offset = indent + 1 + (dash_rest.len() - dash_rest.trim_start().len());
161 let item_text = content[1..].trim_start().to_string();
162 let entry_line = self.pos;
163
164 if item_text.is_empty() {
165 self.pos += 1;
167 let v = self.parse_nested(indent)?;
168 seq.push(v);
169 } else if item_text.starts_with('|') || item_text.starts_with('>') {
170 self.pos += 1;
171 let v = self.parse_block_scalar(indent, &item_text)?;
172 seq.push(v);
173 } else if split_key_value(&item_text).is_some() {
174 let mut rewritten = " ".repeat(item_offset);
178 rewritten.push_str(&item_text);
179 self.lines[entry_line] = rewritten;
180 let v = self.parse_mapping(item_offset)?;
181 seq.push(v);
182 } else {
183 self.pos += 1;
184 let v = self.read_scalar(&item_text, indent + 1, entry_line)?;
185 seq.push(v);
186 }
187 }
188 Ok(Value::Sequence(seq))
189 }
190
191 fn read_scalar(
208 &mut self,
209 first: &str,
210 min_indent: usize,
211 line: usize,
212 ) -> Result<Value, YamlError> {
213 if first.starts_with('[') || first.starts_with('{') {
214 return parse_inline_value(first, line);
215 }
216
217 let mut text = first.to_string();
218 let mut open_quote = unclosed_quote(first);
219 while self.pos < self.lines.len() {
220 let raw = &self.lines[self.pos];
221 if raw.trim().is_empty() {
222 break;
223 }
224 let ind = indent_of(raw).ok_or_else(|| self.err("tab character in indentation"))?;
225 if ind < min_indent {
226 break;
227 }
228 let content = raw.trim();
229 if open_quote.is_none() && starts_a_new_node(content) {
230 break;
231 }
232 text.push(' ');
233 text.push_str(content);
234 if let Some(quote) = open_quote
235 && closes_quote(content, quote, 0)
236 {
237 open_quote = None;
238 }
239 self.pos += 1;
240 }
241
242 parse_scalar(&text, line)
243 }
244
245 fn parse_nested(&mut self, parent_indent: usize) -> Result<Value, YamlError> {
254 self.skip_blank_and_comments();
255 if self.pos >= self.lines.len() {
256 return Ok(Value::Null);
257 }
258 let ind = self.current_indent()?;
259 if ind > parent_indent {
260 self.parse_node(ind)
261 } else if ind == parent_indent && self.line_is_sequence_item(ind) {
262 self.parse_sequence(ind)
263 } else {
264 Ok(Value::Null)
265 }
266 }
267
268 fn line_is_sequence_item(&self, indent: usize) -> bool {
271 let line = &self.lines[self.pos];
272 let content = &line[indent.min(line.len())..];
273 content == "-" || content.starts_with("- ")
274 }
275
276 fn parse_block_scalar(
279 &mut self,
280 parent_indent: usize,
281 header: &str,
282 ) -> Result<Value, YamlError> {
283 let style = header.as_bytes()[0]; let chomp = header[1..].chars().find(|c| *c == '+' || *c == '-');
285
286 let mut body: Vec<String> = Vec::new();
288 let mut block_indent: Option<usize> = None;
289 while self.pos < self.lines.len() {
290 let line = &self.lines[self.pos];
291 if line.trim().is_empty() {
292 body.push(String::new());
293 self.pos += 1;
294 continue;
295 }
296 let ind = indent_of(line).ok_or_else(|| self.err("tab in block scalar indentation"))?;
297 if ind <= parent_indent {
298 break;
299 }
300 if block_indent.is_none() {
301 block_indent = Some(ind);
302 }
303 let bi = block_indent.unwrap();
304 let stripped = if line.len() >= bi {
305 line[bi..].to_string()
306 } else {
307 String::new()
308 };
309 body.push(stripped);
310 self.pos += 1;
311 }
312
313 let mut trailing_blanks = 0;
315 while body.last().is_some_and(std::string::String::is_empty) {
316 body.pop();
317 trailing_blanks += 1;
318 }
319
320 let mut text = if style == b'|' {
321 body.join("\n")
322 } else {
323 fold_lines(&body)
324 };
325
326 match chomp {
327 Some('-') => {} Some('+') => {
329 text.push('\n');
331 for _ in 0..trailing_blanks {
332 text.push('\n');
333 }
334 }
335 _ => {
336 if !text.is_empty() || trailing_blanks > 0 {
338 text.push('\n');
339 }
340 }
341 }
342 Ok(Value::String(text))
343 }
344}
345
346fn fold_lines(lines: &[String]) -> String {
349 let mut out = String::new();
350 let mut prev_nonempty = false;
351 for line in lines {
352 if line.is_empty() {
353 out.push('\n');
354 prev_nonempty = false;
355 } else {
356 if prev_nonempty {
357 out.push(' ');
358 }
359 out.push_str(line);
360 prev_nonempty = true;
361 }
362 }
363 out
364}
365
366fn starts_a_new_node(content: &str) -> bool {
369 content.starts_with('#')
370 || content == "-"
371 || content.starts_with("- ")
372 || split_key_value(content).is_some()
373}
374
375fn unclosed_quote(s: &str) -> Option<char> {
378 match s.chars().next()? {
379 q @ ('\'' | '"') if !closes_quote(s, q, 1) => Some(q),
380 _ => None,
381 }
382}
383
384fn closes_quote(s: &str, quote: char, from: usize) -> bool {
387 let chars: Vec<char> = s.chars().collect();
388 let mut i = from;
389 while i < chars.len() {
390 let c = chars[i];
391 if quote == '"' && c == '\\' {
392 i += 2;
393 continue;
394 }
395 if c == quote {
396 if quote == '\'' && chars.get(i + 1) == Some(&'\'') {
397 i += 2;
398 continue;
399 }
400 return true;
401 }
402 i += 1;
403 }
404 false
405}
406
407fn indent_of(line: &str) -> Option<usize> {
409 let mut n = 0;
410 for c in line.chars() {
411 match c {
412 ' ' => n += 1,
413 '\t' => return None,
414 _ => break,
415 }
416 }
417 Some(n)
418}
419
420fn split_key_value(s: &str) -> Option<(String, Option<String>)> {
424 let chars: Vec<char> = s.chars().collect();
425 let mut i = 0;
426 let mut quote: Option<char> = None;
427 let mut depth: i32 = 0;
428 while i < chars.len() {
429 let c = chars[i];
430 if let Some(q) = quote {
431 if q == '"' && c == '\\' {
432 i += 2;
433 continue;
434 }
435 if c == q {
436 if q == '\'' && chars.get(i + 1) == Some(&'\'') {
437 i += 2;
438 continue;
439 }
440 quote = None;
441 }
442 i += 1;
443 continue;
444 }
445 match c {
446 '\'' | '"' => quote = Some(c),
447 '[' | '{' => depth += 1,
448 ']' | '}' => {
449 if depth > 0 {
450 depth -= 1;
451 }
452 }
453 '#' if depth == 0 && i > 0 && (chars[i - 1] == ' ' || chars[i - 1] == '\t') => {
454 break; }
456 ':' if depth == 0 => {
457 let next = chars.get(i + 1).copied();
458 if next.is_none() || next == Some(' ') || next == Some('\t') {
459 let key: String = chars[..i].iter().collect();
460 let rest: String = chars[i + 1..].iter().collect();
461 let rest = rest.trim();
462 let rest_opt = if rest.is_empty() || rest.starts_with('#') {
463 None
464 } else {
465 Some(rest.to_string())
466 };
467 return Some((key.trim().to_string(), rest_opt));
468 }
469 }
470 _ => {}
471 }
472 i += 1;
473 }
474 None
475}
476
477fn parse_inline_value(s: &str, line: usize) -> Result<Value, YamlError> {
479 let t = s.trim();
480 if t.starts_with('[') || t.starts_with('{') {
481 let mut fp = FlowParser {
482 chars: t.chars().collect(),
483 pos: 0,
484 line,
485 };
486 let v = fp.parse_value()?;
487 fp.skip_ws();
488 if fp.pos < fp.chars.len() && fp.chars[fp.pos] != '#' {
490 return Err(YamlError {
491 line: line + 1,
492 message: "unexpected content after flow collection".into(),
493 });
494 }
495 Ok(v)
496 } else {
497 parse_scalar(t, line)
498 }
499}
500
501fn parse_scalar(token: &str, line: usize) -> Result<Value, YamlError> {
503 let t = token.trim();
504 if t.is_empty() {
505 return Ok(Value::Null);
506 }
507 if t.starts_with('"') {
508 return parse_double_quoted(t, line).map(Value::String);
509 }
510 if t.starts_with('\'') {
511 return parse_single_quoted(t, line).map(Value::String);
512 }
513 let plain = strip_trailing_comment(t);
515 Ok(interpret_plain(plain))
516}
517
518fn strip_trailing_comment(s: &str) -> &str {
520 let bytes = s.as_bytes();
521 let mut i = 0;
522 while i < bytes.len() {
523 if bytes[i] == b'#' && i > 0 && (bytes[i - 1] == b' ' || bytes[i - 1] == b'\t') {
524 return s[..i].trim_end();
525 }
526 i += 1;
527 }
528 s.trim_end()
529}
530
531fn interpret_plain(s: &str) -> Value {
541 match s {
542 "" | "~" | "null" | "Null" | "NULL" => return Value::Null,
543 "true" | "True" | "TRUE" => return Value::Bool(true),
544 "false" | "False" | "FALSE" => return Value::Bool(false),
545 ".inf" | ".Inf" | ".INF" | "+.inf" => return Value::Float(f64::INFINITY),
546 "-.inf" | "-.Inf" | "-.INF" => return Value::Float(f64::NEG_INFINITY),
547 ".nan" | ".NaN" | ".NAN" => return Value::Float(f64::NAN),
548 _ => {}
549 }
550 if is_canonical_int(s)
551 && let Ok(i) = s.parse::<i64>()
552 {
553 return Value::Int(i);
554 }
555 if is_canonical_float(s)
556 && let Ok(f) = s.parse::<f64>()
557 {
558 return Value::Float(f);
559 }
560 Value::String(s.to_string())
561}
562
563fn is_canonical_int(s: &str) -> bool {
565 let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
566 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
567 return false;
568 }
569 digits == "0" || !digits.starts_with('0')
570}
571
572fn is_canonical_float(s: &str) -> bool {
576 if !s.contains('.') || !s.bytes().any(|b| b.is_ascii_digit()) {
577 return false;
578 }
579 s.parse::<f64>().is_ok()
580}
581
582fn parse_double_quoted(s: &str, line: usize) -> Result<String, YamlError> {
583 let chars: Vec<char> = s.chars().collect();
584 debug_assert_eq!(chars[0], '"');
585 let mut out = String::new();
586 let mut i = 1;
587 while i < chars.len() {
588 let c = chars[i];
589 if c == '"' {
590 validate_quoted_tail(&chars, i, line, "double-quoted")?;
591 return Ok(out);
592 }
593 if c == '\\' {
594 i += 1;
595 let e = *chars.get(i).ok_or_else(|| YamlError {
596 line: line + 1,
597 message: "dangling escape in double-quoted string".into(),
598 })?;
599 match e {
600 'n' => out.push('\n'),
601 't' => out.push('\t'),
602 'r' => out.push('\r'),
603 '"' => out.push('"'),
604 '\\' => out.push('\\'),
605 '/' => out.push('/'),
606 '0' => out.push('\0'),
607 'b' => out.push('\u{0008}'),
608 'f' => out.push('\u{000C}'),
609 'u' => {
610 let start = i + 1;
611 let end = start + 4;
612 let hex = chars.get(start..end).ok_or_else(|| YamlError {
613 line: line + 1,
614 message: "truncated Unicode escape in double-quoted string".into(),
615 })?;
616 let hex: String = hex.iter().collect();
617 let cp = u32::from_str_radix(&hex, 16).map_err(|_| YamlError {
618 line: line + 1,
619 message: "malformed Unicode escape in double-quoted string".into(),
620 })?;
621 let ch = char::from_u32(cp).ok_or_else(|| YamlError {
622 line: line + 1,
623 message: "invalid Unicode scalar value in double-quoted string".into(),
624 })?;
625 out.push(ch);
626 i = end - 1;
627 }
628 other => {
629 return Err(YamlError {
630 line: line + 1,
631 message: format!(
632 "unknown escape sequence \\{other} in double-quoted string"
633 ),
634 });
635 }
636 }
637 i += 1;
638 continue;
639 }
640 out.push(c);
641 i += 1;
642 }
643 Err(YamlError {
644 line: line + 1,
645 message: "unterminated double-quoted string".into(),
646 })
647}
648
649fn parse_single_quoted(s: &str, line: usize) -> Result<String, YamlError> {
650 let chars: Vec<char> = s.chars().collect();
651 debug_assert_eq!(chars[0], '\'');
652 let mut out = String::new();
653 let mut i = 1;
654 while i < chars.len() {
655 let c = chars[i];
656 if c == '\'' {
657 if chars.get(i + 1) == Some(&'\'') {
658 out.push('\'');
659 i += 2;
660 continue;
661 }
662 validate_quoted_tail(&chars, i, line, "single-quoted")?;
663 return Ok(out);
664 }
665 out.push(c);
666 i += 1;
667 }
668 Err(YamlError {
669 line: line + 1,
670 message: "unterminated single-quoted string".into(),
671 })
672}
673
674fn validate_quoted_tail(
676 chars: &[char],
677 close: usize,
678 line: usize,
679 quote_name: &str,
680) -> Result<(), YamlError> {
681 for &c in &chars[close + 1..] {
682 if c.is_whitespace() {
683 continue;
684 }
685 if c == '#' {
686 return Ok(());
687 }
688 return Err(YamlError {
689 line: line + 1,
690 message: format!("unexpected content after closing {quote_name} quote"),
691 });
692 }
693 Ok(())
694}
695
696struct FlowParser {
698 chars: Vec<char>,
699 pos: usize,
700 line: usize,
701}
702
703impl FlowParser {
704 fn skip_ws(&mut self) {
705 while self.pos < self.chars.len() && self.chars[self.pos].is_whitespace() {
706 self.pos += 1;
707 }
708 }
709
710 fn err(&self, msg: impl Into<String>) -> YamlError {
711 YamlError {
712 line: self.line + 1,
713 message: msg.into(),
714 }
715 }
716
717 fn parse_value(&mut self) -> Result<Value, YamlError> {
718 self.skip_ws();
719 match self.chars.get(self.pos) {
720 Some('[') => self.parse_seq(),
721 Some('{') => self.parse_map(),
722 Some(_) => self.parse_flow_scalar(),
723 None => Ok(Value::Null),
724 }
725 }
726
727 fn parse_seq(&mut self) -> Result<Value, YamlError> {
728 self.pos += 1; let mut seq = Vec::new();
730 loop {
731 self.skip_ws();
732 match self.chars.get(self.pos) {
733 Some(']') => {
734 self.pos += 1;
735 break;
736 }
737 None => return Err(self.err("unterminated flow sequence")),
738 _ => {}
739 }
740 seq.push(self.parse_value()?);
741 self.skip_ws();
742 match self.chars.get(self.pos) {
743 Some(',') => self.pos += 1,
744 Some(']') => {
745 self.pos += 1;
746 break;
747 }
748 _ => return Err(self.err("expected ',' or ']' in flow sequence")),
749 }
750 }
751 Ok(Value::Sequence(seq))
752 }
753
754 fn parse_map(&mut self) -> Result<Value, YamlError> {
755 self.pos += 1; let mut map = Mapping::new();
757 loop {
758 self.skip_ws();
759 match self.chars.get(self.pos) {
760 Some('}') => {
761 self.pos += 1;
762 break;
763 }
764 None => return Err(self.err("unterminated flow mapping")),
765 _ => {}
766 }
767 let key = self.parse_flow_scalar()?;
768 self.skip_ws();
769 let value = if self.chars.get(self.pos) == Some(&':') {
771 self.pos += 1;
772 self.parse_value()?
773 } else {
774 Value::Null
775 };
776 map.push_raw(key, value);
777 self.skip_ws();
778 match self.chars.get(self.pos) {
779 Some(',') => self.pos += 1,
780 Some('}') => {
781 self.pos += 1;
782 break;
783 }
784 _ => return Err(self.err("expected ',' or '}' in flow mapping")),
785 }
786 }
787 Ok(Value::Mapping(map))
788 }
789
790 fn parse_flow_scalar(&mut self) -> Result<Value, YamlError> {
791 self.skip_ws();
792 let c = *self
793 .chars
794 .get(self.pos)
795 .ok_or_else(|| self.err("expected scalar"))?;
796 if c == '"' || c == '\'' {
797 let start = self.pos;
798 self.pos += 1;
799 while self.pos < self.chars.len() {
800 let cur = self.chars[self.pos];
801 if c == '"' && cur == '\\' {
802 if self.pos + 1 >= self.chars.len() {
803 return Err(self.err("dangling escape in double-quoted string"));
804 }
805 self.pos += 2;
806 continue;
807 }
808 if cur == c {
809 if c == '\'' && self.chars.get(self.pos + 1) == Some(&'\'') {
810 self.pos += 2;
811 continue;
812 }
813 self.pos += 1;
814 break;
815 }
816 self.pos += 1;
817 }
818 let raw: String = self.chars[start..self.pos].iter().collect();
819 let s = if c == '"' {
820 parse_double_quoted(&raw, self.line)?
821 } else {
822 parse_single_quoted(&raw, self.line)?
823 };
824 return Ok(Value::String(s));
825 }
826 let start = self.pos;
829 while self.pos < self.chars.len() {
830 match self.chars[self.pos] {
831 ',' | ']' | '}' => break,
832 ':' if is_separator_colon(&self.chars, self.pos) => break,
833 _ => self.pos += 1,
834 }
835 }
836 let raw: String = self.chars[start..self.pos].iter().collect();
837 Ok(interpret_plain(raw.trim()))
838 }
839}
840
841fn is_separator_colon(chars: &[char], i: usize) -> bool {
850 chars
851 .get(i + 1)
852 .is_none_or(|c| c.is_whitespace() || matches!(c, ',' | '[' | ']' | '{' | '}'))
853}