Skip to main content

ppt_rs/parts/
relationships.rs

1//! Relationships handling for package parts
2//!
3//! Manages relationships between parts in a PPTX package.
4
5use crate::core::ToXml;
6use crate::exc::PptxError;
7use crate::oxml::XmlParser;
8
9/// Relationship types
10#[derive(Debug, Clone, PartialEq)]
11pub enum RelationshipType {
12    OfficeDocument,
13    Slide,
14    SlideLayout,
15    SlideMaster,
16    Theme,
17    Image,
18    Chart,
19    CoreProperties,
20    ExtendedProperties,
21    Custom(String),
22}
23
24impl RelationshipType {
25    /// Get the relationship type URI
26    pub fn uri(&self) -> &str {
27        match self {
28            RelationshipType::OfficeDocument => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
29            RelationshipType::Slide => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
30            RelationshipType::SlideLayout => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
31            RelationshipType::SlideMaster => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster",
32            RelationshipType::Theme => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
33            RelationshipType::Image => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
34            RelationshipType::Chart => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
35            RelationshipType::CoreProperties => "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
36            RelationshipType::ExtendedProperties => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
37            RelationshipType::Custom(uri) => uri,
38        }
39    }
40
41    /// Parse from URI string
42    pub fn from_uri(uri: &str) -> Self {
43        if uri.contains("/slide") && !uri.contains("Layout") && !uri.contains("Master") {
44            RelationshipType::Slide
45        } else if uri.contains("/slideLayout") {
46            RelationshipType::SlideLayout
47        } else if uri.contains("/slideMaster") {
48            RelationshipType::SlideMaster
49        } else if uri.contains("/theme") {
50            RelationshipType::Theme
51        } else if uri.contains("/image") {
52            RelationshipType::Image
53        } else if uri.contains("/chart") {
54            RelationshipType::Chart
55        } else if uri.contains("/officeDocument") {
56            RelationshipType::OfficeDocument
57        } else if uri.contains("core-properties") {
58            RelationshipType::CoreProperties
59        } else if uri.contains("extended-properties") {
60            RelationshipType::ExtendedProperties
61        } else {
62            RelationshipType::Custom(uri.to_string())
63        }
64    }
65}
66
67/// A single relationship
68#[derive(Debug, Clone)]
69pub struct Relationship {
70    pub id: String,
71    pub rel_type: RelationshipType,
72    pub target: String,
73}
74
75impl Relationship {
76    /// Create a new relationship
77    pub fn new(id: &str, rel_type: RelationshipType, target: &str) -> Self {
78        Relationship {
79            id: id.to_string(),
80            rel_type,
81            target: target.to_string(),
82        }
83    }
84
85    /// Generate XML for this relationship
86    pub fn to_xml(&self) -> String {
87        format!(
88            r#"<Relationship Id="{}" Type="{}" Target="{}"/>"#,
89            self.id,
90            self.rel_type.uri(),
91            self.target
92        )
93    }
94}
95
96impl ToXml for Relationship {
97    fn to_xml(&self) -> String {
98        Relationship::to_xml(self)
99    }
100}
101
102/// Collection of relationships
103#[derive(Debug, Clone, Default)]
104pub struct Relationships {
105    relationships: Vec<Relationship>,
106    next_id: u32,
107}
108
109impl Relationships {
110    /// Create new empty relationships
111    pub fn new() -> Self {
112        Relationships {
113            relationships: Vec::new(),
114            next_id: 1,
115        }
116    }
117
118    /// Add a relationship and return its ID
119    pub fn add(&mut self, rel_type: RelationshipType, target: &str) -> String {
120        let id = format!("rId{}", self.next_id);
121        self.next_id += 1;
122        self.relationships
123            .push(Relationship::new(&id, rel_type, target));
124        id
125    }
126
127    /// Add a relationship with specific ID
128    pub fn add_with_id(&mut self, id: &str, rel_type: RelationshipType, target: &str) {
129        self.relationships
130            .push(Relationship::new(id, rel_type, target));
131        // Update next_id if needed
132        if let Some(num) = id.strip_prefix("rId").and_then(|s| s.parse::<u32>().ok()) {
133            if num >= self.next_id {
134                self.next_id = num + 1;
135            }
136        }
137    }
138
139    /// Get relationship by ID
140    pub fn get(&self, id: &str) -> Option<&Relationship> {
141        self.relationships.iter().find(|r| r.id == id)
142    }
143
144    /// Get all relationships of a type
145    pub fn get_by_type(&self, rel_type: &RelationshipType) -> Vec<&Relationship> {
146        self.relationships
147            .iter()
148            .filter(|r| &r.rel_type == rel_type)
149            .collect()
150    }
151
152    /// Get all relationships
153    pub fn all(&self) -> &[Relationship] {
154        &self.relationships
155    }
156
157    /// Get count
158    pub fn len(&self) -> usize {
159        self.relationships.len()
160    }
161
162    /// Check if empty
163    pub fn is_empty(&self) -> bool {
164        self.relationships.is_empty()
165    }
166
167    /// Generate XML for all relationships
168    pub fn to_xml(&self) -> String {
169        let mut xml = String::from(
170            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
171<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
172        );
173
174        for rel in &self.relationships {
175            xml.push_str("\n    ");
176            xml.push_str(&rel.to_xml());
177        }
178
179        xml.push_str("\n</Relationships>");
180        xml
181    }
182}
183
184impl ToXml for Relationships {
185    fn to_xml(&self) -> String {
186        Relationships::to_xml(self)
187    }
188}
189
190impl Relationships {
191    /// Parse relationships from XML
192    pub fn from_xml(xml: &str) -> Result<Self, PptxError> {
193        let root = XmlParser::parse_str(xml)?;
194        let mut rels = Relationships::new();
195
196        for rel_elem in root.find_all("Relationship") {
197            if let (Some(id), Some(rel_type), Some(target)) = (
198                rel_elem.attr("Id"),
199                rel_elem.attr("Type"),
200                rel_elem.attr("Target"),
201            ) {
202                rels.add_with_id(id, RelationshipType::from_uri(rel_type), target);
203            }
204        }
205
206        Ok(rels)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_relationship_type_uri() {
216        assert!(RelationshipType::Slide.uri().contains("/slide"));
217        assert!(RelationshipType::Image.uri().contains("/image"));
218    }
219
220    #[test]
221    fn test_relationship_type_from_uri() {
222        let slide = RelationshipType::from_uri(
223            "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide",
224        );
225        assert_eq!(slide, RelationshipType::Slide);
226    }
227
228    #[test]
229    fn test_relationships_add() {
230        let mut rels = Relationships::new();
231        let id1 = rels.add(RelationshipType::Slide, "slides/slide1.xml");
232        let id2 = rels.add(RelationshipType::Slide, "slides/slide2.xml");
233
234        assert_eq!(id1, "rId1");
235        assert_eq!(id2, "rId2");
236        assert_eq!(rels.len(), 2);
237    }
238
239    #[test]
240    fn test_relationships_to_xml() {
241        let mut rels = Relationships::new();
242        rels.add(
243            RelationshipType::SlideMaster,
244            "slideMasters/slideMaster1.xml",
245        );
246        rels.add(RelationshipType::Theme, "theme/theme1.xml");
247
248        let xml = rels.to_xml();
249        assert!(xml.contains("rId1"));
250        assert!(xml.contains("slideMaster"));
251        assert!(xml.contains("theme"));
252    }
253
254    #[test]
255    fn test_relationships_from_xml() {
256        let xml = r#"<?xml version="1.0"?>
257        <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
258            <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
259            <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
260        </Relationships>"#;
261
262        let rels = Relationships::from_xml(xml).unwrap();
263        assert_eq!(rels.len(), 2);
264        assert!(rels.get("rId1").is_some());
265    }
266}