use crate::error::{AamlError, ErrorDiagnostics};
pub fn strip_comment(line: &str) -> &str {
let mut quote_state: Option<char> = None;
let bytes = line.as_bytes();
for (idx, c) in line.char_indices() {
match (quote_state, c) {
(None, '#') => {
let preceded_by_space =
idx == 0 || bytes.get(idx - 1).is_some_and(|b| b.is_ascii_whitespace());
let followed_by_space = bytes.get(idx + 1).is_none_or(|b| b.is_ascii_whitespace());
if preceded_by_space && followed_by_space {
return &line[..idx];
}
}
(None, '"' | '\'') => quote_state = Some(c),
(Some(q), c) if c == q => quote_state = None,
_ => {}
}
}
line
}
pub(super) fn parse_assignment(line: &str) -> Result<(&str, &str), AamlError> {
let mut depth: i32 = 0;
let mut eq_pos: Option<usize> = None;
for (i, ch) in line.char_indices() {
match ch {
'{' | '[' => depth += 1,
'}' | ']' => depth -= 1,
'=' if depth == 0 => {
eq_pos = Some(i);
break;
}
_ => {}
}
}
let pos = eq_pos.ok_or_else(|| AamlError::MalformedLiteral {
literal_type: "assignment".to_string(),
content: line.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Missing assignment operator",
format!("Line '{}' does not contain '=' separator", line),
"Use format: key = value",
)),
})?;
let key = line[..pos].trim();
let raw_val = line[pos + 1..].trim();
if key.is_empty() {
return Err(AamlError::InvalidValue {
details: "Key is empty".to_string(),
expected: "non-empty key name".to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Empty key in assignment",
format!("Line '{}' has no key before '='", line),
"Provide a valid key name before the '=' operator",
)),
});
}
validate_balanced_delimiters(raw_val, line)?;
let val = if raw_val.starts_with('{') || raw_val.starts_with('[') {
raw_val
} else {
unwrap_quotes(raw_val)
};
Ok((key, val))
}
fn validate_balanced_delimiters(value: &str, line: &str) -> Result<(), AamlError> {
let mut stack: Vec<char> = Vec::new();
let mut quote_state: Option<char> = None;
let mut escaped = false;
for ch in value.chars() {
if let Some(q) = quote_state {
if escaped {
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if ch == q {
quote_state = None;
}
continue;
}
match ch {
'"' | '\'' => quote_state = Some(ch),
'{' | '[' => stack.push(ch),
'}' => {
if stack.pop() != Some('{') {
return Err(AamlError::MalformedLiteral {
literal_type: "assignment".to_string(),
content: line.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Mismatched delimiters",
format!("Assignment '{}' has an unmatched '}}'", line),
"Ensure inline objects and lists use balanced braces/brackets",
)),
});
}
}
']' => {
if stack.pop() != Some('[') {
return Err(AamlError::MalformedLiteral {
literal_type: "assignment".to_string(),
content: line.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Mismatched delimiters",
format!("Assignment '{}' has an unmatched ']'", line),
"Ensure inline objects and lists use balanced braces/brackets",
)),
});
}
}
_ => {}
}
}
if quote_state.is_some() {
return Err(AamlError::MalformedLiteral {
literal_type: "assignment".to_string(),
content: line.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Unterminated quote",
format!(
"Assignment '{}' contains an unterminated quoted value",
line
),
"Close the opening quote in the assignment value",
)),
});
}
if !stack.is_empty() {
return Err(AamlError::MalformedLiteral {
literal_type: "assignment".to_string(),
content: line.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Unclosed delimiters",
format!("Assignment '{}' has unclosed '{{' or '['", line),
"Ensure inline objects and lists use balanced braces/brackets",
)),
});
}
Ok(())
}
pub fn unwrap_quotes(s: &str) -> &str {
let s = s.trim();
if s.len() >= 2 {
if s.starts_with('"') && s.ends_with('"') {
return &s[1..s.len() - 1];
}
if s.starts_with('\'') && s.ends_with('\'') {
return &s[1..s.len() - 1];
}
}
s
}
pub(super) fn needs_accumulation(text: &str) -> bool {
if !text.starts_with('@') {
return false;
}
let opens = text.chars().filter(|&c| c == '{').count();
let closes = text.chars().filter(|&c| c == '}').count();
opens > closes
}
pub(super) fn block_is_complete(buf: &str) -> bool {
let opens = buf.chars().filter(|&c| c == '{').count();
let closes = buf.chars().filter(|&c| c == '}').count();
closes >= opens
}
pub fn is_inline_object(value: &str) -> bool {
let v = value.trim();
v.starts_with('{') && v.ends_with('}')
}
pub fn parse_inline_object(value: &str) -> Result<Vec<(String, String)>, AamlError> {
let inner = value
.trim()
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.ok_or_else(|| AamlError::MalformedLiteral {
literal_type: "inline object".to_string(),
content: value.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Malformed inline object",
format!("Inline object must be wrapped in '{{}}', got: '{}'", value),
"Wrap your object with curly braces: { key = value }",
)),
})?;
split_top_level_fields(inner)
.into_iter()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|entry| {
let (k, v) = split_field_pair(entry)?;
let k = k.trim();
if k.is_empty() {
return Err(AamlError::InvalidValue {
details: format!("Empty key in field '{}'", entry),
expected: "non-empty field name".to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Empty field name in inline object",
format!("Field entry '{}' has no valid key", entry),
"Provide a valid key name before '=' or ':'",
)),
});
}
let v = v.trim();
let final_v = match v.chars().next() {
Some('{') | Some('[') => v,
_ => unwrap_quotes(v),
};
Ok((k.to_string(), final_v.to_string()))
})
.collect()
}
fn split_top_level_fields(s: &str) -> Vec<&str> {
let mut items = Vec::new();
let mut depth: i32 = 0;
let mut start = 0;
for (i, ch) in s.char_indices() {
match ch {
'{' | '[' => depth += 1,
'}' | ']' => depth -= 1,
',' if depth == 0 => {
items.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start <= s.len() {
items.push(&s[start..]);
}
items
}
fn split_field_pair(entry: &str) -> Result<(&str, &str), AamlError> {
let mut depth: i32 = 0;
for (i, ch) in entry.char_indices() {
match ch {
'{' | '[' => depth += 1,
'}' | ']' => depth -= 1,
'=' | ':' if depth == 0 => return Ok((&entry[..i], &entry[i + 1..])),
_ => {}
}
}
Err(AamlError::MalformedLiteral {
literal_type: "field pair".to_string(),
content: entry.to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Missing field separator",
format!("Field entry '{}' has no '=' or ':' separator", entry),
"Use format: key = value or key: value",
)),
})
}