Skip to main content

ppt_rs/export/
html.rs

1use crate::api::Presentation;
2use crate::generator::{SlideContent, Image};
3use crate::exc::Result;
4
5/// Export options for HTML output
6#[derive(Clone, Debug)]
7pub struct HtmlExportOptions {
8    /// Include speaker notes
9    pub include_notes: bool,
10    /// Enable keyboard navigation
11    pub enable_navigation: bool,
12    /// Include code syntax highlighting
13    pub syntax_highlight: bool,
14    /// Export images as separate files instead of base64
15    pub export_images_as_files: bool,
16    /// Image output directory (for file export)
17    pub image_output_dir: Option<String>,
18}
19
20impl Default for HtmlExportOptions {
21    fn default() -> Self {
22        Self {
23            include_notes: true,
24            enable_navigation: true,
25            syntax_highlight: true,
26            export_images_as_files: false,
27            image_output_dir: None,
28        }
29    }
30}
31
32impl HtmlExportOptions {
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    pub fn with_notes(mut self, include: bool) -> Self {
38        self.include_notes = include;
39        self
40    }
41
42    pub fn with_navigation(mut self, enable: bool) -> Self {
43        self.enable_navigation = enable;
44        self
45    }
46
47    pub fn with_syntax_highlight(mut self, enable: bool) -> Self {
48        self.syntax_highlight = enable;
49        self
50    }
51
52    pub fn with_image_files(mut self, enable: bool, dir: Option<&str>) -> Self {
53        self.export_images_as_files = enable;
54        self.image_output_dir = dir.map(|d| d.to_string());
55        self
56    }
57}
58
59/// Export a presentation to a single HTML file
60pub fn export_to_html(presentation: &Presentation) -> Result<String> {
61    export_to_html_with_options(presentation, &HtmlExportOptions::default())
62}
63
64/// Export a presentation to HTML with custom options
65pub fn export_to_html_with_options(presentation: &Presentation, options: &HtmlExportOptions) -> Result<String> {
66    let mut html = String::new();
67
68    // Header
69    html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
70    html.push_str("<meta charset=\"UTF-8\">\n");
71    html.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
72    html.push_str(&format!("<title>{}</title>\n", presentation.get_title()));
73
74    // Enhanced CSS
75    html.push_str("<style>\n");
76    html.push_str(include_str!("html_style.css"));
77
78    // Add additional styles based on options
79    if options.enable_navigation {
80        html.push_str(include_str!("html_navigation.css"));
81    }
82
83    if options.include_notes {
84        html.push_str(include_str!("html_notes.css"));
85    }
86
87    html.push_str("</style>\n");
88
89    // Add JavaScript for navigation
90    if options.enable_navigation {
91        html.push_str("<script>\n");
92        html.push_str(include_str!("html_navigation.js"));
93        html.push_str("</script>\n");
94    }
95
96    html.push_str("</head>\n<body>\n");
97
98    // Title Slide (Presentation Title)
99    html.push_str("<div class=\"slide title-slide\" data-slide=\"0\">\n");
100    html.push_str(&format!("<h1>{}</h1>\n", presentation.get_title()));
101    html.push_str("</div>\n");
102
103    // Slides
104    for (i, slide) in presentation.slides().iter().enumerate() {
105        html.push_str(&render_slide_with_options(slide, i + 1, options));
106    }
107
108    // Navigation controls
109    if options.enable_navigation {
110        html.push_str("<div class=\"navigation-controls\">\n");
111        html.push_str("<button onclick=\"previousSlide()\" id=\"prevBtn\">Previous</button>\n");
112        html.push_str("<button onclick=\"nextSlide()\" id=\"nextBtn\">Next</button>\n");
113        html.push_str("<span id=\"slideCounter\"></span>\n");
114        html.push_str("</div>\n");
115    }
116
117    html.push_str("</body>\n</html>");
118
119    Ok(html)
120}
121
122fn render_slide_with_options(slide: &SlideContent, index: usize, options: &HtmlExportOptions) -> String {
123    let mut html = String::new();
124
125    html.push_str(&format!("<div class=\"slide\" id=\"slide-{}\" data-slide=\"{}\">\n", index, index));
126
127    // Slide Number
128    html.push_str(&format!("<div class=\"slide-number\">{}</div>\n", index));
129
130    // Title
131    html.push_str(&format!("<h2>{}</h2>\n", slide.title));
132
133    // Content Container
134    html.push_str("<div class=\"content\">\n");
135
136    // Bullets / Content
137    if !slide.content.is_empty() {
138        html.push_str("<ul>\n");
139        for item in &slide.content {
140            html.push_str(&format!("<li>{}</li>\n", item));
141        }
142        html.push_str("</ul>\n");
143    }
144
145    // Tables
146    if let Some(ref table) = slide.table {
147        html.push_str(&render_table_html(table));
148    }
149
150    // Images
151    for image in &slide.images {
152        if let Some(img_html) = render_image_with_options(image, options) {
153            html.push_str(&img_html);
154        }
155    }
156
157    // Code Blocks
158    for code in &slide.code_blocks {
159        if options.syntax_highlight {
160            html.push_str(&format!("<pre><code class=\"language-{}\">", code.language));
161        } else {
162            html.push_str("<pre><code>");
163        }
164        html.push_str(&escape_html(&code.code));
165        html.push_str("</code></pre>\n");
166    }
167
168    // Speaker Notes
169    if options.include_notes {
170        if let Some(ref notes) = slide.notes {
171            html.push_str(&format!("<div class=\"speaker-notes\"><strong>Notes:</strong> {}</div>\n", escape_html(notes)));
172        }
173    }
174
175    html.push_str("</div>\n"); // content
176    html.push_str("</div>\n"); // slide
177
178    html
179}
180
181/// Render a table as HTML
182fn render_table_html(_table: &crate::generator::Table) -> String {
183    let mut html = String::new();
184    html.push_str("<table class=\"ppt-table\">\n");
185
186    // Render rows (assuming table has row data structure)
187    // This is a simplified version - you may need to adapt based on actual Table structure
188    html.push_str("<tbody>\n");
189    html.push_str("<tr><td>Table content</td></tr>\n"); // Placeholder
190    html.push_str("</tbody>\n");
191
192    html.push_str("</table>\n");
193    html
194}
195
196/// Escape HTML entities
197fn escape_html(text: &str) -> String {
198    text.replace('&', "&amp;")
199        .replace('<', "&lt;")
200        .replace('>', "&gt;")
201        .replace('"', "&quot;")
202        .replace('\'', "&#39;")
203}
204
205fn render_image_with_options(image: &Image, options: &HtmlExportOptions) -> Option<String> {
206    let bytes = image.get_bytes()?;
207    let mime = match image.format.to_lowercase().as_str() {
208        "jpg" | "jpeg" => "image/jpeg",
209        "png" => "image/png",
210        "gif" => "image/gif",
211        "svg" => "image/svg+xml",
212        _ => "application/octet-stream",
213    };
214
215    if options.export_images_as_files {
216        // Generate image filename and reference
217        let filename = format!("slide_img_{}.{}", image.filename, image.format);
218        let filepath = if let Some(ref dir) = options.image_output_dir {
219            format!("{}/{}", dir, filename)
220        } else {
221            filename.clone()
222        };
223
224        Some(format!(
225            "<div class=\"image-container\"><img src=\"{}\" alt=\"{}\" /></div>\n",
226            filepath, image.filename
227        ))
228    } else {
229        // Use base64 encoding
230        let b64 = base64_encode(&bytes);
231        Some(format!(
232            "<div class=\"image-container\"><img src=\"data:{};base64,{}\" alt=\"{}\" /></div>\n",
233            mime, b64, image.filename
234        ))
235    }
236}
237
238// Simple base64 encoder
239fn base64_encode(data: &[u8]) -> String {
240    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
241    let mut output = String::with_capacity(data.len() * 4 / 3 + 4);
242    
243    let mut i = 0;
244    while i < data.len() {
245        let mut buf = [0u8; 3];
246        let mut len = 0;
247        
248        for j in 0..3 {
249            if i + j < data.len() {
250                buf[j] = data[i + j];
251                len += 1;
252            }
253        }
254        
255        let b0 = (buf[0] >> 2) & 0x3F;
256        let b1 = ((buf[0] & 0x03) << 4) | ((buf[1] >> 4) & 0x0F);
257        let b2 = ((buf[1] & 0x0F) << 2) | ((buf[2] >> 6) & 0x03);
258        let b3 = buf[2] & 0x3F;
259        
260        output.push(ALPHABET[b0 as usize] as char);
261        output.push(ALPHABET[b1 as usize] as char);
262        
263        if len > 1 {
264            output.push(ALPHABET[b2 as usize] as char);
265        } else {
266            output.push('=');
267        }
268        
269        if len > 2 {
270            output.push(ALPHABET[b3 as usize] as char);
271        } else {
272            output.push('=');
273        }
274        
275        i += 3;
276    }
277    
278    output
279}