ansi_to_html/lib.rs
1//! Convert a string that can contain
2//! [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) to HTML.
3//!
4//! This crate currently supports SGR parameters (text style and colors).
5//! The supported styles are:
6//!
7//! - bold
8//! - italic
9//! - underlined
10//! - crossed out
11//! - faint
12//! - foreground and background colors: 3-bit, 4-bit, 8-bit, truecolor (24-bit)
13//!
14//! **Not** supported SGR parameters (note that most of these are niche features
15//! and rarely supported by terminals):
16//!
17//! - slow/rapid blink
18//! - reverse video
19//! - conceal
20//! - alternative fonts
21//! - fraktur
22//! - doubly underlined
23//! - proportional spacing
24//! - framed
25//! - encircled
26//! - overlined
27//! - underline color (not in standard)
28//! - ideogram attributes
29//! - superscript, subscript (not in standard)
30//! - bright foreground/background color (not in standard)
31//!
32//! All unsupported ANSI escape codes are stripped from the output.
33//!
34//! It should be easy to add support for more styles, if there's a straightforward HTML
35//! representation. If you need a different style (e.g. doubly underlined), file an issue.
36//!
37//!
38//! ## Example
39//! ```
40//! let bold = "\x1b[1m";
41//! let red = "\x1b[31m";
42//! let input = format!("<h1> {bold}Hello {red}world! </h1>");
43//! let converted = ansi_to_html::convert(&input).unwrap();
44//! assert_eq!(
45//! converted,
46//! "<h1> <b>Hello <span style='color:var(--red,#a00)'>world! </h1></span></b>"
47//! );
48//! ```
49//!
50//! Use the [`Converter`] builder for customization options.
51#![deny(unsafe_code)]
52
53use std::sync::OnceLock;
54
55mod ansi;
56mod color;
57mod error;
58mod esc;
59mod html;
60
61use ansi::{Ansi, AnsiIter};
62use color::Color;
63
64pub use error::Error;
65pub use esc::Esc;
66
67use regex::Regex;
68
69/// Converts a string containing ANSI escape codes to HTML.
70///
71/// Special html characters (`<>&'"`) are escaped prior to the conversion.
72/// The number of generated HTML tags is minimized.
73///
74/// This behaviour can be customized by using the [`Converter`] builder.
75///
76/// ## Example
77///
78/// ```
79/// let bold = "\x1b[1m";
80/// let red = "\x1b[31m";
81/// let input = format!("<h1> {bold}Hello {red}world! </h1>");
82/// let converted = ansi_to_html::convert(&input).unwrap();
83///
84/// assert_eq!(
85/// converted,
86/// "<h1> <b>Hello <span style='color:var(--red,#a00)'>world! </h1></span></b>",
87/// );
88/// ```
89pub fn convert(ansi_string: &str) -> Result<String, Error> {
90 Converter::new().convert(ansi_string)
91}
92
93/// A builder for converting a string containing ANSI escape codes to HTML.
94///
95/// By default this will:
96///
97/// - Escape special HTML characters (`<>&'"`) prior to conversion.
98/// - Apply optimizations to minimize the number of generated HTML tags.
99/// - Use hardcoded colors.
100///
101/// ## Example
102///
103/// This skips HTML escaping and optimization, and sets a prefix for the CSS
104/// variables to customize 4-bit colors.
105///
106/// ```
107/// use ansi_to_html::Converter;
108///
109/// let converter = Converter::new()
110/// .skip_escape(true)
111/// .skip_optimize(true)
112/// .four_bit_var_prefix(Some("custom-".to_owned()));
113///
114/// let bold = "\x1b[1m";
115/// let red = "\x1b[31m";
116/// let reset = "\x1b[0m";
117/// let input = format!("<h1> <i></i> {bold}Hello {red}world!{reset} </h1>");
118/// let converted = converter.convert(&input).unwrap();
119///
120/// assert_eq!(
121/// converted,
122/// // The `<h1>` and `</h1>` aren't escaped, useless `<i></i>` is kept, and
123/// // `<span class='red'>` is used instead of `<span style='color:#a00'>`
124/// "<h1> <i></i> <b>Hello <span style='color:var(--custom-red,#a00)'>world!</span></b> </h1>",
125/// );
126/// ```
127#[derive(Clone, Debug, Default)]
128pub struct Converter {
129 skip_escape: bool,
130 skip_optimize: bool,
131 four_bit_var_prefix: Option<String>,
132}
133
134#[deprecated(note = "this is now a type alias for the `Converter` builder")]
135pub type Opts = Converter;
136
137impl Converter {
138 /// Creates a new set of default options.
139 pub fn new() -> Self {
140 Converter::default()
141 }
142
143 /// Avoids escaping special HTML characters prior to conversion.
144 pub fn skip_escape(mut self, skip: bool) -> Self {
145 self.skip_escape = skip;
146 self
147 }
148
149 /// Skips removing some useless HTML tags.
150 pub fn skip_optimize(mut self, skip: bool) -> Self {
151 self.skip_optimize = skip;
152 self
153 }
154
155 /// Adds a custom prefix for the CSS variables used for all the 4-bit colors.
156 pub fn four_bit_var_prefix(mut self, prefix: Option<String>) -> Self {
157 self.four_bit_var_prefix = prefix;
158 self
159 }
160
161 /// Converts a string containing ANSI escape codes to HTML.
162 pub fn convert(&self, input: &str) -> Result<String, Error> {
163 let Converter {
164 skip_escape,
165 skip_optimize,
166 ref four_bit_var_prefix,
167 } = *self;
168
169 let html = if skip_escape {
170 html::ansi_to_html(input, ansi_regex(), four_bit_var_prefix.to_owned())?
171 } else {
172 let input = Esc(input).to_string();
173 html::ansi_to_html(&input, ansi_regex(), four_bit_var_prefix.to_owned())?
174 };
175
176 let html = if skip_optimize { html } else { optimize(&html) };
177
178 Ok(html)
179 }
180}
181
182#[deprecated(note = "Use the `convert` method of the `Converter` builder")]
183pub fn convert_with_opts(input: &str, converter: &Converter) -> Result<String, Error> {
184 converter.convert(input)
185}
186
187const ANSI_REGEX: &str = r"\u{1b}(\[[0-9;?]*[A-HJKSTfhilmnsu]|\(B)";
188const OPT_REGEX_1: &str = r"<span \w+='[^']*'></span>|<b></b>|<i></i>|<u></u>|<s></s>";
189const OPT_REGEX_2: &str = "</b><b>|</i><i>|</u><u>|</s><s>";
190
191fn ansi_regex() -> &'static Regex {
192 static REGEX: OnceLock<Regex> = OnceLock::new();
193 REGEX.get_or_init(|| Regex::new(ANSI_REGEX).unwrap())
194}
195
196fn optimize(html: &str) -> String {
197 static REGEXES: OnceLock<(Regex, Regex)> = OnceLock::new();
198 let (regex1, regex2) = REGEXES.get_or_init(|| {
199 (
200 Regex::new(OPT_REGEX_1).unwrap(),
201 Regex::new(OPT_REGEX_2).unwrap(),
202 )
203 });
204
205 let html = regex1.replace_all(html, "");
206 let html = regex2.replace_all(&html, "");
207
208 html.to_string()
209}