use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
};
use crate::util::text::{display_width, wrap_text};
use super::MarkdownRenderer;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TableAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum BorderKind {
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone)]
pub struct TableContext {
pub start_idx: usize,
pub end_idx: usize,
pub col_widths: Vec<usize>,
pub alignments: Vec<TableAlign>,
}
impl MarkdownRenderer {
pub(super) fn calculate_table_render_width_from_col_widths(col_widths: &[usize]) -> usize {
let cols_width: usize = col_widths.iter().map(|cw| cw + 2 + 1).sum();
cols_width + 1 }
pub(super) fn shrink_col_widths(col_widths: &[usize], wrap_width: usize) -> Vec<usize> {
let current = Self::calculate_table_render_width_from_col_widths(col_widths);
if current <= wrap_width {
return col_widths.to_vec();
}
let excess = current.saturating_sub(wrap_width);
let total_col_width: usize = col_widths.iter().sum();
if total_col_width == 0 {
return col_widths.to_vec();
}
let mut remaining_excess = excess;
let mut result = Vec::with_capacity(col_widths.len());
for (i, &cw) in col_widths.iter().enumerate() {
let is_last = i == col_widths.len() - 1;
let shrink = if is_last {
remaining_excess
} else {
let s = (excess * cw) / total_col_width;
remaining_excess = remaining_excess.saturating_sub(s);
s
};
result.push(cw.saturating_sub(shrink).max(1));
}
result
}
pub fn is_table_separator_line(line: &str) -> bool {
let trimmed = line.trim();
if !trimmed.starts_with('|') || !trimmed.ends_with('|') {
return false;
}
let inner = trimmed.trim_matches('|');
inner.split('|').all(|cell| {
let cell = cell.trim();
cell.chars().all(|c| c == '-' || c == ':' || c == ' ')
})
}
pub fn is_table_row(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with('|') && trimmed.ends_with('|') && trimmed.contains('|')
}
pub(super) fn parse_table_alignments(line: &str) -> Vec<TableAlign> {
let trimmed = line.trim();
let inner = trimmed.trim_matches('|');
inner
.split('|')
.map(|cell| {
let cell = cell.trim();
let left = cell.starts_with(':');
let right = cell.ends_with(':');
if left && right {
TableAlign::Center
} else if right {
TableAlign::Right
} else {
TableAlign::Left
}
})
.collect()
}
pub(super) fn parse_table_cells(line: &str) -> Vec<String> {
let trimmed = line.trim();
let inner = trimmed.trim_matches('|');
inner.split('|').map(|s| s.trim().to_string()).collect()
}
pub(super) fn find_table_context(
&self,
line_idx: usize,
lines: &[String],
) -> Option<TableContext> {
let line = lines.get(line_idx)?;
if !Self::is_table_row(line) {
return None;
}
let mut start_idx = line_idx;
while start_idx > 0 {
if let Some(prev) = lines.get(start_idx - 1) {
if Self::is_table_row(prev) {
start_idx -= 1;
} else {
break;
}
} else {
break;
}
}
let mut end_idx = line_idx;
while end_idx < lines.len() - 1 {
if let Some(next) = lines.get(end_idx + 1) {
if Self::is_table_row(next) {
end_idx += 1;
} else {
break;
}
} else {
break;
}
}
if end_idx - start_idx < 1 {
return None;
}
let alignments = if let Some(sep_line) = lines.get(start_idx + 1) {
Self::parse_table_alignments(sep_line)
} else {
return None;
};
let num_cols = alignments.len();
let mut col_widths = vec![1; num_cols];
for row_idx in start_idx..=end_idx {
let row_line = lines.get(row_idx)?;
let cells = Self::parse_table_cells(row_line);
for (i, cell) in cells.iter().enumerate() {
if i < num_cols {
col_widths[i] = col_widths[i].max(display_width(cell));
}
}
}
Some(TableContext {
start_idx,
end_idx,
col_widths,
alignments,
})
}
pub(super) fn render_table_rows(
&self,
line: &str,
line_idx: usize,
ctx: &TableContext,
_lines: &[String],
wrap_width: usize,
) -> Vec<Line<'static>> {
let line_num = self.format_line_number(line_idx);
let line_num_width = display_width(&line_num);
let available_width = wrap_width.saturating_sub(line_num_width);
let col_widths = Self::shrink_col_widths(&ctx.col_widths, available_width);
let border_style = Style::default().fg(self.theme.text_dim);
if Self::is_table_separator_line(line) {
return vec![];
}
let is_header = line_idx == ctx.start_idx;
let is_last_data_row = line_idx == ctx.end_idx;
let content_style = if is_header {
Style::default()
.fg(self.theme.text_bold)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.theme.text_normal)
};
let cells = Self::parse_table_cells(line);
let wrapped_cells: Vec<Vec<String>> = col_widths
.iter()
.enumerate()
.map(|(i, cw)| {
let cell_text = cells.get(i).map(|s| s.as_str()).unwrap_or("");
wrap_text(cell_text, *cw)
})
.collect();
let max_rows = wrapped_cells.iter().map(|r| r.len()).max().unwrap_or(1);
let mut result = Vec::new();
if is_header {
result.push(Self::render_table_border(
&self.format_continuation_line_number(),
&col_widths,
border_style,
BorderKind::Top,
self.theme.bg_primary,
));
}
for sub_row in 0..max_rows {
let num_str = if sub_row == 0 {
line_num.clone()
} else {
self.format_continuation_line_number()
};
let mut spans = vec![Span::styled(
num_str,
Style::default()
.fg(Color::DarkGray)
.bg(self.theme.bg_primary),
)];
spans.push(Span::styled("│", border_style));
for (i, cw) in col_widths.iter().enumerate() {
let cell_line = wrapped_cells
.get(i)
.and_then(|lines| lines.get(sub_row))
.map(|s| s.as_str())
.unwrap_or("");
let cell_width = display_width(cell_line);
let fill = cw.saturating_sub(cell_width);
let align = ctx.alignments.get(i).copied().unwrap_or(TableAlign::Left);
let formatted = match align {
TableAlign::Center => {
let left = fill / 2;
let right = fill - left;
format!(" {}{}{} ", " ".repeat(left), cell_line, " ".repeat(right))
}
TableAlign::Right => {
format!(" {}{} ", " ".repeat(fill), cell_line)
}
TableAlign::Left => {
format!(" {}{} ", cell_line, " ".repeat(fill))
}
};
spans.push(Span::styled(formatted, content_style));
spans.push(Span::styled("│", border_style));
}
result.push(Line::from(spans));
}
if is_header {
result.push(Self::render_table_border(
&self.format_continuation_line_number(),
&col_widths,
border_style,
BorderKind::Middle,
self.theme.bg_primary,
));
}
if !is_header && !is_last_data_row {
result.push(Self::render_table_border(
&self.format_continuation_line_number(),
&col_widths,
border_style,
BorderKind::Middle,
self.theme.bg_primary,
));
}
if is_last_data_row {
result.push(Self::render_table_border(
&self.format_continuation_line_number(),
&col_widths,
border_style,
BorderKind::Bottom,
self.theme.bg_primary,
));
}
result
}
pub(super) fn render_table_border(
line_num: &str,
col_widths: &[usize],
border_style: Style,
kind: BorderKind,
line_num_bg: Color,
) -> Line<'static> {
let (left, mid, right, fill) = match kind {
BorderKind::Top => ("┌", "┬", "┐", "─"),
BorderKind::Middle => ("├", "┼", "┤", "─"),
BorderKind::Bottom => ("└", "┴", "┘", "─"),
};
let mut spans = vec![Span::styled(
line_num.to_string(),
Style::default().fg(Color::DarkGray).bg(line_num_bg),
)];
spans.push(Span::styled(left, border_style));
for (i, cw) in col_widths.iter().enumerate() {
spans.push(Span::styled(fill.repeat(*cw + 2), border_style));
if i < col_widths.len() - 1 {
spans.push(Span::styled(mid, border_style));
}
}
spans.push(Span::styled(right, border_style));
Line::from(spans)
}
}