use crate::blocks::{Block, BlockKind};
use crate::header_rows::tables_in;
const WRAP_ALIGN: f64 = 0.5;
const WRAP_GAP: f64 = 1.0;
fn wraps_under(entry: [f64; 4], line: [f64; 4]) -> bool {
let height = (line[3] - line[1]).max(1.0);
(line[0] - entry[0]).abs() <= WRAP_ALIGN * height
&& entry[1] - line[3] <= WRAP_GAP * height
&& line[3] <= entry[3]
}
pub(crate) fn join_continuation_rows(blocks: &mut Vec<Block>) {
let mut drop = vec![false; blocks.len()];
let mut changed = false;
for (first, end) in tables_in(blocks) {
let table = &blocks[first..end];
let two_columns = table.iter().all(|b| match b.kind {
BlockKind::TableCell {
column,
row_span,
column_span,
..
} => column <= 1 && row_span == 1 && column_span == 1,
_ => true,
});
let columns = table
.iter()
.filter_map(|b| match b.kind {
BlockKind::TableCell { column, .. } => Some(column + 1),
_ => None,
})
.max()
.unwrap_or(0);
if !two_columns || columns != 2 {
continue;
}
let rows = table
.iter()
.filter_map(|b| match b.kind {
BlockKind::TableCell { row, .. } => Some(row + 1),
_ => None,
})
.max()
.unwrap_or(0);
let mut stub: Vec<Option<usize>> = vec![None; rows as usize];
let mut entry: Vec<Option<usize>> = vec![None; rows as usize];
let mut doubled: Vec<bool> = vec![false; rows as usize];
for (i, b) in table.iter().enumerate() {
if let BlockKind::TableCell { row, column, .. } = b.kind {
let slot = if column == 0 { &mut stub } else { &mut entry };
if slot[row as usize].replace(first + i).is_some() {
doubled[row as usize] = true;
}
}
}
let mut target: Vec<u16> = (0..rows).collect();
for r in 1..rows as usize {
let stub_empty = stub[r].is_none_or(|i| blocks[i].text.trim().is_empty());
let Some(here) = entry[r] else {
continue;
};
if doubled[r] || !stub_empty || !is_prose(&blocks[here].text) {
continue;
}
let above = target[r - 1] as usize;
let Some(keep) = entry[above] else {
continue;
};
if doubled[above]
|| !is_prose(&blocks[keep].text)
|| !wraps_under(blocks[keep].bbox, blocks[here].bbox)
{
continue;
}
let (text, bbox, spans) = {
let b = &blocks[here];
(b.text.clone(), b.bbox, b.spans.clone())
};
let kept = &mut blocks[keep];
if !text.trim().is_empty() {
if !kept.text.is_empty() {
kept.text.push(' ');
}
kept.text.push_str(text.trim());
}
kept.bbox = [
kept.bbox[0].min(bbox[0]),
kept.bbox[1].min(bbox[1]),
kept.bbox[2].max(bbox[2]),
kept.bbox[3].max(bbox[3]),
];
kept.spans.extend(spans);
drop[here] = true;
if let Some(s) = stub[r] {
drop[s] = true;
}
target[r] = above as u16;
changed = true;
}
let mut new_row = vec![0u16; rows as usize];
let mut n = 0u16;
for r in 0..rows as usize {
if target[r] == r as u16 {
new_row[r] = n;
n += 1;
}
}
for (i, b) in blocks[first..end].iter_mut().enumerate() {
if drop[first + i] {
continue;
}
if let BlockKind::TableCell { row, .. } = &mut b.kind {
debug_assert_eq!(target[*row as usize], *row);
*row = new_row[*row as usize];
}
}
}
if !changed {
return;
}
let mut out = Vec::with_capacity(blocks.len());
for (i, block) in blocks.drain(..).enumerate() {
if drop[i] {
continue;
}
let mut block = block;
block.index = out.len();
out.push(block);
}
*blocks = out;
}
fn is_prose(text: &str) -> bool {
text.chars().any(|c| c.is_alphabetic())
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(row: u16, column: u16, text: &str) -> Block {
Block {
kind: BlockKind::TableCell {
row,
column,
row_span: 1,
column_span: 1,
header: false,
},
page: 0,
bbox: [
100.0 * column as f64,
700.0 - 12.0 * row as f64,
100.0 * column as f64 + 50.0,
710.0 - 12.0 * row as f64,
],
text: text.to_string(),
spans: Vec::new(),
index: 0,
continues_previous: false,
confidence: None,
grid: None,
table_origin: Default::default(),
trace_learned: None,
}
}
fn grid(blocks: &[Block]) -> Vec<(u16, u16, String)> {
blocks
.iter()
.filter_map(|b| match b.kind {
BlockKind::TableCell { row, column, .. } => Some((row, column, b.text.clone())),
_ => None,
})
.collect()
}
#[test]
fn wrapped_lines_join_the_entry_above() {
let mut blocks = vec![
cell(0, 0, "Number"),
cell(0, 1, "Description"),
cell(1, 0, "10.53"),
cell(1, 1, "Fourth Amendment, dated"),
cell(2, 0, ""),
cell(2, 1, "February 27, 2014 (incorporated"),
cell(3, 1, "by reference)."),
cell(4, 0, "10.54"),
cell(4, 1, "Fifth Amendment."),
];
join_continuation_rows(&mut blocks);
assert_eq!(
grid(&blocks),
vec![
(0, 0, "Number".to_string()),
(0, 1, "Description".to_string()),
(1, 0, "10.53".to_string()),
(
1,
1,
"Fourth Amendment, dated February 27, 2014 (incorporated by reference)."
.to_string()
),
(2, 0, "10.54".to_string()),
(2, 1, "Fifth Amendment.".to_string()),
]
);
assert!(blocks.iter().enumerate().all(|(i, b)| b.index == i));
assert_eq!(blocks[3].bbox[1], 700.0 - 36.0);
}
#[test]
fn a_glossary_set_apart_or_indented_keeps_its_rows() {
let mut blocks = vec![
cell(0, 0, "Term"),
cell(0, 1, "What the first term means"),
cell(1, 0, ""),
cell(1, 1, "a second meaning, set apart"),
];
blocks[2].bbox[3] -= 30.0;
blocks[2].bbox[1] -= 30.0;
blocks[3].bbox[3] -= 30.0;
blocks[3].bbox[1] -= 30.0;
join_continuation_rows(&mut blocks);
assert_eq!(blocks.len(), 4);
let mut blocks = vec![
cell(0, 0, "Key"),
cell(0, 1, "value one"),
cell(1, 0, ""),
cell(1, 1, "value two, indented"),
];
blocks[3].bbox[0] += 20.0;
join_continuation_rows(&mut blocks);
assert_eq!(blocks.len(), 4);
}
#[test]
fn a_figure_under_an_empty_stub_is_a_row() {
let mut blocks = vec![
cell(0, 0, "Revenue"),
cell(0, 1, "12"),
cell(1, 0, ""),
cell(1, 1, "13"),
cell(2, 0, ""),
cell(2, 1, "Total"),
];
join_continuation_rows(&mut blocks);
assert_eq!(blocks.len(), 6);
}
#[test]
fn a_table_of_three_columns_is_left_alone() {
let mut blocks = vec![
cell(0, 0, "a"),
cell(0, 1, "b"),
cell(0, 2, "c"),
cell(1, 0, ""),
cell(1, 1, "wrapped"),
cell(1, 2, ""),
];
join_continuation_rows(&mut blocks);
assert_eq!(blocks.len(), 6);
}
}