1use crate::api::Presentation;
2use crate::generator::{SlideContent, Image};
3use crate::exc::Result;
4
5pub fn export_to_html(presentation: &Presentation) -> Result<String> {
7 let mut html = String::new();
8
9 html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
11 html.push_str("<meta charset=\"UTF-8\">\n");
12 html.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
13 html.push_str(&format!("<title>{}</title>\n", presentation.get_title()));
14
15 html.push_str("<style>\n");
17 html.push_str(include_str!("html_style.css"));
18 html.push_str("</style>\n");
19
20 html.push_str("</head>\n<body>\n");
21
22 html.push_str("<div class=\"slide title-slide\">\n");
24 html.push_str(&format!("<h1>{}</h1>\n", presentation.get_title()));
25 html.push_str("</div>\n");
26
27 for (i, slide) in presentation.slides().iter().enumerate() {
29 html.push_str(&render_slide(slide, i + 1));
30 }
31
32 html.push_str("</body>\n</html>");
33
34 Ok(html)
35}
36
37fn render_slide(slide: &SlideContent, index: usize) -> String {
38 let mut html = String::new();
39
40 html.push_str(&format!("<div class=\"slide\" id=\"slide-{}\">\n", index));
41
42 html.push_str(&format!("<div class=\"slide-number\">{}</div>\n", index));
44
45 html.push_str(&format!("<h2>{}</h2>\n", slide.title));
47
48 html.push_str("<div class=\"content\">\n");
50
51 if !slide.content.is_empty() {
53 html.push_str("<ul>\n");
54 for item in &slide.content {
55 html.push_str(&format!("<li>{}</li>\n", item));
56 }
57 html.push_str("</ul>\n");
58 }
59
60 for image in &slide.images {
62 if let Some(img_html) = render_image(image) {
63 html.push_str(&img_html);
64 }
65 }
66
67 for code in &slide.code_blocks {
69 html.push_str("<pre><code>");
70 html.push_str(&code.code);
71 html.push_str("</code></pre>\n");
72 }
73
74 html.push_str("</div>\n"); html.push_str("</div>\n"); html
78}
79
80fn render_image(image: &Image) -> Option<String> {
81 let bytes = image.get_bytes()?;
82 let b64 = base64_encode(&bytes);
83 let mime = match image.format.to_lowercase().as_str() {
84 "jpg" | "jpeg" => "image/jpeg",
85 "png" => "image/png",
86 "gif" => "image/gif",
87 "svg" => "image/svg+xml",
88 _ => "application/octet-stream",
89 };
90
91 Some(format!(
98 "<div class=\"image-container\"><img src=\"data:{};base64,{}\" alt=\"{}\" /></div>\n",
99 mime, b64, image.filename
100 ))
101}
102
103fn base64_encode(data: &[u8]) -> String {
105 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
106 let mut output = String::with_capacity(data.len() * 4 / 3 + 4);
107
108 let mut i = 0;
109 while i < data.len() {
110 let mut buf = [0u8; 3];
111 let mut len = 0;
112
113 for j in 0..3 {
114 if i + j < data.len() {
115 buf[j] = data[i + j];
116 len += 1;
117 }
118 }
119
120 let b0 = (buf[0] >> 2) & 0x3F;
121 let b1 = ((buf[0] & 0x03) << 4) | ((buf[1] >> 4) & 0x0F);
122 let b2 = ((buf[1] & 0x0F) << 2) | ((buf[2] >> 6) & 0x03);
123 let b3 = buf[2] & 0x3F;
124
125 output.push(ALPHABET[b0 as usize] as char);
126 output.push(ALPHABET[b1 as usize] as char);
127
128 if len > 1 {
129 output.push(ALPHABET[b2 as usize] as char);
130 } else {
131 output.push('=');
132 }
133
134 if len > 2 {
135 output.push(ALPHABET[b3 as usize] as char);
136 } else {
137 output.push('=');
138 }
139
140 i += 3;
141 }
142
143 output
144}