WIP [no ci] tag table headers

This commit is contained in:
Tobias Schmitz 2025-06-27 13:50:43 +02:00
parent 1fa54b751a
commit 46229e73ba
No known key found for this signature in database
2 changed files with 79 additions and 16 deletions

View File

@ -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<TableElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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()
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))
}
.spanned(self.span()))
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<Option<Option<Arc<Stroke>>>>,
// TODO: feature gate
pub header_scope: Smart<TableHeaderScope>,
/// 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<Content> for TableCell {
value.unpack::<Self>().unwrap_or_else(Self::new)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum TableHeaderScope {
Both,
Column,
Row,
}

View File

@ -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<TagNode>) -> Vec<TagNode> {
// 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();
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);
}
}
// TODO: generate `THead`, `TBody`, and `TFoot`
nodes.push(TagNode::Group(TagKind::TR.into(), row_nodes));
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