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