dioxus_mind_elixir/plugins/
export.rs1use crate::{MindMapData, MindMapError, Node};
3use serde_json::{Value, json};
4use std::io::{Cursor, Write};
5
6fn failure(error: impl std::fmt::Display) -> MindMapError {
7 MindMapError::InvalidOperation(format!("Export failed: {error}"))
8}
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ExportFile {
12 pub name: String,
14 pub mime: String,
16 pub bytes: Vec<u8>,
18}
19impl ExportFile {
20 pub fn save_with(
22 &self,
23 sink: impl FnOnce(&Self) -> Result<(), MindMapError>,
24 ) -> Result<(), MindMapError> {
25 sink(self)
26 }
27 #[cfg(all(feature = "web", target_arch = "wasm32"))]
29 pub fn download(&self) -> Result<(), MindMapError> {
30 web::download(self)
31 }
32}
33pub fn xmind(data: &MindMapData) -> Result<ExportFile, MindMapError> {
37 data.validate()?;
38 fn topic(node: &Node, root: bool, direction: crate::LayoutDirection) -> Value {
39 let mut result = json!({"id":node.id,"class":"topic","title":node.topic});
40 if !node.children.is_empty() {
41 result["children"] = json!({"attached":node.children.iter().map(|n|topic(n,false,direction)).collect::<Vec<_>>()});
42 }
43 if !node.expanded {
44 result["branch"] = json!("folded");
45 }
46 if let Some(note) = &node.note {
47 result["notes"] = json!({"plain":{"content":note}});
48 }
49 if root {
50 result["structureClass"] = json!(match direction {
51 crate::LayoutDirection::Down => "org.xmind.ui.logic.down",
52 crate::LayoutDirection::Left => "org.xmind.ui.logic.left",
53 crate::LayoutDirection::Right => "org.xmind.ui.logic.right",
54 crate::LayoutDirection::Side => "org.xmind.ui.map.unbalanced",
55 });
56 }
57 if let Some(style) = &node.style {
58 let mut properties = serde_json::Map::new();
59 for (name, value) in [
60 ("fo:font-size", &style.font_size),
61 ("fo:font-family", &style.font_family),
62 ("fo:font-weight", &style.font_weight),
63 ("fo:color", &style.color),
64 ("svg:fill", &style.background),
65 ] {
66 if let Some(value) = value {
67 properties.insert(name.into(), json!(value));
68 }
69 }
70 result["style"] = json!({"properties":properties});
71 }
72 result
73 }
74 let content = json!([{"id":"dme-sheet","class":"sheet","title":data.node_data.topic,"rootTopic":topic(&data.node_data,true,data.direction)}]);
75 let metadata = json!({"creator":{"name":"dioxus-mind-elixir","version":env!("CARGO_PKG_VERSION")},"activeSheetId":"dme-sheet"});
76 let manifest = json!({"file-entries":{"content.json":{},"metadata.json":{}}});
77 let mut archive = zip::ZipWriter::new(Cursor::new(Vec::new()));
78 for (name, value) in [
79 ("content.json", content),
80 ("metadata.json", metadata),
81 ("manifest.json", manifest),
82 ] {
83 archive
84 .start_file(
85 name,
86 zip::write::SimpleFileOptions::default()
87 .compression_method(zip::CompressionMethod::Stored),
88 )
89 .map_err(failure)?;
90 archive
91 .write_all(&serde_json::to_vec(&value).map_err(failure)?)
92 .map_err(failure)?;
93 }
94 Ok(ExportFile {
95 name: "mind-map.xmind".into(),
96 mime: "application/x-xmind".into(),
97 bytes: archive.finish().map_err(failure)?.into_inner(),
98 })
99}
100pub fn static_html(title: &str, svg: &ExportFile) -> Result<ExportFile, MindMapError> {
103 if svg.mime != "image/svg+xml" {
104 return Err(failure("Expected an SVG capture"));
105 }
106 let body = std::str::from_utf8(&svg.bytes).map_err(failure)?;
107 Ok(ExportFile{name:"mind-map.html".into(),mime:"text/html".into(),bytes:format!("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:;\"><title>{}</title><style>body{{margin:0;overflow:auto}}svg{{display:block;max-width:none}}</style></head><body>{body}</body></html>",escape(title)).into_bytes()})
108}
109pub(crate) fn escape(value: &str) -> String {
110 value
111 .replace('&', "&")
112 .replace('<', "<")
113 .replace('>', ">")
114 .replace('"', """)
115 .replace('\'', "'")
116}
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum ImageFormat {
120 Png,
122 Jpeg,
124 WebP,
126}
127#[derive(Clone, Debug, PartialEq)]
129pub struct ImageOptions {
130 pub scale: f64,
132 pub quality: f64,
134 pub background: String,
136}
137impl Default for ImageOptions {
138 fn default() -> Self {
139 Self {
140 scale: 1.0,
141 quality: 0.9,
142 background: "#ffffff".into(),
143 }
144 }
145}
146impl ImageOptions {
147 pub fn validate(&self) -> Result<(), MindMapError> {
149 if !self.scale.is_finite()
150 || !(0.1..=4.0).contains(&self.scale)
151 || !self.quality.is_finite()
152 || !(0.0..=1.0).contains(&self.quality)
153 || self.background.trim().is_empty()
154 {
155 return Err(failure("Invalid image scale, quality or background"));
156 }
157 Ok(())
158 }
159}
160#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
164pub async fn capture_svg(
165 _element: &dioxus::prelude::MountedData,
166 options: &ImageOptions,
167) -> Result<ExportFile, MindMapError> {
168 options.validate()?;
169 Err(failure(
170 "Live CSS capture requires the web feature and a Web renderer; use a host capture adapter on Desktop",
171 ))
172}
173#[cfg(all(feature = "web", target_arch = "wasm32"))]
174pub use web::{capture_svg, rasterize};
175#[cfg(all(feature = "web", target_arch = "wasm32"))]
176mod web;