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