|
1 | | -//! Convert ANSI escape codes to SVG |
| 1 | +//! Convert ANSI escape codes to SVG and HTML |
2 | 2 | //! |
3 | 3 | //! See [`Term`] |
4 | 4 | //! |
5 | | -//! # Example |
| 5 | +//! # SVG Example |
6 | 6 | //! |
7 | 7 | //! ``` |
8 | 8 | //! # use anstyle_svg::Term; |
|
11 | 11 | //! ``` |
12 | 12 | //! |
13 | 13 | //!  |
| 14 | +//! |
| 15 | +//! # HTML Example |
| 16 | +//! |
| 17 | +//! ``` |
| 18 | +//! # use anstyle_svg::Term; |
| 19 | +//! let vte = std::fs::read_to_string("tests/rainbow.vte").unwrap(); |
| 20 | +//! let html = Term::new().render_html(&vte); |
| 21 | +//! ``` |
14 | 22 |
|
15 | 23 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] |
16 | 24 | #![warn(missing_docs)] |
@@ -200,6 +208,113 @@ impl Term { |
200 | 208 | writeln!(&mut buffer, r#"</svg>"#).unwrap(); |
201 | 209 | buffer |
202 | 210 | } |
| 211 | + |
| 212 | + /// Render the HTML with the terminal defined |
| 213 | + /// |
| 214 | + /// **Note:** Lines are not wrapped. This is intentional as this attempts to convey the exact |
| 215 | + /// output with escape codes translated to HTML elements. |
| 216 | + pub fn render_html(&self, ansi: &str) -> String { |
| 217 | + use std::fmt::Write as _; |
| 218 | + |
| 219 | + const FG: &str = "fg"; |
| 220 | + const BG: &str = "bg"; |
| 221 | + |
| 222 | + let mut styled = adapter::AnsiBytes::new(); |
| 223 | + let mut elements = styled.extract_next(ansi.as_bytes()).collect::<Vec<_>>(); |
| 224 | + preprocess_invert_style(&mut elements, self.bg_color, self.fg_color); |
| 225 | + |
| 226 | + let styled_lines = split_lines(&elements); |
| 227 | + |
| 228 | + let fg_color = rgb_value(self.fg_color, self.palette); |
| 229 | + let bg_color = rgb_value(self.bg_color, self.palette); |
| 230 | + let font_family = self.font_family; |
| 231 | + |
| 232 | + let line_height = 18; |
| 233 | + |
| 234 | + let mut buffer = String::new(); |
| 235 | + writeln!(&mut buffer, r#"<!DOCTYPE html>"#).unwrap(); |
| 236 | + writeln!(&mut buffer, r#"<html>"#).unwrap(); |
| 237 | + writeln!(&mut buffer, r#"<head>"#).unwrap(); |
| 238 | + writeln!(&mut buffer, r#" <meta charset="UTF-8">"#).unwrap(); |
| 239 | + writeln!( |
| 240 | + &mut buffer, |
| 241 | + r#" <meta name="viewport" content="width=device-width, initial-scale=1.0">"# |
| 242 | + ) |
| 243 | + .unwrap(); |
| 244 | + writeln!( |
| 245 | + &mut buffer, |
| 246 | + r#" <meta http-equiv="X-UA-Compatible" content="ie=edge">"# |
| 247 | + ) |
| 248 | + .unwrap(); |
| 249 | + writeln!(&mut buffer, r#" <style>"#).unwrap(); |
| 250 | + writeln!(&mut buffer, r#" .{FG} {{ color: {fg_color} }}"#).unwrap(); |
| 251 | + writeln!(&mut buffer, r#" .{BG} {{ background: {bg_color} }}"#).unwrap(); |
| 252 | + for (name, rgb) in color_styles(&elements, self.palette) { |
| 253 | + if name.starts_with(FG_PREFIX) { |
| 254 | + writeln!(&mut buffer, r#" .{name} {{ color: {rgb} }}"#).unwrap(); |
| 255 | + } |
| 256 | + if name.starts_with(BG_PREFIX) { |
| 257 | + writeln!( |
| 258 | + &mut buffer, |
| 259 | + r#" .{name} {{ background: {rgb}; user-select: none; }}"# |
| 260 | + ) |
| 261 | + .unwrap(); |
| 262 | + } |
| 263 | + if name.starts_with(UNDERLINE_PREFIX) { |
| 264 | + writeln!( |
| 265 | + &mut buffer, |
| 266 | + r#" .{name} {{ text-decoration-line: underline; text-decoration-color: {rgb} }}"# |
| 267 | + ) |
| 268 | + .unwrap(); |
| 269 | + } |
| 270 | + } |
| 271 | + writeln!(&mut buffer, r#" .container {{"#).unwrap(); |
| 272 | + writeln!(&mut buffer, r#" line-height: {line_height}px;"#).unwrap(); |
| 273 | + writeln!(&mut buffer, r#" }}"#).unwrap(); |
| 274 | + write_effects_in_use(&mut buffer, &elements); |
| 275 | + writeln!(&mut buffer, r#" span {{"#).unwrap(); |
| 276 | + writeln!(&mut buffer, r#" font: 14px {font_family};"#).unwrap(); |
| 277 | + writeln!(&mut buffer, r#" white-space: pre;"#).unwrap(); |
| 278 | + writeln!(&mut buffer, r#" line-height: {line_height}px;"#).unwrap(); |
| 279 | + writeln!(&mut buffer, r#" }}"#).unwrap(); |
| 280 | + writeln!(&mut buffer, r#" </style>"#).unwrap(); |
| 281 | + writeln!(&mut buffer, r#"</head>"#).unwrap(); |
| 282 | + writeln!(&mut buffer).unwrap(); |
| 283 | + |
| 284 | + if !self.background { |
| 285 | + writeln!(&mut buffer, r#"<body>"#).unwrap(); |
| 286 | + } else { |
| 287 | + writeln!(&mut buffer, r#"<body class="{BG}">"#).unwrap(); |
| 288 | + } |
| 289 | + writeln!(&mut buffer).unwrap(); |
| 290 | + |
| 291 | + writeln!(&mut buffer, r#" <div class="container {FG}">"#).unwrap(); |
| 292 | + for line in &styled_lines { |
| 293 | + if line.iter().any(|e| e.style.get_bg_color().is_some()) { |
| 294 | + for element in line { |
| 295 | + if element.text.is_empty() { |
| 296 | + continue; |
| 297 | + } |
| 298 | + write_bg_span(&mut buffer, "span", &element.style, &element.text); |
| 299 | + } |
| 300 | + writeln!(&mut buffer, r#"<br />"#).unwrap(); |
| 301 | + } |
| 302 | + |
| 303 | + for element in line { |
| 304 | + if element.text.is_empty() { |
| 305 | + continue; |
| 306 | + } |
| 307 | + write_fg_span(&mut buffer, "span", element, &element.text); |
| 308 | + } |
| 309 | + writeln!(&mut buffer, r#"<br />"#).unwrap(); |
| 310 | + } |
| 311 | + writeln!(&mut buffer, r#" </div>"#).unwrap(); |
| 312 | + writeln!(&mut buffer).unwrap(); |
| 313 | + |
| 314 | + writeln!(&mut buffer, r#"</body>"#).unwrap(); |
| 315 | + writeln!(&mut buffer, r#"</html>"#).unwrap(); |
| 316 | + buffer |
| 317 | + } |
203 | 318 | } |
204 | 319 |
|
205 | 320 | const FG_COLOR: anstyle::Color = anstyle::Color::Ansi(anstyle::AnsiColor::White); |
|
0 commit comments