dioxus_mind_elixir/
text.rs1use crate::{MindMapData, MindMapError, Node};
3
4fn invalid(line: usize, message: &str) -> MindMapError {
5 MindMapError::InvalidData {
6 path: format!("line {line}"),
7 message: message.into(),
8 }
9}
10pub fn from_plaintext(input: &str) -> Result<MindMapData, MindMapError> {
14 let mut lines = Vec::<(usize, Node)>::new();
15 for (number, line) in input.lines().enumerate() {
16 if line.trim().is_empty() {
17 continue;
18 }
19 let spaces = line.bytes().take_while(|b| *b == b' ').count();
20 if spaces % 2 != 0 {
21 return Err(invalid(number + 1, "Indentation must use pairs of spaces"));
22 }
23 let content = line
24 .get(spaces..)
25 .and_then(|s| s.strip_prefix("- "))
26 .ok_or_else(|| invalid(number + 1, "Expected - Topic"))?;
27 if content.starts_with(['>', '}']) || content.contains("[^") || content.contains(" {\"") {
28 return Err(invalid(
29 number + 1,
30 "References, arrows, summaries and style suffixes are unsupported",
31 ));
32 }
33 let mut topic = String::new();
34 let mut chars = content.chars();
35 while let Some(c) = chars.next() {
36 if c == '\\' {
37 match chars.next() {
38 Some('n') => topic.push('\n'),
39 Some('\\') => topic.push('\\'),
40 _ => return Err(invalid(number + 1, "Unknown or incomplete escape")),
41 }
42 } else {
43 topic.push(c);
44 }
45 }
46 let depth = spaces / 2;
47 if depth > 128
48 || (lines.is_empty() && depth != 0)
49 || lines
50 .last()
51 .is_some_and(|(previous, _)| depth > previous + 1)
52 {
53 return Err(invalid(number + 1, "Invalid indentation depth"));
54 }
55 lines.push((depth, Node::new(format!("text-{}", number + 1), topic)));
56 }
57 if lines.is_empty() {
58 return Err(invalid(1, "No nodes"));
59 }
60 let mut stack = Vec::<(usize, Node)>::new();
61 let mut roots = Vec::new();
62 for (depth, node) in lines {
63 while stack.last().is_some_and(|(d, _)| *d >= depth) {
64 attach(&mut stack, &mut roots);
65 }
66 stack.push((depth, node));
67 }
68 while !stack.is_empty() {
69 attach(&mut stack, &mut roots);
70 }
71 let root = if roots.len() == 1 {
72 roots.pop().ok_or_else(|| invalid(1, "No root"))?
73 } else {
74 Node::new("text-root", "Root").with_children(roots)
75 };
76 let document = MindMapData::new(root);
77 document.validate()?;
78 Ok(document)
79}
80fn attach(stack: &mut Vec<(usize, Node)>, roots: &mut Vec<Node>) {
81 if let Some((_, node)) = stack.pop() {
82 if let Some((_, parent)) = stack.last_mut() {
83 parent.children.push(node);
84 } else {
85 roots.push(node);
86 }
87 }
88}
89pub fn to_plaintext(data: &MindMapData) -> Result<String, MindMapError> {
92 data.validate()?;
93 if data.meta.is_some() {
94 return Err(MindMapError::InvalidOperation(
95 "Plaintext cannot preserve document metadata; use JSON".into(),
96 ));
97 }
98 let mut result = String::new();
99 let mut pending = vec![(&data.node_data, 0)];
100 while let Some((node, depth)) = pending.pop() {
101 if node.style.is_some()
102 || node.note.is_some()
103 || node.image.is_some()
104 || node.metadata.is_some()
105 || node.branch_color.is_some()
106 {
107 return Err(MindMapError::InvalidOperation(
108 "Plaintext cannot preserve style, note, image or metadata; use JSON".into(),
109 ));
110 }
111 let topic = node.topic.replace('\\', "\\\\").replace('\n', "\\n");
112 if topic.starts_with(['>', '}']) || topic.contains("[^") || topic.contains(" {\"") {
113 return Err(MindMapError::InvalidOperation(
114 "Label conflicts with reserved plaintext syntax".into(),
115 ));
116 }
117 result.push_str(&format!("{}- {topic}\n", " ".repeat(depth)));
118 pending.extend(node.children.iter().rev().map(|child| (child, depth + 1)));
119 }
120 Ok(result)
121}