fix: table header hierarchy resolution

This commit is contained in:
Tobias Schmitz 2025-07-08 11:28:35 +02:00
parent 7d5b9a716f
commit 2445bb4361
No known key found for this signature in database

View File

@ -133,7 +133,7 @@ impl TableCtx {
// Explicitly set the headers attribute for cells. // Explicitly set the headers attribute for cells.
for x in 0..width { for x in 0..width {
let mut column_header = None; let mut column_header = Vec::new();
for y in 0..height { for y in 0..height {
self.resolve_cell_headers( self.resolve_cell_headers(
(x, y), (x, y),
@ -143,7 +143,7 @@ impl TableCtx {
} }
} }
for y in 0..height { for y in 0..height {
let mut row_header = None; let mut row_header = Vec::new();
for x in 0..width { for x in 0..width {
self.resolve_cell_headers( self.resolve_cell_headers(
(x, y), (x, y),
@ -224,7 +224,7 @@ impl TableCtx {
fn resolve_cell_headers<F>( fn resolve_cell_headers<F>(
&mut self, &mut self,
(x, y): (usize, usize), (x, y): (usize, usize),
current_header: &mut Option<(NonZeroU32, TagId)>, current_header: &mut Vec<(NonZeroU32, TagId)>,
refers_to_dir: F, refers_to_dir: F,
) where ) where
F: Fn(&TableHeaderScope) -> bool, F: Fn(&TableHeaderScope) -> bool,
@ -232,26 +232,24 @@ impl TableCtx {
let table_id = self.id; let table_id = self.id;
let Some(cell) = self.get_mut(x, y) else { return }; let Some(cell) = self.get_mut(x, y) else { return };
if let Some((prev_level, cell_id)) = current_header.clone() { let mut new_header = None;
// The `Headers` attribute is also set for parent headers. if let TableCellKind::Header(level, scope) = cell.unwrap_kind() {
let mut is_parent_header = true; if refers_to_dir(&scope) {
if let TableCellKind::Header(level, scope) = cell.unwrap_kind() { // Remove all headers that are the same or a lower level.
if refers_to_dir(&scope) { while current_header.pop_if(|(l, _)| *l >= level).is_some() {}
is_parent_header = prev_level < level;
}
}
if is_parent_header && !cell.headers.ids.contains(&cell_id) { let tag_id = table_cell_id(table_id, cell.x, cell.y);
new_header = Some((level, tag_id));
}
}
if let Some((_, cell_id)) = current_header.last() {
if !cell.headers.ids.contains(&cell_id) {
cell.headers.ids.push(cell_id.clone()); cell.headers.ids.push(cell_id.clone());
} }
} }
if let TableCellKind::Header(level, scope) = cell.unwrap_kind() { current_header.extend(new_header);
if refers_to_dir(&scope) {
let tag_id = table_cell_id(table_id, cell.x, cell.y);
*current_header = Some((level, tag_id));
}
}
} }
} }
@ -339,7 +337,7 @@ mod tests {
#[track_caller] #[track_caller]
fn test(table: TableCtx, exp_tag: TagNode) { fn test(table: TableCtx, exp_tag: TagNode) {
let tag = table.build_table(Vec::new()); let tag = table.build_table(Vec::new());
assert_eq!(tag, exp_tag); assert_eq!(exp_tag, tag);
} }
#[track_caller] #[track_caller]
@ -348,12 +346,15 @@ mod tests {
for cell in cells { for cell in cells {
table.insert(Packed::new(cell), Vec::new()); table.insert(Packed::new(cell), Vec::new());
} }
table table
} }
#[track_caller] #[track_caller]
fn header_cell(x: usize, y: usize, level: u32, scope: TableHeaderScope) -> TableCell { fn header_cell(
(x, y): (usize, usize),
level: u32,
scope: TableHeaderScope,
) -> TableCell {
TableCell::new(Content::default()) TableCell::new(Content::default())
.with_x(Smart::Custom(x)) .with_x(Smart::Custom(x))
.with_y(Smart::Custom(y)) .with_y(Smart::Custom(y))
@ -363,6 +364,14 @@ mod tests {
))) )))
} }
#[track_caller]
fn footer_cell(x: usize, y: usize) -> TableCell {
TableCell::new(Content::default())
.with_x(Smart::Custom(x))
.with_y(Smart::Custom(y))
.with_kind(Smart::Custom(TableCellKind::Footer))
}
fn cell(x: usize, y: usize) -> TableCell { fn cell(x: usize, y: usize) -> TableCell {
TableCell::new(Content::default()) TableCell::new(Content::default())
.with_x(Smart::Custom(x)) .with_x(Smart::Custom(x))
@ -370,26 +379,36 @@ mod tests {
.with_kind(Smart::Custom(TableCellKind::Data)) .with_kind(Smart::Custom(TableCellKind::Data))
} }
fn empty_cell(x: usize, y: usize) -> TableCell {
TableCell::new(Content::default())
.with_x(Smart::Custom(x))
.with_y(Smart::Custom(y))
.with_kind(Smart::Auto)
}
fn table_tag<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode { fn table_tag<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode {
let tag = TagKind::Table(Some("summary".into())); let tag = TagKind::Table(Some("summary".into()));
TagNode::Group(tag.into(), nodes.into()) TagNode::Group(tag.into(), nodes.into())
} }
fn header<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode { fn thead<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode {
TagNode::Group(TagKind::THead.into(), nodes.into()) TagNode::Group(TagKind::THead.into(), nodes.into())
} }
fn body<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode { fn tbody<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode {
TagNode::Group(TagKind::TBody.into(), nodes.into()) TagNode::Group(TagKind::TBody.into(), nodes.into())
} }
fn row<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode { fn tfoot<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode {
TagNode::Group(TagKind::TFoot.into(), nodes.into())
}
fn trow<const SIZE: usize>(nodes: [TagNode; SIZE]) -> TagNode {
TagNode::Group(TagKind::TR.into(), nodes.into()) TagNode::Group(TagKind::TR.into(), nodes.into())
} }
fn header_cell_tag<const SIZE: usize>( fn th<const SIZE: usize>(
x: u32, (x, y): (u32, u32),
y: u32,
scope: TableHeaderScope, scope: TableHeaderScope,
headers: [(u32, u32); SIZE], headers: [(u32, u32); SIZE],
) -> TagNode { ) -> TagNode {
@ -406,7 +425,7 @@ mod tests {
) )
} }
fn cell_tag<const SIZE: usize>(headers: [(u32, u32); SIZE]) -> TagNode { fn td<const SIZE: usize>(headers: [(u32, u32); SIZE]) -> TagNode {
let ids = headers let ids = headers
.map(|(x, y)| table_cell_id(TableId(324), x, y)) .map(|(x, y)| table_cell_id(TableId(324), x, y))
.into_iter() .into_iter()
@ -421,9 +440,9 @@ mod tests {
fn simple_table() { fn simple_table() {
#[rustfmt::skip] #[rustfmt::skip]
let table = table([ let table = table([
header_cell(0, 0, 1, TableHeaderScope::Column), header_cell((0, 0), 1, TableHeaderScope::Column),
header_cell(1, 0, 1, TableHeaderScope::Column), header_cell((1, 0), 1, TableHeaderScope::Column),
header_cell(2, 0, 1, TableHeaderScope::Column), header_cell((2, 0), 1, TableHeaderScope::Column),
cell(0, 1), cell(0, 1),
cell(1, 1), cell(1, 1),
@ -436,21 +455,21 @@ mod tests {
#[rustfmt::skip] #[rustfmt::skip]
let tag = table_tag([ let tag = table_tag([
header([row([ thead([trow([
header_cell_tag(0, 0, TableHeaderScope::Column, []), th((0, 0), TableHeaderScope::Column, []),
header_cell_tag(1, 0, TableHeaderScope::Column, []), th((1, 0), TableHeaderScope::Column, []),
header_cell_tag(2, 0, TableHeaderScope::Column, []), th((2, 0), TableHeaderScope::Column, []),
])]), ])]),
body([ tbody([
row([ trow([
cell_tag([(0, 0)]), td([(0, 0)]),
cell_tag([(1, 0)]), td([(1, 0)]),
cell_tag([(2, 0)]), td([(2, 0)]),
]), ]),
row([ trow([
cell_tag([(0, 0)]), td([(0, 0)]),
cell_tag([(1, 0)]), td([(1, 0)]),
cell_tag([(2, 0)]), td([(2, 0)]),
]), ]),
]), ]),
]); ]);
@ -462,35 +481,111 @@ mod tests {
fn header_row_and_column() { fn header_row_and_column() {
#[rustfmt::skip] #[rustfmt::skip]
let table = table([ let table = table([
header_cell(0, 0, 1, TableHeaderScope::Column), header_cell((0, 0), 1, TableHeaderScope::Column),
header_cell(1, 0, 1, TableHeaderScope::Column), header_cell((1, 0), 1, TableHeaderScope::Column),
header_cell(2, 0, 1, TableHeaderScope::Column), header_cell((2, 0), 1, TableHeaderScope::Column),
header_cell(0, 1, 1, TableHeaderScope::Row), header_cell((0, 1), 1, TableHeaderScope::Row),
cell(1, 1), cell(1, 1),
cell(2, 1), cell(2, 1),
header_cell(0, 2, 1, TableHeaderScope::Row), header_cell((0, 2), 1, TableHeaderScope::Row),
cell(1, 2), cell(1, 2),
cell(2, 2), cell(2, 2),
]); ]);
#[rustfmt::skip] #[rustfmt::skip]
let tag = table_tag([ let tag = table_tag([
row([ trow([
header_cell_tag(0, 0, TableHeaderScope::Column, []), th((0, 0), TableHeaderScope::Column, []),
header_cell_tag(1, 0, TableHeaderScope::Column, []), th((1, 0), TableHeaderScope::Column, []),
header_cell_tag(2, 0, TableHeaderScope::Column, []), th((2, 0), TableHeaderScope::Column, []),
]), ]),
row([ trow([
header_cell_tag(0, 1, TableHeaderScope::Row, [(0, 0)]), th((0, 1), TableHeaderScope::Row, [(0, 0)]),
cell_tag([(1, 0), (0, 1)]), td([(1, 0), (0, 1)]),
cell_tag([(2, 0), (0, 1)]), td([(2, 0), (0, 1)]),
]), ]),
row([ trow([
header_cell_tag(0, 2, TableHeaderScope::Row, [(0, 0)]), th((0, 2), TableHeaderScope::Row, [(0, 0)]),
cell_tag([(1, 0), (0, 2)]), td([(1, 0), (0, 2)]),
cell_tag([(2, 0), (0, 2)]), td([(2, 0), (0, 2)]),
]),
]);
test(table, tag);
}
#[test]
fn complex_tables() {
#[rustfmt::skip]
let table = table([
header_cell((0, 0), 1, TableHeaderScope::Column),
header_cell((1, 0), 1, TableHeaderScope::Column),
header_cell((2, 0), 1, TableHeaderScope::Column),
header_cell((0, 1), 2, TableHeaderScope::Column),
header_cell((1, 1), 2, TableHeaderScope::Column),
header_cell((2, 1), 2, TableHeaderScope::Column),
cell(0, 2),
empty_cell(1, 2), // the type of empty cells is inferred from the row
cell(2, 2),
header_cell((0, 3), 2, TableHeaderScope::Column),
header_cell((1, 3), 2, TableHeaderScope::Column),
empty_cell(2, 3), // the type of empty cells is inferred from the row
cell(0, 4),
cell(1, 4),
empty_cell(2, 4),
empty_cell(0, 5), // the type of empty cells is inferred from the row
footer_cell(1, 5),
footer_cell(2, 5),
]);
#[rustfmt::skip]
let tag = table_tag([
thead([
trow([
th((0, 0), TableHeaderScope::Column, []),
th((1, 0), TableHeaderScope::Column, []),
th((2, 0), TableHeaderScope::Column, []),
]),
trow([
th((0, 1), TableHeaderScope::Column, [(0, 0)]),
th((1, 1), TableHeaderScope::Column, [(1, 0)]),
th((2, 1), TableHeaderScope::Column, [(2, 0)]),
]),
]),
tbody([
trow([
td([(0, 1)]),
td([(1, 1)]),
td([(2, 1)]),
]),
]),
thead([
trow([
th((0, 3), TableHeaderScope::Column, [(0, 0)]),
th((1, 3), TableHeaderScope::Column, [(1, 0)]),
th((2, 3), TableHeaderScope::Column, [(2, 0)]),
]),
]),
tbody([
trow([
td([(0, 3)]),
td([(1, 3)]),
td([(2, 3)]),
]),
]),
tfoot([
trow([
td([(0, 3)]),
td([(1, 3)]),
td([(2, 3)]),
]),
]), ]),
]); ]);