acorn_lib/powerpoint/
mod.rs

1use crate::schema::{ContactPoint, Metadata, Notes, Other, ResearchActivity, Sections};
2use crate::util::{get_all_files, get_command_folder, path_to_string, read_file, write_file, Label};
3use fancy_regex::Regex;
4use quick_xml::events::Event;
5use quick_xml::{Reader, Writer};
6use std::error::Error;
7use std::fs::create_dir_all;
8use std::fs::File;
9use std::io::{self, Read, Write};
10use std::path::PathBuf;
11use tracing::{debug, error, info};
12use zip::write::SimpleFileOptions;
13use zip::{ZipArchive, ZipWriter};
14
15pub mod ooxml;
16use ooxml::*;
17
18pub fn archive(path: PathBuf, destination: Option<PathBuf>) -> Result<PathBuf, Box<dyn Error>> {
19    let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
20    let zip_file_path = match destination {
21        | Some(value) => value,
22        | None => path.with_extension("zip"),
23    };
24    let mut zip = match File::create(&zip_file_path) {
25        | Ok(zip_file) => ZipWriter::new(zip_file),
26        | Err(why) => {
27            error!(file = path_to_string(path), "=> {} Create zip archive - {why}", Label::fail());
28            std::process::exit(exitcode::IOERR);
29        }
30    };
31    let files = get_all_files(path.clone(), None, None).into_iter().filter(|x| x.is_file());
32    for file_path in files {
33        if let Ok(file) = File::open(file_path.clone()) {
34            let name = match path.canonicalize() {
35                | Ok(relative) => file_path.strip_prefix(relative).unwrap_or_else(|_| &file_path),
36                | Err(_) => &file_path,
37            };
38            debug!(file = path_to_string(name.to_path_buf()), "=> {} Add file to archive", Label::using());
39            match zip.start_file_from_path(name, options) {
40                | Ok(_) => {
41                    let mut buffer = Vec::new();
42                    match io::copy(&mut file.take(u64::MAX), &mut buffer) {
43                        | Ok(_) => match zip.write_all(&buffer) {
44                            | Ok(_) => {}
45                            | Err(why) => {
46                                error!(file = path_to_string(file_path), "=> {} Write zip archive - {why}", Label::fail())
47                            }
48                        },
49                        | Err(why) => {
50                            error!("=> {} Copy buffer - {why}", Label::fail())
51                        }
52                    }
53                }
54                | Err(why) => {
55                    error!(file = path_to_string(file_path), "=> {} Start zip archive - {why}", Label::fail());
56                }
57            }
58        }
59    }
60    match zip.finish() {
61        | Ok(_) => Ok(zip_file_path),
62        | Err(why) => {
63            error!(file = path_to_string(path), "=> {} Finish zip archive - {why}", Label::fail());
64            Err(why.into())
65        }
66    }
67}
68pub fn extract(path: PathBuf, destination: Option<PathBuf>) -> Result<PathBuf, Box<dyn Error>> {
69    let root = get_command_folder("extract", destination);
70    match File::open(path.clone()) {
71        | Ok(zip_file) => match ZipArchive::new(zip_file) {
72            | Ok(mut archive) => {
73                for index in 0..archive.len() {
74                    if let Ok(mut file) = archive.by_index(index) {
75                        let target = root.join(file.name());
76                        if let Some(parent) = target.parent() {
77                            match create_dir_all(parent) {
78                                | Ok(_) => {}
79                                | Err(why) => error!(path = path_to_string(parent.to_path_buf()), "=> {} Create - {}", Label::fail(), why),
80                            }
81                        }
82                        if let Ok(mut output_file) = File::create(&target) {
83                            io::copy(&mut file, &mut output_file).unwrap();
84                        } else {
85                            error!(path = path_to_string(target), "=> {} Create file", Label::fail());
86                            std::process::exit(exitcode::UNAVAILABLE);
87                        }
88                    }
89                }
90                info!(path = root.to_str().unwrap(), "=> {} Extract zip archive", Label::pass());
91                Ok(root)
92            }
93            | Err(err) => {
94                error!(
95                    error = err.to_string(),
96                    path = path.to_str().unwrap(),
97                    "=> {} Read zip archive",
98                    Label::fail()
99                );
100                std::process::exit(exitcode::UNAVAILABLE);
101            }
102        },
103        | Err(err) => {
104            error!(error = err.to_string(), path = path.to_str().unwrap(), "=> {} Read file", Label::fail());
105            std::process::exit(exitcode::UNAVAILABLE);
106        }
107    }
108}
109pub fn interpolate_values(path: PathBuf, data: ResearchActivity) {
110    let updated = match read_file(path.clone()) {
111        | Ok(mut content) => {
112            let ResearchActivity {
113                contact,
114                meta,
115                notes,
116                title,
117                subtitle,
118                sections,
119                ..
120            } = data.clone();
121            let Metadata { doi, partners, .. } = meta.clone();
122            let Sections {
123                achievement,
124                impact,
125                approach,
126                ..
127            } = sections;
128            let ContactPoint {
129                given_name: first,
130                family_name: last,
131                email,
132                ..
133            } = contact;
134            let caption = meta.get_first_image_caption();
135            let presentation_notes = match notes.clone() {
136                | Some(value) => match value {
137                    | Other::Formatted(Notes { presentation, .. }) => match presentation {
138                        | Some(value) => value,
139                        | None => "".to_string(),
140                    },
141                    | Other::Unformatted(notes) => notes,
142                },
143                | None => "".to_string(),
144            };
145            let managers = match notes.clone() {
146                | Some(value) => match value {
147                    | Other::Formatted(Notes { managers, .. }) => match managers {
148                        | Some(value) => value,
149                        | None => vec![],
150                    },
151                    | _ => vec![],
152                },
153                | None => vec![],
154            };
155            let programs = match notes {
156                | Some(value) => match value {
157                    | Other::Formatted(Notes { programs, .. }) => match programs {
158                        | Some(value) => value,
159                        | None => vec![],
160                    },
161                    | _ => vec![],
162                },
163                | None => vec![],
164            };
165            content = replace_placeholder_with_string(&content, "title", &title);
166            content = replace_placeholder_with_string(&content, "subtitle", &subtitle.unwrap_or_else(|| "".to_string()));
167            content = replace_placeholder_with_string(&content, "first", &first);
168            content = replace_placeholder_with_string(&content, "last", &last);
169            content = replace_placeholder_with_string(&content, "email", &email);
170            content = replace_placeholder_with_string(&content, "partners", &partners.unwrap().join(", "));
171            content = replace_placeholder_with_string(&content, "programs", &programs.join(" and "));
172            content = replace_placeholder_with_string(&content, "managers", &managers.join(" and "));
173            // TODO: Use CiteAs API to get in Chicago format
174            content = replace_placeholder_with_string(&content, "citation", &doi.unwrap());
175            content = replace_placeholder_with_string(&content, "caption", &caption);
176            content = replace_placeholder_with_string(&content, "notes", &presentation_notes);
177            content = replace_placeholder_with_bullets(&content, "achievement", achievement.unwrap_or_else(std::vec::Vec::new));
178            content = replace_placeholder_with_bullets(&content, "impact", impact);
179            content = replace_placeholder_with_bullets(&content, "technical", approach);
180            content
181        }
182        | Err(why) => {
183            error!("=> {} Cannot read file for interpolation - {why}", Label::fail());
184            std::process::exit(exitcode::IOERR);
185        }
186    };
187    match write_file(path.clone(), updated) {
188        | Ok(_) => {}
189        | Err(why) => {
190            error!("=> {} Cannot write file for interpolation - {why}", Label::fail());
191            std::process::exit(exitcode::IOERR);
192        }
193    }
194}
195pub fn parse_ooxml_paragraph(content: &str) -> Result<TextParagraph, quick_xml::DeError> {
196    let parsed = quick_xml::de::from_str::<TextParagraph>(content);
197    debug!("=> {} OOXMLParagraph = {:#?}", Label::using(), parsed);
198    parsed
199}
200pub fn prettify_xml(xml: &str) -> String {
201    let mut buf: Vec<u8> = Vec::new();
202    let mut reader = Reader::from_str(xml);
203    let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
204    loop {
205        let ev = reader.read_event();
206        match ev {
207            | Ok(Event::Eof) => break, // exits the loop when reaching end of file
208            | Ok(event) => writer.write_event(event),
209            | Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
210        }
211        .expect("Failed to parse XML");
212        buf.clear();
213    }
214    let result = std::str::from_utf8(&writer.into_inner())
215        .expect("Failed to convert a slice of bytes to a string slice")
216        .to_string();
217    result
218}
219pub fn read_xml_rel(path: PathBuf) -> Result<Relationships, quick_xml::DeError> {
220    match read_file(path) {
221        | Ok(content) => {
222            let parsed = quick_xml::de::from_str::<Relationships>(&content);
223            debug!("=> {} Relationships = {:#?}", Label::using(), parsed);
224            parsed
225        }
226        | Err(why) => {
227            error!("=> {} Cannot read xml.rels file - {why}", Label::fail());
228            std::process::exit(exitcode::IOERR);
229        }
230    }
231}
232pub fn replace_placeholder_with_bullets(xml: &str, placeholder: &str, values: Vec<String>) -> String {
233    let paragraphs = match Regex::new(r#"<a:p>(?:(?!<a:p>|</a:p>)[\s\S])*</a:p>"#) {
234        | Ok(re) => re
235            .find_iter(xml)
236            .flat_map(|m| m.ok())
237            .map(|m| m.as_str().to_string())
238            .collect::<Vec<String>>(),
239        | Err(_) => unreachable!(),
240    };
241    let selected = match paragraphs
242        .into_iter()
243        .find(|x| match Regex::new(&format!(r"{{{{\s*{}\s*}}}}", placeholder)) {
244            | Ok(re) => re.is_match(x).unwrap_or_default(),
245            | Err(_) => false,
246        }) {
247        | Some(value) => value,
248        | None => "".to_string(),
249    };
250    match parse_ooxml_paragraph(&selected) {
251        | Ok(paragraph) => {
252            let TextParagraph {
253                text_paragraph_properties,
254                text_run,
255                end_paragraph_run_properties,
256                ..
257            } = paragraph.clone();
258            let bullet_properties = match text_paragraph_properties.first() {
259                | Some(value) => value,
260                | None => &TextParagraphProperties::init().build(),
261            };
262            let text_properties = match text_run.first() {
263                | Some(first) => match first.text_run_properties.first() {
264                    | Some(value) => value,
265                    | None => todo!(),
266                },
267                | None => todo!(),
268            };
269            let content: Vec<String> = values
270                .into_iter()
271                .map(|value| {
272                    let run = TextRun::init()
273                        .text_run_properties(vec![text_properties.clone()])
274                        .text(TextString { value })
275                        .build();
276                    let custom = TextParagraph::init()
277                        .text_paragraph_properties(vec![bullet_properties.clone()])
278                        .text_run(vec![run])
279                        .maybe_end_paragraph_run_properties(end_paragraph_run_properties.clone())
280                        .build();
281                    prettify_xml(&quick_xml::se::to_string(&custom).unwrap())
282                })
283                .collect();
284            let formatted = content.join("");
285            xml.replace(&selected, &formatted)
286        }
287        | Err(why) => {
288            debug!(selected, "=> {} Parse OOXML Paragraph - {why}", Label::fail());
289            xml.to_string()
290        }
291    }
292}
293pub fn replace_placeholder_with_string(content: &str, placeholder: &str, value: &str) -> String {
294    match Regex::new(&format!(r"{{{{\s*{}\s*}}}}", placeholder)) {
295        | Ok(re) => re.replace_all(content, value).to_string(),
296        | Err(err) => {
297            error!("=> {} Regex replacement - {err}", Label::fail());
298            content.to_string()
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests;