nmd_core/assembler/
html_assembler.rs

1use std::path::PathBuf;
2use build_html::{HtmlPage, HtmlContainer, Html, Container};
3use getset::{Getters, Setters};
4use crate::{compilation::compilation_outcome::CompilationOutcome, dossier::{document::chapter::chapter_tag::{ChapterTag, ChapterTagKey}, dossier_configuration::DossierConfiguration}, resource::{disk_resource::DiskResource, Resource}, theme::Theme};
5
6use super::{assembler_configuration::AssemblerConfiguration, Assembler, AssemblerError};
7
8
9
10#[derive(Debug, Getters, Setters)]
11pub struct HtmlAssembler {
12}
13
14impl HtmlAssembler {
15
16    pub fn new() -> Self {
17        Self {
18        }
19    }
20
21    fn apply_standard_remote_addons(mut page: HtmlPage, theme: &Theme) -> HtmlPage {
22
23        // add code block js/css
24        match theme {
25            Theme::Light | Theme::HighContrast | Theme::None => {
26                page = page
27                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js", [
28                        ("crossorigin", "anonymous"),
29                    ])
30                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js", [
31                        ("crossorigin", "anonymous"),
32                    ])
33                    .with_head_link("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.css", "stylesheet")
34                    .with_head_link("https://emoji-css.afeld.me/emoji.css", "stylesheet");
35                
36            },
37            Theme::Dark => {
38                page = page
39                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js", [
40                        ("crossorigin", "anonymous"),
41                    ])
42                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js", [
43                        ("crossorigin", "anonymous"),
44                    ])
45                    .with_head_link("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.css", "stylesheet")
46                    .with_head_link("https://emoji-css.afeld.me/emoji.css", "stylesheet");
47            },
48            Theme::Scientific => {
49                page = page
50                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js", [
51                        ("crossorigin", "anonymous"),
52                    ])
53                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js", [
54                        ("crossorigin", "anonymous"),
55                    ])
56                    .with_head_link("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-coy.css", "stylesheet")
57                    .with_head_link("https://emoji-css.afeld.me/emoji.css", "stylesheet");
58            },
59            Theme::Vintage => {
60                page = page
61                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js", [
62                        ("crossorigin", "anonymous"),
63                    ])
64                    .with_script_link_attr("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js", [
65                        ("crossorigin", "anonymous"),
66                    ])
67                    .with_head_link("https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-solarizedlight.css", "stylesheet")
68                    .with_head_link("https://emoji-css.afeld.me/emoji.css", "stylesheet");
69            }
70        };
71
72        // add math block js/css
73        page = page
74                .with_head_link_attr("https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css", "stylesheet", [
75                    ("integrity", "sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV"),
76                    ("crossorigin", "anonymous")
77                ])
78                .with_script_link_attr("https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js", [
79                    ("integrity", "sha384-XjKyOOlGwcjNTAIQHIpgOno0Hl1YQqzUOEleOLALmuqehneUG+vnGctmUb0ZY0l8"),
80                    ("crossorigin", "anonymous")
81                ])
82                .with_script_link_attr("https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js", [
83                    ("integrity", "sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05"),
84                    ("crossorigin", "anonymous")
85                ]);
86
87        page.add_script_literal(r#"
88                document.addEventListener("DOMContentLoaded", function() {
89                    renderMathInElement(document.body, {
90                        
91                        delimiters: [
92                            {left: '$$', right: '$$', display: true},
93                            {left: '$', right: '$', display: false},
94                        ],
95                        
96                        throwOnError : false
97                    });
98                });"#);
99
100        page
101    }
102
103    fn apply_standard_local_addons(mut page: HtmlPage, theme: &Theme) -> HtmlPage {
104
105        page.add_style(include_str!("html_assembler/emoji/emoji.min.css"));
106        
107        page.add_style(include_str!("html_assembler/math_block/katex.css"));
108        page.add_style(include_str!("html_assembler/math_block/katex-fonts.css"));
109        page.add_script_literal(include_str!("html_assembler/math_block/katex.min.js"));
110        page.add_script_literal(include_str!("html_assembler/math_block/auto-render.min.js"));
111        page.add_script_literal(r#"window.onload = function() {
112            renderMathInElement(document.body, {
113                delimiters: [
114                    {left: '$$', right: '$$', display: true},
115                    {left: '$', right: '$', display: false},
116                ],
117                throwOnError : false
118                });
119        }"#);
120
121        // add code block js/css                        
122        match theme {
123            Theme::Light | Theme::HighContrast | Theme::None => {
124                page.add_style(include_str!("html_assembler/code_block/light_theme/prismjs.css"));
125                page.add_script_literal(include_str!("html_assembler/code_block/light_theme/prismjs.js"));
126            },
127            Theme::Dark => {
128                page.add_style(include_str!("html_assembler/code_block/dark_theme/prismjs.css"));
129                page.add_script_literal(include_str!("html_assembler/code_block/dark_theme/prismjs.js"));
130            },
131            Theme::Scientific => {
132                page.add_style(include_str!("html_assembler/code_block/scientific_theme/prismjs.css"));
133                page.add_script_literal(include_str!("html_assembler/code_block/scientific_theme/prismjs.js"));
134            },
135            Theme::Vintage => {
136                page.add_style(include_str!("html_assembler/code_block/vintage_theme/prismjs.css"));
137                page.add_script_literal(include_str!("html_assembler/code_block/vintage_theme/prismjs.js"));
138            },
139        };
140
141        page
142    }
143
144    fn apply_theme_style(mut page: HtmlPage, theme: &Theme) -> HtmlPage {
145
146        page.add_style(include_str!("html_assembler/style/default_style.css"));
147
148        match theme {
149            Theme::Light => page.add_style(include_str!("html_assembler/style/light_theme.css")),
150            Theme::Dark => page.add_style(include_str!("html_assembler/style/dark_theme.css")),
151            Theme::Scientific => page.add_style(include_str!("html_assembler/style/scientific_theme.css")),
152            Theme::Vintage => page.add_style(include_str!("html_assembler/style/vintage_theme.css")),
153            Theme::HighContrast => page.add_style(include_str!("html_assembler/style/high_contrast_theme.css")),
154            Theme::None => ()       // nothing,
155        }
156
157        page
158    }
159
160    fn create_default_html_page(page_title: &str, external_styles_paths: &Vec<PathBuf>, external_styles: &Vec<String>, external_scripts_paths: &Vec<PathBuf>, external_scripts: &Vec<String>, theme: &Theme, use_remote_addons: bool) -> Result<HtmlPage, AssemblerError> {
161
162        let mut page = HtmlPage::new()
163                                    .with_title(page_title)
164                                    .with_meta(vec![("charset", "utf-8")]);
165
166        if use_remote_addons {
167        page = Self::apply_standard_remote_addons(page, theme);
168
169        } else {
170        page = Self::apply_standard_local_addons(page, theme);
171        }
172
173        page = Self::apply_theme_style(page, theme);
174
175
176        for style in external_styles {
177            page.add_style(style);
178        }
179
180        for style_path in external_styles_paths {
181
182            let resource = DiskResource::new(style_path.clone())?;
183
184            page.add_style(resource.read()?);
185        }
186
187
188        for script in external_scripts {
189            page.add_script_literal(script);
190        }
191
192        for script_path in external_scripts_paths {
193
194            let resource = DiskResource::new(script_path.clone())?;
195
196            page.add_script_literal(resource.read()?);
197        }
198
199        Ok(page)
200    }
201}
202
203impl Assembler for HtmlAssembler {
204
205    fn assemble_dossier(&self, compiled_documents: &Vec<CompilationOutcome>, compiled_toc: Option<&CompilationOutcome>, compiled_bib: Option<&CompilationOutcome>, dossier_configuration: &DossierConfiguration, configuration: &AssemblerConfiguration) -> Result<String, AssemblerError> {
206               
207        let mut styles_references: Vec<PathBuf> = dossier_configuration.style().styles_references().iter()
208                                                        .map(|p| PathBuf::from(p))
209                                                        .collect();
210
211        log::info!("appending {} custom styles", styles_references.len());
212
213        let mut other_styles = configuration.external_styles_paths().clone();
214        styles_references.append(&mut other_styles);
215
216        let mut page = Self::create_default_html_page(
217            dossier_configuration.name(), 
218            &styles_references, 
219            configuration.external_styles(), 
220            configuration.external_scripts_paths(), 
221            configuration.external_scripts(), 
222            configuration.theme(), 
223            configuration.use_remote_addons()
224        )?;
225        
226        if let Some(toc) = compiled_toc {
227            page.add_raw(toc.content());
228        }
229
230        for document in compiled_documents {
231            let section = Container::new(build_html::ContainerType::Section)
232                                            .with_attributes(vec![
233                                                ("class", "document")
234                                            ])
235                                            .with_raw(document.content());
236
237            page.add_container(section);
238        }
239
240        if let Some(bib) = compiled_bib {
241            page.add_raw(bib.content());
242        }
243
244        Ok(page.to_html_string())
245    }
246    
247    fn assemble_bundle(&self, compiled_preamble: &Vec<CompilationOutcome>, compiled_chapters: &Vec<CompilationOutcome>, _configuration: &AssemblerConfiguration) -> Result<String, AssemblerError> {
248
249        let mut result = String::new();
250
251        for paragraph in compiled_preamble {
252
253            result.push_str(&paragraph.content());
254        }
255
256        for chapter in compiled_chapters {
257
258            result.push_str(&chapter.content());
259        }
260
261        Ok(result)
262    }
263
264    fn assemble_document_standalone(&self, page_title: &str, compiled_document: &CompilationOutcome, compiled_toc: Option<&CompilationOutcome>, compiled_bib: Option<&CompilationOutcome>, configuration: &AssemblerConfiguration) -> Result<String, AssemblerError> {
265        
266        let mut page = Self::create_default_html_page(
267                                    page_title,
268                                    configuration.external_styles_paths(),
269                                    configuration.external_styles(),
270                                    configuration.external_scripts_paths(),
271                                    configuration.external_scripts(),
272                                    configuration.theme(),
273                                    configuration.use_remote_addons()
274                                )?;
275
276        if let Some(toc) = compiled_toc {
277            page.add_raw(toc.content());
278        }
279
280        page.add_raw(compiled_document.content());
281
282        if let Some(bib) = compiled_bib {
283            page.add_raw(bib.content());
284        }
285
286        Ok(page.to_html_string())
287    }
288    
289    fn assemble_chapter(&self, chapter_tags: &Vec<ChapterTag>, compiled_heading: &CompilationOutcome, compiled_paragraphs: &Vec<CompilationOutcome>, _configuration: &AssemblerConfiguration) -> Result<String, AssemblerError> {
290
291        let mut div_chapter = Container::new(build_html::ContainerType::Div);
292        let mut style = String::new();
293
294        for tag in chapter_tags {
295
296            match tag.key() {
297                ChapterTagKey::Id => {
298                    div_chapter = div_chapter.with_attributes(vec![("id", tag.value().as_ref().unwrap().as_str())])
299                }
300                ChapterTagKey::Style => {
301                    style.push_str(format!("{};", tag.value().as_ref().unwrap().as_str()).as_str())
302                },
303                ChapterTagKey::StyleClass => {
304                    div_chapter = div_chapter.with_attributes(vec![(String::from("class"), format!("chapter {}", tag.value().as_ref().unwrap().as_str()))])
305                },
306
307                _ => {
308                    log::warn!("{:?} chapter tag key not supported yet", tag.key())
309                }
310            }
311        }
312
313        div_chapter = div_chapter.with_attributes(vec![("style", style.as_str())]);
314        let mut div_chapter_content = String::new();
315
316        div_chapter_content.push_str(&compiled_heading.content());
317
318        for compiled_paragraph in compiled_paragraphs {
319
320            let compiled_content = compiled_paragraph.content();
321
322            div_chapter_content.push_str(&compiled_content);
323        }
324
325        Ok(div_chapter.with_raw(div_chapter_content).to_html_string())
326    }
327}
328
329
330#[cfg(test)]
331mod test {
332}