ansi_to_tui/
parser.rs

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