1use std::path::Path;
7use indexmap::IndexMap;
8use crate::constants::{at_regexp, end_curly_regexp};
9use crate::elements::Element;
10use crate::error::MpsError;
11
12pub fn parse_file(path: &Path) -> Result<IndexMap<String, Element>, MpsError> {
20 let content = std::fs::read_to_string(path)
21 .map_err(|e| MpsError::ParseError {
22 file: path.display().to_string(),
23 msg: e.to_string(),
24 })?;
25
26 let basename = path
27 .file_name()
28 .and_then(|n| n.to_str())
29 .unwrap_or("0");
30
31 let base_ref: u64 = if basename.len() >= 8 {
35 basename[..8].parse().unwrap_or(0)
36 } else {
37 0
38 };
39
40 let wrapped = format!("@mps[]{{\n{}\n}}", content);
41 parse_wrapped(&wrapped, base_ref)
42}
43
44pub fn parse_wrapped(wrapped: &str, base_ref: u64) -> Result<IndexMap<String, Element>, MpsError> {
46 let open_re = at_regexp();
47 let close_re = end_curly_regexp();
48
49 struct Frame {
50 sign: String,
51 args: String,
52 body_start: usize,
53 child_counter: u64,
54 ref_path: Vec<u64>,
55 }
56
57 let mut elements: IndexMap<String, Element> = IndexMap::new();
58 let mut stack: Vec<Frame> = Vec::new();
59 let mut pos = 0usize;
60
61 loop {
62 if pos >= wrapped.len() { break; }
63
64 let open_m = open_re.find_at(wrapped, pos);
65 let close_m = close_re.find_at(wrapped, pos);
66
67 let use_open = match (open_m, close_m) {
68 (None, None) => break,
69 (Some(_), None) => true,
70 (None, Some(_)) => false,
71 (Some(o), Some(c)) => o.start() < c.start(),
72 };
73
74 if use_open {
75 let om = open_m.unwrap();
76
77 let ref_path = if stack.is_empty() {
79 vec![base_ref]
80 } else {
81 let parent = stack.last_mut().unwrap();
82 parent.child_counter += 1;
83 let mut p = parent.ref_path.clone();
84 p.push(parent.child_counter);
85 p
86 };
87
88 let caps = open_re.captures_at(wrapped, om.start()).unwrap();
89 let sign = caps.name("element_sign")
90 .map_or("", |m| m.as_str())
91 .to_string();
92 let args = caps.name("args")
93 .map_or("", |m| m.as_str())
94 .to_string();
95
96 stack.push(Frame { sign, args, body_start: om.end(), child_counter: 0, ref_path });
97 pos = om.end();
98 } else {
99 let cm = close_m.unwrap();
100 if stack.is_empty() { break; }
101
102 let frame = stack.pop().unwrap();
103 let body_str = wrapped[frame.body_start..cm.start()].to_string();
104 let ref_key = frame.ref_path.iter()
105 .map(|n| n.to_string())
106 .collect::<Vec<_>>()
107 .join(".");
108
109 let el = Element::from_parts(&frame.sign, frame.args, frame.ref_path, body_str);
110 elements.insert(ref_key, el);
111 pos = cm.end();
112 }
113 }
114
115 Ok(elements)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::elements::ElementKind;
122 use std::io::Write;
123
124 fn parse_content(content: &str) -> IndexMap<String, Element> {
125 let wrapped = format!("@mps[]{{\n{}\n}}", content);
126 parse_wrapped(&wrapped, 20260101).unwrap()
127 }
128
129 #[test]
130 fn test_empty_file() {
131 let els = parse_content("");
132 assert_eq!(els.len(), 1, "only root @mps wrapper");
133 let (key, el) = els.iter().next().unwrap();
134 assert_eq!(key, "20260101");
135 assert!(el.is_mps_group());
136 }
137
138 #[test]
139 fn test_single_task() {
140 let els = parse_content("@task[work]{\n Do the thing\n}");
141 assert_eq!(els.len(), 2);
142 let task = els.get("20260101.1").unwrap();
143 assert_eq!(task.kind(), ElementKind::Task);
144 assert!(task.tags().contains(&"work".to_string()));
145 }
146
147 #[test]
148 fn test_multiple_elements_sequential_refs() {
149 let els = parse_content("@task{ First }\n@note{ Second }");
150 assert!(els.contains_key("20260101.1"), "first child");
151 assert!(els.contains_key("20260101.2"), "second child");
152 }
153
154 #[test]
155 fn test_nested_mps_block() {
156 let els = parse_content("@mps{\n @task{ Nested task }\n}");
157 assert!(els.contains_key("20260101.1"), "@mps child");
158 assert!(els.contains_key("20260101.1.1"), "@task inside @mps");
159 }
160
161 #[test]
162 fn test_args_captured() {
163 let els = parse_content("@task[work, status: done]{ Done thing }");
164 let el = els.get("20260101.1").unwrap();
165 if let Element::Task { data, .. } = el {
166 assert!(data.is_done());
167 assert_eq!(data.tags, vec!["work"]);
168 } else {
169 panic!("expected Task variant");
170 }
171 }
172
173 #[test]
174 fn test_optional_brackets() {
175 let els = parse_content("@task{ No brackets }");
176 assert_eq!(els.get("20260101.1").unwrap().kind(), ElementKind::Task);
177 }
178
179 #[test]
180 fn test_unknown_element() {
181 let els = parse_content("@widget{ Unknown type }");
182 assert_eq!(els.get("20260101.1").unwrap().kind(), ElementKind::Unknown);
183 }
184
185 #[test]
186 fn test_deeply_nested() {
187 let content = "@mps{\n @mps{\n @task{ Deep }\n }\n}";
188 let els = parse_content(content);
189 assert!(els.contains_key("20260101.1.1.1"), "deeply nested task ref");
190 }
191
192 #[test]
193 fn test_parse_real_file() {
194 let dir = tempfile::tempdir().unwrap();
195 let path = dir.path().join("20260101.1700000000.mps");
196 let mut f = std::fs::File::create(&path).unwrap();
197 writeln!(f, "@task[work]{{ Do the thing }}").unwrap();
198 writeln!(f, "@note{{ A note }}").unwrap();
199 drop(f);
200
201 let els = parse_file(&path).unwrap();
202 assert_eq!(els.len(), 3); }
204}