use crate::error::HkError;
use crate::value::{HkConfig, HkValue};
use indexmap::IndexMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::str::FromStr;
pub fn parse_hk(input: &str) -> Result<HkConfig, HkError> {
let lines: Vec<&str> = input.lines().collect();
let mut config = IndexMap::new();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim_start();
if line.is_empty() || line.starts_with('!') {
i += 1;
continue;
}
if line.starts_with('[') {
let close = line.find(']').ok_or_else(|| HkError::Parse {
line: (i + 1) as u32,
column: line.find('[').unwrap() + 1,
message: "Unclosed section header".to_string(),
})?;
let section_name = line[1..close].trim();
if section_name.is_empty() {
return Err(HkError::Parse {
line: (i + 1) as u32,
column: close + 1,
message: "Empty section name".to_string(),
});
}
let mut end = i + 1;
let mut array_depth: i32 = 0;
while end < lines.len() {
let next_line = lines[end];
let next_trimmed = next_line.trim_start();
if array_depth == 0 && next_trimmed.starts_with('[') {
break;
}
array_depth += net_bracket_depth(next_line);
end += 1;
}
let section_lines = &lines[i + 1..end];
let map = parse_map(1, section_lines, i + 2)?;
config.insert(section_name.to_string(), HkValue::Map(map));
i = end;
} else {
return Err(HkError::Parse {
line: (i + 1) as u32,
column: 1,
message: "Expected section header".to_string(),
});
}
}
Ok(config)
}
fn parse_map(level: usize, lines: &[&str], start_line: usize) -> Result<IndexMap<String, HkValue>, HkError> {
let mut map = IndexMap::new();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim_start();
if trimmed.is_empty() || trimmed.starts_with('!') {
i += 1;
continue;
}
let dash_count = trimmed.chars().take_while(|c| *c == '-').count();
if dash_count == 0 {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: 1,
message: "Expected key or map header".to_string(),
});
}
if dash_count < level {
break;
}
if dash_count > level {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: 1,
message: format!(
"Inconsistent nesting level: expected {} dash(es) (\"{}\") at this depth, found {} (\"{}\"). Nesting must increase by exactly one dash per level.",
level,
"-".repeat(level),
dash_count,
"-".repeat(dash_count)
),
});
}
let after_dashes = &trimmed[dash_count..];
let rest = after_dashes.trim_start();
if !rest.starts_with('>') {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: dash_count + 1,
message: "Expected '>' after dashes".to_string(),
});
}
let after_gt = &rest[1..].trim_start();
if after_gt.is_empty() {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: dash_count + 1,
message: "Missing key after '>'".to_string(),
});
}
if let Some(arrow_pos) = after_gt.find("=>") {
let key = after_gt[..arrow_pos].trim();
let value_part = after_gt[arrow_pos + 2..].trim();
let key = unquote_key(key);
if key.is_empty() {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: dash_count + 1,
message: "Empty key".to_string(),
});
}
let value_col = arrow_pos + dash_count + 2;
if value_part.starts_with('[') && net_bracket_depth(value_part) > 0 {
let mut buf = value_part.to_string();
let mut consumed = 1usize;
let mut j = i + 1;
while net_bracket_depth(&buf) > 0 {
if j >= lines.len() {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: value_col,
message: "Unclosed array: reached end of section before a matching ']'".to_string(),
});
}
buf.push('\n');
buf.push_str(lines[j]);
consumed += 1;
j += 1;
}
let first = buf.find('[').unwrap();
let last = buf.rfind(']').unwrap();
let inner = &buf[first + 1..last];
let items = parse_array_inner(inner, start_line + i, value_col)?;
insert_key(&mut map, &key, HkValue::Array(items))?;
i += consumed;
} else {
let value = parse_value(value_part, start_line + i, value_col)?;
insert_key(&mut map, &key, value)?;
i += 1;
}
} else {
let key = after_gt.trim();
let key = unquote_key(key);
if key.is_empty() {
return Err(HkError::Parse {
line: (start_line + i) as u32,
column: dash_count + 1,
message: "Empty map key".to_string(),
});
}
let next_level = level + 1;
let mut j = i + 1;
while j < lines.len() {
let sub_line = lines[j];
let sub_trimmed = sub_line.trim_start();
if sub_trimmed.is_empty() || sub_trimmed.starts_with('!') {
j += 1;
continue;
}
let sub_dash_count = sub_trimmed.chars().take_while(|c| *c == '-').count();
if sub_dash_count < next_level {
break;
}
j += 1;
}
let sub_lines = &lines[i + 1..j];
let sub_map = parse_map(next_level, sub_lines, start_line + i + 1)?;
insert_key(&mut map, &key, HkValue::Map(sub_map))?;
i = j;
}
}
Ok(map)
}
fn insert_key(map: &mut IndexMap<String, HkValue>, key: &str, value: HkValue) -> Result<(), HkError> {
if key.contains('.') && !key.starts_with('.') && !key.ends_with('.') {
let parts: Vec<&str> = key.split('.').collect();
insert_nested(map, parts, value)
} else {
if map.contains_key(key) {
return Err(HkError::KeyConflict(key.to_string()));
}
map.insert(key.to_string(), value);
Ok(())
}
}
fn insert_nested(map: &mut IndexMap<String, HkValue>, keys: Vec<&str>, value: HkValue) -> Result<(), HkError> {
let mut current = map;
for key in &keys[0..keys.len() - 1] {
let entry = current
.entry(key.to_string())
.or_insert(HkValue::Map(IndexMap::new()));
if let HkValue::Map(submap) = entry {
current = submap;
} else {
return Err(HkError::KeyConflict(key.to_string()));
}
}
if let Some(last_key) = keys.last() {
current.insert(last_key.to_string(), value);
}
Ok(())
}
fn unquote_key(s: &str) -> String {
let s = s.trim();
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
let inner = &s[1..s.len() - 1];
inner.replace("\\\"", "\"")
} else {
s.to_string()
}
}
fn parse_value(s: &str, line: usize, column: usize) -> Result<HkValue, HkError> {
let s = s.trim();
if s.is_empty() {
return Err(HkError::Parse {
line: line as u32,
column,
message: "Empty value".to_string(),
});
}
if s.starts_with('[') && s.ends_with(']') && net_bracket_depth(s) == 0 {
let inner = &s[1..s.len() - 1];
let items = parse_array_inner(inner, line, column)?;
Ok(HkValue::Array(items))
} else {
parse_simple_value(s, line, column)
}
}
fn net_bracket_depth(s: &str) -> i32 {
let mut depth = 0i32;
let mut in_quotes = false;
let mut escape = false;
for c in s.chars() {
if escape {
escape = false;
continue;
}
match c {
'\\' if in_quotes => escape = true,
'"' => in_quotes = !in_quotes,
'[' if !in_quotes => depth += 1,
']' if !in_quotes => depth -= 1,
_ => {}
}
}
depth
}
fn parse_array_inner(inner: &str, line: usize, column: usize) -> Result<Vec<HkValue>, HkError> {
let mut items = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut escape = false;
let mut depth = 0i32;
macro_rules! flush_item {
() => {{
let trimmed = current.trim();
let trimmed = trimmed.strip_suffix(',').unwrap_or(trimmed).trim();
if !trimmed.is_empty() && !trimmed.starts_with('!') {
items.push(parse_value(trimmed, line, column)?);
}
current.clear();
}};
}
for c in inner.chars() {
if escape {
current.push(c);
escape = false;
continue;
}
match c {
'\\' if in_quotes => {
current.push(c);
escape = true;
}
'"' => {
in_quotes = !in_quotes;
current.push(c);
}
'[' if !in_quotes => {
depth += 1;
current.push(c);
}
']' if !in_quotes => {
depth -= 1;
current.push(c);
}
',' if !in_quotes && depth == 0 => flush_item!(),
'\n' if !in_quotes && depth == 0 => flush_item!(),
_ => current.push(c),
}
}
flush_item!();
Ok(items)
}
fn parse_simple_value(s: &str, line: usize, column: usize) -> Result<HkValue, HkError> {
let s = s.trim();
if s.is_empty() {
return Err(HkError::Parse {
line: line as u32,
column,
message: "Empty value".to_string(),
});
}
if s.eq_ignore_ascii_case("true") {
return Ok(HkValue::Bool(true));
}
if s.eq_ignore_ascii_case("false") {
return Ok(HkValue::Bool(false));
}
if let Ok(n) = f64::from_str(s) {
return Ok(HkValue::Number(n));
}
if s.starts_with('"') && s.ends_with('"') {
let inner = &s[1..s.len() - 1];
let mut result = String::new();
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
match next {
'n' => result.push('\n'),
'r' => result.push('\r'),
't' => result.push('\t'),
'"' => result.push('"'),
'\\' => result.push('\\'),
_ => result.push(next),
}
}
} else {
result.push(c);
}
}
Ok(HkValue::String(result))
} else {
Ok(HkValue::String(s.to_string()))
}
}
pub fn load_hk_file<P: AsRef<Path>>(path: P) -> Result<HkConfig, HkError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut contents = String::new();
for line in reader.lines() {
let line = line?;
contents.push_str(&line);
contents.push('\n');
}
parse_hk(&contents)
}