use crate::error::Result;
use crate::options::ParserOptions;
use indexmap::IndexMap;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Entry {
key: String,
value: String,
indent: usize,
}
fn normalize_multiline_keys(input: &str, preserve_cr: bool) -> String {
let lines: Vec<&str> = if preserve_cr {
input.split('\n').collect()
} else {
input.lines().collect()
};
let mut result = String::new();
let mut i = 0;
let mut base_indent: Option<usize> = None;
let mut prev_had_complete_entry = false;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
let line_indent = line.len() - line.trim_start().len();
if trimmed.is_empty() && result.is_empty() {
i += 1;
continue;
}
if base_indent.is_none() && !trimmed.is_empty() {
base_indent = Some(line_indent);
}
let base = base_indent.unwrap_or(0);
let is_complete_entry = trimmed.contains('=');
if line_indent > base {
result.push_str(line);
result.push('\n');
i += 1;
continue;
}
if !trimmed.is_empty() && !trimmed.contains('=') && !prev_had_complete_entry {
let mut j = i + 1;
let mut found_equals = false;
while j < lines.len() {
let next_line = lines[j];
let next_trimmed = next_line.trim();
let next_indent = next_line.len() - next_line.trim_start().len();
if next_trimmed.is_empty() {
j += 1;
continue;
}
if next_indent > line_indent {
break;
}
if next_trimmed.contains('=') {
found_equals = true;
break;
}
j += 1;
}
if found_equals && j < lines.len() {
let mut key_parts = vec![trimmed];
for part_line in lines.iter().take(j).skip(i + 1) {
let part_trimmed = part_line.trim();
let part_indent = part_line.len() - part_line.trim_start().len();
if !part_trimmed.is_empty() && part_indent <= line_indent {
key_parts.push(part_trimmed);
}
}
let joined_key = key_parts.join(" ");
result.push_str(&joined_key);
result.push_str(lines[j].trim());
result.push('\n');
i = j + 1;
prev_had_complete_entry = true; continue;
}
}
result.push_str(line);
result.push('\n');
prev_had_complete_entry = is_complete_entry;
i += 1;
}
result
}
fn trim_spaces_start(s: &str) -> &str {
s.trim_start_matches(' ')
}
fn trim_with_cr_option(s: &str, preserve_cr: bool) -> &str {
if preserve_cr {
s.trim_matches([' ', '\t'])
} else {
s.trim()
}
}
fn trim_start_with_cr_option(s: &str, preserve_cr: bool) -> &str {
if preserve_cr {
s.trim_start_matches([' ', '\t'])
} else {
s.trim_start()
}
}
fn find_delimiter(s: &str, options: &ParserOptions) -> Option<usize> {
if options.is_strict_spacing() {
if let Some(pos) = s.find(" = ") {
return Some(pos + 1);
}
if s.ends_with(" =") {
return Some(s.len() - 1);
}
None
} else if options.prefer_spaced_delimiter() {
if let Some(pos) = s.find(" = ") {
return Some(pos + 1);
}
if s.ends_with(" =") {
return Some(s.len() - 1);
}
s.find('=')
} else {
s.find('=')
}
}
fn trim_value(s: &str, options: &ParserOptions) -> String {
if options.preserve_tabs() {
trim_spaces_start(s).to_string()
} else {
s.trim_start().to_string()
}
}
fn parse_entries(input: &str, options: &ParserOptions) -> Vec<Entry> {
let input = options.process_crlf(input);
let normalized = normalize_multiline_keys(&input, options.preserve_crlf());
let mut entries = Vec::new();
let mut current_key: Option<(String, usize)> = None;
let mut value_lines: Vec<String> = Vec::new();
let mut base_indent: Option<usize> = None;
let lines_iter: Box<dyn Iterator<Item = &str>> = if options.preserve_crlf() {
Box::new(normalized.split('\n'))
} else {
Box::new(normalized.lines())
};
let preserve_cr = options.preserve_crlf();
for line in lines_iter {
let indent = line.len() - trim_start_with_cr_option(line, preserve_cr).len();
let trimmed = trim_with_cr_option(line, preserve_cr);
if trimmed.is_empty() {
if current_key.is_some() {
value_lines.push(String::new());
}
continue;
}
if base_indent.is_none() {
base_indent = Some(indent);
}
let base = base_indent.unwrap_or(0);
let has_equals = trimmed.contains('=');
if indent <= base && has_equals {
if let Some((key, key_indent)) = current_key.take() {
let value = finalize_value(&value_lines.join("\n"), options);
entries.push(Entry {
key,
value,
indent: key_indent,
});
value_lines.clear();
}
if let Some(eq_pos) = find_delimiter(trimmed, options) {
let key = trimmed[..eq_pos].trim().to_string();
let value_raw = &trimmed[eq_pos + 1..];
let value = trim_value(value_raw, options);
current_key = Some((key, indent));
if value.is_empty() {
value_lines.push(String::new());
} else {
value_lines.push(value);
}
} else {
current_key = Some((trimmed.to_string(), indent));
value_lines.push(String::new());
}
} else if let Some((_, key_indent)) = current_key {
if indent > key_indent {
value_lines.push(line.to_string());
} else {
let (key, key_indent_final) = current_key.take().unwrap();
let value = finalize_value(&value_lines.join("\n"), options);
entries.push(Entry {
key,
value,
indent: key_indent_final,
});
value_lines.clear();
current_key = Some((trimmed.to_string(), indent));
}
}
}
if let Some((key, key_indent)) = current_key {
let value = finalize_value(&value_lines.join("\n"), options);
entries.push(Entry {
key,
value,
indent: key_indent,
});
}
entries
}
fn finalize_value(value: &str, options: &ParserOptions) -> String {
let trimmed = if options.preserve_crlf() {
value.trim_end_matches([' ', '\t', '\n'])
} else {
value.trim_end()
};
options.process_tabs(trimmed).into_owned()
}
#[allow(dead_code)]
pub(crate) fn parse_to_map(
input: &str,
options: &ParserOptions,
) -> Result<IndexMap<String, Vec<String>>> {
let entries = parse_entries(input, options);
let mut result: IndexMap<String, Vec<String>> = IndexMap::new();
for entry in entries {
result.entry(entry.key).or_default().push(entry.value);
}
Ok(result)
}
pub(crate) fn parse_to_entries(input: &str, options: &ParserOptions) -> Result<Vec<crate::Entry>> {
let entries = parse_entries(input, options);
Ok(entries
.into_iter()
.map(|e| crate::Entry::new(e.key, e.value))
.collect())
}