nmd_core/
dossier.rs

1pub mod document;
2pub mod dossier_configuration;
3
4
5use std::{collections::HashSet, path::PathBuf, time::Instant};
6use document::chapter::heading::Heading;
7use document::Document;
8use getset::{Getters, MutGetters, Setters};
9use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
10use thiserror::Error;
11use crate::{codex::Codex, compilation::{compilable::Compilable, compilation_configuration::{compilation_configuration_overlay::CompilationConfigurationOverLay, CompilationConfiguration}, compilation_error::CompilationError, compilation_outcome::CompilationOutcome}, load::{LoadConfiguration, LoadConfigurationOverLay, LoadError}, output_format::OutputFormat, resource::ResourceError};
12
13use self::dossier_configuration::DossierConfiguration;
14use super::{bibliography::Bibliography, table_of_contents::TableOfContents};
15use serde::Serialize;
16
17
18pub const ASSETS_DIR: &str = "assets";
19pub const IMAGES_DIR: &str = "images";
20pub const DOCUMENTS_DIR: &str = "documents";
21pub const STYLES_DIR: &str = "styles";
22
23
24#[derive(Error, Debug)]
25pub enum DossierError {
26    #[error("dossier loading failed: '{0}'")]
27    Load(#[from] ResourceError)
28}
29
30
31/// NMD Dossier struct. It has own documents list
32#[derive(Debug, Getters, MutGetters, Setters, Serialize)]
33pub struct Dossier {
34
35    #[getset(get = "pub", set = "pub")]
36    configuration: DossierConfiguration,
37
38    #[getset(get = "pub", set = "pub", get_mut = "pub")]
39    documents: Vec<Document>,
40}
41
42impl Dossier {
43
44    pub fn new(configuration: DossierConfiguration, documents: Vec<Document>) -> Self {
45
46        Self {
47            configuration,
48            documents,
49        }
50    }
51
52    pub fn name(&self) -> &String {
53        self.configuration.name()
54    }
55
56    /// replace document by name if it is found
57    pub fn replace_document(&mut self, document_name: &str, new_document: Document) {
58        let index = self.documents.iter().position(|document| document.name().eq(document_name));
59
60        if let Some(index) = index {
61            self.documents[index] = new_document;
62        }
63    }
64
65    /// Load dossier from its filesystem path
66    pub fn load_dossier_from_path_buf(path_buf: &PathBuf, codex: &Codex, configuration: &LoadConfiguration, configuration_overlay: LoadConfigurationOverLay) -> Result<Dossier, LoadError> {
67        let dossier_configuration = DossierConfiguration::try_from(path_buf)?;
68
69        Self::load_dossier_from_dossier_configuration(&dossier_configuration, codex, configuration, configuration_overlay.clone())
70    }
71
72    /// Load dossier from its filesystem path considering only a subset of documents
73    pub fn load_dossier_from_path_buf_only_documents(path_buf: &PathBuf, only_documents: &HashSet<String>, codex: &Codex, configuration: &LoadConfiguration, configuration_overlay: LoadConfigurationOverLay) -> Result<Dossier, LoadError> {
74        let mut dossier_configuration = DossierConfiguration::try_from(path_buf)?;
75
76        let d: Vec<String> = dossier_configuration.raw_documents_paths().iter()
77                                                    .filter(|item| {
78
79                                                        let file_name = PathBuf::from(*item).file_name().unwrap().to_string_lossy().to_string();
80
81                                                        only_documents.contains(file_name.as_str())
82                                                    })
83                                                    .map(|item| item.clone())
84                                                    .collect();
85
86        dossier_configuration.set_raw_documents_paths(d);
87
88        let mut configuration_overlay = configuration_overlay.clone();
89
90        configuration_overlay.set_dossier_name(Some(dossier_configuration.name().clone()));
91
92        Self::load_dossier_from_dossier_configuration(&dossier_configuration, codex, configuration, configuration_overlay)
93    }
94
95    /// Load dossier from its dossier configuration
96    pub fn load_dossier_from_dossier_configuration(dossier_configuration: &DossierConfiguration, codex: &Codex, configuration: &LoadConfiguration, configuration_overlay: LoadConfigurationOverLay) -> Result<Dossier, LoadError> {
97
98        // TODO: are really mandatory?
99        if dossier_configuration.documents_paths().is_empty() {
100            return Err(LoadError::ResourceError(ResourceError::InvalidResourceVerbose("there are no documents".to_string())))
101        }
102
103        // TODO: is really mandatory?
104        if dossier_configuration.name().is_empty() {
105            return Err(LoadError::ResourceError(ResourceError::InvalidResourceVerbose("there is no name".to_string())))
106        }
107
108        if configuration.parallelization() {
109
110            let mut documents_res: Vec<Result<Document, LoadError>> = Vec::new();
111
112            dossier_configuration.documents_paths().par_iter()
113            .map(|document_path| {
114                Document::load_document_from_path(&PathBuf::from(document_path), codex, configuration, configuration_overlay.clone())
115            }).collect_into_vec(&mut documents_res);
116            
117            let error = documents_res.par_iter().find_any(|result| result.is_err());
118
119            // handle errors
120            if let Some(Err(err)) = error.as_ref() {
121                return Err(err.clone())
122            }
123
124            let documents = documents_res.into_iter().map(|d| d.unwrap()).collect();
125
126            return Ok(Dossier::new(dossier_configuration.clone(), documents))
127
128
129        } else {
130
131            let mut documents: Vec<Document> = Vec::new();
132
133            for document_path in dossier_configuration.documents_paths() {
134    
135                let document = Document::load_document_from_path(&PathBuf::from(document_path), codex, configuration, configuration_overlay.clone())?;
136    
137                documents.push(document)
138            }
139
140            return Ok(Dossier::new(dossier_configuration.clone(), documents))
141        }
142    }
143}
144
145
146impl Compilable for Dossier {
147    fn standard_compile(&mut self, format: &OutputFormat, codex: &Codex, compilation_configuration: &CompilationConfiguration, mut compilation_configuration_overlay: CompilationConfigurationOverLay) -> Result<CompilationOutcome, CompilationError> {
148    
149        log::info!("compile dossier {} with ({} documents, parallelization: {})", self.name(), self.documents().len(), compilation_configuration.parallelization());
150
151        compilation_configuration_overlay.set_dossier_name(Some(self.name().clone()));
152
153        let fast_draft = compilation_configuration.fast_draft();
154
155        let mut documents_outcomes: Vec<CompilationOutcome> = Vec::new();
156
157        if compilation_configuration.parallelization() {
158
159            let compile_only_documents = compilation_configuration_overlay.compile_only_documents();
160
161            let documents_results: Vec<Result<CompilationOutcome, CompilationError>> = self.documents_mut().par_iter_mut()
162                .filter(|document| {
163                    if fast_draft {
164    
165                        if let Some(subset) = compile_only_documents {
166
167                            let skip = !subset.contains(document.name());
168        
169                            if skip {
170                                log::info!("document {} compilation is skipped", document.name());
171                            }
172
173                            return !skip;
174                        }
175                    }
176
177                    true
178                })
179                .map(|document| {
180
181                    let now = Instant::now();
182
183                    let res = document.compile(format, codex, compilation_configuration, compilation_configuration_overlay.clone());
184
185                    log::info!("document '{}' compiled in {} ms", document.name(), now.elapsed().as_millis());
186
187                    res
188                })
189                .collect();
190
191            let mut errors: Vec<CompilationError> = Vec::new();
192
193            for result in documents_results {
194                match result {
195                    Ok(outcome) => documents_outcomes.push(outcome),
196                    Err(err) => errors.push(err),
197                }
198            }
199
200            if !errors.is_empty() {
201                return Err(CompilationError::BucketOfErrors(errors))
202            }
203
204            
205        } else {
206
207            let compile_only_documents = compilation_configuration_overlay.compile_only_documents();
208
209            let documents_to_compile = self.documents_mut().iter_mut()
210                .filter(|document| {
211
212                    if fast_draft {
213
214                        if let Some(subset) = compile_only_documents {
215
216                            let skip = !subset.contains(document.name());
217        
218                            if skip {
219                                log::info!("document {} compilation is skipped", document.name());
220                            }
221
222                            return !skip;
223                        }
224                    }
225
226                    true
227                });
228
229            for document in documents_to_compile {let now = Instant::now();
230
231                let outcome = document.compile(format, codex, compilation_configuration, compilation_configuration_overlay.clone())?;
232
233                log::info!("document '{}' compiled in {} ms", document.name(), now.elapsed().as_millis());
234
235                documents_outcomes.push(outcome);
236
237            }
238        }
239
240        let mut compiled_toc: Option<CompilationOutcome> = None;
241        let mut compiled_bib: Option<CompilationOutcome> = None;
242
243        if self.configuration().table_of_contents_configuration().include_in_output() {
244
245            log::info!("dossier table of contents will be included in output");
246
247            let mut headings: Vec<Heading> = Vec::new();
248
249            for document in self.documents() {
250                for chapter in document.content().chapters() {
251                    headings.push(chapter.header().heading().clone());
252                }
253            }
254
255            let mut table_of_contents = TableOfContents::new(
256                self.configuration().table_of_contents_configuration().title().clone(),
257                self.configuration().table_of_contents_configuration().page_numbers(),
258                self.configuration().table_of_contents_configuration().plain(),
259                self.configuration().table_of_contents_configuration().maximum_heading_level(),
260                headings
261            );
262
263            compiled_toc = Some(table_of_contents.compile(format, codex, compilation_configuration, compilation_configuration_overlay.clone())?);
264        }
265
266        if self.configuration().bibliography().include_in_output() {
267            let mut bibliography = Bibliography::new(
268                self.configuration().bibliography().title().clone(),
269                self.configuration().bibliography().records().clone()
270            );
271
272            compiled_bib = Some(bibliography.compile(format, codex, compilation_configuration, compilation_configuration_overlay.clone())?);
273        }
274
275        Ok(CompilationOutcome::from(codex.assembler().assemble_dossier(&documents_outcomes, compiled_toc.as_ref(), compiled_bib.as_ref(), &self.configuration, compilation_configuration_overlay.assembler_configuration())?))
276    }
277} 
278
279
280// #[cfg(test)]
281// mod test {
282//     use std::path::PathBuf;
283
284
285//     use crate::{codex::Codex, load::{LoadConfiguration, LoadConfigurationOverLay}};
286
287//     use super::Dossier;
288
289
290
291//     #[test]
292//     fn load_dossier() {
293
294//         let dossier_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-resources").join("nmd-test-dossier-1");
295
296//         let codex = Codex::of_html();
297
298//         let mut loader_configuration = LoadConfiguration::default();
299
300//         loader_configuration.set_parallelization(false);
301
302//         let _dossier = Dossier::load_dossier_from_path_buf(&dossier_path, &codex, &loader_configuration, LoadConfigurationOverLay::default()).unwrap();
303//     }
304// }