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