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 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, | 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;