mermaid_text/parser/
mindmap.rs1use crate::Error;
46use crate::mindmap::{Mindmap, MindmapNode};
47use crate::parser::common::strip_inline_comment;
48
49pub fn parse(src: &str) -> Result<Mindmap, Error> {
56 let mut header_seen = false;
57 let mut node_lines: Vec<(usize, String)> = Vec::new();
59
60 for raw in src.lines() {
61 let stripped = strip_inline_comment(raw);
62
63 if !header_seen {
64 let trimmed = stripped.trim();
65 if trimmed.is_empty() || trimmed.starts_with("%%") {
66 continue;
67 }
68 if !trimmed.eq_ignore_ascii_case("mindmap") {
69 return Err(Error::ParseError(format!(
70 "expected `mindmap` header, got {trimmed:?}"
71 )));
72 }
73 header_seen = true;
74 continue;
75 }
76
77 let trimmed = stripped.trim();
78
79 if trimmed.is_empty() || trimmed.starts_with("%%") {
81 continue;
82 }
83
84 if trimmed.starts_with("accTitle") || trimmed.starts_with("accDescr") {
86 continue;
87 }
88
89 if trimmed.starts_with("::icon(") {
91 continue;
92 }
93
94 let indent = measure_indent(raw);
96
97 let text = strip_node_shape(trimmed);
99
100 node_lines.push((indent, text));
101 }
102
103 if !header_seen {
104 return Err(Error::ParseError(
105 "missing `mindmap` header line".to_string(),
106 ));
107 }
108
109 if node_lines.is_empty() {
110 return Err(Error::ParseError(
111 "mindmap has no nodes (at least a root node is required)".to_string(),
112 ));
113 }
114
115 let root = build_tree(&node_lines);
116 Ok(Mindmap { root })
117}
118
119fn measure_indent(line: &str) -> usize {
125 let mut count = 0;
126 for ch in line.chars() {
127 match ch {
128 ' ' => count += 1,
129 '\t' => count += 4,
130 _ => break,
131 }
132 }
133 count
134}
135
136fn strip_node_shape(s: &str) -> String {
142 let body = if let Some(bracket_start) = s.find(['[', '(', '{', ')']) {
146 let prefix = &s[..bracket_start];
149 if prefix.chars().all(|c: char| !c.is_whitespace()) && !prefix.is_empty() {
150 &s[bracket_start..]
151 } else {
152 s
153 }
154 } else {
155 s
156 };
157
158 if let Some(inner) = body.strip_prefix("((").and_then(|t| t.strip_suffix("))")) {
160 return inner.trim().to_string();
161 }
162 if let Some(inner) = body.strip_prefix("))").and_then(|t| t.strip_suffix("((")) {
164 return inner.trim().to_string();
165 }
166 if let Some(inner) = body.strip_prefix(')').and_then(|t| t.strip_suffix('(')) {
168 return inner.trim().to_string();
169 }
170 if let Some(inner) = body.strip_prefix('(').and_then(|t| t.strip_suffix(')')) {
172 return inner.trim().to_string();
173 }
174 if let Some(inner) = body.strip_prefix("{{").and_then(|t| t.strip_suffix("}}")) {
176 return inner.trim().to_string();
177 }
178 if let Some(inner) = body.strip_prefix('[').and_then(|t| t.strip_suffix(']')) {
180 return inner.trim().to_string();
181 }
182
183 body.to_string()
186}
187
188fn build_tree(lines: &[(usize, String)]) -> MindmapNode {
205 let mut nodes: Vec<MindmapNode> = Vec::with_capacity(lines.len());
207
208 let mut stack: Vec<(usize, usize)> = Vec::new();
210
211 let mut children_map: Vec<Vec<usize>> = Vec::with_capacity(lines.len());
213
214 for (indent, text) in lines {
215 let new_idx = nodes.len();
216 nodes.push(MindmapNode::new(text));
217 children_map.push(Vec::new());
218
219 while let Some(&(stack_indent, _)) = stack.last() {
222 if stack_indent >= *indent {
223 stack.pop();
224 } else {
225 break;
226 }
227 }
228
229 if let Some(&(_, parent_idx)) = stack.last() {
231 children_map[parent_idx].push(new_idx);
232 }
233
234 stack.push((*indent, new_idx));
235 }
236
237 for parent_idx in (0..nodes.len()).rev() {
240 let child_indices: Vec<usize> = children_map[parent_idx].clone();
241 let children: Vec<MindmapNode> = child_indices
245 .into_iter()
246 .map(|ci| {
247 std::mem::replace(&mut nodes[ci], MindmapNode::new(""))
250 })
251 .collect();
252 nodes[parent_idx].children = children;
253 }
254
255 std::mem::replace(&mut nodes[0], MindmapNode::new(""))
257}
258
259#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn parses_root_only() {
269 let diag = parse("mindmap\n root").unwrap();
270 assert_eq!(diag.root.text, "root");
271 assert!(diag.root.children.is_empty());
272 }
273
274 #[test]
275 fn parses_one_level_children() {
276 let diag = parse("mindmap\n root\n A\n B\n C").unwrap();
277 assert_eq!(diag.root.text, "root");
278 assert_eq!(diag.root.children.len(), 3);
279 assert_eq!(diag.root.children[0].text, "A");
280 assert_eq!(diag.root.children[1].text, "B");
281 assert_eq!(diag.root.children[2].text, "C");
282 }
283
284 #[test]
285 fn parses_nested_two_levels() {
286 let src = "mindmap\n root\n Parent\n Child1\n Child2\n Sibling";
287 let diag = parse(src).unwrap();
288 assert_eq!(diag.root.children.len(), 2);
289 let parent = &diag.root.children[0];
290 assert_eq!(parent.text, "Parent");
291 assert_eq!(parent.children.len(), 2);
292 assert_eq!(parent.children[0].text, "Child1");
293 assert_eq!(parent.children[1].text, "Child2");
294 let sibling = &diag.root.children[1];
295 assert_eq!(sibling.text, "Sibling");
296 assert!(sibling.children.is_empty());
297 }
298
299 #[test]
300 fn parses_node_shapes_strips_brackets() {
301 let src = "mindmap\n root((circle))\n rounded(text)\n hex{{hexa}}\n plain text";
302 let diag = parse(src).unwrap();
303 assert_eq!(diag.root.text, "circle");
304 assert_eq!(diag.root.children[0].text, "text");
305 assert_eq!(diag.root.children[1].text, "hexa");
306 assert_eq!(diag.root.children[2].text, "plain text");
307 }
308
309 #[test]
310 fn ignores_icon_directive() {
311 let src = "mindmap\n root\n Origins\n ::icon(fa fa-book)\n Long history";
312 let diag = parse(src).unwrap();
313 let origins = &diag.root.children[0];
314 assert_eq!(origins.text, "Origins");
315 assert_eq!(origins.children.len(), 1);
317 assert_eq!(origins.children[0].text, "Long history");
318 }
319
320 #[test]
321 fn comment_lines_skipped() {
322 let src = "%% preamble\nmindmap\n %% inner comment\n root\n child %% trailing";
323 let diag = parse(src).unwrap();
324 assert_eq!(diag.root.text, "root");
325 assert_eq!(diag.root.children.len(), 1);
326 assert_eq!(diag.root.children[0].text, "child");
327 }
328
329 #[test]
330 fn tabs_count_as_four_spaces() {
331 let src = "mindmap\n\troot\n\t\tchild";
333 let diag = parse(src).unwrap();
334 assert_eq!(diag.root.text, "root");
335 assert_eq!(diag.root.children.len(), 1);
336 assert_eq!(diag.root.children[0].text, "child");
337 }
338
339 #[test]
340 fn dedent_attaches_sibling_to_correct_parent() {
341 let src = "mindmap\n root\n A\n A1\n A1a\n B";
343 let diag = parse(src).unwrap();
344 assert_eq!(diag.root.children.len(), 2);
345 let a = &diag.root.children[0];
346 assert_eq!(a.text, "A");
347 assert_eq!(a.children.len(), 1);
348 assert_eq!(a.children[0].text, "A1");
349 assert_eq!(a.children[0].children[0].text, "A1a");
350 let b = &diag.root.children[1];
351 assert_eq!(b.text, "B");
352 assert!(b.children.is_empty());
353 }
354
355 #[test]
356 fn missing_header_returns_error() {
357 let err = parse("root\n child").unwrap_err();
358 assert!(
359 err.to_string().contains("mindmap"),
360 "unexpected error: {err}"
361 );
362 }
363
364 #[test]
365 fn accessibility_metadata_is_silently_ignored() {
366 let src = "mindmap\n accTitle: My title\n accDescr: A description\n root\n child";
367 let diag = parse(src).unwrap();
368 assert_eq!(diag.root.text, "root");
370 assert_eq!(diag.root.children.len(), 1);
371 assert_eq!(diag.root.children[0].text, "child");
372 }
373}