1use crate::error::{Result, ToonError};
23use serde_json::{Map, Value};
24
25pub fn decode(toon: &str) -> Result<String> {
31 let value = parse_toon(toon)?;
32 Ok(serde_json::to_string(&value)?)
33}
34
35fn parse_toon(toon: &str) -> Result<Value> {
37 let toon = toon.trim_end_matches('\n');
38
39 if toon.is_empty() {
40 return Ok(Value::Object(Map::new()));
41 }
42
43 if toon.starts_with('[') {
45 if let Some(val) = try_parse_root_array(toon)? {
46 return Ok(val);
47 }
48 }
49
50 let lines: Vec<&str> = toon.lines().collect();
52 if lines.len() == 1 && !line_has_key_colon(lines[0]) {
53 return parse_primitive_value(lines[0].trim());
54 }
55
56 parse_object_from_lines(&lines, 0, 0, lines.len())
58}
59
60fn try_parse_root_array(toon: &str) -> Result<Option<Value>> {
62 let lines: Vec<&str> = toon.lines().collect();
63 if lines.is_empty() {
64 return Ok(None);
65 }
66 let first_line = lines[0];
67
68 if let Some(header) = parse_array_header(first_line) {
70 let arr = parse_array_body(&header, &lines, 0, 0)?;
71 return Ok(Some(arr));
72 }
73 Ok(None)
74}
75
76fn line_has_key_colon(line: &str) -> bool {
78 let trimmed = line.trim();
79 if trimmed.starts_with('"') {
81 if let Some(end) = find_closing_quote(trimmed, 1) {
83 return end + 1 < trimmed.len() && trimmed.as_bytes()[end + 1] == b':';
85 }
86 return false;
87 }
88 if trimmed.starts_with('[') {
90 return false;
91 }
92 if let Some(colon_pos) = trimmed.find(':') {
95 let before = &trimmed[..colon_pos];
96 !before.contains(' ') && !before.is_empty()
98 } else {
99 false
100 }
101}
102
103struct ArrayHeader {
109 len: usize,
110 fields: Option<Vec<String>>,
111 inline_values: Option<String>,
112}
113
114fn parse_array_header(line: &str) -> Option<ArrayHeader> {
116 let trimmed = line.trim();
117 let bracket_start = trimmed.find('[')?;
118 let bracket_end = trimmed[bracket_start..].find(']')? + bracket_start;
119 let len_str = &trimmed[bracket_start + 1..bracket_end];
120 let len: usize = len_str.parse().ok()?;
121
122 let after_bracket = &trimmed[bracket_end + 1..];
123
124 if after_bracket.starts_with('{') {
126 let brace_end = after_bracket.find('}')?;
127 let fields_str = &after_bracket[1..brace_end];
128 let fields: Vec<String> = fields_str.split(',').map(|s| s.to_string()).collect();
129 let after_brace = &after_bracket[brace_end + 1..];
130 if after_brace.starts_with(':') {
131 return Some(ArrayHeader {
132 len,
133 fields: Some(fields),
134 inline_values: None,
135 });
136 }
137 return None;
138 }
139
140 if let Some(values) = after_bracket.strip_prefix(": ") {
142 return Some(ArrayHeader {
143 len,
144 fields: None,
145 inline_values: Some(values.to_string()),
146 });
147 }
148
149 if after_bracket.starts_with(':') {
151 return Some(ArrayHeader {
152 len,
153 fields: None,
154 inline_values: None,
155 });
156 }
157
158 None
159}
160
161fn parse_array_body(
167 header: &ArrayHeader,
168 lines: &[&str],
169 line_idx: usize,
170 base_indent: usize,
171) -> Result<Value> {
172 if header.len == 0 {
174 return Ok(Value::Array(vec![]));
175 }
176
177 if let Some(ref inline) = header.inline_values {
179 let values = parse_inline_values(inline)?;
180 return Ok(Value::Array(values));
181 }
182
183 if let Some(ref fields) = header.fields {
185 let mut rows = Vec::new();
186 for (i, line) in lines.iter().enumerate().skip(line_idx + 1) {
187 let trimmed = line.trim();
188 if trimmed.is_empty() {
189 continue;
190 }
191 let indent = count_indent(line);
193 if indent <= base_indent && i > line_idx + 1 {
194 break;
195 }
196 let obj = parse_tabular_row(trimmed, fields)?;
197 rows.push(obj);
198 }
199 return Ok(Value::Array(rows));
200 }
201
202 let mut detected_indent = base_indent + 2;
205 for line in &lines[line_idx + 1..] {
206 let trimmed = line.trim();
207 if trimmed.is_empty() {
208 continue;
209 }
210 if trimmed.starts_with("- ") {
211 detected_indent = count_indent(line);
212 break;
213 }
214 break;
215 }
216 parse_list_items(lines, line_idx + 1, detected_indent)
217}
218
219fn parse_inline_values(s: &str) -> Result<Vec<Value>> {
222 let mut values = Vec::new();
223 let mut i = 0;
224 let bytes = s.as_bytes();
225
226 while i < bytes.len() {
227 if bytes[i] == b'"' {
228 let end = find_closing_quote(s, i + 1).ok_or_else(|| ToonError::ToonParse {
230 line: 0,
231 message: "Unterminated quoted string in inline array".to_string(),
232 })?;
233 let inner = &s[i + 1..end];
234 let unescaped = unescape_string(inner);
235 values.push(Value::String(unescaped));
236 i = end + 1;
237 if i < bytes.len() && bytes[i] == b',' {
239 i += 1;
240 }
241 } else {
242 let end = s[i..].find(',').map(|p| p + i).unwrap_or(s.len());
244 let token = &s[i..end];
245 values.push(parse_primitive_token(token));
246 i = end;
247 if i < bytes.len() && bytes[i] == b',' {
248 i += 1;
249 }
250 }
251 }
252
253 Ok(values)
254}
255
256fn parse_tabular_row(row: &str, fields: &[String]) -> Result<Value> {
258 let values = parse_inline_values(row)?;
259 let mut map = Map::new();
260 for (i, field) in fields.iter().enumerate() {
261 let val = values.get(i).cloned().unwrap_or(Value::Null);
262 map.insert(field.clone(), val);
263 }
264 Ok(Value::Object(map))
265}
266
267fn parse_list_items(lines: &[&str], start_line: usize, item_indent: usize) -> Result<Value> {
274 let mut items = Vec::new();
275 let mut i = start_line;
276
277 while i < lines.len() {
278 let line = lines[i];
279 let indent = count_indent(line);
280 let trimmed = line.trim();
281
282 if trimmed.is_empty() {
283 i += 1;
284 continue;
285 }
286
287 if indent < item_indent {
289 break;
290 }
291
292 if indent > item_indent {
294 i += 1;
295 continue;
296 }
297
298 if !trimmed.starts_with("- ") {
300 break;
301 }
302
303 let content = &trimmed[2..]; if content.starts_with('[') {
307 if let Some(header) = parse_array_header(content) {
308 let arr = parse_array_body(&header, lines, i, indent + 2)?;
309 items.push(arr);
310 i = skip_nested_lines(lines, i + 1, indent + 2);
311 continue;
312 }
313 }
314
315 if item_content_is_object(content) {
317 let (obj, next_i) = parse_list_item_object(lines, i, indent + 2, content)?;
318 items.push(obj);
319 i = next_i;
320 continue;
321 }
322
323 items.push(parse_primitive_value(content)?);
325 i += 1;
326 }
327
328 Ok(Value::Array(items))
329}
330
331fn item_content_is_object(content: &str) -> bool {
334 if content.starts_with('"') {
336 if let Some(end) = find_closing_quote(content, 1) {
337 return end + 1 < content.len() && content.as_bytes()[end + 1] == b':';
338 }
339 return false;
340 }
341 if let Some(pos) = content.find(':') {
343 let before = &content[..pos];
344 return !before.contains(' ') && !before.is_empty();
345 }
346 if let Some(pos) = content.find('[') {
347 let before = &content[..pos];
348 return !before.contains(' ') && !before.is_empty();
349 }
350 false
351}
352
353fn parse_list_item_object(
359 lines: &[&str],
360 start_line: usize,
361 hyphen_content_indent: usize,
362 first_field_content: &str,
363) -> Result<(Value, usize)> {
364 let mut map = Map::new();
365
366 let mut i = parse_key_value_into_map(
368 first_field_content,
369 &mut map,
370 lines,
371 start_line,
372 hyphen_content_indent,
373 )?;
374
375 let sibling_indent = hyphen_content_indent;
376
377 while i < lines.len() {
378 let line = lines[i];
379 let indent = count_indent(line);
380 let trimmed = line.trim();
381
382 if trimmed.is_empty() {
383 i += 1;
384 continue;
385 }
386
387 if indent != sibling_indent {
389 break;
390 }
391
392 if !line_has_key_colon(trimmed) && !trimmed.contains('[') {
394 break;
395 }
396
397 i = parse_key_value_into_map(trimmed, &mut map, lines, i, indent)?;
398 }
399
400 Ok((Value::Object(map), i))
401}
402
403fn skip_array_body(lines: &[&str], start: usize, base_indent: usize) -> usize {
412 if start >= lines.len() {
413 return start;
414 }
415
416 let mut first_line_indent = base_indent + 2;
418 let mut is_list = false;
419 for line in &lines[start..] {
420 let trimmed = line.trim();
421 if trimmed.is_empty() {
422 continue;
423 }
424 first_line_indent = count_indent(line);
425 is_list = trimmed.starts_with("- ");
426 break;
427 }
428
429 if !is_list {
430 return skip_nested_lines(lines, start, first_line_indent);
432 }
433
434 let mut i = start;
436 while i < lines.len() {
437 let line = lines[i];
438 let trimmed = line.trim();
439 if trimmed.is_empty() {
440 i += 1;
441 continue;
442 }
443 let indent = count_indent(line);
444 if indent < first_line_indent {
445 break;
446 }
447 if indent == first_line_indent && !trimmed.starts_with("- ") {
448 break;
450 }
451 i += 1;
452 }
453 i
454}
455
456fn skip_nested_lines(lines: &[&str], start: usize, base_indent: usize) -> usize {
459 let mut i = start;
460 while i < lines.len() {
461 let line = lines[i];
462 let trimmed = line.trim();
463 if trimmed.is_empty() {
464 i += 1;
465 continue;
466 }
467 let indent = count_indent(line);
468 if indent < base_indent {
469 break;
470 }
471 i += 1;
472 }
473 i
474}
475
476fn parse_key_value_into_map(
488 content: &str,
489 map: &mut Map<String, Value>,
490 lines: &[&str],
491 line_idx: usize,
492 base_indent: usize,
493) -> Result<usize> {
494 let (key, rest) = parse_key_from_content(content)?;
495
496 if rest.starts_with('[') {
498 let arr_line = format!("x{}", rest);
500 if let Some(header) = parse_array_header(&arr_line) {
501 let is_empty = header.len == 0;
502 let is_inline = header.inline_values.is_some();
503 let arr = parse_array_body(&header, lines, line_idx, base_indent)?;
504 map.insert(key, arr);
505 if is_empty || is_inline {
507 return Ok(line_idx + 1);
508 }
509 let next = skip_array_body(lines, line_idx + 1, base_indent);
511 return Ok(next);
512 }
513 }
514
515 if rest == ":" {
517 let child_indent = base_indent + 2;
519 if line_idx + 1 < lines.len() {
520 let next_indent = count_indent(lines[line_idx + 1]);
521 if next_indent >= child_indent && !lines[line_idx + 1].trim().is_empty() {
522 let end = find_block_end(lines, line_idx + 1, child_indent);
524 let obj = parse_object_from_lines(lines, child_indent, line_idx + 1, end)?;
525 map.insert(key, obj);
526 return Ok(end);
527 }
528 }
529 map.insert(key, Value::Object(Map::new()));
531 } else if let Some(value_str) = rest.strip_prefix(": ") {
532 let value = parse_primitive_value(value_str)?;
533 map.insert(key, value);
534 } else {
535 map.insert(key, Value::Null);
537 }
538
539 Ok(line_idx + 1)
540}
541
542fn parse_key_from_content(content: &str) -> Result<(String, String)> {
548 if content.starts_with('"') {
549 let end = find_closing_quote(content, 1).ok_or_else(|| ToonError::ToonParse {
551 line: 0,
552 message: "Unterminated quoted key".to_string(),
553 })?;
554 let key = unescape_string(&content[1..end]);
555 let rest = content[end + 1..].to_string();
556 Ok((key, rest))
557 } else {
558 let colon_pos = content.find(':');
560 let bracket_pos = content.find('[');
561 let end = match (colon_pos, bracket_pos) {
562 (Some(c), Some(b)) => c.min(b),
563 (Some(c), None) => c,
564 (None, Some(b)) => b,
565 (None, None) => content.len(),
566 };
567 let key = content[..end].to_string();
568 let rest = content[end..].to_string();
569 Ok((key, rest))
570 }
571}
572
573fn parse_object_from_lines(
575 lines: &[&str],
576 expected_indent: usize,
577 start: usize,
578 end: usize,
579) -> Result<Value> {
580 let mut map = Map::new();
581 let mut i = start;
582
583 while i < end {
584 let line = lines[i];
585 let trimmed = line.trim();
586
587 if trimmed.is_empty() {
588 i += 1;
589 continue;
590 }
591
592 let indent = count_indent(line);
593 if indent < expected_indent {
594 break;
595 }
596 if indent > expected_indent {
597 i += 1;
599 continue;
600 }
601
602 i = parse_key_value_into_map(trimmed, &mut map, lines, i, indent)?;
604 while i < end {
606 let next_line = lines[i];
607 let next_trimmed = next_line.trim();
608 if next_trimmed.is_empty() {
609 i += 1;
610 continue;
611 }
612 let next_indent = count_indent(next_line);
613 if next_indent <= expected_indent {
614 break;
615 }
616 i += 1;
617 }
618 }
619
620 Ok(Value::Object(map))
621}
622
623fn find_block_end(lines: &[&str], start: usize, min_indent: usize) -> usize {
625 let mut i = start;
626 while i < lines.len() {
627 let line = lines[i];
628 let trimmed = line.trim();
629 if trimmed.is_empty() {
630 i += 1;
631 continue;
632 }
633 let indent = count_indent(line);
634 if indent < min_indent {
635 break;
636 }
637 i += 1;
638 }
639 i
640}
641
642fn parse_primitive_value(s: &str) -> Result<Value> {
644 Ok(parse_primitive_token(s))
645}
646
647fn parse_primitive_token(s: &str) -> Value {
653 let s = s.trim();
654
655 if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
657 let inner = &s[1..s.len() - 1];
658 return Value::String(unescape_string(inner));
659 }
660
661 if s == "null" {
663 return Value::Null;
664 }
665
666 if s == "true" {
668 return Value::Bool(true);
669 }
670 if s == "false" {
671 return Value::Bool(false);
672 }
673
674 if let Ok(n) = s.parse::<i64>() {
676 return Value::Number(n.into());
677 }
678
679 if let Ok(f) = s.parse::<f64>() {
681 if let Some(n) = serde_json::Number::from_f64(f) {
682 return Value::Number(n);
683 }
684 }
685
686 Value::String(s.to_string())
688}
689
690fn count_indent(line: &str) -> usize {
692 line.len() - line.trim_start().len()
693}
694
695fn find_closing_quote(s: &str, start: usize) -> Option<usize> {
697 let bytes = s.as_bytes();
698 let mut i = start;
699 while i < bytes.len() {
700 if bytes[i] == b'\\' {
701 i += 2; } else if bytes[i] == b'"' {
703 return Some(i);
704 } else {
705 i += 1;
706 }
707 }
708 None
709}
710
711fn unescape_string(s: &str) -> String {
713 let mut out = String::with_capacity(s.len());
714 let mut chars = s.chars();
715 while let Some(c) = chars.next() {
716 if c == '\\' {
717 match chars.next() {
718 Some('n') => out.push('\n'),
719 Some('r') => out.push('\r'),
720 Some('t') => out.push('\t'),
721 Some('\\') => out.push('\\'),
722 Some('"') => out.push('"'),
723 Some(other) => {
724 out.push('\\');
725 out.push(other);
726 }
727 None => out.push('\\'),
728 }
729 } else {
730 out.push(c);
731 }
732 }
733 out
734}