Skip to main content

ppt_rs/parts/
coreprops.rs

1//! Core properties part
2//!
3//! Represents docProps/core.xml with document metadata.
4
5use super::base::{ContentType, Part, PartType};
6use crate::exc::PptxError;
7use crate::oxml::XmlParser;
8use chrono::Utc;
9
10/// Core properties part (docProps/core.xml)
11#[derive(Debug, Clone)]
12pub struct CorePropertiesPart {
13    path: String,
14    pub title: Option<String>,
15    pub subject: Option<String>,
16    pub creator: Option<String>,
17    pub keywords: Option<String>,
18    pub description: Option<String>,
19    pub last_modified_by: Option<String>,
20    pub revision: Option<u32>,
21    pub created: Option<String>,
22    pub modified: Option<String>,
23}
24
25impl CorePropertiesPart {
26    /// Create a new core properties part
27    pub fn new() -> Self {
28        let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
29
30        CorePropertiesPart {
31            path: "docProps/core.xml".to_string(),
32            title: None,
33            subject: None,
34            creator: Some("pptx-rs".to_string()),
35            keywords: None,
36            description: None,
37            last_modified_by: Some("pptx-rs".to_string()),
38            revision: Some(1),
39            created: Some(now.clone()),
40            modified: Some(now),
41        }
42    }
43
44    /// Set title
45    pub fn set_title(&mut self, title: &str) -> &mut Self {
46        self.title = Some(title.to_string());
47        self
48    }
49
50    /// Set subject
51    pub fn set_subject(&mut self, subject: &str) -> &mut Self {
52        self.subject = Some(subject.to_string());
53        self
54    }
55
56    /// Set creator
57    pub fn set_creator(&mut self, creator: &str) -> &mut Self {
58        self.creator = Some(creator.to_string());
59        self
60    }
61
62    /// Set keywords
63    pub fn set_keywords(&mut self, keywords: &str) -> &mut Self {
64        self.keywords = Some(keywords.to_string());
65        self
66    }
67
68    /// Set description
69    pub fn set_description(&mut self, description: &str) -> &mut Self {
70        self.description = Some(description.to_string());
71        self
72    }
73
74    /// Update modified timestamp
75    pub fn touch(&mut self) {
76        self.modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string());
77        if let Some(ref mut rev) = self.revision {
78            *rev += 1;
79        }
80    }
81
82    fn escape_xml(s: &str) -> String {
83        s.replace('&', "&amp;")
84            .replace('<', "&lt;")
85            .replace('>', "&gt;")
86            .replace('"', "&quot;")
87            .replace('\'', "&apos;")
88    }
89}
90
91impl Default for CorePropertiesPart {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl Part for CorePropertiesPart {
98    fn path(&self) -> &str {
99        &self.path
100    }
101
102    fn part_type(&self) -> PartType {
103        PartType::CoreProperties
104    }
105
106    fn content_type(&self) -> ContentType {
107        ContentType::CoreProperties
108    }
109
110    fn to_xml(&self) -> Result<String, PptxError> {
111        let mut elements = Vec::new();
112
113        if let Some(ref title) = self.title {
114            elements.push(format!("<dc:title>{}</dc:title>", Self::escape_xml(title)));
115        }
116        if let Some(ref subject) = self.subject {
117            elements.push(format!(
118                "<dc:subject>{}</dc:subject>",
119                Self::escape_xml(subject)
120            ));
121        }
122        if let Some(ref creator) = self.creator {
123            elements.push(format!(
124                "<dc:creator>{}</dc:creator>",
125                Self::escape_xml(creator)
126            ));
127        }
128        if let Some(ref keywords) = self.keywords {
129            elements.push(format!(
130                "<cp:keywords>{}</cp:keywords>",
131                Self::escape_xml(keywords)
132            ));
133        }
134        if let Some(ref description) = self.description {
135            elements.push(format!(
136                "<dc:description>{}</dc:description>",
137                Self::escape_xml(description)
138            ));
139        }
140        if let Some(ref last_modified_by) = self.last_modified_by {
141            elements.push(format!(
142                "<cp:lastModifiedBy>{}</cp:lastModifiedBy>",
143                Self::escape_xml(last_modified_by)
144            ));
145        }
146        if let Some(revision) = self.revision {
147            elements.push(format!("<cp:revision>{}</cp:revision>", revision));
148        }
149        if let Some(ref created) = self.created {
150            elements.push(format!(
151                r#"<dcterms:created xsi:type="dcterms:W3CDTF">{}</dcterms:created>"#,
152                created
153            ));
154        }
155        if let Some(ref modified) = self.modified {
156            elements.push(format!(
157                r#"<dcterms:modified xsi:type="dcterms:W3CDTF">{}</dcterms:modified>"#,
158                modified
159            ));
160        }
161
162        let xml = format!(
163            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
164<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
165{}
166</cp:coreProperties>"#,
167            elements.join("\n")
168        );
169
170        Ok(xml)
171    }
172
173    fn from_xml(xml: &str) -> Result<Self, PptxError> {
174        let root = XmlParser::parse_str(xml)?;
175        let mut part = CorePropertiesPart::new();
176
177        part.title = root
178            .find_descendant("title")
179            .map(|e| e.text_content())
180            .filter(|s| !s.is_empty());
181        part.subject = root
182            .find_descendant("subject")
183            .map(|e| e.text_content())
184            .filter(|s| !s.is_empty());
185        part.creator = root
186            .find_descendant("creator")
187            .map(|e| e.text_content())
188            .filter(|s| !s.is_empty());
189        part.keywords = root
190            .find_descendant("keywords")
191            .map(|e| e.text_content())
192            .filter(|s| !s.is_empty());
193        part.description = root
194            .find_descendant("description")
195            .map(|e| e.text_content())
196            .filter(|s| !s.is_empty());
197        part.last_modified_by = root
198            .find_descendant("lastModifiedBy")
199            .map(|e| e.text_content())
200            .filter(|s| !s.is_empty());
201        part.revision = root
202            .find_descendant("revision")
203            .and_then(|e| e.text_content().parse().ok());
204        part.created = root
205            .find_descendant("created")
206            .map(|e| e.text_content())
207            .filter(|s| !s.is_empty());
208        part.modified = root
209            .find_descendant("modified")
210            .map(|e| e.text_content())
211            .filter(|s| !s.is_empty());
212
213        Ok(part)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_core_props_new() {
223        let part = CorePropertiesPart::new();
224        assert_eq!(part.path(), "docProps/core.xml");
225        assert!(part.creator.is_some());
226        assert!(part.created.is_some());
227    }
228
229    #[test]
230    fn test_core_props_set_title() {
231        let mut part = CorePropertiesPart::new();
232        part.set_title("My Presentation");
233
234        assert_eq!(part.title, Some("My Presentation".to_string()));
235    }
236
237    #[test]
238    fn test_core_props_to_xml() {
239        let mut part = CorePropertiesPart::new();
240        part.set_title("Test Title");
241        part.set_creator("Test Author");
242
243        let xml = part.to_xml().unwrap();
244        assert!(xml.contains("dc:title"));
245        assert!(xml.contains("Test Title"));
246        assert!(xml.contains("dc:creator"));
247    }
248
249    #[test]
250    fn test_core_props_from_xml() {
251        let xml = r#"<?xml version="1.0"?>
252        <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
253                          xmlns:dc="http://purl.org/dc/elements/1.1/"
254                          xmlns:dcterms="http://purl.org/dc/terms/">
255            <dc:title>Parsed Title</dc:title>
256            <dc:creator>Parsed Author</dc:creator>
257            <cp:revision>5</cp:revision>
258        </cp:coreProperties>"#;
259
260        let part = CorePropertiesPart::from_xml(xml).unwrap();
261        assert_eq!(part.title, Some("Parsed Title".to_string()));
262        assert_eq!(part.creator, Some("Parsed Author".to_string()));
263        assert_eq!(part.revision, Some(5));
264    }
265
266    #[test]
267    fn test_core_props_touch() {
268        let mut part = CorePropertiesPart::new();
269        let original_rev = part.revision;
270
271        part.touch();
272
273        assert_eq!(part.revision, Some(original_rev.unwrap() + 1));
274    }
275}