use crate::api::Presentation;
use crate::generator::{SlideContent, Image};
use crate::exc::Result;
#[derive(Clone, Debug)]
pub struct HtmlExportOptions {
pub include_notes: bool,
pub enable_navigation: bool,
pub syntax_highlight: bool,
pub export_images_as_files: bool,
pub image_output_dir: Option<String>,
}
impl Default for HtmlExportOptions {
fn default() -> Self {
Self {
include_notes: true,
enable_navigation: true,
syntax_highlight: true,
export_images_as_files: false,
image_output_dir: None,
}
}
}
impl HtmlExportOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_notes(mut self, include: bool) -> Self {
self.include_notes = include;
self
}
pub fn with_navigation(mut self, enable: bool) -> Self {
self.enable_navigation = enable;
self
}
pub fn with_syntax_highlight(mut self, enable: bool) -> Self {
self.syntax_highlight = enable;
self
}
pub fn with_image_files(mut self, enable: bool, dir: Option<&str>) -> Self {
self.export_images_as_files = enable;
self.image_output_dir = dir.map(|d| d.to_string());
self
}
}
pub fn export_to_html(presentation: &Presentation) -> Result<String> {
export_to_html_with_options(presentation, &HtmlExportOptions::default())
}
pub fn export_to_html_with_options(presentation: &Presentation, options: &HtmlExportOptions) -> Result<String> {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
html.push_str("<meta charset=\"UTF-8\">\n");
html.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
html.push_str(&format!("<title>{}</title>\n", presentation.get_title()));
html.push_str("<style>\n");
html.push_str(include_str!("html_style.css"));
if options.enable_navigation {
html.push_str(include_str!("html_navigation.css"));
}
if options.include_notes {
html.push_str(include_str!("html_notes.css"));
}
html.push_str("</style>\n");
if options.enable_navigation {
html.push_str("<script>\n");
html.push_str(include_str!("html_navigation.js"));
html.push_str("</script>\n");
}
html.push_str("</head>\n<body>\n");
html.push_str("<div class=\"slide title-slide\" data-slide=\"0\">\n");
html.push_str(&format!("<h1>{}</h1>\n", presentation.get_title()));
html.push_str("</div>\n");
for (i, slide) in presentation.slides().iter().enumerate() {
html.push_str(&render_slide_with_options(slide, i + 1, options));
}
if options.enable_navigation {
html.push_str("<div class=\"navigation-controls\">\n");
html.push_str("<button onclick=\"previousSlide()\" id=\"prevBtn\">Previous</button>\n");
html.push_str("<button onclick=\"nextSlide()\" id=\"nextBtn\">Next</button>\n");
html.push_str("<span id=\"slideCounter\"></span>\n");
html.push_str("</div>\n");
}
html.push_str("</body>\n</html>");
Ok(html)
}
fn render_slide_with_options(slide: &SlideContent, index: usize, options: &HtmlExportOptions) -> String {
let mut html = String::new();
html.push_str(&format!("<div class=\"slide\" id=\"slide-{}\" data-slide=\"{}\">\n", index, index));
html.push_str(&format!("<div class=\"slide-number\">{}</div>\n", index));
html.push_str(&format!("<h2>{}</h2>\n", slide.title));
html.push_str("<div class=\"content\">\n");
if !slide.content.is_empty() {
html.push_str("<ul>\n");
for item in &slide.content {
html.push_str(&format!("<li>{}</li>\n", item));
}
html.push_str("</ul>\n");
}
if let Some(ref table) = slide.table {
html.push_str(&render_table_html(table));
}
for image in &slide.images {
if let Some(img_html) = render_image_with_options(image, options) {
html.push_str(&img_html);
}
}
for code in &slide.code_blocks {
if options.syntax_highlight {
html.push_str(&format!("<pre><code class=\"language-{}\">", code.language));
} else {
html.push_str("<pre><code>");
}
html.push_str(&escape_html(&code.code));
html.push_str("</code></pre>\n");
}
if options.include_notes {
if let Some(ref notes) = slide.notes {
html.push_str(&format!("<div class=\"speaker-notes\"><strong>Notes:</strong> {}</div>\n", escape_html(notes)));
}
}
html.push_str("</div>\n"); html.push_str("</div>\n");
html
}
fn render_table_html(_table: &crate::generator::Table) -> String {
let mut html = String::new();
html.push_str("<table class=\"ppt-table\">\n");
html.push_str("<tbody>\n");
html.push_str("<tr><td>Table content</td></tr>\n"); html.push_str("</tbody>\n");
html.push_str("</table>\n");
html
}
fn escape_html(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn render_image_with_options(image: &Image, options: &HtmlExportOptions) -> Option<String> {
let bytes = image.get_bytes()?;
let mime = match image.format.to_lowercase().as_str() {
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"gif" => "image/gif",
"svg" => "image/svg+xml",
_ => "application/octet-stream",
};
if options.export_images_as_files {
let filename = format!("slide_img_{}.{}", image.filename, image.format);
let filepath = if let Some(ref dir) = options.image_output_dir {
format!("{}/{}", dir, filename)
} else {
filename.clone()
};
Some(format!(
"<div class=\"image-container\"><img src=\"{}\" alt=\"{}\" /></div>\n",
filepath, image.filename
))
} else {
let b64 = base64_encode(&bytes);
Some(format!(
"<div class=\"image-container\"><img src=\"data:{};base64,{}\" alt=\"{}\" /></div>\n",
mime, b64, image.filename
))
}
}
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut output = String::with_capacity(data.len() * 4 / 3 + 4);
let mut i = 0;
while i < data.len() {
let mut buf = [0u8; 3];
let mut len = 0;
for j in 0..3 {
if i + j < data.len() {
buf[j] = data[i + j];
len += 1;
}
}
let b0 = (buf[0] >> 2) & 0x3F;
let b1 = ((buf[0] & 0x03) << 4) | ((buf[1] >> 4) & 0x0F);
let b2 = ((buf[1] & 0x0F) << 2) | ((buf[2] >> 6) & 0x03);
let b3 = buf[2] & 0x3F;
output.push(ALPHABET[b0 as usize] as char);
output.push(ALPHABET[b1 as usize] as char);
if len > 1 {
output.push(ALPHABET[b2 as usize] as char);
} else {
output.push('=');
}
if len > 2 {
output.push(ALPHABET[b3 as usize] as char);
} else {
output.push('=');
}
i += 3;
}
output
}