use ratatui::{
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
};
pub fn render_markdown(text: &str) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut in_code_block = false;
let mut table_buffer: Vec<Vec<Vec<Span<'static>>>> = Vec::new();
for raw_line in text.lines() {
if raw_line.trim_start().starts_with("```") {
if !table_buffer.is_empty() {
flush_markdown_table(&mut lines, &mut table_buffer);
}
in_code_block = !in_code_block;
if in_code_block {
lines.push(Line::from(Span::styled(
"\u{2500}".repeat(40),
Style::default().fg(Color::DarkGray),
)));
} else {
lines.push(Line::from(Span::styled(
"\u{2500}".repeat(40),
Style::default().fg(Color::DarkGray),
)));
lines.push(Line::from(""));
}
continue;
}
if in_code_block {
lines.push(Line::from(Span::styled(
format!(" {}", raw_line),
Style::default().fg(Color::Cyan).bg(Color::Reset),
)));
continue;
}
let trimmed = raw_line.trim_start();
if trimmed.starts_with('|') {
let cells: Vec<&str> = trimmed
.trim_matches('|')
.split('|')
.map(|s| s.trim())
.collect();
if cells.iter().all(|c| c.chars().all(|ch| ch == '-' || ch == ':' || ch == ' ')) {
continue;
}
let cell_spans: Vec<Vec<Span<'static>>> = cells
.iter()
.map(|cell| render_inline_spans(cell))
.collect();
table_buffer.push(cell_spans);
continue;
}
if !table_buffer.is_empty() {
flush_markdown_table(&mut lines, &mut table_buffer);
}
if let Some(rest) = trimmed.strip_prefix("### ") {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!(" {}", rest.to_string()),
Style::default().fg(Color::Cyan).bold(),
)));
lines.push(Line::from(""));
continue;
}
if let Some(rest) = trimmed.strip_prefix("## ") {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!("\u{258e} {}", rest.to_string()),
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)));
lines.push(Line::from(""));
continue;
}
if let Some(rest) = trimmed.strip_prefix("# ") {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
rest.to_string(),
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
)));
lines.push(Line::from(""));
continue;
}
if let Some(rest) = trimmed.strip_prefix("> ") {
lines.push(Line::from(Span::styled(
format!(" \u{258c} {}", rest.to_string()),
Style::default().fg(Color::Magenta).italic(),
)));
continue;
}
if let Some(rest) = trimmed.strip_prefix("- ") {
let mut list_spans = vec![
Span::styled(" \u{2022} ", Style::default().fg(Color::Cyan)),
];
list_spans.extend(render_inline_spans(rest));
lines.push(Line::from(list_spans));
continue;
}
if trimmed.len() > 2 && trimmed.as_bytes().get(1) == Some(&b'.') && trimmed.as_bytes().first().map(|b| b.is_ascii_digit()).unwrap_or(false) {
let num = &trimmed[..2];
let rest = &trimmed[3..];
let mut list_spans = vec![
Span::styled(format!(" {} ", num), Style::default().fg(Color::Yellow)),
];
list_spans.extend(render_inline_spans(rest));
lines.push(Line::from(list_spans));
continue;
}
if trimmed == "---" || trimmed == "***" || trimmed == "___" {
lines.push(Line::from(Span::styled(
"\u{2500}".repeat(40),
Style::default().fg(Color::DarkGray),
)));
continue;
}
if trimmed.is_empty() {
lines.push(Line::from(""));
continue;
}
lines.push(Line::from(render_inline_spans(trimmed)));
}
if !table_buffer.is_empty() {
flush_markdown_table(&mut lines, &mut table_buffer);
}
lines
}
fn render_inline_spans(text: &str) -> Vec<Span<'static>> {
let mut segments: Vec<(String, bool, bool, bool)> = Vec::new();
let mut current = String::new();
let mut bold = false;
let mut italic = false;
let mut code = false;
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if !code && i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
if !current.is_empty() {
segments.push((std::mem::take(&mut current), bold, italic, code));
}
bold = !bold;
i += 2;
continue;
}
if !code && i + 1 < chars.len() && chars[i] == '_' && chars[i + 1] == '_' {
if !current.is_empty() {
segments.push((std::mem::take(&mut current), bold, italic, code));
}
bold = !bold;
i += 2;
continue;
}
if !code && chars[i] == '*' {
if !current.is_empty() {
segments.push((std::mem::take(&mut current), bold, italic, code));
}
italic = !italic;
i += 1;
continue;
}
if chars[i] == '`' {
if !current.is_empty() {
segments.push((std::mem::take(&mut current), bold, italic, code));
}
code = !code;
i += 1;
continue;
}
current.push(chars[i]);
i += 1;
}
if !current.is_empty() {
segments.push((current, bold, italic, code));
}
if segments.is_empty() {
return vec![Span::styled(text.to_string(), Style::default().fg(Color::Reset))];
}
segments
.into_iter()
.map(|(s, b, it, c)| Span::styled(s, inline_style(b, it, c)))
.collect()
}
fn inline_style(bold: bool, italic: bool, code: bool) -> Style {
let mut style = Style::default().fg(Color::Reset);
if code {
style = Style::default().fg(Color::Yellow).bg(Color::Reset);
}
if bold {
style = style.bold();
}
if italic {
style = style.italic();
}
style
}
fn flush_markdown_table(
lines: &mut Vec<Line<'static>>,
table_buffer: &mut Vec<Vec<Vec<Span<'static>>>>,
) {
if table_buffer.is_empty() {
return;
}
let rows = std::mem::take(table_buffer);
let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
if col_count == 0 {
return;
}
let mut col_widths = vec![0usize; col_count];
for row in &rows {
for (i, cell_spans) in row.iter().enumerate() {
if i < col_count {
let w: usize = cell_spans.iter().map(|s| {
unicode_width::UnicodeWidthStr::width(s.content.as_ref())
}).sum();
col_widths[i] = col_widths[i].max(w);
}
}
}
let border_parts: Vec<String> = col_widths.iter().map(|w| "\u{2500}".repeat(w + 2)).collect();
let top_border = format!("\u{250c}{}\u{2510}", border_parts.join("\u{252c}"));
let header_sep = format!("\u{251c}{}\u{2524}", border_parts.join("\u{253c}"));
let bottom_border = format!("\u{2514}{}\u{2518}", border_parts.join("\u{2534}"));
lines.push(Line::from(Span::styled(
top_border,
Style::default().fg(Color::DarkGray),
)));
for (row_idx, row) in rows.iter().enumerate() {
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::styled("\u{2502} ", Style::default().fg(Color::DarkGray)));
for (col_idx, cell_spans) in row.iter().enumerate() {
if col_idx > 0 {
spans.push(Span::styled(" \u{2502} ", Style::default().fg(Color::DarkGray)));
}
let target_w = col_widths.get(col_idx).copied().unwrap_or(0);
for s in cell_spans {
spans.push(s.clone());
}
let current_w: usize = cell_spans.iter().map(|s| {
unicode_width::UnicodeWidthStr::width(s.content.as_ref())
}).sum();
if current_w < target_w {
spans.push(Span::raw(" ".repeat(target_w - current_w)));
}
}
spans.push(Span::styled(" \u{2502}", Style::default().fg(Color::DarkGray)));
if row_idx == 0 {
spans = spans.into_iter().map(|s| {
Span::styled(
s.content.to_string(),
s.style.add_modifier(Modifier::BOLD),
)
}).collect();
}
lines.push(Line::from(spans));
if row_idx == 0 {
lines.push(Line::from(Span::styled(
header_sep.clone(),
Style::default().fg(Color::DarkGray),
)));
}
}
lines.push(Line::from(Span::styled(
bottom_border,
Style::default().fg(Color::DarkGray),
)));
lines.push(Line::from(""));
}