Skip to content

Commit c4d708c

Browse files
committed
feat(tui): change markdown table format to simple ASCII style
- Add render_table_simple() function for minimal ASCII table rendering - Tables now render without outer box borders - Uses simple format: 'Header | Header' with '---+---' separator - More readable and clean table display in conversations - Keep original render_table() for backwards compatibility
1 parent 0d83858 commit c4d708c

File tree

2 files changed

+225
-7
lines changed

2 files changed

+225
-7
lines changed

cortex-core/src/markdown/renderer.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use unicode_width::UnicodeWidthStr;
3939
use crate::markdown::code_block::CodeBlockRenderer;
4040
use crate::markdown::inline::{render_blockquote_prefix, render_hr};
4141
use crate::markdown::list::ListContext;
42-
use crate::markdown::table::{TableBuilder, render_table};
42+
use crate::markdown::table::{TableBuilder, render_table_simple};
4343
use crate::markdown::theme::MarkdownTheme;
4444

4545
// ============================================================
@@ -593,10 +593,9 @@ impl<'a> RenderState<'a> {
593593
let mut table = builder.build();
594594
table.calculate_column_widths(self.renderer.width);
595595

596-
let table_lines = render_table(
596+
// Use simple ASCII table format without outer borders
597+
let table_lines = render_table_simple(
597598
&table,
598-
self.renderer.theme.table_border,
599-
self.renderer.theme.table_header_text,
600599
self.renderer.theme.table_cell_text,
601600
self.renderer.width,
602601
);
@@ -863,10 +862,9 @@ impl<'a> RenderState<'a> {
863862
let mut table = builder.build();
864863
table.calculate_column_widths(self.renderer.width);
865864

866-
let table_lines = render_table(
865+
// Use simple ASCII table format without outer borders
866+
let table_lines = render_table_simple(
867867
&table,
868-
self.renderer.theme.table_border,
869-
self.renderer.theme.table_header_text,
870868
self.renderer.theme.table_cell_text,
871869
self.renderer.width,
872870
);

cortex-core/src/markdown/table.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,115 @@ pub fn render_table(
493493
lines
494494
}
495495

496+
/// Renders a table as a simple ASCII code block without outer borders.
497+
///
498+
/// This produces a cleaner, minimal table format suitable for code blocks:
499+
/// ```text
500+
/// Header 1 | Header 2 | Header 3
501+
/// ---------+-----------+---------
502+
/// Cell 1 | Cell 2 | Cell 3
503+
/// Cell 4 | Cell 5 | Cell 6
504+
/// ```
505+
///
506+
/// # Arguments
507+
/// * `table` - The table to render
508+
/// * `style` - Style for all text (headers and cells use the same style)
509+
/// * `max_width` - Maximum total width for the table
510+
///
511+
/// # Returns
512+
/// A vector of `Line`s ready for display in ratatui.
513+
pub fn render_table_simple(table: &Table, style: Style, max_width: u16) -> Vec<Line<'static>> {
514+
// Handle empty table
515+
if table.is_empty() {
516+
return Vec::new();
517+
}
518+
519+
// Clone and calculate widths if not already done
520+
let mut table = table.clone();
521+
if table.column_widths.iter().all(|&w| w == 0) {
522+
table.calculate_column_widths(max_width);
523+
}
524+
525+
// Handle case where we still have no columns
526+
if table.num_columns() == 0 {
527+
return Vec::new();
528+
}
529+
530+
let widths = &table.column_widths;
531+
let alignments = &table.alignments;
532+
533+
let mut lines = Vec::new();
534+
535+
// Header row (if present)
536+
if !table.headers.is_empty() {
537+
lines.push(render_simple_row(&table.headers, widths, alignments, style));
538+
539+
// Header separator line: ---+---+---
540+
lines.push(render_simple_separator(widths, style));
541+
}
542+
543+
// Data rows
544+
for row in &table.rows {
545+
lines.push(render_simple_row(row, widths, alignments, style));
546+
}
547+
548+
lines
549+
}
550+
551+
/// Renders a simple row without outer borders.
552+
///
553+
/// Format: `content | content | content`
554+
fn render_simple_row(
555+
cells: &[TableCell],
556+
widths: &[usize],
557+
alignments: &[Alignment],
558+
style: Style,
559+
) -> Line<'static> {
560+
let mut spans = Vec::new();
561+
562+
for (i, width) in widths.iter().enumerate() {
563+
// Get cell content or empty string if missing
564+
let cell = cells.get(i);
565+
let content = cell.map(|c| c.content.as_str()).unwrap_or("");
566+
let alignment = alignments.get(i).copied().unwrap_or_default();
567+
568+
// Truncate and align
569+
let truncated = truncate_with_ellipsis(content, *width);
570+
let aligned = align_text(&truncated, *width, alignment);
571+
572+
// Add cell content with padding
573+
spans.push(Span::styled(format!(" {} ", aligned), style));
574+
575+
// Add separator between columns (not after last)
576+
if i < widths.len() - 1 {
577+
spans.push(Span::styled("|", style));
578+
}
579+
}
580+
581+
Line::from(spans)
582+
}
583+
584+
/// Renders a simple separator line for the header.
585+
///
586+
/// Format: `---+---+---`
587+
fn render_simple_separator(widths: &[usize], style: Style) -> Line<'static> {
588+
let mut spans = Vec::new();
589+
590+
for (i, &width) in widths.iter().enumerate() {
591+
// Each column segment: padding + content + padding (same as cell)
592+
let segment_width = width + 2; // +2 for the spaces on each side
593+
let segment: String = std::iter::repeat('-').take(segment_width).collect();
594+
spans.push(Span::styled(segment, style));
595+
596+
// Add separator between columns (not after last)
597+
if i < widths.len() - 1 {
598+
spans.push(Span::styled("+", style));
599+
}
600+
}
601+
602+
Line::from(spans)
603+
}
604+
496605
/// Renders a horizontal border line.
497606
///
498607
/// # Arguments
@@ -996,4 +1105,115 @@ mod tests {
9961105
assert_eq!(cell.content, "");
9971106
assert_eq!(cell.width(), 0);
9981107
}
1108+
1109+
// ============================================================
1110+
// Simple Table Tests
1111+
// ============================================================
1112+
1113+
#[test]
1114+
fn test_simple_table_empty() {
1115+
let table = Table::default();
1116+
let lines = render_table_simple(&table, Style::default(), 80);
1117+
assert!(lines.is_empty());
1118+
}
1119+
1120+
#[test]
1121+
fn test_simple_table_basic() {
1122+
let mut builder = TableBuilder::new();
1123+
builder.start_header();
1124+
builder.add_cell("Header".to_string());
1125+
builder.end_header();
1126+
1127+
builder.start_row();
1128+
builder.add_cell("Value".to_string());
1129+
builder.end_row();
1130+
1131+
let table = builder.build();
1132+
let lines = render_table_simple(&table, Style::default(), 80);
1133+
1134+
// Should have: header, separator, data row = 3 lines (no outer borders)
1135+
assert_eq!(lines.len(), 3);
1136+
}
1137+
1138+
#[test]
1139+
fn test_simple_table_multiple_columns() {
1140+
let mut builder = TableBuilder::new();
1141+
builder.start_header();
1142+
builder.add_cell("A".to_string());
1143+
builder.add_cell("B".to_string());
1144+
builder.add_cell("C".to_string());
1145+
builder.end_header();
1146+
1147+
builder.start_row();
1148+
builder.add_cell("1".to_string());
1149+
builder.add_cell("2".to_string());
1150+
builder.add_cell("3".to_string());
1151+
builder.end_row();
1152+
1153+
let table = builder.build();
1154+
let lines = render_table_simple(&table, Style::default(), 80);
1155+
1156+
// Check that separator line contains + characters
1157+
let separator_content: String = lines[1].spans.iter().map(|s| &*s.content).collect();
1158+
assert!(
1159+
separator_content.contains('+'),
1160+
"Separator should contain + for column separation"
1161+
);
1162+
assert!(
1163+
separator_content.contains('-'),
1164+
"Separator should contain - for dashes"
1165+
);
1166+
}
1167+
1168+
#[test]
1169+
fn test_simple_table_without_headers() {
1170+
let mut builder = TableBuilder::new();
1171+
1172+
builder.start_row();
1173+
builder.add_cell("A".to_string());
1174+
builder.add_cell("B".to_string());
1175+
builder.end_row();
1176+
1177+
builder.start_row();
1178+
builder.add_cell("C".to_string());
1179+
builder.add_cell("D".to_string());
1180+
builder.end_row();
1181+
1182+
let table = builder.build();
1183+
let lines = render_table_simple(&table, Style::default(), 80);
1184+
1185+
// Should have: 2 data rows only (no header, no separator)
1186+
assert_eq!(lines.len(), 2);
1187+
}
1188+
1189+
#[test]
1190+
fn test_simple_table_format() {
1191+
let mut builder = TableBuilder::new();
1192+
builder.start_header();
1193+
builder.add_cell("Name".to_string());
1194+
builder.add_cell("Value".to_string());
1195+
builder.end_header();
1196+
1197+
builder.start_row();
1198+
builder.add_cell("foo".to_string());
1199+
builder.add_cell("bar".to_string());
1200+
builder.end_row();
1201+
1202+
let table = builder.build();
1203+
let lines = render_table_simple(&table, Style::default(), 80);
1204+
1205+
// First line should be header with | separator
1206+
let header_content: String = lines[0].spans.iter().map(|s| &*s.content).collect();
1207+
assert!(
1208+
header_content.contains('|'),
1209+
"Header should have | separator between columns"
1210+
);
1211+
1212+
// Second line should be separator with -+-
1213+
let separator_content: String = lines[1].spans.iter().map(|s| &*s.content).collect();
1214+
assert!(
1215+
separator_content.contains('+'),
1216+
"Separator should have + at column intersections"
1217+
);
1218+
}
9991219
}

0 commit comments

Comments
 (0)