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), &'static str> {
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("Missing assignment operator '='")?;
let key = line[..pos].trim();
let raw_val = line[pos + 1..].trim();
if key.is_empty() {
return Err("Key cannot be empty");
}
let val = if raw_val.starts_with('{') || raw_val.starts_with('[') {
raw_val
} else {
unwrap_quotes(raw_val)
};
Ok((key, val))
}
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)>, String> {
let inner = value
.trim()
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.ok_or_else(|| format!("Inline object must be wrapped in '{{}}', got: '{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(format!("Empty key in inline object field '{entry}'"));
}
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), String> {
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(format!(
"Inline object field '{entry}' has no '=' or ':' separator"
))
}