#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeType {
Default, RoundedRect, Rect, Circle, Cloud, Bang, Hexagon, }
#[derive(Debug, Clone)]
pub struct MindmapNode {
pub id: usize,
pub level: usize, pub descr: String,
pub node_type: NodeType,
pub children: Vec<MindmapNode>,
pub padding: f64,
pub section: Option<usize>,
pub is_root: bool,
}
impl MindmapNode {
pub fn new(
id: usize,
level: usize,
descr: String,
node_type: NodeType,
is_root: bool,
padding: f64,
) -> Self {
MindmapNode {
id,
level,
descr,
node_type,
children: Vec::new(),
padding,
section: None,
is_root,
}
}
}
pub struct MindmapDiagram {
pub root: Option<MindmapNode>,
}
pub fn parse(input: &str) -> crate::error::ParseResult<MindmapDiagram> {
let mut counter = 0usize;
let mut nodes_flat: Vec<(usize, MindmapNode)> = Vec::new(); let mut base_indent: Option<usize> = None;
for raw_line in input.lines() {
let trimmed = raw_line.trim_end();
if trimmed.trim().is_empty() || trimmed.trim().starts_with("%%") {
continue;
}
if trimmed.trim() == "mindmap" {
continue;
}
let indent = raw_line.len() - raw_line.trim_start().len();
let relative_level = if let Some(base) = base_indent {
let delta = indent.saturating_sub(base);
delta / 2
} else {
base_indent = Some(indent);
0
};
let content = trimmed.trim();
if content.is_empty() {
continue;
}
let (descr, node_type) = parse_node_content(content);
let base_padding = 15.0; let padding = match node_type {
NodeType::RoundedRect | NodeType::Rect | NodeType::Hexagon => base_padding * 2.0,
NodeType::Circle => 10.0,
_ => base_padding,
};
let is_root = nodes_flat.is_empty();
let node = MindmapNode::new(counter, relative_level, descr, node_type, is_root, padding);
counter += 1;
nodes_flat.push((relative_level, node));
}
if nodes_flat.is_empty() {
return crate::error::ParseResult::ok(MindmapDiagram { root: None });
}
let mut built: Vec<MindmapNode> = Vec::new();
for (level, node) in nodes_flat {
if level == 0 {
built.push(node);
} else {
insert_node(&mut built, node, level);
}
}
let mut root = built.into_iter().next();
if let Some(ref mut r) = root {
assign_sections(r, None);
}
crate::error::ParseResult::ok(MindmapDiagram { root })
}
fn insert_node(siblings: &mut Vec<MindmapNode>, node: MindmapNode, target_level: usize) {
if let Some(last) = siblings.last_mut() {
if target_level > last.level + 1 {
insert_node(&mut last.children, node, target_level);
} else if target_level == last.level + 1 {
last.children.push(node);
} else {
siblings.push(node);
}
} else {
siblings.push(node);
}
}
fn parse_node_content(content: &str) -> (String, NodeType) {
if let Some(pos) = content.find("((") {
if content.ends_with("))") && content.len() > pos + 4 {
let inner = &content[pos + 2..content.len() - 2];
if !inner.is_empty() {
return (inner.to_string(), NodeType::Circle);
}
}
}
if let Some(pos) = content.find("{{") {
if content.ends_with("}}") && content.len() > pos + 4 {
let inner = &content[pos + 2..content.len() - 2];
if !inner.is_empty() {
return (inner.to_string(), NodeType::Hexagon);
}
}
}
if let Some(pos) = content.find("))") {
if content.ends_with("((") && content.len() > pos + 4 {
let inner = &content[pos + 2..content.len() - 2];
if !inner.is_empty() {
return (inner.to_string(), NodeType::Bang);
}
}
}
if let Some(pos) = content.find('[') {
if content.ends_with(']') && content.len() > pos + 2 {
let inner = &content[pos + 1..content.len() - 1];
if !inner.is_empty() {
return (inner.to_string(), NodeType::Rect);
}
}
}
if let Some(pos) = find_shape_open_paren(content) {
if content.ends_with(')') && !content.ends_with("))") && content.len() > pos + 2 {
let inner = &content[pos + 1..content.len() - 1];
if !inner.is_empty() {
return (inner.to_string(), NodeType::RoundedRect);
}
}
}
if let Some(pos) = content.find(')') {
if content.ends_with('(') && !content.ends_with("((") && content.len() > pos + 2 {
let inner = &content[pos + 1..content.len() - 1];
if !inner.is_empty() {
return (inner.to_string(), NodeType::Cloud);
}
}
}
(content.to_string(), NodeType::Default)
}
fn find_shape_open_paren(content: &str) -> Option<usize> {
let bytes = content.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b'(' {
let next = bytes.get(i + 1).copied();
if next != Some(b'(') {
return Some(i);
}
}
}
None
}
fn assign_sections(node: &mut MindmapNode, section: Option<usize>) {
const MAX_SECTIONS: usize = 12;
if node.level == 0 {
node.section = None;
} else {
node.section = section;
}
let n_children = node.children.len();
for i in 0..n_children {
let child_section = if node.level == 0 {
Some(i % (MAX_SECTIONS - 1))
} else {
section
};
assign_sections(&mut node.children[i], child_section);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_mindmap() {
let input = "mindmap\n root((Root))\n Topic A\n Sub A1\n Topic B";
let d = parse(input).diagram;
let root = d.root.as_ref().unwrap();
assert_eq!(root.descr, "Root");
assert_eq!(root.node_type, NodeType::Circle);
assert_eq!(root.children.len(), 2);
assert_eq!(root.children[0].descr, "Topic A");
assert_eq!(root.children[0].children.len(), 1);
assert_eq!(root.children[0].children[0].descr, "Sub A1");
assert_eq!(root.children[1].descr, "Topic B");
}
#[test]
fn node_shapes() {
let input = "mindmap\n root\n A[Rect]\n B(Rounded)\n C((Circle))\n D{{Hex}}";
let d = parse(input).diagram;
let root = d.root.as_ref().unwrap();
assert_eq!(root.children[0].node_type, NodeType::Rect);
assert_eq!(root.children[1].node_type, NodeType::RoundedRect);
assert_eq!(root.children[2].node_type, NodeType::Circle);
assert_eq!(root.children[3].node_type, NodeType::Hexagon);
}
#[test]
fn section_assignment() {
let input = "mindmap\n root((R))\n A\n B\n C";
let d = parse(input).diagram;
let root = d.root.as_ref().unwrap();
assert_eq!(root.section, None);
assert_eq!(root.children[0].section, Some(0));
assert_eq!(root.children[1].section, Some(1));
assert_eq!(root.children[2].section, Some(2));
}
}