1use crate::segment::Segment;
12use crate::terminal_theme::TerminalTheme;
13
14const CONSOLE_HTML_FORMAT: &str = r#"<!DOCTYPE html>
17<html>
18<head>
19<meta charset="UTF-8">
20<style>
21{stylesheet}
22body {
23 color: {foreground};
24 background-color: {background};
25}
26</style>
27</head>
28<body>
29 <pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><code style="font-family:inherit">{code}</code></pre>
30</body>
31</html>
32"#;
33
34fn escape(text: &str) -> String {
36 text.replace('&', "&")
37 .replace('<', "<")
38 .replace('>', ">")
39 .replace('"', """)
40 .replace('\'', "'")
41}
42
43fn fill_template(code: &str, stylesheet: &str, theme: &TerminalTheme) -> String {
46 CONSOLE_HTML_FORMAT
47 .replace("{stylesheet}", stylesheet)
48 .replace("{foreground}", &theme.foreground.hex())
49 .replace("{background}", &theme.background.hex())
50 .replace("{code}", code)
51}
52
53pub fn export_html_inline(segments: &[Segment], theme: &TerminalTheme) -> String {
56 let simplified = Segment::simplify(segments);
57 let mut code = String::new();
58 for segment in &simplified {
59 if segment.control {
60 continue;
61 }
62 let text = escape(&segment.text);
63 match &segment.style {
64 Some(style) if !style.is_null() => {
65 let rule = style.get_html_style(theme);
66 if rule.is_empty() {
67 code.push_str(&text);
68 } else {
69 code.push_str(&format!("<span style=\"{rule}\">{text}</span>"));
70 }
71 }
72 _ => code.push_str(&text),
73 }
74 }
75 fill_template(&code, "", theme)
76}
77
78pub fn export_html_classes(segments: &[Segment], theme: &TerminalTheme) -> String {
83 let simplified = Segment::simplify(segments);
84 let mut styles: Vec<(String, usize)> = Vec::new();
86 let mut code = String::new();
87 for segment in &simplified {
88 if segment.control {
89 continue;
90 }
91 let text = escape(&segment.text);
92 match &segment.style {
93 Some(style) if !style.is_null() => {
94 let rule = style.get_html_style(theme);
95 if rule.is_empty() {
96 code.push_str(&text);
97 } else {
98 let number = match styles.iter().find(|(existing, _)| *existing == rule) {
99 Some((_, n)) => *n,
100 None => {
101 let n = styles.len() + 1;
102 styles.push((rule, n));
103 n
104 }
105 };
106 code.push_str(&format!("<span class=\"r{number}\">{text}</span>"));
107 }
108 }
109 _ => code.push_str(&text),
110 }
111 }
112 let stylesheet = styles
113 .iter()
114 .map(|(rule, number)| format!(".r{number} {{{rule}}}"))
115 .collect::<Vec<_>>()
116 .join("\n");
117 fill_template(&code, &stylesheet, theme)
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::style::Style;
124
125 #[test]
126 fn escapes_html_special_chars() {
127 assert_eq!(escape("a<b>&\"'c"), "a<b>&"'c");
128 }
129
130 #[test]
131 fn bold_red_html_style() {
132 let style = Style::parse("bold red").unwrap();
134 assert_eq!(
135 style.get_html_style(&crate::terminal_theme::DEFAULT_TERMINAL_THEME),
136 "color: #800000; text-decoration-color: #800000; font-weight: bold"
137 );
138 }
139}