use crate::error::Result;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use super::SvgEncoder;
#[derive(Debug, Clone)]
pub struct HtmlExporter {
title: String,
svg_content: String,
width: u32,
#[allow(dead_code)]
height: u32,
dark_mode: bool,
responsive: bool,
}
impl HtmlExporter {
#[must_use]
pub fn from_svg(svg: &SvgEncoder) -> Self {
Self {
title: "Chart".to_string(),
svg_content: svg.to_string(),
width: svg.width(),
height: svg.height(),
dark_mode: false,
responsive: true,
}
}
#[must_use]
pub fn from_svg_string(svg: String, width: u32, height: u32) -> Self {
Self {
title: "Chart".to_string(),
svg_content: svg,
width,
height,
dark_mode: false,
responsive: true,
}
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn dark_mode(mut self, enabled: bool) -> Self {
self.dark_mode = enabled;
self
}
#[must_use]
pub fn responsive(mut self, enabled: bool) -> Self {
self.responsive = enabled;
self
}
#[must_use]
pub fn to_html(&self) -> String {
let dark_mode_css = if self.dark_mode {
r"
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #e0e0e0;
}
.chart-container {
background-color: #2d2d2d;
}
}
"
} else {
""
};
let responsive_css = if self.responsive {
r"
.chart-container svg {
max-width: 100%;
height: auto;
}
"
} else {
""
};
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
}}
.chart-container {{
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
max-width: {width}px;
}}
.chart-title {{
text-align: center;
margin-bottom: 15px;
font-size: 1.2em;
font-weight: 600;
color: #333;
}}{dark_mode_css}{responsive_css}
</style>
</head>
<body>
<div class="chart-container">
<h1 class="chart-title">{title}</h1>
{svg}
</div>
<!-- Generated by trueno-viz -->
<!-- https://github.com/paiml/trueno-viz -->
</body>
</html>
"#,
title = self.title,
width = self.width + 40, svg = self.svg_content,
dark_mode_css = dark_mode_css,
responsive_css = responsive_css,
)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let html = self.to_html();
let mut file = File::create(path)?;
file.write_all(html.as_bytes())?;
Ok(())
}
}
impl std::fmt::Display for HtmlExporter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_html())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Rgba;
#[test]
fn test_html_exporter_basic() {
let svg = SvgEncoder::new(400, 300).rect(10.0, 10.0, 100.0, 50.0, Rgba::BLUE);
let html = HtmlExporter::from_svg(&svg).title("Test Chart").to_html();
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("<title>Test Chart</title>"));
assert!(html.contains("<svg"));
assert!(html.contains("trueno-viz"));
}
#[test]
fn test_html_exporter_dark_mode() {
let svg = SvgEncoder::new(400, 300);
let html = HtmlExporter::from_svg(&svg).dark_mode(true).to_html();
assert!(html.contains("prefers-color-scheme: dark"));
}
#[test]
fn test_html_exporter_responsive() {
let svg = SvgEncoder::new(400, 300);
let html = HtmlExporter::from_svg(&svg).responsive(true).to_html();
assert!(html.contains("max-width: 100%"));
}
#[test]
fn test_html_exporter_display() {
let svg = SvgEncoder::new(400, 300);
let exporter = HtmlExporter::from_svg(&svg);
let display_str = format!("{exporter}");
assert!(display_str.contains("<!DOCTYPE html>"));
}
#[test]
fn test_html_exporter_from_svg_string() {
let svg_str =
r#"<svg width="100" height="100"><rect x="0" y="0" width="100" height="100"/></svg>"#;
let exporter = HtmlExporter::from_svg_string(svg_str.to_string(), 100, 100);
let html = exporter.to_html();
assert!(html.contains(svg_str));
}
#[test]
fn test_html_exporter_dark_mode_disabled() {
let svg = SvgEncoder::new(400, 300);
let html = HtmlExporter::from_svg(&svg).dark_mode(false).to_html();
assert!(!html.contains("prefers-color-scheme: dark"));
}
#[test]
fn test_html_exporter_responsive_disabled() {
let svg = SvgEncoder::new(400, 300);
let html = HtmlExporter::from_svg(&svg).responsive(false).to_html();
assert!(!html.contains("max-width: 100%"));
assert!(!html.contains("height: auto"));
}
#[test]
fn test_html_exporter_save() {
let svg = SvgEncoder::new(400, 300);
let exporter = HtmlExporter::from_svg(&svg).title("Save Test");
let temp_path = std::env::temp_dir().join("test_chart.html");
let result = exporter.save(&temp_path);
assert!(result.is_ok());
let content = std::fs::read_to_string(&temp_path).expect("file read should succeed");
assert!(content.contains("Save Test"));
let _ = std::fs::remove_file(&temp_path);
}
#[test]
fn test_html_exporter_clone() {
let svg = SvgEncoder::new(400, 300);
let exporter =
HtmlExporter::from_svg(&svg).title("Clone Test").dark_mode(true).responsive(false);
let cloned = exporter.clone();
assert_eq!(exporter.to_html(), cloned.to_html());
}
#[test]
fn test_html_exporter_all_options() {
let svg = SvgEncoder::new(800, 600).rect(0.0, 0.0, 800.0, 600.0, Rgba::WHITE);
let html = HtmlExporter::from_svg(&svg)
.title("Full Featured Chart")
.dark_mode(true)
.responsive(true)
.to_html();
assert!(html.contains("Full Featured Chart"));
assert!(html.contains("prefers-color-scheme: dark"));
assert!(html.contains("max-width: 100%"));
assert!(html.contains("trueno-viz"));
}
}