Table Improvements

1. Merge Continuation Rows (clean_table_cells)
2. Footnote Extraction (is_footnote_row)
3. Empty Row Removal
This commit is contained in:
Abimael Martell
2026-02-08 22:04:25 -08:00
parent 5e44e468ac
commit 0f2f890a5c
+106 -4
View File
@@ -367,14 +367,20 @@ pub fn table_to_markdown(table: &Table) -> String {
return String::new(); return String::new();
} }
let num_cols = table.cells[0].len(); // Clean up the table: merge continuation rows, extract footnotes, remove empty rows
let (cleaned_cells, footnotes) = clean_table_cells(&table.cells);
if cleaned_cells.is_empty() {
return String::new();
}
let num_cols = cleaned_cells[0].len();
let mut output = String::new(); let mut output = String::new();
// Calculate column widths for alignment // Calculate column widths for alignment
let col_widths: Vec<usize> = (0..num_cols) let col_widths: Vec<usize> = (0..num_cols)
.map(|col| { .map(|col| {
table cleaned_cells
.cells
.iter() .iter()
.map(|row| row.get(col).map(|c| c.len()).unwrap_or(0)) .map(|row| row.get(col).map(|c| c.len()).unwrap_or(0))
.max() .max()
@@ -384,7 +390,7 @@ pub fn table_to_markdown(table: &Table) -> String {
.collect(); .collect();
// Output each row // Output each row
for (row_idx, row) in table.cells.iter().enumerate() { for (row_idx, row) in cleaned_cells.iter().enumerate() {
output.push('|'); output.push('|');
for (col_idx, cell) in row.iter().enumerate() { for (col_idx, cell) in row.iter().enumerate() {
let width = col_widths[col_idx]; let width = col_widths[col_idx];
@@ -402,9 +408,105 @@ pub fn table_to_markdown(table: &Table) -> String {
} }
} }
// Add footnotes below the table
if !footnotes.is_empty() {
output.push('\n');
for footnote in footnotes {
output.push_str(&footnote);
output.push('\n');
}
}
output output
} }
/// Clean up table cells: merge continuation rows, extract footnotes, remove empty rows
fn clean_table_cells(cells: &[Vec<String>]) -> (Vec<Vec<String>>, Vec<String>) {
let mut cleaned: Vec<Vec<String>> = Vec::new();
let mut footnotes: Vec<String> = Vec::new();
for row in cells {
// Check if this row is empty
if row.iter().all(|c| c.trim().is_empty()) {
continue;
}
// Check if this row is a footnote (starts with (1), (2), etc. or just a number reference)
let first_cell = row.first().map(|s| s.trim()).unwrap_or("");
if is_footnote_row(first_cell) {
// Combine all cells into a single footnote line
let footnote_text: String = row
.iter()
.map(|c| c.trim())
.filter(|c| !c.is_empty())
.collect::<Vec<_>>()
.join(" ");
footnotes.push(footnote_text);
continue;
}
// Check if this is a continuation row (first column is empty but others have content)
let is_continuation = first_cell.is_empty()
&& row.iter().skip(1).any(|c| !c.trim().is_empty())
&& !cleaned.is_empty();
if is_continuation {
// Merge with previous row
if let Some(prev_row) = cleaned.last_mut() {
for (col_idx, cell) in row.iter().enumerate() {
let cell_text = cell.trim();
if !cell_text.is_empty() && col_idx < prev_row.len() {
if !prev_row[col_idx].is_empty() {
prev_row[col_idx].push(' ');
}
prev_row[col_idx].push_str(cell_text);
}
}
}
} else {
// Regular row - add as new row
cleaned.push(row.iter().map(|c| c.trim().to_string()).collect());
}
}
(cleaned, footnotes)
}
/// Check if a cell value indicates a footnote row
fn is_footnote_row(text: &str) -> bool {
let trimmed = text.trim();
// Check for common footnote patterns
// (1), (2), etc.
if trimmed.starts_with('(') && trimmed.len() >= 2 {
let inside = &trimmed[1..];
if let Some(close_idx) = inside.find(')') {
let num_part = &inside[..close_idx];
if num_part.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
// 1), 2), etc.
if trimmed.len() >= 2 {
if let Some(paren_idx) = trimmed.find(')') {
let num_part = &trimmed[..paren_idx];
if !num_part.is_empty() && num_part.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
// Check for "Note:" or "Notes:" at the start
let lower = trimmed.to_lowercase();
if lower.starts_with("note:") || lower.starts_with("notes:") {
return true;
}
false
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;