use std::path::Path;
use indexmap::IndexMap;
use crate::constants::{at_regexp, end_curly_regexp};
use crate::elements::Element;
use crate::error::MpsError;
pub fn parse_file(path: &Path) -> Result<IndexMap<String, Element>, MpsError> {
let content = std::fs::read_to_string(path)
.map_err(|e| MpsError::ParseError {
file: path.display().to_string(),
msg: e.to_string(),
})?;
let basename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("0");
let base_ref: u64 = if basename.len() >= 8 {
basename[..8].parse().unwrap_or(0)
} else {
0
};
let wrapped = format!("@mps[]{{\n{}\n}}", content);
parse_wrapped(&wrapped, base_ref)
}
pub fn parse_str(content: &str) -> IndexMap<String, Element> {
let wrapped = format!("@mps[]{{\n{}\n}}", content);
parse_wrapped(&wrapped, 0).unwrap_or_default()
}
pub fn parse_wrapped(wrapped: &str, base_ref: u64) -> Result<IndexMap<String, Element>, MpsError> {
let open_re = at_regexp();
let close_re = end_curly_regexp();
struct Frame {
sign: String,
args: String,
body_start: usize,
child_counter: u64,
ref_path: Vec<u64>,
}
let mut elements: IndexMap<String, Element> = IndexMap::new();
let mut stack: Vec<Frame> = Vec::new();
let mut pos = 0usize;
loop {
if pos >= wrapped.len() { break; }
let open_m = open_re.find_at(wrapped, pos);
let close_m = close_re.find_at(wrapped, pos);
let use_open = match (open_m, close_m) {
(None, None) => break,
(Some(_), None) => true,
(None, Some(_)) => false,
(Some(o), Some(c)) => o.start() < c.start(),
};
if use_open {
let om = open_m.unwrap();
let ref_path = if stack.is_empty() {
vec![base_ref]
} else {
let parent = stack.last_mut().unwrap();
parent.child_counter += 1;
let mut p = parent.ref_path.clone();
p.push(parent.child_counter);
p
};
let caps = open_re.captures_at(wrapped, om.start()).unwrap();
let sign = caps.name("element_sign")
.map_or("", |m| m.as_str())
.to_string();
let args = caps.name("args")
.map_or("", |m| m.as_str())
.to_string();
stack.push(Frame { sign, args, body_start: om.end(), child_counter: 0, ref_path });
pos = om.end();
} else {
let cm = close_m.unwrap();
if stack.is_empty() { break; }
let frame = stack.pop().unwrap();
let body_str = wrapped[frame.body_start..cm.start()].to_string();
let ref_key = frame.ref_path.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(".");
let el = Element::from_parts(&frame.sign, frame.args, frame.ref_path, body_str);
elements.insert(ref_key, el);
pos = cm.end();
}
}
Ok(elements)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::elements::ElementKind;
use std::io::Write;
fn parse_content(content: &str) -> IndexMap<String, Element> {
let wrapped = format!("@mps[]{{\n{}\n}}", content);
parse_wrapped(&wrapped, 20260101).unwrap()
}
#[test]
fn test_empty_file() {
let els = parse_content("");
assert_eq!(els.len(), 1, "only root @mps wrapper");
let (key, el) = els.iter().next().unwrap();
assert_eq!(key, "20260101");
assert!(el.is_mps_group());
}
#[test]
fn test_single_task() {
let els = parse_content("@task[work]{\n Do the thing\n}");
assert_eq!(els.len(), 2);
let task = els.get("20260101.1").unwrap();
assert_eq!(task.kind(), ElementKind::Task);
assert!(task.tags().contains(&"work".to_string()));
}
#[test]
fn test_multiple_elements_sequential_refs() {
let els = parse_content("@task{ First }\n@note{ Second }");
assert!(els.contains_key("20260101.1"), "first child");
assert!(els.contains_key("20260101.2"), "second child");
}
#[test]
fn test_nested_mps_block() {
let els = parse_content("@mps{\n @task{ Nested task }\n}");
assert!(els.contains_key("20260101.1"), "@mps child");
assert!(els.contains_key("20260101.1.1"), "@task inside @mps");
}
#[test]
fn test_args_captured() {
let els = parse_content("@task[work, status: done]{ Done thing }");
let el = els.get("20260101.1").unwrap();
if let Element::Task { data, .. } = el {
assert!(data.is_done());
assert_eq!(data.tags, vec!["work"]);
} else {
panic!("expected Task variant");
}
}
#[test]
fn test_optional_brackets() {
let els = parse_content("@task{ No brackets }");
assert_eq!(els.get("20260101.1").unwrap().kind(), ElementKind::Task);
}
#[test]
fn test_unknown_element() {
let els = parse_content("@widget{ Unknown type }");
assert_eq!(els.get("20260101.1").unwrap().kind(), ElementKind::Unknown);
}
#[test]
fn test_deeply_nested() {
let content = "@mps{\n @mps{\n @task{ Deep }\n }\n}";
let els = parse_content(content);
assert!(els.contains_key("20260101.1.1.1"), "deeply nested task ref");
}
#[test]
fn test_parse_real_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("20260101.1700000000.mps");
let mut f = std::fs::File::create(&path).unwrap();
writeln!(f, "@task[work]{{ Do the thing }}").unwrap();
writeln!(f, "@note{{ A note }}").unwrap();
drop(f);
let els = parse_file(&path).unwrap();
assert_eq!(els.len(), 3); }
}