1use std::collections::HashMap;
6use std::io::{Cursor, Read};
7
8use crate::error::{Result, VisioError};
9use crate::model::*;
10use crate::vsdx::image;
11use crate::vsdx::theme;
12
13const VNS: &str = "http://schemas.microsoft.com/office/visio/2012/main";
14const RNS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
15
16fn is_visio_tag(node: &roxmltree::Node, name: &str) -> bool {
17 node.tag_name().name() == name
18 && (node.tag_name().namespace().is_none() || node.tag_name().namespace() == Some(VNS))
19}
20
21fn find_child<'a>(
22 node: &'a roxmltree::Node<'a, 'a>,
23 name: &str,
24) -> Option<roxmltree::Node<'a, 'a>> {
25 node.children().find(|c| is_visio_tag(c, name))
26}
27
28fn cell_val(node: &roxmltree::Node, cell_name: &str) -> String {
29 for child in node.children() {
30 if is_visio_tag(&child, "Cell") && child.attribute("N") == Some(cell_name) {
31 return child.attribute("V").unwrap_or("").to_string();
32 }
33 }
34 String::new()
35}
36
37pub fn parse_vsdx(data: &[u8]) -> Result<Document> {
39 let cursor = Cursor::new(data);
40 let mut zip = zip::ZipArchive::new(cursor).map_err(VisioError::Zip)?;
41
42 let mut doc = Document::default();
43
44 doc.theme_colors = theme::parse_theme(&mut zip);
46
47 doc.media = image::extract_media(&mut zip);
49
50 doc.masters = parse_master_shapes(&mut zip);
52
53 doc.stylesheets = parse_stylesheets(&mut zip);
55
56 doc.background_map = parse_background_pages(&mut zip);
58
59 let page_names = parse_page_names(&mut zip);
61
62 let all_dims = parse_all_page_dimensions(&mut zip);
64
65 let page_files = get_page_files(&mut zip);
67
68 let master_rels = parse_master_rels(&mut zip);
70
71 let mut page_cache: HashMap<
73 usize,
74 (
75 Vec<Shape>,
76 Vec<Connect>,
77 HashMap<String, String>,
78 HashMap<String, LayerDef>,
79 ),
80 > = HashMap::new();
81
82 for (i, page_file) in page_files.iter().enumerate() {
83 let page_xml = match read_zip_file(&mut zip, page_file) {
84 Some(data) => data,
85 None => continue,
86 };
87 let xml_str = match std::str::from_utf8(&page_xml) {
88 Ok(s) => s,
89 Err(_) => continue,
90 };
91 let xml_doc = match roxmltree::Document::parse(xml_str) {
92 Ok(d) => d,
93 Err(_) => continue,
94 };
95
96 let shapes = parse_page_shapes(&xml_doc);
97 let connects = parse_connects(&xml_doc);
98 let layers = parse_layers(&xml_doc);
99 let page_rels = image::parse_rels(&mut zip, page_file);
100
101 page_cache.insert(i, (shapes, connects, page_rels, layers));
102 }
103
104 for (i, _page_file) in page_files.iter().enumerate() {
106 let (shapes, connects, page_rels, layers) = match page_cache.remove(&i) {
107 Some(data) => data,
108 None => continue,
109 };
110
111 if shapes.is_empty() {
112 continue;
113 }
114
115 let (page_w, page_h) = if i < all_dims.len() {
116 all_dims[i]
117 } else {
118 (8.5, 11.0)
119 };
120
121 let name = page_names
122 .get(i)
123 .cloned()
124 .unwrap_or_else(|| format!("Page {}", i + 1));
125
126 let mut all_rels = master_rels.clone();
127 all_rels.extend(page_rels);
128
129 let page = Page {
130 name,
131 index: i,
132 width: page_w,
133 height: page_h,
134 shapes,
135 connects,
136 layers,
137 background: false,
138 };
139
140 doc.pages.push(page);
141 }
142
143 Ok(doc)
144}
145
146fn read_zip_file(zip: &mut zip::ZipArchive<Cursor<&[u8]>>, name: &str) -> Option<Vec<u8>> {
147 let mut f = zip.by_name(name).ok()?;
148 let mut buf = Vec::new();
149 f.read_to_end(&mut buf).ok()?;
150 Some(buf)
151}
152
153fn get_page_files(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> Vec<String> {
154 let mut page_files: Vec<String> = (0..zip.len())
155 .filter_map(|i| {
156 let f = zip.by_index(i).ok()?;
157 let name = f.name().to_string();
158 if name.starts_with("visio/pages/page")
159 && name.ends_with(".xml")
160 && !name.ends_with("pages.xml")
161 {
162 Some(name)
163 } else {
164 None
165 }
166 })
167 .collect();
168 page_files.sort();
169 page_files
170}
171
172fn parse_page_names(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> Vec<String> {
173 let mut names = Vec::new();
174 let data = match read_zip_file(zip, "visio/pages/pages.xml") {
175 Some(d) => d,
176 None => return names,
177 };
178 let xml_str = match std::str::from_utf8(&data) {
179 Ok(s) => s,
180 Err(_) => return names,
181 };
182 let doc = match roxmltree::Document::parse(xml_str) {
183 Ok(d) => d,
184 Err(_) => return names,
185 };
186 for node in doc.descendants() {
187 if is_visio_tag(&node, "Page") {
188 names.push(node.attribute("Name").unwrap_or("").to_string());
189 }
190 }
191 names
192}
193
194fn parse_all_page_dimensions(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> Vec<(f64, f64)> {
195 let mut dims = Vec::new();
196 let data = match read_zip_file(zip, "visio/pages/pages.xml") {
197 Some(d) => d,
198 None => return dims,
199 };
200 let xml_str = match std::str::from_utf8(&data) {
201 Ok(s) => s,
202 Err(_) => return dims,
203 };
204 let doc = match roxmltree::Document::parse(xml_str) {
205 Ok(d) => d,
206 Err(_) => return dims,
207 };
208 for node in doc.descendants() {
209 if is_visio_tag(&node, "Page") {
210 let mut pw = 8.5;
211 let mut ph = 11.0;
212 if let Some(ps) = find_child(&node, "PageSheet") {
213 for cell in ps.children() {
214 if is_visio_tag(&cell, "Cell") {
215 match cell.attribute("N") {
216 Some("PageWidth") => {
217 pw = cell
218 .attribute("V")
219 .and_then(|v| v.parse().ok())
220 .unwrap_or(8.5);
221 }
222 Some("PageHeight") => {
223 ph = cell
224 .attribute("V")
225 .and_then(|v| v.parse().ok())
226 .unwrap_or(11.0);
227 }
228 _ => {}
229 }
230 }
231 }
232 }
233 dims.push((pw, ph));
234 }
235 }
236 dims
237}
238
239fn parse_background_pages(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> HashMap<usize, usize> {
240 let mut bg_map = HashMap::new();
241 let data = match read_zip_file(zip, "visio/pages/pages.xml") {
242 Some(d) => d,
243 None => return bg_map,
244 };
245 let xml_str = match std::str::from_utf8(&data) {
246 Ok(s) => s,
247 Err(_) => return bg_map,
248 };
249 let doc = match roxmltree::Document::parse(xml_str) {
250 Ok(d) => d,
251 Err(_) => return bg_map,
252 };
253
254 let mut page_id_to_idx: HashMap<String, usize> = HashMap::new();
255 let mut pages_data: Vec<(usize, roxmltree::Node)> = Vec::new();
256
257 for (i, node) in doc
258 .descendants()
259 .filter(|n| is_visio_tag(n, "Page"))
260 .enumerate()
261 {
262 if let Some(pid) = node.attribute("ID") {
263 page_id_to_idx.insert(pid.to_string(), i);
264 }
265 pages_data.push((i, node));
266 }
267
268 for (i, node) in &pages_data {
269 if let Some(ps) = find_child(node, "PageSheet") {
270 for cell in ps.children() {
271 if is_visio_tag(&cell, "Cell") && cell.attribute("N") == Some("BackPage") {
272 if let Some(back_id) = cell.attribute("V") {
273 if let Some(&bg_idx) = page_id_to_idx.get(back_id) {
274 bg_map.insert(*i, bg_idx);
275 }
276 }
277 }
278 }
279 }
280 }
281 bg_map
282}
283
284fn parse_stylesheets(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> HashMap<String, StyleSheet> {
285 let mut styles = HashMap::new();
286 let data = match read_zip_file(zip, "visio/document.xml") {
287 Some(d) => d,
288 None => return styles,
289 };
290 let xml_str = match std::str::from_utf8(&data) {
291 Ok(s) => s,
292 Err(_) => return styles,
293 };
294 let doc = match roxmltree::Document::parse(xml_str) {
295 Ok(d) => d,
296 Err(_) => return styles,
297 };
298
299 for node in doc.descendants() {
300 if is_visio_tag(&node, "StyleSheet") {
301 let sid = node.attribute("ID").unwrap_or("").to_string();
302 if sid.is_empty() {
303 continue;
304 }
305 let mut ss = StyleSheet::default();
306 ss.line_style = node.attribute("LineStyle").unwrap_or("").to_string();
307 ss.fill_style = node.attribute("FillStyle").unwrap_or("").to_string();
308 ss.text_style = node.attribute("TextStyle").unwrap_or("").to_string();
309 for cell in node.children() {
310 if is_visio_tag(&cell, "Cell") {
311 let n = cell.attribute("N").unwrap_or("");
312 let v = cell.attribute("V").unwrap_or("");
313 let f = cell.attribute("F").unwrap_or("");
314 ss.cells.insert(n.to_string(), CellValue::new(v, f));
315 }
316 }
317 styles.insert(sid, ss);
318 }
319 }
320 styles
321}
322
323fn parse_master_rels(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> HashMap<String, String> {
324 let mut rels = HashMap::new();
325 let names: Vec<String> = (0..zip.len())
326 .filter_map(|i| {
327 let f = zip.by_index(i).ok()?;
328 let n = f.name().to_string();
329 if n.starts_with("visio/masters/_rels/master") && n.ends_with(".xml.rels") {
330 Some(n)
331 } else {
332 None
333 }
334 })
335 .collect();
336
337 for name in names {
338 if let Some(data) = read_zip_file(zip, &name) {
339 if let Ok(xml_str) = std::str::from_utf8(&data) {
340 if let Ok(doc) = roxmltree::Document::parse(xml_str) {
341 for node in doc.descendants() {
342 if node.tag_name().name() == "Relationship" {
343 let rid = node.attribute("Id").unwrap_or("");
344 let target = node.attribute("Target").unwrap_or("");
345 if !rid.is_empty() && !target.is_empty() {
346 rels.insert(rid.to_string(), target.to_string());
347 }
348 }
349 }
350 }
351 }
352 }
353 }
354 rels
355}
356
357pub fn parse_master_shapes(
359 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
360) -> HashMap<String, HashMap<String, Shape>> {
361 let mut masters: HashMap<String, HashMap<String, Shape>> = HashMap::new();
362
363 let mut master_id_to_file: HashMap<String, String> = HashMap::new();
365
366 if let Some(data) = read_zip_file(zip, "visio/masters/masters.xml") {
367 if let Ok(xml_str) = std::str::from_utf8(&data) {
368 if let Ok(doc) = roxmltree::Document::parse(xml_str) {
369 let mut rid_to_file: HashMap<String, String> = HashMap::new();
371 if let Some(rels_data) = read_zip_file(zip, "visio/masters/_rels/masters.xml.rels")
372 {
373 if let Ok(rels_str) = std::str::from_utf8(&rels_data) {
374 if let Ok(rels_doc) = roxmltree::Document::parse(rels_str) {
375 for node in rels_doc.descendants() {
376 if node.tag_name().name() == "Relationship" {
377 let rid = node.attribute("Id").unwrap_or("");
378 let target = node.attribute("Target").unwrap_or("");
379 let fname = std::path::Path::new(target)
380 .file_stem()
381 .and_then(|s| s.to_str())
382 .unwrap_or("")
383 .replace("master", "");
384 if !rid.is_empty() {
385 rid_to_file.insert(rid.to_string(), fname);
386 }
387 }
388 }
389 }
390 }
391 }
392
393 for node in doc.descendants() {
394 if is_visio_tag(&node, "Master") {
395 let mid = node.attribute("ID").unwrap_or("").to_string();
396 if mid.is_empty() {
397 continue;
398 }
399
400 let mut mapped = false;
402 for child in node.children() {
403 if child.tag_name().name() == "Rel" {
404 let rid = child
406 .attribute((RNS, "id"))
407 .or_else(|| child.attribute("id"))
408 .unwrap_or("");
409 if let Some(fnum) = rid_to_file.get(rid) {
410 master_id_to_file.insert(mid.clone(), fnum.clone());
411 mapped = true;
412 }
413 break;
414 }
415 }
416 if !mapped {
417 master_id_to_file.insert(mid.clone(), mid.clone());
418 }
419 }
420 }
421 }
422 }
423 }
424
425 let master_file_names: Vec<String> = (0..zip.len())
427 .filter_map(|i| {
428 let f = zip.by_index(i).ok()?;
429 let n = f.name().to_string();
430 if n.starts_with("visio/masters/master")
431 && n.ends_with(".xml")
432 && !n.contains("masters.xml")
433 {
434 Some(n)
435 } else {
436 None
437 }
438 })
439 .collect();
440
441 let mut file_to_shapes: HashMap<String, HashMap<String, Shape>> = HashMap::new();
442
443 for name in master_file_names {
444 let master_num = std::path::Path::new(&name)
445 .file_stem()
446 .and_then(|s| s.to_str())
447 .unwrap_or("")
448 .replace("master", "");
449
450 if let Some(data) = read_zip_file(zip, &name) {
451 if let Ok(xml_str) = std::str::from_utf8(&data) {
452 if let Ok(doc) = roxmltree::Document::parse(xml_str) {
453 let mut shapes_data = HashMap::new();
454 for node in doc.descendants() {
455 if is_visio_tag(&node, "Shape") {
456 let _parent_is_shape = node
458 .parent()
459 .map(|p| is_visio_tag(&p, "Shape") || is_visio_tag(&p, "Shapes"))
460 .unwrap_or(false);
461 let parent_is_shapes_in_shape = node
462 .parent()
463 .and_then(|p| p.parent())
464 .map(|gp| is_visio_tag(&gp, "Shape"))
465 .unwrap_or(false);
466 if parent_is_shapes_in_shape {
467 continue; }
469 let sd = parse_single_shape(&node);
470 shapes_data.insert(sd.id.clone(), sd);
471 }
472 }
473 if !shapes_data.is_empty() {
474 file_to_shapes.insert(master_num, shapes_data);
475 }
476 }
477 }
478 }
479 }
480
481 for (mid, fnum) in &master_id_to_file {
483 if let Some(shapes) = file_to_shapes.get(fnum) {
484 masters.insert(mid.clone(), shapes.clone());
485 }
486 }
487
488 let mapped_files: std::collections::HashSet<&String> = master_id_to_file.values().collect();
490 for (fnum, shapes) in &file_to_shapes {
491 if !mapped_files.contains(fnum) {
492 masters.insert(fnum.clone(), shapes.clone());
493 }
494 }
495
496 masters
497}
498
499pub fn parse_page_shapes(doc: &roxmltree::Document) -> Vec<Shape> {
501 let mut shapes = Vec::new();
502 for node in doc.root().children() {
503 for child in node.children() {
505 if is_visio_tag(&child, "Shapes") {
506 for shape_node in child.children() {
507 if is_visio_tag(&shape_node, "Shape") {
508 shapes.push(parse_single_shape(&shape_node));
509 }
510 }
511 }
512 }
513 }
514 shapes
515}
516
517pub fn parse_connects(doc: &roxmltree::Document) -> Vec<Connect> {
519 let mut connects = Vec::new();
520 for node in doc.descendants() {
521 if is_visio_tag(&node, "Connect") {
522 connects.push(Connect {
523 from_sheet: node.attribute("FromSheet").unwrap_or("").to_string(),
524 from_cell: node.attribute("FromCell").unwrap_or("").to_string(),
525 to_sheet: node.attribute("ToSheet").unwrap_or("").to_string(),
526 to_cell: node.attribute("ToCell").unwrap_or("").to_string(),
527 });
528 }
529 }
530 connects
531}
532
533pub fn parse_layers(doc: &roxmltree::Document) -> HashMap<String, LayerDef> {
535 let mut layers = HashMap::new();
536 for node in doc.descendants() {
537 if is_visio_tag(&node, "PageSheet") {
538 for section in node.children() {
539 if is_visio_tag(§ion, "Section") && section.attribute("N") == Some("Layer") {
540 for row in section.children() {
541 if is_visio_tag(&row, "Row") {
542 let ix = row.attribute("IX").unwrap_or("").to_string();
543 let visible = cell_val(&row, "Visible") != "0";
544 let name = cell_val(&row, "Name");
545 let name = if name.is_empty() {
546 format!("Layer {}", ix)
547 } else {
548 name
549 };
550 layers.insert(ix, LayerDef { name, visible });
551 }
552 }
553 }
554 }
555 }
556 }
557 layers
558}
559
560pub fn parse_single_shape(node: &roxmltree::Node) -> Shape {
562 let mut shape = Shape::default();
563 shape.id = node.attribute("ID").unwrap_or("").to_string();
564 shape.name = node.attribute("Name").unwrap_or("").to_string();
565 shape.name_u = node.attribute("NameU").unwrap_or("").to_string();
566 shape.shape_type = node.attribute("Type").unwrap_or("Shape").to_string();
567 shape.master = node.attribute("Master").unwrap_or("").to_string();
568 shape.master_shape = node.attribute("MasterShape").unwrap_or("").to_string();
569 shape.line_style = node.attribute("LineStyle").unwrap_or("").to_string();
570 shape.fill_style = node.attribute("FillStyle").unwrap_or("").to_string();
571 shape.text_style = node.attribute("TextStyle").unwrap_or("").to_string();
572
573 for child in node.children() {
575 if is_visio_tag(&child, "Cell") {
576 let n = child.attribute("N").unwrap_or("");
577 let v = child.attribute("V").unwrap_or("");
578 let f = child.attribute("F").unwrap_or("");
579 shape.cells.insert(n.to_string(), CellValue::new(v, f));
580 }
581 }
582
583 for child in node.children() {
585 if is_visio_tag(&child, "Section") {
586 let sec_name = child.attribute("N").unwrap_or("");
587 match sec_name {
588 "Geometry" => {
589 let geo = parse_geometry_section(&child);
590 shape.geometry.push(geo);
591 }
592 "Character" => {
593 for row in child.children() {
594 if is_visio_tag(&row, "Row") {
595 let ix = row.attribute("IX").unwrap_or("0").to_string();
596 let mut fmt = CharFormat::default();
597 for cell in row.children() {
598 if is_visio_tag(&cell, "Cell") {
599 let n = cell.attribute("N").unwrap_or("");
600 let v = cell.attribute("V").unwrap_or("").to_string();
601 match n {
602 "Size" => fmt.size = v,
603 "Color" => fmt.color = v,
604 "Style" => fmt.style = v,
605 "Font" => fmt.font = v,
606 _ => {}
607 }
608 }
609 }
610 shape.char_formats.insert(ix, fmt);
611 }
612 }
613 }
614 "Paragraph" => {
615 for row in child.children() {
616 if is_visio_tag(&row, "Row") {
617 let ix = row.attribute("IX").unwrap_or("0").to_string();
618 let mut fmt = ParaFormat::default();
619 for cell in row.children() {
620 if is_visio_tag(&cell, "Cell") {
621 let n = cell.attribute("N").unwrap_or("");
622 let v = cell.attribute("V").unwrap_or("").to_string();
623 match n {
624 "HorzAlign" => fmt.horiz_align = v,
625 "IndFirst" => fmt.indent_first = v,
626 "IndLeft" => fmt.indent_left = v,
627 "IndRight" => fmt.indent_right = v,
628 "Bullet" => fmt.bullet = v,
629 "BulletStr" => fmt.bullet_str = v,
630 "SpLine" => fmt.sp_line = v,
631 "SpBefore" => fmt.sp_before = v,
632 "SpAfter" => fmt.sp_after = v,
633 _ => {}
634 }
635 }
636 }
637 shape.para_formats.insert(ix, fmt);
638 }
639 }
640 }
641 "Controls" => {
642 for row in child.children() {
643 if is_visio_tag(&row, "Row") {
644 let row_ix = format!("Row_{}", row.attribute("IX").unwrap_or("0"));
645 let mut ctrl = HashMap::new();
646 for cell in row.children() {
647 if is_visio_tag(&cell, "Cell") {
648 ctrl.insert(
649 cell.attribute("N").unwrap_or("").to_string(),
650 cell.attribute("V").unwrap_or("").to_string(),
651 );
652 }
653 }
654 shape.controls.insert(row_ix, ctrl);
655 }
656 }
657 }
658 "Connection" => {
659 for row in child.children() {
660 if is_visio_tag(&row, "Row") {
661 let ix = row.attribute("IX").unwrap_or("0").to_string();
662 let mut conn = HashMap::new();
663 for cell in row.children() {
664 if is_visio_tag(&cell, "Cell") {
665 conn.insert(
666 cell.attribute("N").unwrap_or("").to_string(),
667 CellValue::new(
668 cell.attribute("V").unwrap_or(""),
669 cell.attribute("F").unwrap_or(""),
670 ),
671 );
672 }
673 }
674 shape.connections.insert(ix, conn);
675 }
676 }
677 }
678 "User" => {
679 for row in child.children() {
680 if is_visio_tag(&row, "Row") {
681 let row_name = row.attribute("N").unwrap_or("").to_string();
682 let mut user_vals = HashMap::new();
683 for cell in row.children() {
684 if is_visio_tag(&cell, "Cell") {
685 user_vals.insert(
686 cell.attribute("N").unwrap_or("").to_string(),
687 cell.attribute("V").unwrap_or("").to_string(),
688 );
689 }
690 }
691 shape.user.insert(row_name, user_vals);
692 }
693 }
694 }
695 "FillGradientDef" => {
696 let mut stops = Vec::new();
697 for row in child.children() {
698 if is_visio_tag(&row, "Row") {
699 let pos_str = cell_val(&row, "GradientStopPosition");
700 let color = cell_val(&row, "GradientStopColor");
701 let pos: f64 = pos_str.parse().unwrap_or(0.0) * 100.0;
702 if !color.is_empty() {
703 stops.push(GradientStop {
704 position: pos,
705 color,
706 });
707 }
708 }
709 }
710 if !stops.is_empty() {
711 shape.gradient_stops.push(stops);
712 }
713 }
714 "Hyperlink" => {
715 for row in child.children() {
716 if is_visio_tag(&row, "Row") {
717 let mut link = Hyperlink::default();
718 for cell in row.children() {
719 if is_visio_tag(&cell, "Cell") {
720 let n = cell.attribute("N").unwrap_or("");
721 let v = cell.attribute("V").unwrap_or("").to_string();
722 match n {
723 "Description" => link.description = v,
724 "Address" => link.address = v,
725 "SubAddress" => link.sub_address = v,
726 "Frame" => link.frame = v,
727 _ => {}
728 }
729 }
730 }
731 shape.hyperlinks.push(link);
732 }
733 }
734 }
735 _ => {}
736 }
737 }
738 }
739
740 if let Some(text_node) = find_child(node, "Text") {
742 shape.has_text_elem = true;
743 let text = collect_text(&text_node);
744 shape.text = text.trim().to_string();
745 shape.text_parts = parse_text_parts(&text_node);
746 }
747
748 if let Some(shapes_container) = find_child(node, "Shapes") {
750 for sub_node in shapes_container.children() {
751 if is_visio_tag(&sub_node, "Shape") {
752 shape.sub_shapes.push(parse_single_shape(&sub_node));
753 }
754 }
755 }
756
757 if let Some(fd_node) = find_child(node, "ForeignData") {
759 let mut fdi = ForeignDataInfo::default();
760 fdi.foreign_type = fd_node.attribute("ForeignType").unwrap_or("").to_string();
761 fdi.compression = fd_node
762 .attribute("CompressionType")
763 .unwrap_or("")
764 .to_string();
765
766 let mut found_rel = false;
768 for child in fd_node.children() {
769 if child.tag_name().name() == "Rel" {
770 let rid = child
771 .attribute((RNS, "id"))
772 .or_else(|| child.attribute("id"))
773 .unwrap_or("");
774 if !rid.is_empty() {
775 fdi.rel_id = Some(rid.to_string());
776 found_rel = true;
777 }
778 break;
779 }
780 }
781 if !found_rel {
782 if let Some(text) = fd_node.text() {
783 let trimmed = text.trim();
784 if !trimmed.is_empty() {
785 fdi.data = Some(trimmed.to_string());
786 }
787 }
788 }
789 shape.foreign_data = Some(fdi);
790 }
791
792 shape
793}
794
795fn parse_geometry_section(section: &roxmltree::Node) -> GeomSection {
796 let mut geo = GeomSection::default();
797 geo.ix = section.attribute("IX").unwrap_or("0").to_string();
798
799 for child in section.children() {
801 if is_visio_tag(&child, "Cell") {
802 match child.attribute("N") {
803 Some("NoFill") if child.attribute("V") == Some("1") => geo.no_fill = true,
804 Some("NoLine") if child.attribute("V") == Some("1") => geo.no_line = true,
805 Some("NoShow") if child.attribute("V") == Some("1") => geo.no_show = true,
806 _ => {}
807 }
808 }
809 }
810
811 for child in section.children() {
812 if is_visio_tag(&child, "Row") {
813 let row_type = child.attribute("T").unwrap_or("").to_string();
814 let row_ix = child.attribute("IX").unwrap_or("").to_string();
815 let mut cells = HashMap::new();
816 for cell in child.children() {
817 if is_visio_tag(&cell, "Cell") {
818 let n = cell.attribute("N").unwrap_or("");
819 let v = cell.attribute("V").unwrap_or("");
820 let f = cell.attribute("F").unwrap_or("");
821 cells.insert(n.to_string(), CellValue::new(v, f));
822 }
823 }
824 geo.rows.push(GeomRow {
825 row_type,
826 ix: row_ix,
827 cells,
828 });
829 }
830 }
831
832 geo
833}
834
835fn collect_text(node: &roxmltree::Node) -> String {
836 let mut text = String::new();
837 if let Some(t) = node.text() {
838 text.push_str(t);
839 }
840 for child in node.children() {
841 if child.is_element() && child.tag_name().name() == "fld" {
842 text.push_str(&collect_text(&child));
843 }
844 if let Some(tail) = child.tail() {
845 text.push_str(tail);
846 }
847 }
848 text
849}
850
851fn parse_text_parts(text_node: &roxmltree::Node) -> Vec<TextPart> {
852 let mut parts = Vec::new();
853 let mut current_cp = "0".to_string();
854 let mut current_pp = "0".to_string();
855
856 if let Some(text) = text_node.text() {
857 if !text.is_empty() {
858 parts.push(TextPart {
859 text: text.to_string(),
860 cp: current_cp.clone(),
861 pp: current_pp.clone(),
862 });
863 }
864 }
865
866 for child in text_node.children() {
867 if child.is_element() {
868 match child.tag_name().name() {
869 "cp" => {
870 current_cp = child.attribute("IX").unwrap_or("0").to_string();
871 }
872 "pp" => {
873 current_pp = child.attribute("IX").unwrap_or("0").to_string();
874 }
875 "fld" => {
876 let field_text = collect_text(&child);
877 let trimmed = field_text.trim();
878 if !trimmed.is_empty() {
879 parts.push(TextPart {
880 text: trimmed.to_string(),
881 cp: current_cp.clone(),
882 pp: current_pp.clone(),
883 });
884 }
885 }
886 _ => {}
887 }
888 }
889 if let Some(tail) = child.tail() {
890 if !tail.is_empty() {
891 parts.push(TextPart {
892 text: tail.to_string(),
893 cp: current_cp.clone(),
894 pp: current_pp.clone(),
895 });
896 }
897 }
898 }
899
900 parts
901}