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 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 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 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 => () }
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(¶graph.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}