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