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;
1111//! ```
1212//!
1313//! 
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) ]
@@ -92,18 +100,8 @@ impl Term {
92100
93101 let mut styled = adapter:: AnsiBytes :: new ( ) ;
94102 let mut elements = styled. extract_next ( ansi. as_bytes ( ) ) . collect :: < Vec < _ > > ( ) ;
95- let mut effects_in_use = anstyle:: Effects :: new ( ) ;
96- for element in & mut elements {
97- let style = & mut element. style ;
98- // Pre-process INVERT to make fg/bg calculations easier
99- if style. get_effects ( ) . contains ( anstyle:: Effects :: INVERT ) {
100- * style = style
101- . fg_color ( Some ( style. get_bg_color ( ) . unwrap_or ( self . bg_color ) ) )
102- . bg_color ( Some ( style. get_fg_color ( ) . unwrap_or ( self . fg_color ) ) )
103- . effects ( style. get_effects ( ) . remove ( anstyle:: Effects :: INVERT ) ) ;
104- }
105- effects_in_use |= style. get_effects ( ) ;
106- }
103+ preprocess_invert_style ( & mut elements, self . bg_color , self . fg_color ) ;
104+
107105 let styled_lines = split_lines ( & elements) ;
108106
109107 let fg_color = rgb_value ( self . fg_color , self . palette ) ;
@@ -152,60 +150,7 @@ impl Term {
152150 writeln ! ( & mut buffer, r#" padding: 0 10px;"# ) . unwrap ( ) ;
153151 writeln ! ( & mut buffer, r#" line-height: {line_height}px;"# ) . unwrap ( ) ;
154152 writeln ! ( & mut buffer, r#" }}"# ) . unwrap ( ) ;
155- if effects_in_use. contains ( anstyle:: Effects :: BOLD ) {
156- writeln ! ( & mut buffer, r#" .bold {{ font-weight: bold; }}"# ) . unwrap ( ) ;
157- }
158- if effects_in_use. contains ( anstyle:: Effects :: ITALIC ) {
159- writeln ! ( & mut buffer, r#" .italic {{ font-style: italic; }}"# ) . unwrap ( ) ;
160- }
161- if effects_in_use. contains ( anstyle:: Effects :: UNDERLINE ) {
162- writeln ! (
163- & mut buffer,
164- r#" .underline {{ text-decoration-line: underline; }}"#
165- )
166- . unwrap ( ) ;
167- }
168- if effects_in_use. contains ( anstyle:: Effects :: DOUBLE_UNDERLINE ) {
169- writeln ! (
170- & mut buffer,
171- r#" .double-underline {{ text-decoration-line: underline; text-decoration-style: double; }}"#
172- )
173- . unwrap ( ) ;
174- }
175- if effects_in_use. contains ( anstyle:: Effects :: CURLY_UNDERLINE ) {
176- writeln ! (
177- & mut buffer,
178- r#" .curly-underline {{ text-decoration-line: underline; text-decoration-style: wavy; }}"#
179- )
180- . unwrap ( ) ;
181- }
182- if effects_in_use. contains ( anstyle:: Effects :: DOTTED_UNDERLINE ) {
183- writeln ! (
184- & mut buffer,
185- r#" .dotted-underline {{ text-decoration-line: underline; text-decoration-style: dotted; }}"#
186- )
187- . unwrap ( ) ;
188- }
189- if effects_in_use. contains ( anstyle:: Effects :: DASHED_UNDERLINE ) {
190- writeln ! (
191- & mut buffer,
192- r#" .dashed-underline {{ text-decoration-line: underline; text-decoration-style: dashed; }}"#
193- )
194- . unwrap ( ) ;
195- }
196- if effects_in_use. contains ( anstyle:: Effects :: STRIKETHROUGH ) {
197- writeln ! (
198- & mut buffer,
199- r#" .strikethrough {{ text-decoration-line: line-through; }}"#
200- )
201- . unwrap ( ) ;
202- }
203- if effects_in_use. contains ( anstyle:: Effects :: DIMMED ) {
204- writeln ! ( & mut buffer, r#" .dimmed {{ opacity: 0.7; }}"# ) . unwrap ( ) ;
205- }
206- if effects_in_use. contains ( anstyle:: Effects :: HIDDEN ) {
207- writeln ! ( & mut buffer, r#" .hidden {{ opacity: 0; }}"# ) . unwrap ( ) ;
208- }
153+ write_effects_in_use ( & mut buffer, & elements) ;
209154 writeln ! ( & mut buffer, r#" tspan {{"# ) . unwrap ( ) ;
210155 writeln ! ( & mut buffer, r#" font: 14px {font_family};"# ) . unwrap ( ) ;
211156 writeln ! ( & mut buffer, r#" white-space: pre;"# ) . unwrap ( ) ;
@@ -237,7 +182,7 @@ impl Term {
237182 if element. text . is_empty ( ) {
238183 continue ;
239184 }
240- write_bg_span ( & mut buffer, & element. style , & element. text ) ;
185+ write_bg_span ( & mut buffer, "tspan" , & element. style , & element. text ) ;
241186 }
242187 // HACK: must close tspan on newline to include them in copy/paste
243188 writeln ! ( & mut buffer) . unwrap ( ) ;
@@ -249,7 +194,7 @@ impl Term {
249194 if element. text . is_empty ( ) {
250195 continue ;
251196 }
252- write_fg_span ( & mut buffer, element, & element. text ) ;
197+ write_fg_span ( & mut buffer, "tspan" , element, & element. text ) ;
253198 }
254199 // HACK: must close tspan on newline to include them in copy/paste
255200 writeln ! ( & mut buffer) . unwrap ( ) ;
@@ -263,12 +208,183 @@ impl Term {
263208 writeln ! ( & mut buffer, r#"</svg>"# ) . unwrap ( ) ;
264209 buffer
265210 }
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+ }
266318}
267319
268320const FG_COLOR : anstyle:: Color = anstyle:: Color :: Ansi ( anstyle:: AnsiColor :: White ) ;
269321const BG_COLOR : anstyle:: Color = anstyle:: Color :: Ansi ( anstyle:: AnsiColor :: Black ) ;
270322
271- fn write_fg_span ( buffer : & mut String , element : & adapter:: Element , fragment : & str ) {
323+ fn write_effects_in_use ( buffer : & mut String , elements : & [ adapter:: Element ] ) {
324+ use std:: fmt:: Write as _;
325+
326+ let mut effects_in_use = anstyle:: Effects :: new ( ) ;
327+ for element in elements {
328+ effects_in_use |= element. style . get_effects ( ) ;
329+ }
330+
331+ if effects_in_use. contains ( anstyle:: Effects :: BOLD ) {
332+ writeln ! ( buffer, r#" .bold {{ font-weight: bold; }}"# ) . unwrap ( ) ;
333+ }
334+ if effects_in_use. contains ( anstyle:: Effects :: ITALIC ) {
335+ writeln ! ( buffer, r#" .italic {{ font-style: italic; }}"# ) . unwrap ( ) ;
336+ }
337+ if effects_in_use. contains ( anstyle:: Effects :: UNDERLINE ) {
338+ writeln ! (
339+ buffer,
340+ r#" .underline {{ text-decoration-line: underline; }}"#
341+ )
342+ . unwrap ( ) ;
343+ }
344+ if effects_in_use. contains ( anstyle:: Effects :: DOUBLE_UNDERLINE ) {
345+ writeln ! (
346+ buffer,
347+ r#" .double-underline {{ text-decoration-line: underline; text-decoration-style: double; }}"#
348+ )
349+ . unwrap ( ) ;
350+ }
351+ if effects_in_use. contains ( anstyle:: Effects :: CURLY_UNDERLINE ) {
352+ writeln ! (
353+ buffer,
354+ r#" .curly-underline {{ text-decoration-line: underline; text-decoration-style: wavy; }}"#
355+ )
356+ . unwrap ( ) ;
357+ }
358+ if effects_in_use. contains ( anstyle:: Effects :: DOTTED_UNDERLINE ) {
359+ writeln ! (
360+ buffer,
361+ r#" .dotted-underline {{ text-decoration-line: underline; text-decoration-style: dotted; }}"#
362+ )
363+ . unwrap ( ) ;
364+ }
365+ if effects_in_use. contains ( anstyle:: Effects :: DASHED_UNDERLINE ) {
366+ writeln ! (
367+ buffer,
368+ r#" .dashed-underline {{ text-decoration-line: underline; text-decoration-style: dashed; }}"#
369+ )
370+ . unwrap ( ) ;
371+ }
372+ if effects_in_use. contains ( anstyle:: Effects :: STRIKETHROUGH ) {
373+ writeln ! (
374+ buffer,
375+ r#" .strikethrough {{ text-decoration-line: line-through; }}"#
376+ )
377+ . unwrap ( ) ;
378+ }
379+ if effects_in_use. contains ( anstyle:: Effects :: DIMMED ) {
380+ writeln ! ( buffer, r#" .dimmed {{ opacity: 0.7; }}"# ) . unwrap ( ) ;
381+ }
382+ if effects_in_use. contains ( anstyle:: Effects :: HIDDEN ) {
383+ writeln ! ( buffer, r#" .hidden {{ opacity: 0; }}"# ) . unwrap ( ) ;
384+ }
385+ }
386+
387+ fn write_fg_span ( buffer : & mut String , span : & str , element : & adapter:: Element , fragment : & str ) {
272388 use std:: fmt:: Write as _;
273389 let style = element. style ;
274390 let fg_color = style. get_fg_color ( ) . map ( |c| color_name ( FG_PREFIX , c) ) ;
@@ -329,7 +445,7 @@ fn write_fg_span(buffer: &mut String, element: &adapter::Element, fragment: &str
329445
330446 let mut need_closing_a = false ;
331447
332- write ! ( buffer, r#"<tspan "# ) . unwrap ( ) ;
448+ write ! ( buffer, r#"<{span} "# ) . unwrap ( ) ;
333449 if !classes. is_empty ( ) {
334450 let classes = classes. join ( " " ) ;
335451 write ! ( buffer, r#" class="{classes}""# ) . unwrap ( ) ;
@@ -343,10 +459,10 @@ fn write_fg_span(buffer: &mut String, element: &adapter::Element, fragment: &str
343459 if need_closing_a {
344460 write ! ( buffer, r#"</a>"# ) . unwrap ( ) ;
345461 }
346- write ! ( buffer, r#"</tspan >"# ) . unwrap ( ) ;
462+ write ! ( buffer, r#"</{span} >"# ) . unwrap ( ) ;
347463}
348464
349- fn write_bg_span ( buffer : & mut String , style : & anstyle:: Style , fragment : & str ) {
465+ fn write_bg_span ( buffer : & mut String , span : & str , style : & anstyle:: Style , fragment : & str ) {
350466 use std:: fmt:: Write as _;
351467 use unicode_width:: UnicodeWidthStr ;
352468
@@ -361,14 +477,14 @@ fn write_bg_span(buffer: &mut String, style: &anstyle::Style, fragment: &str) {
361477 if let Some ( class) = bg_color. as_deref ( ) {
362478 classes. push ( class) ;
363479 }
364- write ! ( buffer, r#"<tspan "# ) . unwrap ( ) ;
480+ write ! ( buffer, r#"<{span} "# ) . unwrap ( ) ;
365481 if !classes. is_empty ( ) {
366482 let classes = classes. join ( " " ) ;
367483 write ! ( buffer, r#" class="{classes}""# ) . unwrap ( ) ;
368484 }
369485 write ! ( buffer, r#">"# ) . unwrap ( ) ;
370486 write ! ( buffer, "{fragment}" ) . unwrap ( ) ;
371- write ! ( buffer, r#"</tspan >"# ) . unwrap ( ) ;
487+ write ! ( buffer, r#"</{span} >"# ) . unwrap ( ) ;
372488}
373489
374490impl Default for Term {
@@ -449,6 +565,23 @@ fn color_styles(
449565 colors. into_iter ( )
450566}
451567
568+ fn preprocess_invert_style (
569+ elements : & mut [ adapter:: Element ] ,
570+ bg_color : anstyle:: Color ,
571+ fg_color : anstyle:: Color ,
572+ ) {
573+ for element in elements {
574+ let style = & mut element. style ;
575+ // Pre-process INVERT to make fg/bg calculations easier
576+ if style. get_effects ( ) . contains ( anstyle:: Effects :: INVERT ) {
577+ * style = style
578+ . fg_color ( Some ( style. get_bg_color ( ) . unwrap_or ( bg_color) ) )
579+ . bg_color ( Some ( style. get_fg_color ( ) . unwrap_or ( fg_color) ) )
580+ . effects ( style. get_effects ( ) . remove ( anstyle:: Effects :: INVERT ) ) ;
581+ }
582+ }
583+ }
584+
452585fn split_lines ( styled : & [ adapter:: Element ] ) -> Vec < Vec < adapter:: Element > > {
453586 let mut lines = Vec :: new ( ) ;
454587 let mut current_line = Vec :: new ( ) ;
0 commit comments