use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
};
pub fn parse_ansi_to_spans(text: &str) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let mut current_style = Style::default();
let mut current_text = String::new();
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
if !current_text.is_empty() {
spans.push(Span::styled(
std::mem::take(&mut current_text),
current_style,
));
}
let mut params = String::new();
while let Some(&next) = chars.peek() {
if next.is_ascii_digit() || next == ';' {
params.push(chars.next().unwrap());
} else {
break;
}
}
let cmd = chars.next();
if cmd == Some('m') {
current_style = parse_sgr_params(¶ms, current_style);
}
} else {
current_text.push(c);
}
} else {
current_text.push(c);
}
}
if !current_text.is_empty() {
spans.push(Span::styled(current_text, current_style));
}
if spans.is_empty() {
spans.push(Span::raw(""));
}
spans
}
fn parse_sgr_params(params: &str, mut style: Style) -> Style {
if params.is_empty() || params == "0" {
return Style::default();
}
let codes: Vec<u8> = params
.split(';')
.filter_map(|s| s.parse::<u8>().ok())
.collect();
let mut i = 0;
while i < codes.len() {
match codes[i] {
0 => style = Style::default(),
1 => style = style.add_modifier(Modifier::BOLD),
2 => style = style.add_modifier(Modifier::DIM),
3 => style = style.add_modifier(Modifier::ITALIC),
4 => style = style.add_modifier(Modifier::UNDERLINED),
7 => style = style.add_modifier(Modifier::REVERSED),
9 => style = style.add_modifier(Modifier::CROSSED_OUT),
22 => style = style.remove_modifier(Modifier::BOLD | Modifier::DIM),
23 => style = style.remove_modifier(Modifier::ITALIC),
24 => style = style.remove_modifier(Modifier::UNDERLINED),
27 => style = style.remove_modifier(Modifier::REVERSED),
29 => style = style.remove_modifier(Modifier::CROSSED_OUT),
30 => style = style.fg(Color::Black),
31 => style = style.fg(Color::Red),
32 => style = style.fg(Color::Green),
33 => style = style.fg(Color::Yellow),
34 => style = style.fg(Color::Blue),
35 => style = style.fg(Color::Magenta),
36 => style = style.fg(Color::Cyan),
37 => style = style.fg(Color::White),
38 => {
if i + 2 < codes.len() && codes[i + 1] == 5 {
style = style.fg(Color::Indexed(codes[i + 2]));
i += 2;
} else if i + 4 < codes.len() && codes[i + 1] == 2 {
style = style.fg(Color::Rgb(codes[i + 2], codes[i + 3], codes[i + 4]));
i += 4;
}
}
39 => style = style.fg(Color::Reset),
40 => style = style.bg(Color::Black),
41 => style = style.bg(Color::Red),
42 => style = style.bg(Color::Green),
43 => style = style.bg(Color::Yellow),
44 => style = style.bg(Color::Blue),
45 => style = style.bg(Color::Magenta),
46 => style = style.bg(Color::Cyan),
47 => style = style.bg(Color::White),
48 => {
if i + 2 < codes.len() && codes[i + 1] == 5 {
style = style.bg(Color::Indexed(codes[i + 2]));
i += 2;
} else if i + 4 < codes.len() && codes[i + 1] == 2 {
style = style.bg(Color::Rgb(codes[i + 2], codes[i + 3], codes[i + 4]));
i += 4;
}
}
49 => style = style.bg(Color::Reset),
90 => style = style.fg(Color::DarkGray),
91 => style = style.fg(Color::LightRed),
92 => style = style.fg(Color::LightGreen),
93 => style = style.fg(Color::LightYellow),
94 => style = style.fg(Color::LightBlue),
95 => style = style.fg(Color::LightMagenta),
96 => style = style.fg(Color::LightCyan),
97 => style = style.fg(Color::White),
100 => style = style.bg(Color::DarkGray),
101 => style = style.bg(Color::LightRed),
102 => style = style.bg(Color::LightGreen),
103 => style = style.bg(Color::LightYellow),
104 => style = style.bg(Color::LightBlue),
105 => style = style.bg(Color::LightMagenta),
106 => style = style.bg(Color::LightCyan),
107 => style = style.bg(Color::White),
_ => {} }
i += 1;
}
style
}
#[cfg(feature = "markdown")]
pub fn render_markdown_to_lines(markdown: &str) -> Vec<Line<'static>> {
use termimad::{MadSkin, crossterm::style::Color as TermColor};
let mut skin = MadSkin::default();
skin.headers[0].set_fg(TermColor::Cyan);
skin.headers[1].set_fg(TermColor::Blue);
skin.bold.set_fg(TermColor::Yellow);
skin.italic.set_fg(TermColor::Magenta);
skin.inline_code.set_fg(TermColor::Green);
skin.code_block.set_fg(TermColor::Green);
let rendered = skin.text(markdown, None);
let rendered_str = rendered.to_string();
let mut lines = Vec::new();
for line in rendered_str.lines() {
let spans = parse_ansi_to_spans(line);
lines.push(Line::from(spans));
}
if lines.is_empty() {
lines.push(Line::from(""));
}
lines
}
#[cfg(not(feature = "markdown"))]
pub fn render_markdown_to_lines(markdown: &str) -> Vec<Line<'static>> {
markdown
.lines()
.map(|l| Line::from(l.to_string()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_plain_text() {
let spans = parse_ansi_to_spans("Hello world");
assert_eq!(spans.len(), 1);
}
#[test]
fn test_parse_red_text() {
let spans = parse_ansi_to_spans("\x1b[31mRed\x1b[0m");
assert_eq!(spans.len(), 1); assert_eq!(spans[0].content, "Red");
}
#[test]
fn test_parse_bold() {
let spans = parse_ansi_to_spans("\x1b[1mBold\x1b[0m normal");
assert_eq!(spans.len(), 2); }
#[test]
fn test_parse_256_color() {
let spans = parse_ansi_to_spans("\x1b[38;5;196mRed 256\x1b[0m");
assert_eq!(spans.len(), 1); assert_eq!(spans[0].content, "Red 256");
}
#[test]
fn test_parse_rgb_color() {
let spans = parse_ansi_to_spans("\x1b[38;2;255;0;0mRGB Red\x1b[0m");
assert_eq!(spans.len(), 1); assert_eq!(spans[0].content, "RGB Red");
}
#[test]
fn test_empty_string() {
let spans = parse_ansi_to_spans("");
assert_eq!(spans.len(), 1);
}
}