Skip to main content

libvisio_rs/vsdx/
parser.rs

1//! .vsdx (ZIP + XML) parser.
2//!
3//! Parses the Open Packaging Convention ZIP file containing Visio XML.
4
5use 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
37/// Parse a complete .vsdx file from bytes.
38pub 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    // Parse theme colors
45    doc.theme_colors = theme::parse_theme(&mut zip);
46
47    // Parse media
48    doc.media = image::extract_media(&mut zip);
49
50    // Parse master shapes
51    doc.masters = parse_master_shapes(&mut zip);
52
53    // Parse stylesheets
54    doc.stylesheets = parse_stylesheets(&mut zip);
55
56    // Parse background page map
57    doc.background_map = parse_background_pages(&mut zip);
58
59    // Parse page names
60    let page_names = parse_page_names(&mut zip);
61
62    // Parse page dimensions from pages.xml
63    let all_dims = parse_all_page_dimensions(&mut zip);
64
65    // Get page files
66    let page_files = get_page_files(&mut zip);
67
68    // Parse master rels
69    let master_rels = parse_master_rels(&mut zip);
70
71    // Pre-parse all pages
72    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    // Build pages
105    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
357/// Parse master shapes from all master XML files.
358pub 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    // Parse masters.xml to map Master ID -> rel ID -> file
364    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                // Parse rels
370                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                        // Find Rel element
401                        let mut mapped = false;
402                        for child in node.children() {
403                            if child.tag_name().name() == "Rel" {
404                                // Try various attribute patterns for r:id
405                                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    // Parse all master files
426    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                            // Only top-level shapes (parent is not another Shape)
457                            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; // Skip sub-shapes, they're parsed recursively
468                            }
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    // Re-key by Master ID
482    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    // Add unmapped files
489    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
499/// Parse shapes from a page XML document.
500pub fn parse_page_shapes(doc: &roxmltree::Document) -> Vec<Shape> {
501    let mut shapes = Vec::new();
502    for node in doc.root().children() {
503        // Find the root element
504        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
517/// Parse Connect elements from a page XML.
518pub 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
533/// Parse layer definitions from a page.
534pub 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(&section, "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
560/// Parse a single <Shape> element into a Shape struct.
561pub 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    // Parse top-level cells
574    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    // Parse sections
584    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    // Parse Text element
741    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    // Parse sub-shapes (for groups)
749    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    // Parse ForeignData
758    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        // Look for Rel element
767        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    // Section-level cells
800    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}