//! HTML output formatter with math rendering support use super::{HtmlEngine, LineData}; /// HTML formatter with math rendering pub struct HtmlFormatter { engine: HtmlEngine, css_styling: bool, accessibility: bool, responsive: bool, theme: HtmlTheme, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HtmlTheme { Light, Dark, Auto, } impl HtmlFormatter { pub fn new() -> Self { Self { engine: HtmlEngine::MathJax, css_styling: true, accessibility: true, responsive: true, theme: HtmlTheme::Light, } } pub fn with_engine(mut self, engine: HtmlEngine) -> Self { self.engine = engine; self } pub fn with_styling(mut self, styling: bool) -> Self { self.css_styling = styling; self } pub fn accessibility(mut self, enabled: bool) -> Self { self.accessibility = enabled; self } pub fn responsive(mut self, enabled: bool) -> Self { self.responsive = enabled; self } pub fn theme(mut self, theme: HtmlTheme) -> Self { self.theme = theme; self } /// Format content to HTML pub fn format(&self, content: &str, lines: Option<&[LineData]>) -> String { let mut html = String::new(); // HTML header with math rendering scripts html.push_str(&self.html_header()); // Body start with theme class html.push_str("\n"); // Main content container html.push_str(r#"
"#); html.push_str("\n"); // Format content if let Some(line_data) = lines { html.push_str(&self.format_lines(line_data)); } else { html.push_str(&self.format_text(content)); } html.push_str("
\n"); html.push_str("\n"); html } /// Generate HTML header with scripts and styles fn html_header(&self) -> String { let mut header = String::from("\n\n\n"); header.push_str(r#" "#); header.push_str("\n"); if self.responsive { header.push_str( r#" "#, ); header.push_str("\n"); } header.push_str(" Mathematical Content\n"); // Math rendering scripts match self.engine { HtmlEngine::MathJax => { header.push_str(r#" "#); header.push_str("\n"); header.push_str(r#" "#); header.push_str("\n"); header.push_str(" \n"); } HtmlEngine::KaTeX => { header.push_str(r#" "#); header.push_str("\n"); header.push_str(r#" "#); header.push_str("\n"); header.push_str(r#" "#); header.push_str("\n"); } HtmlEngine::Raw => { // No math rendering } } // CSS styling if self.css_styling { header.push_str(" \n"); } header.push_str("\n"); header } /// Generate CSS styles fn generate_css(&self) -> String { let mut css = String::new(); css.push_str(" body {\n"); css.push_str(" font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n"); css.push_str(" line-height: 1.6;\n"); css.push_str(" max-width: 800px;\n"); css.push_str(" margin: 0 auto;\n"); css.push_str(" padding: 20px;\n"); css.push_str(" }\n"); // Theme colors match self.theme { HtmlTheme::Light => { css.push_str(" body.theme-light {\n"); css.push_str(" background-color: #ffffff;\n"); css.push_str(" color: #333333;\n"); css.push_str(" }\n"); } HtmlTheme::Dark => { css.push_str(" body.theme-dark {\n"); css.push_str(" background-color: #1e1e1e;\n"); css.push_str(" color: #d4d4d4;\n"); css.push_str(" }\n"); } HtmlTheme::Auto => { css.push_str(" @media (prefers-color-scheme: dark) {\n"); css.push_str(" body { background-color: #1e1e1e; color: #d4d4d4; }\n"); css.push_str(" }\n"); } } css.push_str(" .content { padding: 20px; }\n"); css.push_str(" .math-display { text-align: center; margin: 20px 0; }\n"); css.push_str(" .math-inline { display: inline; }\n"); css.push_str(" .equation-block { margin: 15px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }\n"); css.push_str(" table { border-collapse: collapse; width: 100%; margin: 20px 0; }\n"); css.push_str( " th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n", ); css.push_str(" th { background-color: #f2f2f2; }\n"); if self.accessibility { css.push_str(" .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }\n"); } css } /// Format plain text to HTML fn format_text(&self, text: &str) -> String { let escaped = self.escape_html(text); // Convert math delimiters if present let mut html = escaped; // Display math $$...$$ html = html.replace("$$", "
$$"); html = html.replace("$$", "$$
"); // Inline math $...$ // This is simplistic - a real implementation would need proper parsing format!("

{}

", html) } /// Format line data to HTML fn format_lines(&self, lines: &[LineData]) -> String { let mut html = String::new(); for line in lines { match line.line_type.as_str() { "text" => { html.push_str("

"); html.push_str(&self.escape_html(&line.text)); html.push_str("

\n"); } "math" | "equation" => { let latex = line.latex.as_ref().unwrap_or(&line.text); html.push_str(r#"
"#); if self.accessibility { html.push_str(&format!( r#"Equation: {}"#, self.escape_html(&line.text) )); } html.push_str(&format!("$${}$$", latex)); html.push_str("
\n"); } "inline_math" => { let latex = line.latex.as_ref().unwrap_or(&line.text); html.push_str(&format!(r#"${}$"#, latex)); } "heading" => { html.push_str(&format!("

{}

\n", self.escape_html(&line.text))); } "table" => { html.push_str(&self.format_table(&line.text)); } "image" => { html.push_str(&format!( r#"Image"#, self.escape_html(&line.text) )); html.push_str("\n"); } _ => { html.push_str("

"); html.push_str(&self.escape_html(&line.text)); html.push_str("

\n"); } } } html } /// Format table to HTML fn format_table(&self, table: &str) -> String { let mut html = String::from("\n"); let rows: Vec<&str> = table.lines().collect(); for (i, row) in rows.iter().enumerate() { html.push_str(" \n"); let cells: Vec<&str> = row .split('|') .map(|s| s.trim()) .filter(|s| !s.is_empty()) .collect(); let tag = if i == 0 { "th" } else { "td" }; for cell in cells { html.push_str(&format!( " <{}>{}\n", tag, self.escape_html(cell), tag )); } html.push_str(" \n"); } html.push_str("
\n"); html } /// Escape HTML special characters fn escape_html(&self, text: &str) -> String { text.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } } impl Default for HtmlFormatter { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::output::BoundingBox; #[test] fn test_html_header() { let formatter = HtmlFormatter::new().with_engine(HtmlEngine::MathJax); let header = formatter.html_header(); assert!(header.contains("")); assert!(header.contains("MathJax")); } #[test] fn test_katex_header() { let formatter = HtmlFormatter::new().with_engine(HtmlEngine::KaTeX); let header = formatter.html_header(); assert!(header.contains("katex")); } #[test] fn test_escape_html() { let formatter = HtmlFormatter::new(); let result = formatter.escape_html(""); assert!(result.contains("<")); assert!(result.contains(">")); assert!(!result.contains("