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