Skip to main content

dioxus_mind_elixir/
types.rs

1use std::{collections::HashSet, error::Error, fmt};
2
3use serde::{Deserialize, Serialize};
4
5/// Document layout; numeric values match Mind Elixir JSON.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(try_from = "u8", into = "u8")]
8pub enum LayoutDirection {
9    /// All branches left of the root.
10    Left,
11    /// All branches right of the root.
12    #[default]
13    Right,
14    /// Main branches distributed between both sides.
15    Side,
16    /// Root above the branches.
17    Down,
18}
19impl TryFrom<u8> for LayoutDirection {
20    type Error = String;
21    fn try_from(value: u8) -> Result<Self, Self::Error> {
22        match value {
23            0 => Ok(Self::Left),
24            1 => Ok(Self::Right),
25            2 => Ok(Self::Side),
26            3 => Ok(Self::Down),
27            _ => Err(format!("Invalid layout direction: {value}")),
28        }
29    }
30}
31impl From<LayoutDirection> for u8 {
32    fn from(value: LayoutDirection) -> Self {
33        match value {
34            LayoutDirection::Left => 0,
35            LayoutDirection::Right => 1,
36            LayoutDirection::Side => 2,
37            LayoutDirection::Down => 3,
38        }
39    }
40}
41
42/// Per-main-branch side, independent of the document layout.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(try_from = "u8", into = "u8")]
45pub enum NodeDirection {
46    /// Left side.
47    Left,
48    /// Right side.
49    Right,
50}
51impl TryFrom<u8> for NodeDirection {
52    type Error = String;
53    fn try_from(value: u8) -> Result<Self, Self::Error> {
54        match value {
55            0 => Ok(Self::Left),
56            1 => Ok(Self::Right),
57            _ => Err(format!("Invalid node direction: {value}")),
58        }
59    }
60}
61impl From<NodeDirection> for u8 {
62    fn from(value: NodeDirection) -> Self {
63        match value {
64            NodeDirection::Left => 0,
65            NodeDirection::Right => 1,
66        }
67    }
68}
69
70/// Supported plain-text styles, mapped to upstream field names.
71#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct NodeStyle {
74    /// CSS font size.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub font_size: Option<String>,
77    /// CSS font family.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub font_family: Option<String>,
80    /// Text color.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub color: Option<String>,
83    /// Background.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub background: Option<String>,
86    /// CSS font weight.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub font_weight: Option<String>,
89    /// Node width.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub width: Option<String>,
92    /// Border shorthand.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub border: Option<String>,
95    /// Text decoration.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub text_decoration: Option<String>,
98}
99
100fn present_json<'de, D: serde::Deserializer<'de>>(
101    deserializer: D,
102) -> Result<Option<serde_json::Value>, D::Error> {
103    serde_json::Value::deserialize(deserializer).map(Some)
104}
105
106fn expanded_default() -> bool {
107    true
108}
109
110/// A plain-text node in the persistent nested tree.
111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase", deny_unknown_fields)]
113pub struct Node {
114    /// Stable ID, unique within this document.
115    pub id: String,
116    /// Plain-text label, including optional line breaks.
117    pub topic: String,
118    /// Ordered children.
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub children: Vec<Node>,
121    /// Whether children are visible; missing means expanded.
122    #[serde(default = "expanded_default")]
123    pub expanded: bool,
124    /// Side assigned to a main branch.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub direction: Option<NodeDirection>,
127    /// Explicit per-node appearance.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub style: Option<NodeStyle>,
130    /// Explicit branch stroke, inherited by descendants.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub branch_color: Option<String>,
133    /// Preserved plain-text note; not rendered.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub note: Option<String>,
136    /// Opaque JSON metadata, including explicit null, preserved without interpretation.
137    #[serde(
138        default,
139        deserialize_with = "present_json",
140        skip_serializing_if = "Option::is_none"
141    )]
142    pub metadata: Option<serde_json::Value>,
143}
144impl Node {
145    /// Construct a leaf.
146    pub fn new(id: impl Into<String>, topic: impl Into<String>) -> Self {
147        Self {
148            id: id.into(),
149            topic: topic.into(),
150            children: Vec::new(),
151            expanded: true,
152            direction: None,
153            style: None,
154            branch_color: None,
155            note: None,
156            metadata: None,
157        }
158    }
159    /// Set ordered children.
160    pub fn with_children(mut self, children: Vec<Node>) -> Self {
161        self.children = children;
162        self
163    }
164    /// Find a node, including collapsed descendants.
165    pub fn find(&self, id: &str) -> Option<&Node> {
166        let mut pending = vec![self];
167        while let Some(node) = pending.pop() {
168            if node.id == id {
169                return Some(node);
170            }
171            pending.extend(node.children.iter().rev());
172        }
173        None
174    }
175    /// Validate this subtree before cloning or inserting externally supplied nodes.
176    pub fn validate(&self) -> Result<(), MindMapError> {
177        let mut pending = vec![(self, 0usize)];
178        let mut ids = HashSet::new();
179        while let Some((node, depth)) = pending.pop() {
180            if depth > 128 {
181                return Err(MindMapError::TreeTooDeep);
182            }
183            if node.id.trim().is_empty() {
184                return Err(MindMapError::EmptyNodeId);
185            }
186            if !ids.insert(node.id.as_str()) {
187                return Err(MindMapError::DuplicateNodeId(node.id.clone()));
188            }
189            pending.extend(node.children.iter().map(|child| (child, depth + 1)));
190        }
191        Ok(())
192    }
193    pub(crate) fn find_mut(&mut self, id: &str) -> Option<&mut Node> {
194        if self.id == id {
195            return Some(self);
196        }
197        self.children
198            .iter_mut()
199            .find_map(|child| child.find_mut(id))
200    }
201}
202
203/// Nested persistent document. Runtime selection, viewports and history are excluded.
204#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase", deny_unknown_fields)]
206pub struct MindMapData {
207    /// Root of the document.
208    pub node_data: Node,
209    /// Layout with upstream numeric encoding.
210    #[serde(default)]
211    pub direction: LayoutDirection,
212    /// Compact spacing (still overridable by host CSS variables).
213    #[serde(default)]
214    pub compact: bool,
215    /// Opaque document metadata, including explicit null.
216    #[serde(
217        default,
218        deserialize_with = "present_json",
219        skip_serializing_if = "Option::is_none"
220    )]
221    pub meta: Option<serde_json::Value>,
222}
223impl MindMapData {
224    /// Construct a right-facing document.
225    pub fn new(root: Node) -> Self {
226        Self {
227            node_data: root,
228            direction: LayoutDirection::Right,
229            compact: false,
230            meta: None,
231        }
232    }
233    /// Reject empty/duplicate IDs and trees deeper than 128 levels.
234    pub fn validate(&self) -> Result<(), MindMapError> {
235        self.node_data.validate()
236    }
237
238    /// Strict JSON import. Unknown fields (including rich content) are errors.
239    pub fn from_json(json: &str) -> Result<Self, MindMapError> {
240        let mut deserializer = serde_json::Deserializer::from_str(json);
241        let document: Self =
242            serde_path_to_error::deserialize(&mut deserializer).map_err(|error| {
243                MindMapError::InvalidData {
244                    path: error.path().to_string(),
245                    message: error.inner().to_string(),
246                }
247            })?;
248        deserializer
249            .end()
250            .map_err(|error| MindMapError::InvalidData {
251                path: String::new(),
252                message: error.to_string(),
253            })?;
254        document.validate()?;
255        Ok(document)
256    }
257    /// Validate and export all supported persistent fields.
258    pub fn to_json(&self) -> Result<String, MindMapError> {
259        self.validate()?;
260        serde_json::to_string_pretty(self).map_err(|error| MindMapError::InvalidData {
261            path: String::new(),
262            message: error.to_string(),
263        })
264    }
265}
266
267/// A recoverable document, operation or renderer failure.
268#[derive(Clone, Debug, PartialEq, Eq)]
269#[non_exhaustive]
270pub enum MindMapError {
271    /// A node ID was empty.
272    EmptyNodeId,
273    /// Duplicate ID in one document.
274    DuplicateNodeId(String),
275    /// Recursive rendering depth limit exceeded.
276    TreeTooDeep,
277    /// Unknown/invalid JSON field, including its path.
278    InvalidData {
279        /// Field path.
280        path: String,
281        /// Parse diagnostic.
282        message: String,
283    },
284    /// Requested node does not exist.
285    NodeNotFound(String),
286    /// Operation would remove or relocate the root.
287    RootOperation,
288    /// Destination is inside the moved subtree.
289    AncestorMove,
290    /// Mutation is disabled.
291    ReadOnly,
292    /// Before hook vetoed the operation.
293    Rejected,
294    /// State changed while a callback was running.
295    StaleOperation,
296    /// History has no entry in this direction.
297    NoHistory,
298    /// Supplied input is invalid.
299    InvalidOperation(String),
300    /// Mounted node measurement failed.
301    Measurement(String),
302    /// Non-finite or empty geometry.
303    InvalidGeometry,
304}
305impl fmt::Display for MindMapError {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        match self {
308            Self::EmptyNodeId => f.write_str("Node IDs must not be empty"),
309            Self::DuplicateNodeId(id) => write!(f, "Duplicate node ID: {id}"),
310            Self::TreeTooDeep => f.write_str("At most 128 nested levels are supported"),
311            Self::InvalidData { path, message } => {
312                write!(f, "Invalid document at {path}: {message}")
313            }
314            Self::NodeNotFound(id) => write!(f, "Node not found: {id}"),
315            Self::RootOperation => f.write_str("The root cannot be removed or relocated"),
316            Self::AncestorMove => f.write_str("Cannot move a node into its own subtree"),
317            Self::ReadOnly => f.write_str("The document is read-only"),
318            Self::Rejected => f.write_str("The operation was rejected by the before hook"),
319            Self::StaleOperation => f.write_str("The document changed during the operation"),
320            Self::NoHistory => f.write_str("No history entry is available"),
321            Self::InvalidOperation(message) => write!(f, "Invalid operation: {message}"),
322            Self::Measurement(message) => write!(f, "Cannot measure the mind map: {message}"),
323            Self::InvalidGeometry => f.write_str("The renderer returned invalid geometry"),
324        }
325    }
326}
327impl Error for MindMapError {}