use crate::model::{Chapter, Node, NodeId, Role};
pub fn normalize_table_structure(chapter: &mut Chapter) {
let tables: Vec<NodeId> = (0..chapter.node_count())
.filter_map(|i| {
let id = NodeId(i as u32);
chapter.node(id).and_then(|node| {
if node.role == Role::Table {
Some(id)
} else {
None
}
})
})
.collect();
for table_id in tables {
normalize_single_table(chapter, table_id);
}
}
fn normalize_single_table(chapter: &mut Chapter, table_id: NodeId) {
let has_section_wrapper = chapter.children(table_id).any(|child_id| {
chapter
.node(child_id)
.map(|n| matches!(n.role, Role::TableHead | Role::TableBody))
.unwrap_or(false)
});
if has_section_wrapper {
return;
}
let mut captions: Vec<NodeId> = Vec::new();
let mut header_rows: Vec<NodeId> = Vec::new();
let mut body_rows: Vec<NodeId> = Vec::new();
let mut others: Vec<NodeId> = Vec::new();
for child_id in chapter.children(table_id).collect::<Vec<_>>() {
match chapter.node(child_id).map(|n| n.role) {
Some(Role::Caption) => captions.push(child_id),
Some(Role::TableRow) => {
if is_table_header_row(chapter, child_id) {
header_rows.push(child_id);
} else {
body_rows.push(child_id);
}
}
_ => others.push(child_id),
}
}
for &child_id in captions
.iter()
.chain(&header_rows)
.chain(&body_rows)
.chain(&others)
{
if let Some(child) = chapter.node_mut(child_id) {
child.next_sibling = None;
}
}
if let Some(table) = chapter.node_mut(table_id) {
table.first_child = None;
table.last_child = None;
}
for &caption_id in &captions {
chapter.append_child(table_id, caption_id);
}
if !header_rows.is_empty() {
let head_id = chapter.alloc_node(Node::new(Role::TableHead));
chapter.append_child(table_id, head_id);
for &row_id in &header_rows {
chapter.append_child(head_id, row_id);
}
}
if !body_rows.is_empty() {
let body_id = chapter.alloc_node(Node::new(Role::TableBody));
chapter.append_child(table_id, body_id);
for &row_id in &body_rows {
chapter.append_child(body_id, row_id);
}
}
if header_rows.is_empty() && body_rows.is_empty() {
let body_id = chapter.alloc_node(Node::new(Role::TableBody));
chapter.append_child(table_id, body_id);
}
for &other_id in &others {
chapter.append_child(table_id, other_id);
}
}
fn is_table_header_row(chapter: &Chapter, row_id: NodeId) -> bool {
let mut has_cells = false;
let mut all_header = true;
for cell_id in chapter.children(row_id) {
if let Some(cell) = chapter.node(cell_id)
&& cell.role == Role::TableCell
{
has_cells = true;
if !chapter.semantics.is_header_cell(cell_id) {
all_header = false;
break;
}
}
}
has_cells && all_header
}
#[cfg(test)]
mod tests {
use super::*;
fn add_row(chapter: &mut Chapter, parent: NodeId, header: bool, cells: usize) -> NodeId {
let row = chapter.alloc_node(Node::new(Role::TableRow));
chapter.append_child(parent, row);
for _ in 0..cells {
let cell = chapter.alloc_node(Node::new(Role::TableCell));
chapter.append_child(row, cell);
if header {
chapter.semantics.set_header_cell(cell, true);
}
}
row
}
fn role_of(chapter: &Chapter, id: NodeId) -> Role {
chapter.node(id).unwrap().role
}
fn assert_tree_consistent(chapter: &Chapter) {
let mut seen = vec![false; chapter.node_count()];
for parent in chapter.iter_dfs() {
assert!(
!seen[parent.0 as usize],
"node {parent:?} reachable more than once (broken sibling chain)"
);
seen[parent.0 as usize] = true;
for child in chapter.children(parent) {
assert_eq!(
chapter.node(child).unwrap().parent,
Some(parent),
"stale parent pointer on {child:?} (role {:?})",
role_of(chapter, child)
);
}
}
}
fn child_roles(chapter: &Chapter, id: NodeId) -> Vec<Role> {
chapter.children(id).map(|c| role_of(chapter, c)).collect()
}
#[test]
fn parent_pointers_correct_after_normalization() {
let mut chapter = Chapter::new();
let table = chapter.alloc_node(Node::new(Role::Table));
chapter.append_child(NodeId::ROOT, table);
let caption = chapter.alloc_node(Node::new(Role::Caption));
chapter.append_child(table, caption);
add_row(&mut chapter, table, true, 2);
add_row(&mut chapter, table, false, 2);
add_row(&mut chapter, table, false, 2);
normalize_table_structure(&mut chapter);
assert_tree_consistent(&chapter);
let reachable = chapter.iter_dfs().count();
assert_eq!(reachable, chapter.node_count());
for wrapper in chapter.children(table) {
match role_of(&chapter, wrapper) {
Role::TableHead | Role::TableBody => {
for row in chapter.children(wrapper) {
assert_eq!(chapter.node(row).unwrap().parent, Some(wrapper));
}
}
Role::Caption => {
assert_eq!(chapter.node(wrapper).unwrap().parent, Some(table));
}
other => panic!("unexpected table child role {other:?}"),
}
}
}
#[test]
fn caption_stays_direct_child_of_table_and_first() {
let mut chapter = Chapter::new();
let table = chapter.alloc_node(Node::new(Role::Table));
chapter.append_child(NodeId::ROOT, table);
add_row(&mut chapter, table, true, 1);
add_row(&mut chapter, table, false, 1);
let caption = chapter.alloc_node(Node::new(Role::Caption));
chapter.append_child(table, caption);
normalize_table_structure(&mut chapter);
assert_eq!(
child_roles(&chapter, table),
vec![Role::Caption, Role::TableHead, Role::TableBody]
);
let first = chapter.children(table).next().unwrap();
assert_eq!(first, caption);
assert_eq!(chapter.node(caption).unwrap().parent, Some(table));
for wrapper in chapter.children(table).skip(1) {
assert!(
chapter
.children(wrapper)
.all(|c| role_of(&chapter, c) == Role::TableRow),
"wrapper contains a non-row child"
);
}
assert_tree_consistent(&chapter);
}
#[test]
fn th_rows_to_head_td_rows_to_body() {
let mut chapter = Chapter::new();
let table = chapter.alloc_node(Node::new(Role::Table));
chapter.append_child(NodeId::ROOT, table);
let th_row = add_row(&mut chapter, table, true, 3);
let td_row1 = add_row(&mut chapter, table, false, 3);
let td_row2 = add_row(&mut chapter, table, false, 3);
normalize_table_structure(&mut chapter);
let sections: Vec<NodeId> = chapter.children(table).collect();
assert_eq!(sections.len(), 2);
assert_eq!(role_of(&chapter, sections[0]), Role::TableHead);
assert_eq!(role_of(&chapter, sections[1]), Role::TableBody);
assert_eq!(
chapter.children(sections[0]).collect::<Vec<_>>(),
vec![th_row]
);
assert_eq!(
chapter.children(sections[1]).collect::<Vec<_>>(),
vec![td_row1, td_row2]
);
assert_tree_consistent(&chapter);
}
#[test]
fn already_normalized_table_unchanged() {
let mut chapter = Chapter::new();
let table = chapter.alloc_node(Node::new(Role::Table));
chapter.append_child(NodeId::ROOT, table);
let caption = chapter.alloc_node(Node::new(Role::Caption));
chapter.append_child(table, caption);
let head = chapter.alloc_node(Node::new(Role::TableHead));
chapter.append_child(table, head);
add_row(&mut chapter, head, true, 2);
let body = chapter.alloc_node(Node::new(Role::TableBody));
chapter.append_child(table, body);
add_row(&mut chapter, body, false, 2);
let before: Vec<NodeId> = chapter.iter_dfs().collect();
let count_before = chapter.node_count();
normalize_table_structure(&mut chapter);
assert_eq!(chapter.node_count(), count_before, "no nodes allocated");
assert_eq!(
chapter.iter_dfs().collect::<Vec<_>>(),
before,
"tree structure unchanged"
);
assert_tree_consistent(&chapter);
}
#[test]
fn normalization_is_idempotent() {
let mut chapter = Chapter::new();
let table = chapter.alloc_node(Node::new(Role::Table));
chapter.append_child(NodeId::ROOT, table);
let caption = chapter.alloc_node(Node::new(Role::Caption));
chapter.append_child(table, caption);
add_row(&mut chapter, table, true, 2);
add_row(&mut chapter, table, false, 2);
normalize_table_structure(&mut chapter);
let after_first: Vec<NodeId> = chapter.iter_dfs().collect();
let count_first = chapter.node_count();
normalize_table_structure(&mut chapter);
assert_eq!(chapter.node_count(), count_first);
assert_eq!(chapter.iter_dfs().collect::<Vec<_>>(), after_first);
assert_tree_consistent(&chapter);
}
#[test]
fn empty_table_gets_empty_body() {
let mut chapter = Chapter::new();
let table = chapter.alloc_node(Node::new(Role::Table));
chapter.append_child(NodeId::ROOT, table);
normalize_table_structure(&mut chapter);
assert_eq!(child_roles(&chapter, table), vec![Role::TableBody]);
let body = chapter.children(table).next().unwrap();
assert_eq!(chapter.node(body).unwrap().parent, Some(table));
assert_eq!(chapter.children(body).count(), 0);
assert_tree_consistent(&chapter);
}
}