television_previewers/ansi/
parser.rs

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