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