Skip to content

Commit 7a0eaaa

Browse files
committed
feat: Add HTML rendering
1 parent 0d17a12 commit 7a0eaaa

6 files changed

Lines changed: 2493 additions & 2 deletions

File tree

crates/anstyle-svg/src/lib.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
//! Convert ANSI escape codes to SVG
1+
//! Convert ANSI escape codes to SVG and HTML
22
//!
33
//! See [`Term`]
44
//!
5-
//! # Example
5+
//! # SVG Example
66
//!
77
//! ```
88
//! # use anstyle_svg::Term;
@@ -11,6 +11,14 @@
1111
//! ```
1212
//!
1313
//! ![demo of supported styles](https://raw.githubusercontent.com/rust-cli/anstyle/main/crates/anstyle-svg/tests/rainbow.svg "Example output")
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+
//! ```
1422
1523
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
1624
#![warn(missing_docs)]
@@ -200,6 +208,113 @@ impl Term {
200208
writeln!(&mut buffer, r#"</svg>"#).unwrap();
201209
buffer
202210
}
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+
}
203318
}
204319

205320
const FG_COLOR: anstyle::Color = anstyle::Color::Ansi(anstyle::AnsiColor::White);

0 commit comments

Comments
 (0)