diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index a120423b3..8cd2915d9 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -2,6 +2,7 @@ use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; use ecow::EcoString; +use typst_macros::Cast; use typst_utils::NonZeroExt; use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; @@ -361,15 +362,40 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(if TargetElem::target_in(styles).is_html() { + let elem = if TargetElem::target_in(styles).is_html() { // TODO: This is a hack, it is not clear whether the locator is actually used by HTML. // How can we find out whether locator is actually used? let locator = Locator::root(); show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles) } else { - BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack() - } - .spanned(self.span())) + let children = self + .children + .iter() + .map(|c| match c.clone() { + TableChild::Header(header) => { + let mut header = header.unpack(); + header.children = header + .children + .into_iter() + .map(|item| match item { + TableItem::Cell(cell) => { + let mut cell = cell.unpack(); + cell.push_header_scope(Smart::Custom(TableHeaderScope::Column)); + TableItem::Cell(Packed::new(cell)) + } + item => item, + }) + .collect(); + TableChild::Header(Packed::new(header)) + } + child => child, + }) + .collect(); + let mut table = self.clone().unpack(); + table.children = children; + BlockElem::multi_layouter(Packed::new(table), engine.routines.layout_table).pack() + }; + Ok(elem.spanned(self.span())) } } @@ -810,6 +836,9 @@ pub struct TableCell { #[fold] pub stroke: Sides>>>, + // TODO: feature gate + pub header_scope: Smart, + /// Whether rows spanned by this cell can be placed in different pages. /// When equal to `{auto}`, a cell spanning only fixed-size rows is /// unbreakable, while a cell spanning at least one `{auto}`-sized row is @@ -847,3 +876,10 @@ impl From for TableCell { value.unpack::().unwrap_or_else(Self::new) } } + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum TableHeaderScope { + Both, + Column, + Row, +} diff --git a/crates/typst-pdf/src/tags.rs b/crates/typst-pdf/src/tags.rs index 5b5963946..993c22f67 100644 --- a/crates/typst-pdf/src/tags.rs +++ b/crates/typst-pdf/src/tags.rs @@ -5,14 +5,14 @@ use krilla::page::Page; use krilla::surface::Surface; use krilla::tagging::{ ArtifactType, ContentTag, Identifier, Node, SpanTag, TableCellSpan, TableDataCell, - TableHeaderCell, TableHeaderScope, Tag, TagBuilder, TagGroup, TagKind, TagTree, + TableHeaderCell, Tag, TagBuilder, TagGroup, TagKind, TagTree, }; -use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain}; +use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain, Smart}; use typst_library::introspection::Location; use typst_library::layout::RepeatElem; use typst_library::model::{ Destination, FigureCaption, FigureElem, HeadingElem, Outlinable, OutlineBody, - OutlineEntry, TableCell, TableElem, + OutlineEntry, TableCell, TableElem, TableHeaderScope, }; use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfTagElem, PdfTagKind}; use typst_library::visualize::ImageElem; @@ -141,10 +141,14 @@ impl TableCtx { // TODO: possibly set internal field on TableCell when resolving // the cell grid. - let is_header = false; + let header_scope = cell.header_scope(StyleChain::default()); let span = TableCellSpan { rows: rowspan as i32, cols: colspan as i32 }; - let tag = if is_header { - let scope = TableHeaderScope::Column; // TODO + let tag = if let Smart::Custom(scope) = header_scope { + let scope = match scope { + TableHeaderScope::Both => krilla::tagging::TableHeaderScope::Both, + TableHeaderScope::Column => krilla::tagging::TableHeaderScope::Column, + TableHeaderScope::Row => krilla::tagging::TableHeaderScope::Row, + }; TagKind::TH(TableHeaderCell::new(scope).with_span(span)) } else { TagKind::TD(TableDataCell::new().with_span(span)) @@ -167,14 +171,37 @@ impl TableCtx { fn build_table(self, mut nodes: Vec) -> Vec { // Table layouting ensures that there are no overlapping cells, and that // any gaps left by the user are filled with empty cells. + let mut row_chunk = Vec::new(); + let mut is_header_chunk = false; for row in self.rows.into_iter() { - let mut row_nodes = Vec::new(); - for (_, tag, nodes) in row.into_iter().flatten() { - row_nodes.push(TagNode::Group(tag, nodes)); - } + let mut is_header_row = true; + let row_nodes = row + .into_iter() + .flatten() + .map(|(cell, tag, nodes)| { + is_header_row &= cell.header_scope(StyleChain::default()).is_custom(); + TagNode::Group(tag, nodes) + }) + .collect(); - // TODO: generate `THead`, `TBody`, and `TFoot` - nodes.push(TagNode::Group(TagKind::TR.into(), row_nodes)); + let row = TagNode::Group(TagKind::TR.into(), row_nodes); + if row_chunk.is_empty() { + is_header_chunk = is_header_row; + row_chunk.push(row); + } else if is_header_chunk == is_header_row { + row_chunk.push(row); + } else { + let tag = if is_header_chunk { TagKind::THead } else { TagKind::TBody }; + nodes.push(TagNode::Group(tag.into(), std::mem::take(&mut row_chunk))); + + is_header_chunk = is_header_row; + row_chunk.push(row); + } + } + + if !row_chunk.is_empty() { + let tag = if is_header_chunk { TagKind::THead } else { TagKind::TBody }; + nodes.push(TagNode::Group(tag.into(), row_chunk)); } nodes