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 EightBit,
21 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 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 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 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 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 !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
172fn 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
244fn 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 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
280fn 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 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}