ppt_rs/parts/
coreprops.rs1use super::base::{ContentType, Part, PartType};
6use crate::exc::PptxError;
7use crate::oxml::XmlParser;
8use chrono::Utc;
9
10#[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 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 pub fn set_title(&mut self, title: &str) -> &mut Self {
46 self.title = Some(title.to_string());
47 self
48 }
49
50 pub fn set_subject(&mut self, subject: &str) -> &mut Self {
52 self.subject = Some(subject.to_string());
53 self
54 }
55
56 pub fn set_creator(&mut self, creator: &str) -> &mut Self {
58 self.creator = Some(creator.to_string());
59 self
60 }
61
62 pub fn set_keywords(&mut self, keywords: &str) -> &mut Self {
64 self.keywords = Some(keywords.to_string());
65 self
66 }
67
68 pub fn set_description(&mut self, description: &str) -> &mut Self {
70 self.description = Some(description.to_string());
71 self
72 }
73
74 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('&', "&")
84 .replace('<', "<")
85 .replace('>', ">")
86 .replace('"', """)
87 .replace('\'', "'")
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}