use crate::primitives::highlighter::HighlightSpan;
use crate::view::theme::Theme;
use ratatui::style::Color;
fn color_to_css(color: Color, default: &str) -> String {
match color {
Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b),
Color::Black => "#000000".to_string(),
Color::Red => "#cd3131".to_string(),
Color::Green => "#0dbc79".to_string(),
Color::Yellow => "#e5e510".to_string(),
Color::Blue => "#2472c8".to_string(),
Color::Magenta => "#bc3fbc".to_string(),
Color::Cyan => "#11a8cd".to_string(),
Color::Gray => "#808080".to_string(),
Color::DarkGray => "#505050".to_string(),
Color::LightRed => "#f14c4c".to_string(),
Color::LightGreen => "#23d18b".to_string(),
Color::LightYellow => "#f5f543".to_string(),
Color::LightBlue => "#3b8eea".to_string(),
Color::LightMagenta => "#d670d6".to_string(),
Color::LightCyan => "#29b8db".to_string(),
Color::White => "#e5e5e5".to_string(),
Color::Reset | Color::Indexed(_) => default.to_string(),
}
}
pub fn render_styled_html(text: &str, highlight_spans: &[HighlightSpan], theme: &Theme) -> String {
let bg_color = color_to_css(theme.editor_bg, "#1e1e1e");
let fg_color = color_to_css(theme.editor_fg, "#d4d4d4");
let mut color_map: Vec<Option<Color>> = vec![None; text.len()];
for span in highlight_spans {
let start = span.range.start.min(text.len());
let end = span.range.end.min(text.len());
for slot in &mut color_map[start..end] {
*slot = Some(span.color);
}
}
let mut html = String::new();
html.push_str(&format!(
"<pre style=\"background-color:{};color:{};font-family:'Fira Mono','Fira Code',Consolas,'Courier New',monospace;font-size:14px;padding:12px 16px;border-radius:6px;margin:0;white-space:pre;overflow-x:auto;\">",
bg_color, fg_color
));
let mut current_color: Option<Color> = None;
let mut span_open = false;
let mut byte_offset = 0;
for ch in text.chars() {
let char_byte_len = ch.len_utf8();
let char_color = if byte_offset < color_map.len() {
color_map[byte_offset]
} else {
None
};
if char_color != current_color {
if span_open {
html.push_str("</span>");
span_open = false;
}
if let Some(color) = char_color {
let css_color = color_to_css(color, &fg_color);
html.push_str(&format!("<span style=\"color:{};\">", css_color));
span_open = true;
}
current_color = char_color;
}
match ch {
'<' => html.push_str("<"),
'>' => html.push_str(">"),
'&' => html.push_str("&"),
'"' => html.push_str("""),
'\'' => html.push_str("'"),
_ => html.push(ch),
}
byte_offset += char_byte_len;
}
if span_open {
html.push_str("</span>");
}
html.push_str("</pre>");
html
}
#[cfg(test)]
mod tests {
use super::*;
use crate::view::theme;
#[test]
fn test_render_html_simple() {
let text = "Hello, World!";
let spans = vec![];
let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
let html = render_styled_html(text, &spans, &theme);
assert!(html.starts_with("<pre style=\""));
assert!(html.ends_with("</pre>"));
assert!(html.contains("Hello, World!"));
}
#[test]
fn test_render_html_escapes_special_chars() {
let text = "<script>&test</script>";
let spans = vec![];
let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
let html = render_styled_html(text, &spans, &theme);
assert!(html.contains("<script>"));
assert!(html.contains("&test"));
assert!(!html.contains("<script>"));
}
#[test]
fn test_render_html_with_highlights() {
use std::ops::Range;
let text = "fn main()";
let spans = vec![HighlightSpan {
range: Range { start: 0, end: 2 },
color: Color::Blue,
}];
let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
let html = render_styled_html(text, &spans, &theme);
assert!(html.contains("<span style=\"color:#2472c8;\">fn</span>"));
assert!(html.contains("main()"));
}
#[test]
fn test_color_to_css() {
assert_eq!(color_to_css(Color::Black, "#fff"), "#000000");
assert_eq!(color_to_css(Color::Rgb(255, 128, 0), "#fff"), "#ff8000");
assert_eq!(color_to_css(Color::Reset, "#default"), "#default");
}
}