Table Improvements
1. Merge Continuation Rows (clean_table_cells) 2. Footnote Extraction (is_footnote_row) 3. Empty Row Removal
This commit is contained in:
+106
-4
@@ -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::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user