Skip to main content

farben_core/
ansi.rs

1use std::fmt::Write;
2
3use crate::errors::LexError;
4use crate::lexer::{EmphasisType, TagType, Token, tokenize};
5
6/// Whether a color applies to the foreground (text) or background.
7#[derive(Debug, PartialEq)]
8pub enum Ground {
9    /// Applies the color to the text itself (SGR 30-series / 38).
10    Foreground,
11    /// Applies the color to the cell background (SGR 40-series / 48).
12    Background,
13}
14
15/// A complete set of visual attributes for a span of text.
16#[derive(Default, Clone)]
17pub struct Style {
18    /// Foreground color. `None` leaves the terminal default unchanged.
19    pub fg: Option<Color>,
20    /// Background color. `None` leaves the terminal default unchanged.
21    pub bg: Option<Color>,
22    /// Bold text (SGR 1).
23    pub bold: bool,
24    /// Reduced intensity text (SGR 2).
25    pub dim: bool,
26    /// Italic text (SGR 3).
27    pub italic: bool,
28    /// Underlined text (SGR 4).
29    pub underline: bool,
30    /// Crossed-out text (SGR 9).
31    pub strikethrough: bool,
32    /// Blinking text (SGR 5). Terminal support varies.
33    pub blink: bool,
34    /// Full reset. Enabling this option overrides all previous options.
35    pub reset: bool,
36}
37
38/// One of the eight standard ANSI named colors.
39#[derive(Debug, PartialEq, Clone)]
40pub enum NamedColor {
41    Black,
42    Red,
43    Green,
44    Yellow,
45    Blue,
46    Magenta,
47    Cyan,
48    White,
49}
50
51/// A terminal color, expressed as a named color, an ANSI 256-palette index, or an RGB triple.
52#[derive(Debug, PartialEq, Clone)]
53pub enum Color {
54    Named(NamedColor),
55    Ansi256(u8),
56    Rgb(u8, u8, u8),
57}
58
59impl Style {
60    pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
61        let mut res = Self {
62            ..Default::default()
63        };
64        for tok in tokenize(markup.into())? {
65            match tok {
66                Token::Text(_) => continue,
67                Token::Tag(tag) => match tag {
68                    TagType::Reset => res.reset = true,
69                    TagType::Emphasis(emphasis) => match emphasis {
70                        EmphasisType::Dim => res.dim = true,
71                        EmphasisType::Blink => res.blink = true,
72                        EmphasisType::Bold => res.bold = true,
73                        EmphasisType::Italic => res.italic = true,
74                        EmphasisType::Strikethrough => res.strikethrough = true,
75                        EmphasisType::Underline => res.underline = true,
76                    },
77                    TagType::Color { color, ground } => match ground {
78                        Ground::Background => res.bg = Some(color),
79                        Ground::Foreground => res.fg = Some(color),
80                    },
81                },
82            }
83        }
84
85        Ok(res)
86    }
87}
88
89impl NamedColor {
90    /// Parses a color name into a `NamedColor`.
91    ///
92    /// Returns `None` if the string does not match any of the eight standard names.
93    /// Matching is case-sensitive.
94    pub(crate) fn from_str(input: &str) -> Option<Self> {
95        match input {
96            "black" => Some(Self::Black),
97            "red" => Some(Self::Red),
98            "green" => Some(Self::Green),
99            "yellow" => Some(Self::Yellow),
100            "blue" => Some(Self::Blue),
101            "magenta" => Some(Self::Magenta),
102            "cyan" => Some(Self::Cyan),
103            "white" => Some(Self::White),
104            _ => None,
105        }
106    }
107}
108
109/// Joins a slice of SGR parameter bytes into a complete ANSI escape sequence.
110///
111/// Produces a string of the form `\x1b[n;n;...m`.
112fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
113    let mut seq = String::from("\x1b[");
114
115    for (i, n) in vec.iter().enumerate() {
116        if i != 0 {
117            seq.push(';');
118        }
119        write!(seq, "{n}").unwrap();
120    }
121
122    seq.push('m');
123    seq
124}
125
126/// Appends the SGR parameter bytes for `color` onto `ansi`, using the correct base codes for
127/// foreground (30-series) or background (40-series) output.
128fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
129    let addend: u8 = match param {
130        Ground::Background => 10,
131        Ground::Foreground => 0,
132    };
133    match color {
134        Color::Named(named) => {
135            ansi.push(match named {
136                NamedColor::Black => 30 + addend,
137                NamedColor::Red => 31 + addend,
138                NamedColor::Green => 32 + addend,
139                NamedColor::Yellow => 33 + addend,
140                NamedColor::Blue => 34 + addend,
141                NamedColor::Magenta => 35 + addend,
142                NamedColor::Cyan => 36 + addend,
143                NamedColor::White => 37 + addend,
144            });
145        }
146        Color::Ansi256(v) => {
147            ansi.extend_from_slice(&[38 + addend, 5, *v]);
148        }
149        Color::Rgb(r, g, b) => {
150            ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
151        }
152    }
153}
154
155/// Converts a `Color` into a complete ANSI escape sequence for the given ground.
156///
157/// # Example
158/// ```ignore
159/// let seq = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
160/// assert_eq!(seq, "\x1b[31m");
161/// ```
162pub(crate) fn color_to_ansi(color: &Color, ground: Ground) -> String {
163    let mut ansi: Vec<u8> = Vec::new();
164    encode_color_sgr(&mut ansi, ground, color);
165
166    vec_to_ansi_seq(ansi)
167}
168
169/// Converts an `EmphasisType` into the corresponding SGR escape sequence.
170pub(crate) fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
171    let code = match emphasis {
172        EmphasisType::Bold => 1,
173        EmphasisType::Dim => 2,
174        EmphasisType::Italic => 3,
175        EmphasisType::Underline => 4,
176        EmphasisType::Blink => 5,
177        EmphasisType::Strikethrough => 9,
178    };
179    vec_to_ansi_seq(vec![code])
180}
181
182/// Converts a `Style` into a single combined SGR escape sequence.
183///
184/// All active attributes and colors are merged into one sequence. Returns an empty string
185/// if the style carries no active attributes and no colors.
186pub(crate) fn style_to_ansi(style: &Style) -> String {
187    let mut ansi: Vec<u8> = Vec::new();
188
189    if style.reset {
190        return String::from("\x1b[0m");
191    }
192
193    for (enabled, code) in [
194        (style.bold, 1),
195        (style.dim, 2),
196        (style.italic, 3),
197        (style.underline, 4),
198        (style.blink, 5),
199        (style.strikethrough, 9),
200    ] {
201        if enabled {
202            ansi.push(code);
203        }
204    }
205
206    if let Some(fg) = &style.fg {
207        encode_color_sgr(&mut ansi, Ground::Foreground, fg);
208    }
209    if let Some(bg) = &style.bg {
210        encode_color_sgr(&mut ansi, Ground::Background, bg);
211    }
212
213    if ansi.is_empty() {
214        return String::new();
215    }
216
217    vec_to_ansi_seq(ansi)
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::lexer::EmphasisType;
224
225    // --- NamedColor::from_str ---
226
227    #[test]
228    fn test_named_color_from_str_known_colors() {
229        assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
230        assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
231        assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
232        assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
233        assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
234        assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
235        assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
236        assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
237    }
238
239    #[test]
240    fn test_named_color_from_str_unknown_returns_none() {
241        assert_eq!(NamedColor::from_str("purple"), None);
242    }
243
244    #[test]
245    fn test_named_color_from_str_case_sensitive() {
246        assert_eq!(NamedColor::from_str("Red"), None);
247        assert_eq!(NamedColor::from_str("RED"), None);
248    }
249
250    #[test]
251    fn test_named_color_from_str_empty_returns_none() {
252        assert_eq!(NamedColor::from_str(""), None);
253    }
254
255    // --- vec_to_ansi_seq ---
256
257    #[test]
258    fn test_vec_to_ansi_seq_single_param() {
259        let result = vec_to_ansi_seq(vec![1]);
260        assert_eq!(result, "\x1b[1m");
261    }
262
263    #[test]
264    fn test_vec_to_ansi_seq_multiple_params() {
265        let result = vec_to_ansi_seq(vec![1, 31]);
266        assert_eq!(result, "\x1b[1;31m");
267    }
268
269    #[test]
270    fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
271        let result = vec_to_ansi_seq(vec![]);
272        assert_eq!(result, "\x1b[m");
273    }
274
275    // --- color_to_ansi ---
276
277    #[test]
278    fn test_color_to_ansi_named_foreground() {
279        let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
280        assert_eq!(result, "\x1b[31m");
281    }
282
283    #[test]
284    fn test_color_to_ansi_named_background() {
285        let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
286        assert_eq!(result, "\x1b[41m");
287    }
288
289    #[test]
290    fn test_color_to_ansi_ansi256_foreground() {
291        let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
292        assert_eq!(result, "\x1b[38;5;200m");
293    }
294
295    #[test]
296    fn test_color_to_ansi_ansi256_background() {
297        let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
298        assert_eq!(result, "\x1b[48;5;100m");
299    }
300
301    #[test]
302    fn test_color_to_ansi_rgb_foreground() {
303        let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
304        assert_eq!(result, "\x1b[38;2;255;128;0m");
305    }
306
307    #[test]
308    fn test_color_to_ansi_rgb_background() {
309        let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
310        assert_eq!(result, "\x1b[48;2;0;0;255m");
311    }
312
313    #[test]
314    fn test_color_to_ansi_rgb_zero_values() {
315        let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
316        assert_eq!(result, "\x1b[38;2;0;0;0m");
317    }
318
319    // --- emphasis_to_ansi ---
320
321    #[test]
322    fn test_emphasis_to_ansi_bold() {
323        assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
324    }
325
326    #[test]
327    fn test_emphasis_to_ansi_dim() {
328        assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
329    }
330
331    #[test]
332    fn test_emphasis_to_ansi_italic() {
333        assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
334    }
335
336    #[test]
337    fn test_emphasis_to_ansi_underline() {
338        assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
339    }
340
341    #[test]
342    fn test_emphasis_to_ansi_blink() {
343        assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
344    }
345
346    #[test]
347    fn test_emphasis_to_ansi_strikethrough() {
348        assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
349    }
350
351    // --- style_to_ansi ---
352
353    #[test]
354    fn test_style_to_ansi_empty_style_returns_empty_string() {
355        let style = Style {
356            fg: None,
357            bg: None,
358            bold: false,
359            dim: false,
360            italic: false,
361            underline: false,
362            strikethrough: false,
363            blink: false,
364            ..Default::default()
365        };
366        assert_eq!(style_to_ansi(&style), "");
367    }
368
369    #[test]
370    fn test_style_to_ansi_bold_only() {
371        let style = Style {
372            fg: None,
373            bg: None,
374            bold: true,
375            dim: false,
376            italic: false,
377            underline: false,
378            strikethrough: false,
379            blink: false,
380            ..Default::default()
381        };
382        assert_eq!(style_to_ansi(&style), "\x1b[1m");
383    }
384
385    #[test]
386    fn test_style_to_ansi_bold_with_foreground_color() {
387        let style = Style {
388            fg: Some(Color::Named(NamedColor::Green)),
389            bg: None,
390            bold: true,
391            dim: false,
392            italic: false,
393            underline: false,
394            strikethrough: false,
395            blink: false,
396            ..Default::default()
397        };
398        assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
399    }
400
401    #[test]
402    fn test_style_to_ansi_fg_and_bg() {
403        let style = Style {
404            fg: Some(Color::Named(NamedColor::White)),
405            bg: Some(Color::Named(NamedColor::Blue)),
406            bold: false,
407            dim: false,
408            italic: false,
409            underline: false,
410            strikethrough: false,
411            blink: false,
412            ..Default::default()
413        };
414        assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
415    }
416
417    #[test]
418    fn test_style_to_ansi_all_emphasis_flags() {
419        let style = Style {
420            fg: None,
421            bg: None,
422            bold: true,
423            dim: true,
424            italic: true,
425            underline: true,
426            strikethrough: true,
427            blink: true,
428            ..Default::default()
429        };
430        assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
431    }
432}
433
434// Skipped (side effects): none: all functions in ansi.rs are pure.