use pulldown_cmark::Alignment;
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
};
use super::{GfmSegment, GfmTable};
fn admonition_color(kind: &str) -> Color {
match kind.to_lowercase().as_str() {
"note" => Color::Blue,
"tip" => Color::Green,
"important" => Color::Magenta,
"warning" => Color::Yellow,
"caution" => Color::Red,
_ => Color::White,
}
}
fn align_text(text: &str, width: usize, alignment: &Alignment) -> String {
let text_len = text.len();
if text_len >= width {
return text[..width].to_string();
}
let padding = width - text_len;
match alignment {
Alignment::Right => format!("{}{}", " ".repeat(padding), text),
Alignment::Center => {
let left = padding / 2;
let right = padding - left;
format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
}
_ => format!("{}{}", text, " ".repeat(padding)),
}
}
pub fn render_table(table: &GfmTable, max_width: u16) -> Vec<Line<'static>> {
let max_width = max_width as usize;
let col_count = table.headers.len();
if col_count == 0 {
return vec![];
}
let mut col_widths: Vec<usize> = table.headers.iter().map(|h| h.len()).collect();
for row in &table.rows {
for (i, cell) in row.iter().enumerate() {
if i < col_widths.len() {
let longest_segment = cell.split('\n').map(str::len).max().unwrap_or(0);
col_widths[i] = col_widths[i].max(longest_segment);
}
}
}
let separator_width = if col_count > 1 {
(col_count - 1) * 3
} else {
0
};
let available = max_width.saturating_sub(separator_width);
let total_content: usize = col_widths.iter().sum();
if total_content > available && available > 0 {
let scale = available as f64 / total_content as f64;
for w in &mut col_widths {
*w = (*w as f64 * scale).max(1.0) as usize;
}
}
let alignments = &table.alignments;
let mut lines: Vec<Line<'static>> = Vec::new();
let header_spans: Vec<Span<'static>> = table
.headers
.iter()
.enumerate()
.flat_map(|(i, h)| {
let alignment = alignments.get(i).unwrap_or(&Alignment::None);
let width = col_widths.get(i).copied().unwrap_or(h.len());
let text = align_text(h, width, alignment);
let mut spans = vec![Span::styled(
text,
Style::default().add_modifier(Modifier::BOLD),
)];
if i < col_count - 1 {
spans.push(Span::raw(" │ ".to_string()));
}
spans
})
.collect();
lines.push(Line::from(header_spans));
let sep_spans: Vec<Span<'static>> = col_widths
.iter()
.enumerate()
.flat_map(|(i, &w)| {
let mut spans = vec![Span::raw("─".repeat(w))];
if i < col_count - 1 {
spans.push(Span::raw("─┼─".to_string()));
}
spans
})
.collect();
lines.push(Line::from(sep_spans));
for row in &table.rows {
let cell_lines: Vec<Vec<String>> = row
.iter()
.enumerate()
.map(|(i, cell)| {
let width = col_widths.get(i).copied().unwrap_or(cell.len());
wrap_cell(cell, width)
})
.collect();
let row_height = cell_lines.iter().map(|c| c.len()).max().unwrap_or(1).max(1);
for vis in 0..row_height {
let row_spans: Vec<Span<'static>> = (0..col_count)
.flat_map(|i| {
let alignment = alignments.get(i).unwrap_or(&Alignment::None);
let width = col_widths.get(i).copied().unwrap_or(0);
let text_ref: &str = cell_lines
.get(i)
.and_then(|cl| cl.get(vis))
.map(String::as_str)
.unwrap_or("");
let text = align_text(text_ref, width, alignment);
let mut spans = vec![Span::raw(text)];
if i < col_count - 1 {
spans.push(Span::raw(" │ ".to_string()));
}
spans
})
.collect();
lines.push(Line::from(row_spans));
}
}
lines
}
fn wrap_cell(cell: &str, width: usize) -> Vec<String> {
if cell.is_empty() {
return vec![String::new()];
}
if width == 0 {
return cell.split('\n').map(String::from).collect();
}
let mut out: Vec<String> = Vec::new();
for segment in cell.split('\n') {
if segment.is_empty() {
out.push(String::new());
continue;
}
let wrapped = textwrap::wrap(segment, width);
if wrapped.is_empty() {
out.push(String::new());
} else {
for piece in wrapped {
out.push(piece.into_owned());
}
}
}
if out.is_empty() {
out.push(String::new());
}
out
}
pub fn render_admonition(kind: &str, body: &str) -> Vec<Line<'static>> {
let color = admonition_color(kind);
let label = kind.to_uppercase();
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(Span::styled(
label,
Style::default().fg(color).add_modifier(Modifier::BOLD),
)));
for line in body.lines() {
lines.push(Line::from(vec![
Span::styled("▌ ".to_string(), Style::default().fg(color)),
Span::raw(line.to_string()),
]));
}
lines
}
pub fn render_footnotes(footnotes: &[(String, String)]) -> Vec<Line<'static>> {
if footnotes.is_empty() {
return vec![];
}
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(Span::raw("───────────────".to_string())));
for (label, definition) in footnotes {
lines.push(Line::from(vec![
Span::styled(
format!("[^{}]: ", label),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(definition.clone()),
]));
}
lines
}
pub fn render_gfm_segments(segments: &[GfmSegment], max_width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut collected_footnotes: Vec<(String, String)> = Vec::new();
for segment in segments {
match segment {
GfmSegment::Markdown(text) => {
let md = tui_markdown::from_str(text);
for line in md.lines {
let owned_spans: Vec<Span<'static>> = line
.spans
.into_iter()
.map(|s| Span::styled(s.content.to_string(), s.style))
.collect();
lines.push(Line::from(owned_spans).style(line.style));
}
}
GfmSegment::Table(table) => {
lines.push(Line::default());
lines.extend(render_table(table, max_width));
lines.push(Line::default());
}
GfmSegment::Admonition { kind, body } => {
lines.push(Line::default());
lines.extend(render_admonition(kind, body));
lines.push(Line::default());
}
GfmSegment::Footnote { label, body } => {
collected_footnotes.push((label.clone(), body.clone()));
}
}
}
if !collected_footnotes.is_empty() {
lines.push(Line::default());
lines.extend(render_footnotes(&collected_footnotes));
}
lines
}