Skip to main content

rusty_rich/
ansi.rs

1//! ANSI escape sequence decoder — parse ANSI text into styled Text.
2
3use crate::style::Style;
4use crate::text::Text;
5use regex::Regex;
6
7/// Decode ANSI-escaped text into styled Text components.
8pub struct AnsiDecoder;
9
10impl AnsiDecoder {
11    /// Parse ANSI text and return styled Text.
12    pub fn decode(ansi_text: &str) -> Text {
13        let mut text = Text::new("");
14        let mut current_style = Style::new();
15        let mut last_end = 0usize;
16
17        // Match ANSI SGR escape sequences
18        let re = Regex::new(r"\x1b\[([\d;]*)m").unwrap();
19
20        for caps in re.captures_iter(ansi_text) {
21            let m = caps.get(0).unwrap();
22            let start = m.start();
23
24            // Add text before this escape code
25            if start > last_end {
26                let plain = &ansi_text[last_end..start];
27                text.append_styled(plain, current_style.clone());
28            }
29
30            // Parse SGR parameters
31            let params = caps.get(1).map_or("", |p| p.as_str());
32            current_style = apply_sgr(&current_style, params);
33            last_end = m.end();
34        }
35
36        // Add remaining text
37        if last_end < ansi_text.len() {
38            text.append_styled(&ansi_text[last_end..], current_style);
39        }
40
41        text
42    }
43}
44
45/// Apply SGR parameters to a style.
46fn apply_sgr(style: &Style, params: &str) -> Style {
47    if params.is_empty() || params == "0" {
48        return Style::new(); // Reset
49    }
50
51    let mut s = style.clone();
52    for param in params.split(';') {
53        if let Ok(n) = param.parse::<u32>() {
54            match n {
55                0 => s = Style::new(),                    // Reset
56                1 => { s = s.bold(true); }                // Bold
57                2 => { s = s.dim(true); }                 // Dim
58                3 => { s = s.italic(true); }              // Italic
59                4 => { s = s.underline(true); }           // Underline
60                5 => { s = s.blink(true); }               // Slow blink
61                6 => { s = s.blink2(true); }              // Fast blink
62                7 => { s = s.reverse(true); }             // Reverse
63                8 => { s = s.conceal(true); }             // Conceal
64                9 => { s = s.strike(true); }              // Strikethrough
65                21 => { s = s.underline2(true); }         // Double underline
66                22 => { s = s.bold(false); }              // Normal intensity
67                23 => { s = s.italic(false); }            // Not italic
68                24 => { s = s.underline(false); }         // Not underline
69                25 => { s = s.blink(false); }             // Not blink
70                27 => { s = s.reverse(false); }           // Not reverse
71                28 => { s = s.conceal(false); }           // Not conceal
72                29 => { s = s.strike(false); }            // Not strikethrough
73                30..=37 => {                               // Standard fg
74                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 30)) {
75                        s = s.color(c);
76                    }
77                }
78                38 => { /* Extended fg - skip for simplicity */ }
79                39 => { s = s.color(crate::color::Color::default()); } // Default fg
80                40..=47 => {                               // Standard bg
81                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 40)) {
82                        s = s.bgcolor(c);
83                    }
84                }
85                48 => { /* Extended bg - skip */ }
86                49 => { s = s.bgcolor(crate::color::Color::default()); } // Default bg
87                90..=97 => {                               // Bright fg
88                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 90 + 8)) {
89                        s = s.color(c);
90                    }
91                }
92                100..=107 => {                             // Bright bg
93                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 100 + 8)) {
94                        s = s.bgcolor(c);
95                    }
96                }
97                _ => {}
98            }
99        }
100    }
101    s
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_decode_bold() {
110        let text = AnsiDecoder::decode("\x1b[1mBold Text\x1b[0m");
111        assert!(text.plain.contains("Bold Text"));
112        assert!(!text.spans.is_empty());
113    }
114
115    #[test]
116    fn test_decode_reset() {
117        let text = AnsiDecoder::decode("\x1b[31mRed\x1b[0m Normal");
118        assert!(text.plain.contains("Red"));
119        assert!(text.plain.contains("Normal"));
120    }
121}