diff --git a/src/extractor.rs b/src/extractor.rs index 47c944b..045608a 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -43,13 +43,32 @@ impl TextLine { if i == 0 { result.push_str(text); } else { + let prev_item = &self.items[i - 1]; + // Don't add space before/after hyphens for hyphenated words let prev_ends_with_hyphen = result.ends_with('-'); let curr_is_hyphen = text.trim() == "-"; let curr_starts_with_hyphen = text.starts_with('-'); - if prev_ends_with_hyphen || curr_is_hyphen || curr_starts_with_hyphen { - // No space for hyphenated words + // Detect subscript/superscript: smaller font size and/or Y offset + // Subscripts/superscripts are typically 60-80% of normal font size + // and have a vertical offset of 1-3 points + let font_ratio = item.font_size / prev_item.font_size; + let reverse_font_ratio = prev_item.font_size / item.font_size; + let y_diff = (item.y - prev_item.y).abs(); + + // Current item is subscript/superscript (smaller than previous) + let is_sub_super = font_ratio < 0.85 && y_diff > 1.0; + // Previous item was subscript/superscript (returning to normal size) + let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0; + + if prev_ends_with_hyphen + || curr_is_hyphen + || curr_starts_with_hyphen + || is_sub_super + || was_sub_super + { + // No space for hyphenated words or subscript/superscript result.push_str(text); } else { result.push(' '); diff --git a/src/tables.rs b/src/tables.rs index e235ef7..8ffc892 100644 --- a/src/tables.rs +++ b/src/tables.rs @@ -123,8 +123,9 @@ fn detect_table_in_region(items: &[(usize, &TextItem)]) -> Option { return None; } - // Build the table grid - let mut cells: Vec> = vec![vec![String::new(); columns.len()]; rows.len()]; + // Build the table grid - first collect items per cell, then join properly + let mut cell_items: Vec>> = + vec![vec![Vec::new(); columns.len()]; rows.len()]; let mut item_indices = Vec::new(); for (idx, item) in items { @@ -132,14 +133,26 @@ fn detect_table_in_region(items: &[(usize, &TextItem)]) -> Option
{ let row = find_row_index(&rows, item.y); if let (Some(col), Some(row)) = (col, row) { - if !cells[row][col].is_empty() { - cells[row][col].push(' '); - } - cells[row][col].push_str(item.text.trim()); + cell_items[row][col].push(item); item_indices.push(*idx); } } + // Sort items within each cell by X position and join with subscript-aware spacing + let mut cells: Vec> = Vec::with_capacity(rows.len()); + for row_items in &mut cell_items { + let mut row_cells = Vec::with_capacity(columns.len()); + for col_items in row_items.iter_mut() { + // Sort by X position + col_items.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)); + + // Join items with subscript-aware spacing + let text = join_cell_items(col_items); + row_cells.push(text); + } + cells.push(row_cells); + } + // Validation 1: most rows should have content in first column let rows_with_first_col = cells.iter().filter(|row| !row[0].is_empty()).count(); if rows_with_first_col < rows.len() / 2 { @@ -300,6 +313,54 @@ fn find_row_index(rows: &[f32], y: f32) -> Option { .map(|(idx, _)| idx) } +/// Join cell items with subscript/superscript-aware spacing +/// Same logic as TextLine::text() but for table cells +fn join_cell_items(items: &[&TextItem]) -> String { + let mut result = String::new(); + + for (i, item) in items.iter().enumerate() { + let text = item.text.trim(); + if text.is_empty() { + continue; + } + + if result.is_empty() { + result.push_str(text); + } else { + let prev_item = items[i - 1]; + + // Don't add space before/after hyphens + let prev_ends_with_hyphen = result.ends_with('-'); + let curr_is_hyphen = text == "-"; + let curr_starts_with_hyphen = text.starts_with('-'); + + // Detect subscript/superscript: smaller font size and/or Y offset + let font_ratio = item.font_size / prev_item.font_size; + let reverse_font_ratio = prev_item.font_size / item.font_size; + let y_diff = (item.y - prev_item.y).abs(); + + // Current item is subscript/superscript (smaller than previous) + let is_sub_super = font_ratio < 0.85 && y_diff > 1.0; + // Previous item was subscript/superscript (returning to normal size) + let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0; + + if prev_ends_with_hyphen + || curr_is_hyphen + || curr_starts_with_hyphen + || is_sub_super + || was_sub_super + { + result.push_str(text); + } else { + result.push(' '); + result.push_str(text); + } + } + } + + result +} + /// Format a table as markdown pub fn table_to_markdown(table: &Table) -> String { if table.cells.is_empty() || table.cells[0].is_empty() {