ansi_to_tui/
parser.rs

1use crate::code::AnsiCode;
2use nom::{
3    AsChar, IResult, Parser,
4    branch::alt,
5    bytes::complete::*,
6    character::complete::*,
7    combinator::{map_res, opt},
8    multi::*,
9    sequence::{delimited, preceded},
10};
11use ratatui_core::{
12    style::{Color, Modifier, Style, Stylize},
13    text::{Line, Span, Text},
14};
15use std::str::FromStr;
16
17#[derive(Debug, Clone, Copy, Eq, PartialEq)]
18enum ColorType {
19    /// Eight Bit color
20    EightBit,
21    /// 24-bit color or true color
22    TrueColor,
23}
24
25#[derive(Debug, Clone, PartialEq)]
26struct AnsiItem {
27    code: AnsiCode,
28    color: Option<Color>,
29}
30
31#[derive(Debug, Clone, PartialEq)]
32struct AnsiStates {
33    pub items: smallvec::SmallVec<[AnsiItem; 2]>,
34    pub style: Style,
35}
36
37impl From<AnsiStates> for ratatui_core::style::Style {
38    fn from(states: AnsiStates) -> Self {
39        let mut style = states.style;
40        if states.items.is_empty() {
41            // https://github.com/uttarayan21/ansi-to-tui/issues/40
42            // [m should be treated as a reset as well
43            style = Style::reset();
44        }
45        for item in states.items {
46            match item.code {
47                AnsiCode::Reset => style = Style::reset(),
48                AnsiCode::Bold => style = style.add_modifier(Modifier::BOLD),
49                AnsiCode::Faint => style = style.add_modifier(Modifier::DIM),
50                AnsiCode::Normal => {
51                    style = style.remove_modifier(Modifier::BOLD | Modifier::DIM);
52                }
53                AnsiCode::Italic => style = style.add_modifier(Modifier::ITALIC),
54                AnsiCode::NotItalic => style = style.remove_modifier(Modifier::ITALIC),
55                AnsiCode::Underline => style = style.add_modifier(Modifier::UNDERLINED),
56                AnsiCode::UnderlineOff => style = style.remove_modifier(Modifier::UNDERLINED),
57                AnsiCode::SlowBlink => style = style.add_modifier(Modifier::SLOW_BLINK),
58                AnsiCode::RapidBlink => style = style.add_modifier(Modifier::RAPID_BLINK),
59                AnsiCode::BlinkOff => {
60                    style = style.remove_modifier(Modifier::SLOW_BLINK | Modifier::RAPID_BLINK)
61                }
62                AnsiCode::Reverse => style = style.add_modifier(Modifier::REVERSED),
63                AnsiCode::InvertOff => style = style.remove_modifier(Modifier::REVERSED),
64                AnsiCode::Conceal => style = style.add_modifier(Modifier::HIDDEN),
65                AnsiCode::Reveal => style = style.remove_modifier(Modifier::HIDDEN),
66                AnsiCode::CrossedOut => style = style.add_modifier(Modifier::CROSSED_OUT),
67                AnsiCode::CrossedOutOff => style = style.remove_modifier(Modifier::CROSSED_OUT),
68                AnsiCode::DefaultForegroundColor => style = style.fg(Color::Reset),
69                AnsiCode::DefaultBackgroundColor => style = style.bg(Color::Reset),
70                AnsiCode::SetForegroundColor => {
71                    if let Some(color) = item.color {
72                        style = style.fg(color)
73                    }
74                }
75                AnsiCode::SetBackgroundColor => {
76                    if let Some(color) = item.color {
77                        style = style.bg(color)
78                    }
79                }
80                AnsiCode::ForegroundColor(color) => style = style.fg(color),
81                AnsiCode::BackgroundColor(color) => style = style.bg(color),
82                _ => (),
83            }
84        }
85        style
86    }
87}
88
89pub(crate) fn text(mut s: &[u8]) -> IResult<&[u8], Text<'static>> {
90    let mut lines = Vec::new();
91    let mut last = Style::new();
92    while let Ok((_s, (line, style))) = line(last)(s) {
93        lines.push(line);
94        last = style;
95        s = _s;
96        if s.is_empty() {
97            break;
98        }
99    }
100    Ok((s, Text::from(lines)))
101}
102
103#[cfg(feature = "zero-copy")]
104pub(crate) fn text_fast(mut s: &[u8]) -> IResult<&[u8], Text<'_>> {
105    let mut lines = Vec::new();
106    let mut last = Style::new();
107    while let Ok((_s, (line, style))) = line_fast(last)(s) {
108        lines.push(line);
109        last = style;
110        s = _s;
111        if s.is_empty() {
112            break;
113        }
114    }
115    Ok((s, Text::from(lines)))
116}
117
118fn newline(s: &[u8]) -> IResult<&[u8], ()> {
119    let (s, _) = alt((tag("\r\n"), tag("\n"), tag("\r"))).parse(s)?;
120    Ok((s, ()))
121}
122
123fn line(style: Style) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'static>, Style)> {
124    // let style_: Style = Default::default();
125    move |s: &[u8]| -> IResult<&[u8], (Line<'static>, Style)> {
126        let (s, mut text) = take_while(|c| c != b'\n' && c != b'\r').parse(s)?;
127        let (s, _) = opt(newline).parse(s)?;
128        let mut spans = Vec::new();
129        let mut last = style;
130        while let Ok((s, span)) = span(last)(text) {
131            // Since reset now tracks seperately we can skip the reset check
132            last = last.patch(span.style);
133
134            if !span.content.is_empty() {
135                spans.push(span);
136            }
137            text = s;
138            if text.is_empty() {
139                break;
140            }
141        }
142
143        Ok((s, (Line::from(spans), last)))
144    }
145}
146
147#[cfg(feature = "zero-copy")]
148fn line_fast(style: Style) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'_>, Style)> {
149    // let style_: Style = Default::default();
150    move |s: &[u8]| -> IResult<&[u8], (Line<'_>, Style)> {
151        let (s, mut text) = take_while(|c| c != b'\n' && c != b'\r').parse(s)?;
152        let (s, _) = opt(newline).parse(s)?;
153        let mut spans = Vec::new();
154        let mut last = style;
155        while let Ok((s, span)) = span_fast(last)(text) {
156            last = last.patch(span.style);
157            // If the spans is empty then it might be possible that the style changes
158            // but there is no text change
159            if !span.content.is_empty() {
160                spans.push(span);
161            }
162            text = s;
163            if text.is_empty() {
164                break;
165            }
166        }
167
168        Ok((s, (Line::from(spans), last)))
169    }
170}
171
172// fn span(s: &[u8]) -> IResult<&[u8], ratatui::text::Span> {
173fn span(last: Style) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'static>, nom::error::Error<&[u8]>> {
174    move |s: &[u8]| -> IResult<&[u8], Span<'static>> {
175        let mut last = last;
176        let (s, style) = opt(style(last)).parse(s)?;
177
178        #[cfg(feature = "simd")]
179        let (s, text) = map_res(
180            take_while(|c| c != b'\x1b' && c != b'\n' && c != b'\r'),
181            |t| simdutf8::basic::from_utf8(t),
182        )
183        .parse(s)?;
184
185        #[cfg(not(feature = "simd"))]
186        let (s, text) = map_res(
187            take_while(|c| c != b'\x1b' && c != b'\n' && c != b'\r'),
188            |t| std::str::from_utf8(t),
189        )
190        .parse(s)?;
191
192        if let Some(style) = style.flatten() {
193            last = last.patch(style);
194        }
195
196        Ok((s, Span::styled(text.to_owned(), last)))
197    }
198}
199
200#[cfg(feature = "zero-copy")]
201fn span_fast(last: Style) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'_>, nom::error::Error<&[u8]>> {
202    move |s: &[u8]| -> IResult<&[u8], Span<'_>> {
203        let mut last = last;
204        let (s, style) = opt(style(last)).parse(s)?;
205
206        #[cfg(feature = "simd")]
207        let (s, text) = map_res(
208            take_while(|c| c != b'\x1b' && c != b'\n' && c != b'\r'),
209            |t| simdutf8::basic::from_utf8(t),
210        )
211        .parse(s)?;
212
213        #[cfg(not(feature = "simd"))]
214        let (s, text) = map_res(
215            take_while(|c| c != b'\x1b' && c != b'\n' && c != b'\r'),
216            |t| std::str::from_utf8(t),
217        )
218        .parse(s)?;
219
220        if let Some(style) = style.flatten() {
221            last = last.patch(style);
222        }
223
224        Ok((s, Span::styled(text, last)))
225    }
226}
227
228#[allow(clippy::type_complexity)]
229fn style(
230    style: Style,
231) -> impl Fn(&[u8]) -> IResult<&[u8], Option<Style>, nom::error::Error<&[u8]>> {
232    move |s: &[u8]| -> IResult<&[u8], Option<Style>> {
233        let (s, r) = match opt(ansi_sgr_code).parse(s)? {
234            (s, Some(r)) => (s, Some(r)),
235            (s, None) => {
236                let (s, _) = any_escape_sequence(s)?;
237                (s, None)
238            }
239        };
240        Ok((s, r.map(|r| Style::from(AnsiStates { style, items: r }))))
241    }
242}
243
244/// A complete ANSI SGR code
245fn ansi_sgr_code(
246    s: &[u8],
247) -> IResult<&[u8], smallvec::SmallVec<[AnsiItem; 2]>, nom::error::Error<&[u8]>> {
248    delimited(
249        tag("\x1b["),
250        fold_many0(ansi_sgr_item, smallvec::SmallVec::new, |mut items, item| {
251            items.push(item);
252            items
253        }),
254        char('m'),
255    )
256    .parse(s)
257}
258
259fn any_escape_sequence(s: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
260    // Attempt to consume most escape codes, including a single escape char.
261    //
262    // Most escape codes begin with ESC[ and are terminated by an alphabetic character,
263    // but OSC codes begin with ESC] and are terminated by an ascii bell (\x07)
264    // and a truncated/invalid code may just be a standalone ESC or not be terminated.
265    //
266    // We should try to consume as much of it as possible to match behavior of most terminals;
267    // where we fail at that we should at least consume the escape char to avoid infinitely looping
268
269    let (input, garbage) = preceded(
270        char('\x1b'),
271        opt(alt((
272            delimited(char('['), take_till(AsChar::is_alpha), opt(take(1u8))),
273            delimited(char(']'), take_till(|c| c == b'\x07'), opt(take(1u8))),
274        ))),
275    )
276    .parse(s)?;
277    Ok((input, garbage))
278}
279
280/// An ANSI SGR attribute
281fn ansi_sgr_item(s: &[u8]) -> IResult<&[u8], AnsiItem> {
282    let (s, c) = u8(s)?;
283    let code = AnsiCode::from(c);
284    let (s, color) = match code {
285        AnsiCode::SetForegroundColor | AnsiCode::SetBackgroundColor => {
286            let (s, _) = opt(tag(";")).parse(s)?;
287            let (s, color) = color(s)?;
288            (s, Some(color))
289        }
290        _ => (s, None),
291    };
292    let (s, _) = opt(tag(";")).parse(s)?;
293    Ok((s, AnsiItem { code, color }))
294}
295
296fn color(s: &[u8]) -> IResult<&[u8], Color> {
297    let (s, c_type) = color_type(s)?;
298    let (s, _) = opt(tag(";")).parse(s)?;
299    match c_type {
300        ColorType::TrueColor => {
301            let (s, (r, _, g, _, b)) = (u8, tag(";"), u8, tag(";"), u8).parse(s)?;
302            Ok((s, Color::Rgb(r, g, b)))
303        }
304        ColorType::EightBit => {
305            let (s, index) = u8(s)?;
306            Ok((s, Color::Indexed(index)))
307        }
308    }
309}
310
311fn color_type(s: &[u8]) -> IResult<&[u8], ColorType> {
312    let (s, t) = i64(s)?;
313    // NOTE: This isn't opt because a color type must always be followed by a color
314    // let (s, _) = opt(tag(";")).parse(s)?;
315    let (s, _) = tag(";").parse(s)?;
316    match t {
317        2 => Ok((s, ColorType::TrueColor)),
318        5 => Ok((s, ColorType::EightBit)),
319        _ => Err(nom::Err::Error(nom::error::Error::new(
320            s,
321            nom::error::ErrorKind::Alt,
322        ))),
323    }
324}
325
326#[test]
327fn color_test() {
328    let c = color(b"2;255;255;255").unwrap();
329    assert_eq!(c.1, Color::Rgb(255, 255, 255));
330    let c = color(b"5;255").unwrap();
331    assert_eq!(c.1, Color::Indexed(255));
332    let err = color(b"10;255");
333    assert_ne!(err, Ok(c));
334}
335
336#[test]
337fn ansi_items_test() {
338    let sc = Default::default();
339    let t = style(sc)(b"\x1b[38;2;3;3;3m").unwrap().1.unwrap();
340    assert_eq!(
341        t,
342        Style::from(AnsiStates {
343            style: sc,
344            items: vec![AnsiItem {
345                code: AnsiCode::SetForegroundColor,
346                color: Some(Color::Rgb(3, 3, 3))
347            }]
348            .into()
349        })
350    );
351    assert_eq!(
352        style(sc)(b"\x1b[38;5;3m").unwrap().1.unwrap(),
353        Style::from(AnsiStates {
354            style: sc,
355            items: vec![AnsiItem {
356                code: AnsiCode::SetForegroundColor,
357                color: Some(Color::Indexed(3))
358            }]
359            .into()
360        })
361    );
362    assert_eq!(
363        style(sc)(b"\x1b[38;5;3;48;5;3m").unwrap().1.unwrap(),
364        Style::from(AnsiStates {
365            style: sc,
366            items: vec![
367                AnsiItem {
368                    code: AnsiCode::SetForegroundColor,
369                    color: Some(Color::Indexed(3))
370                },
371                AnsiItem {
372                    code: AnsiCode::SetBackgroundColor,
373                    color: Some(Color::Indexed(3))
374                }
375            ]
376            .into()
377        })
378    );
379    assert_eq!(
380        style(sc)(b"\x1b[38;5;3;48;5;3;1m").unwrap().1.unwrap(),
381        Style::from(AnsiStates {
382            style: sc,
383            items: vec![
384                AnsiItem {
385                    code: AnsiCode::SetForegroundColor,
386                    color: Some(Color::Indexed(3))
387                },
388                AnsiItem {
389                    code: AnsiCode::SetBackgroundColor,
390                    color: Some(Color::Indexed(3))
391                },
392                AnsiItem {
393                    code: AnsiCode::Bold,
394                    color: None
395                }
396            ]
397            .into()
398        })
399    );
400}