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 => {
57                    s = s.bold(true);
58                } // Bold
59                2 => {
60                    s = s.dim(true);
61                } // Dim
62                3 => {
63                    s = s.italic(true);
64                } // Italic
65                4 => {
66                    s = s.underline(true);
67                } // Underline
68                5 => {
69                    s = s.blink(true);
70                } // Slow blink
71                6 => {
72                    s = s.blink2(true);
73                } // Fast blink
74                7 => {
75                    s = s.reverse(true);
76                } // Reverse
77                8 => {
78                    s = s.conceal(true);
79                } // Conceal
80                9 => {
81                    s = s.strike(true);
82                } // Strikethrough
83                21 => {
84                    s = s.underline2(true);
85                } // Double underline
86                22 => {
87                    s = s.bold(false);
88                } // Normal intensity
89                23 => {
90                    s = s.italic(false);
91                } // Not italic
92                24 => {
93                    s = s.underline(false);
94                } // Not underline
95                25 => {
96                    s = s.blink(false);
97                } // Not blink
98                27 => {
99                    s = s.reverse(false);
100                } // Not reverse
101                28 => {
102                    s = s.conceal(false);
103                } // Not conceal
104                29 => {
105                    s = s.strike(false);
106                } // Not strikethrough
107                30..=37 => {
108                    // Standard fg
109                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 30)) {
110                        s = s.color(c);
111                    }
112                }
113                38 => { /* Extended fg - skip for simplicity */ }
114                39 => {
115                    s = s.color(crate::color::Color::default());
116                } // Default fg
117                40..=47 => {
118                    // Standard bg
119                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 40)) {
120                        s = s.bgcolor(c);
121                    }
122                }
123                48 => { /* Extended bg - skip */ }
124                49 => {
125                    s = s.bgcolor(crate::color::Color::default());
126                } // Default bg
127                90..=97 => {
128                    // Bright fg
129                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 90 + 8)) {
130                        s = s.color(c);
131                    }
132                }
133                100..=107 => {
134                    // Bright bg
135                    if let Ok(c) = crate::color::Color::parse(&format!("color({})", n - 100 + 8)) {
136                        s = s.bgcolor(c);
137                    }
138                }
139                _ => {}
140            }
141        }
142    }
143    s
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_decode_bold() {
152        let text = AnsiDecoder::decode("\x1b[1mBold Text\x1b[0m");
153        assert!(text.plain.contains("Bold Text"));
154        assert!(!text.spans.is_empty());
155    }
156
157    #[test]
158    fn test_decode_reset() {
159        let text = AnsiDecoder::decode("\x1b[31mRed\x1b[0m Normal");
160        assert!(text.plain.contains("Red"));
161        assert!(text.plain.contains("Normal"));
162    }
163}