fresh/services/
styled_html.rs1use crate::primitives::highlighter::HighlightSpan;
7use crate::view::theme::Theme;
8use ratatui::style::Color;
9
10fn color_to_css(color: Color, default: &str) -> String {
12 match color {
13 Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b),
14 Color::Black => "#000000".to_string(),
15 Color::Red => "#cd3131".to_string(),
16 Color::Green => "#0dbc79".to_string(),
17 Color::Yellow => "#e5e510".to_string(),
18 Color::Blue => "#2472c8".to_string(),
19 Color::Magenta => "#bc3fbc".to_string(),
20 Color::Cyan => "#11a8cd".to_string(),
21 Color::Gray => "#808080".to_string(),
22 Color::DarkGray => "#505050".to_string(),
23 Color::LightRed => "#f14c4c".to_string(),
24 Color::LightGreen => "#23d18b".to_string(),
25 Color::LightYellow => "#f5f543".to_string(),
26 Color::LightBlue => "#3b8eea".to_string(),
27 Color::LightMagenta => "#d670d6".to_string(),
28 Color::LightCyan => "#29b8db".to_string(),
29 Color::White => "#e5e5e5".to_string(),
30 Color::Reset | Color::Indexed(_) => default.to_string(),
31 }
32}
33
34pub fn render_styled_html(text: &str, highlight_spans: &[HighlightSpan], theme: &Theme) -> String {
48 let bg_color = color_to_css(theme.editor_bg, "#1e1e1e");
49 let fg_color = color_to_css(theme.editor_fg, "#d4d4d4");
50
51 let mut color_map: Vec<Option<Color>> = vec![None; text.len()];
53 for span in highlight_spans {
54 let start = span.range.start.min(text.len());
55 let end = span.range.end.min(text.len());
56 for slot in &mut color_map[start..end] {
57 *slot = Some(span.color);
58 }
59 }
60
61 let mut html = String::new();
63 html.push_str(&format!(
64 "<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;\">",
65 bg_color, fg_color
66 ));
67
68 let mut current_color: Option<Color> = None;
69 let mut span_open = false;
70 let mut byte_offset = 0;
71
72 for ch in text.chars() {
73 let char_byte_len = ch.len_utf8();
74
75 let char_color = if byte_offset < color_map.len() {
77 color_map[byte_offset]
78 } else {
79 None
80 };
81
82 if char_color != current_color {
84 if span_open {
86 html.push_str("</span>");
87 span_open = false;
88 }
89
90 if let Some(color) = char_color {
92 let css_color = color_to_css(color, &fg_color);
93 html.push_str(&format!("<span style=\"color:{};\">", css_color));
94 span_open = true;
95 }
96
97 current_color = char_color;
98 }
99
100 match ch {
102 '<' => html.push_str("<"),
103 '>' => html.push_str(">"),
104 '&' => html.push_str("&"),
105 '"' => html.push_str("""),
106 '\'' => html.push_str("'"),
107 _ => html.push(ch),
108 }
109
110 byte_offset += char_byte_len;
111 }
112
113 if span_open {
115 html.push_str("</span>");
116 }
117
118 html.push_str("</pre>");
119 html
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::view::theme;
126
127 #[test]
128 fn test_render_html_simple() {
129 let text = "Hello, World!";
130 let spans = vec![];
131 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
132
133 let html = render_styled_html(text, &spans, &theme);
134
135 assert!(html.starts_with("<pre style=\""));
136 assert!(html.ends_with("</pre>"));
137 assert!(html.contains("Hello, World!"));
138 }
139
140 #[test]
141 fn test_render_html_escapes_special_chars() {
142 let text = "<script>&test</script>";
143 let spans = vec![];
144 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
145
146 let html = render_styled_html(text, &spans, &theme);
147
148 assert!(html.contains("<script>"));
149 assert!(html.contains("&test"));
150 assert!(!html.contains("<script>"));
151 }
152
153 #[test]
154 fn test_render_html_with_highlights() {
155 use std::ops::Range;
156
157 let text = "fn main()";
158 let spans = vec![HighlightSpan {
159 range: Range { start: 0, end: 2 },
160 color: Color::Blue,
161 category: None,
162 }];
163 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
164
165 let html = render_styled_html(text, &spans, &theme);
166
167 assert!(html.contains("<span style=\"color:#2472c8;\">fn</span>"));
169 assert!(html.contains("main()"));
170 }
171
172 #[test]
173 fn test_color_to_css() {
174 assert_eq!(color_to_css(Color::Black, "#fff"), "#000000");
175 assert_eq!(color_to_css(Color::Rgb(255, 128, 0), "#fff"), "#ff8000");
176 assert_eq!(color_to_css(Color::Reset, "#default"), "#default");
177 }
178}