ansi_to_tui/
parser.rs

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