use crate::error::{Result, ToonError};
use serde_json::{Map, Value};
pub fn decode(toon: &str) -> Result<String> {
let value = parse_toon(toon)?;
Ok(serde_json::to_string(&value)?)
}
fn parse_toon(toon: &str) -> Result<Value> {
let toon = toon.trim_end_matches('\n');
if toon.is_empty() {
return Ok(Value::Object(Map::new()));
}
if toon.starts_with('[') {
if let Some(val) = try_parse_root_array(toon)? {
return Ok(val);
}
}
let lines: Vec<&str> = toon.lines().collect();
if lines.len() == 1 && !line_has_key_colon(lines[0]) {
return parse_primitive_value(lines[0].trim());
}
parse_object_from_lines(&lines, 0, 0, lines.len())
}
fn try_parse_root_array(toon: &str) -> Result<Option<Value>> {
let lines: Vec<&str> = toon.lines().collect();
if lines.is_empty() {
return Ok(None);
}
let first_line = lines[0];
if let Some(header) = parse_array_header(first_line) {
let arr = parse_array_body(&header, &lines, 0, 0)?;
return Ok(Some(arr));
}
Ok(None)
}
fn line_has_key_colon(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.starts_with('"') {
if let Some(end) = find_closing_quote(trimmed, 1) {
return end + 1 < trimmed.len() && trimmed.as_bytes()[end + 1] == b':';
}
return false;
}
if trimmed.starts_with('[') {
return false;
}
if let Some(colon_pos) = trimmed.find(':') {
let before = &trimmed[..colon_pos];
!before.contains(' ') && !before.is_empty()
} else {
false
}
}
struct ArrayHeader {
len: usize,
fields: Option<Vec<String>>,
inline_values: Option<String>,
}
fn parse_array_header(line: &str) -> Option<ArrayHeader> {
let trimmed = line.trim();
let bracket_start = trimmed.find('[')?;
let bracket_end = trimmed[bracket_start..].find(']')? + bracket_start;
let len_str = &trimmed[bracket_start + 1..bracket_end];
let len: usize = len_str.parse().ok()?;
let after_bracket = &trimmed[bracket_end + 1..];
if after_bracket.starts_with('{') {
let brace_end = after_bracket.find('}')?;
let fields_str = &after_bracket[1..brace_end];
let fields: Vec<String> = fields_str.split(',').map(|s| s.to_string()).collect();
let after_brace = &after_bracket[brace_end + 1..];
if after_brace.starts_with(':') {
return Some(ArrayHeader {
len,
fields: Some(fields),
inline_values: None,
});
}
return None;
}
if let Some(values) = after_bracket.strip_prefix(": ") {
return Some(ArrayHeader {
len,
fields: None,
inline_values: Some(values.to_string()),
});
}
if after_bracket.starts_with(':') {
return Some(ArrayHeader {
len,
fields: None,
inline_values: None,
});
}
None
}
fn parse_array_body(
header: &ArrayHeader,
lines: &[&str],
line_idx: usize,
base_indent: usize,
) -> Result<Value> {
if header.len == 0 {
return Ok(Value::Array(vec![]));
}
if let Some(ref inline) = header.inline_values {
let values = parse_inline_values(inline)?;
return Ok(Value::Array(values));
}
if let Some(ref fields) = header.fields {
let mut rows = Vec::new();
for (i, line) in lines.iter().enumerate().skip(line_idx + 1) {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let indent = count_indent(line);
if indent <= base_indent && i > line_idx + 1 {
break;
}
let obj = parse_tabular_row(trimmed, fields)?;
rows.push(obj);
}
return Ok(Value::Array(rows));
}
let mut detected_indent = base_indent + 2;
for line in &lines[line_idx + 1..] {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("- ") {
detected_indent = count_indent(line);
break;
}
break;
}
parse_list_items(lines, line_idx + 1, detected_indent)
}
fn parse_inline_values(s: &str) -> Result<Vec<Value>> {
let mut values = Vec::new();
let mut i = 0;
let bytes = s.as_bytes();
while i < bytes.len() {
if bytes[i] == b'"' {
let end = find_closing_quote(s, i + 1).ok_or_else(|| ToonError::ToonParse {
line: 0,
message: "Unterminated quoted string in inline array".to_string(),
})?;
let inner = &s[i + 1..end];
let unescaped = unescape_string(inner);
values.push(Value::String(unescaped));
i = end + 1;
if i < bytes.len() && bytes[i] == b',' {
i += 1;
}
} else {
let end = s[i..].find(',').map(|p| p + i).unwrap_or(s.len());
let token = &s[i..end];
values.push(parse_primitive_token(token));
i = end;
if i < bytes.len() && bytes[i] == b',' {
i += 1;
}
}
}
Ok(values)
}
fn parse_tabular_row(row: &str, fields: &[String]) -> Result<Value> {
let values = parse_inline_values(row)?;
let mut map = Map::new();
for (i, field) in fields.iter().enumerate() {
let val = values.get(i).cloned().unwrap_or(Value::Null);
map.insert(field.clone(), val);
}
Ok(Value::Object(map))
}
fn parse_list_items(lines: &[&str], start_line: usize, item_indent: usize) -> Result<Value> {
let mut items = Vec::new();
let mut i = start_line;
while i < lines.len() {
let line = lines[i];
let indent = count_indent(line);
let trimmed = line.trim();
if trimmed.is_empty() {
i += 1;
continue;
}
if indent < item_indent {
break;
}
if indent > item_indent {
i += 1;
continue;
}
if !trimmed.starts_with("- ") {
break;
}
let content = &trimmed[2..];
if content.starts_with('[') {
if let Some(header) = parse_array_header(content) {
let arr = parse_array_body(&header, lines, i, indent + 2)?;
items.push(arr);
i = skip_nested_lines(lines, i + 1, indent + 2);
continue;
}
}
if item_content_is_object(content) {
let (obj, next_i) = parse_list_item_object(lines, i, indent + 2, content)?;
items.push(obj);
i = next_i;
continue;
}
items.push(parse_primitive_value(content)?);
i += 1;
}
Ok(Value::Array(items))
}
fn item_content_is_object(content: &str) -> bool {
if content.starts_with('"') {
if let Some(end) = find_closing_quote(content, 1) {
return end + 1 < content.len() && content.as_bytes()[end + 1] == b':';
}
return false;
}
if let Some(pos) = content.find(':') {
let before = &content[..pos];
return !before.contains(' ') && !before.is_empty();
}
if let Some(pos) = content.find('[') {
let before = &content[..pos];
return !before.contains(' ') && !before.is_empty();
}
false
}
fn parse_list_item_object(
lines: &[&str],
start_line: usize,
hyphen_content_indent: usize,
first_field_content: &str,
) -> Result<(Value, usize)> {
let mut map = Map::new();
let mut i = parse_key_value_into_map(
first_field_content,
&mut map,
lines,
start_line,
hyphen_content_indent,
)?;
let sibling_indent = hyphen_content_indent;
while i < lines.len() {
let line = lines[i];
let indent = count_indent(line);
let trimmed = line.trim();
if trimmed.is_empty() {
i += 1;
continue;
}
if indent != sibling_indent {
break;
}
if !line_has_key_colon(trimmed) && !trimmed.contains('[') {
break;
}
i = parse_key_value_into_map(trimmed, &mut map, lines, i, indent)?;
}
Ok((Value::Object(map), i))
}
fn skip_array_body(lines: &[&str], start: usize, base_indent: usize) -> usize {
if start >= lines.len() {
return start;
}
let mut first_line_indent = base_indent + 2;
let mut is_list = false;
for line in &lines[start..] {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
first_line_indent = count_indent(line);
is_list = trimmed.starts_with("- ");
break;
}
if !is_list {
return skip_nested_lines(lines, start, first_line_indent);
}
let mut i = start;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.is_empty() {
i += 1;
continue;
}
let indent = count_indent(line);
if indent < first_line_indent {
break;
}
if indent == first_line_indent && !trimmed.starts_with("- ") {
break;
}
i += 1;
}
i
}
fn skip_nested_lines(lines: &[&str], start: usize, base_indent: usize) -> usize {
let mut i = start;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.is_empty() {
i += 1;
continue;
}
let indent = count_indent(line);
if indent < base_indent {
break;
}
i += 1;
}
i
}
fn parse_key_value_into_map(
content: &str,
map: &mut Map<String, Value>,
lines: &[&str],
line_idx: usize,
base_indent: usize,
) -> Result<usize> {
let (key, rest) = parse_key_from_content(content)?;
if rest.starts_with('[') {
let arr_line = format!("x{}", rest);
if let Some(header) = parse_array_header(&arr_line) {
let is_empty = header.len == 0;
let is_inline = header.inline_values.is_some();
let arr = parse_array_body(&header, lines, line_idx, base_indent)?;
map.insert(key, arr);
if is_empty || is_inline {
return Ok(line_idx + 1);
}
let next = skip_array_body(lines, line_idx + 1, base_indent);
return Ok(next);
}
}
if rest == ":" {
let child_indent = base_indent + 2;
if line_idx + 1 < lines.len() {
let next_indent = count_indent(lines[line_idx + 1]);
if next_indent >= child_indent && !lines[line_idx + 1].trim().is_empty() {
let end = find_block_end(lines, line_idx + 1, child_indent);
let obj = parse_object_from_lines(lines, child_indent, line_idx + 1, end)?;
map.insert(key, obj);
return Ok(end);
}
}
map.insert(key, Value::Object(Map::new()));
} else if let Some(value_str) = rest.strip_prefix(": ") {
let value = parse_primitive_value(value_str)?;
map.insert(key, value);
} else {
map.insert(key, Value::Null);
}
Ok(line_idx + 1)
}
fn parse_key_from_content(content: &str) -> Result<(String, String)> {
if content.starts_with('"') {
let end = find_closing_quote(content, 1).ok_or_else(|| ToonError::ToonParse {
line: 0,
message: "Unterminated quoted key".to_string(),
})?;
let key = unescape_string(&content[1..end]);
let rest = content[end + 1..].to_string();
Ok((key, rest))
} else {
let colon_pos = content.find(':');
let bracket_pos = content.find('[');
let end = match (colon_pos, bracket_pos) {
(Some(c), Some(b)) => c.min(b),
(Some(c), None) => c,
(None, Some(b)) => b,
(None, None) => content.len(),
};
let key = content[..end].to_string();
let rest = content[end..].to_string();
Ok((key, rest))
}
}
fn parse_object_from_lines(
lines: &[&str],
expected_indent: usize,
start: usize,
end: usize,
) -> Result<Value> {
let mut map = Map::new();
let mut i = start;
while i < end {
let line = lines[i];
let trimmed = line.trim();
if trimmed.is_empty() {
i += 1;
continue;
}
let indent = count_indent(line);
if indent < expected_indent {
break;
}
if indent > expected_indent {
i += 1;
continue;
}
i = parse_key_value_into_map(trimmed, &mut map, lines, i, indent)?;
while i < end {
let next_line = lines[i];
let next_trimmed = next_line.trim();
if next_trimmed.is_empty() {
i += 1;
continue;
}
let next_indent = count_indent(next_line);
if next_indent <= expected_indent {
break;
}
i += 1;
}
}
Ok(Value::Object(map))
}
fn find_block_end(lines: &[&str], start: usize, min_indent: usize) -> usize {
let mut i = start;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.is_empty() {
i += 1;
continue;
}
let indent = count_indent(line);
if indent < min_indent {
break;
}
i += 1;
}
i
}
fn parse_primitive_value(s: &str) -> Result<Value> {
Ok(parse_primitive_token(s))
}
fn parse_primitive_token(s: &str) -> Value {
let s = s.trim();
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
let inner = &s[1..s.len() - 1];
return Value::String(unescape_string(inner));
}
if s == "null" {
return Value::Null;
}
if s == "true" {
return Value::Bool(true);
}
if s == "false" {
return Value::Bool(false);
}
if let Ok(n) = s.parse::<i64>() {
return Value::Number(n.into());
}
if let Ok(f) = s.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
return Value::Number(n);
}
}
Value::String(s.to_string())
}
fn count_indent(line: &str) -> usize {
line.len() - line.trim_start().len()
}
fn find_closing_quote(s: &str, start: usize) -> Option<usize> {
let bytes = s.as_bytes();
let mut i = start;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2; } else if bytes[i] == b'"' {
return Some(i);
} else {
i += 1;
}
}
None
}
fn unescape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('\\') => out.push('\\'),
Some('"') => out.push('"'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}