diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 76ba500a2..a120423b3 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; +use ecow::EcoString; use typst_utils::NonZeroExt; use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; @@ -237,6 +238,9 @@ pub struct TableElem { #[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))] pub inset: Celled>>>, + // TODO: docs + pub summary: Option, + /// The contents of the table cells, plus any extra table lines specified /// with the [`table.hline`]($table.hline) and /// [`table.vline`]($table.vline) elements. diff --git a/crates/typst-pdf/src/link.rs b/crates/typst-pdf/src/link.rs index 2d360cfc3..d489df781 100644 --- a/crates/typst-pdf/src/link.rs +++ b/crates/typst-pdf/src/link.rs @@ -10,7 +10,7 @@ use typst_library::layout::{Abs, Point, Position, Size}; use typst_library::model::Destination; use crate::convert::{FrameContext, GlobalContext}; -use crate::tags::{Placeholder, TagNode}; +use crate::tags::{Placeholder, StackEntryKind, TagNode}; use crate::util::{AbsExt, PointExt}; pub(crate) struct LinkAnnotation { @@ -51,7 +51,9 @@ pub(crate) fn handle_link( }; let entry = gc.tags.stack.last_mut().expect("a link parent"); - let link_id = entry.link_id.expect("a link parent"); + let StackEntryKind::Link(link_id, _) = entry.kind else { + unreachable!("expected a link parent") + }; let rect = to_rect(fc, size); let quadpoints = quadpoints(rect); diff --git a/crates/typst-pdf/src/tags.rs b/crates/typst-pdf/src/tags.rs index e36c15ef0..0446eebce 100644 --- a/crates/typst-pdf/src/tags.rs +++ b/crates/typst-pdf/src/tags.rs @@ -1,17 +1,18 @@ use std::cell::OnceCell; use std::collections::HashMap; +use ecow::EcoString; use krilla::page::Page; use krilla::surface::Surface; use krilla::tagging::{ - ArtifactType, ContentTag, Identifier, Node, Tag, TagBuilder, TagGroup, TagKind, - TagTree, + ArtifactType, ContentTag, Identifier, Node, TableCellSpan, TableDataCell, + TableHeaderCell, TableHeaderScope, Tag, TagBuilder, TagGroup, TagKind, TagTree, }; -use typst_library::foundations::{Content, LinkMarker, StyleChain}; +use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain}; use typst_library::introspection::Location; use typst_library::model::{ Destination, FigureCaption, FigureElem, HeadingElem, Outlinable, OutlineElem, - OutlineEntry, + OutlineEntry, TableCell, TableElem, TableHLine, TableVLine, }; use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfTagElem, PdfTagKind}; use typst_library::visualize::ImageElem; @@ -33,14 +34,71 @@ pub(crate) struct Tags { pub(crate) struct StackEntry { pub(crate) loc: Location, - pub(crate) link_id: Option, - /// A list of tags that are wrapped around this tag when it is inserted into - /// the tag tree. - pub(crate) wrappers: Vec, - pub(crate) tag: Tag, + pub(crate) kind: StackEntryKind, pub(crate) nodes: Vec, } +pub(crate) enum StackEntryKind { + Standard(Tag), + Link(LinkId, Packed), + Table(TableCtx), + TableCell(Packed), +} + +pub(crate) struct TableCtx { + table: Packed, + rows: Vec, Tag, Vec)>>>, +} + +impl TableCtx { + fn insert(&mut self, cell: Packed, nodes: Vec) { + let x = cell.x(StyleChain::default()).unwrap_or_else(|| unreachable!()); + let y = cell.y(StyleChain::default()).unwrap_or_else(|| unreachable!()); + let rowspan = cell.rowspan(StyleChain::default()).get(); + let colspan = cell.colspan(StyleChain::default()).get(); + + // TODO: possibly set internal field on TableCell when resolving + // the cell grid. + let is_header = false; + let span = TableCellSpan { rows: rowspan as i32, cols: colspan as i32 }; + let tag = if is_header { + let scope = TableHeaderScope::Column; // TODO + TagKind::TH(TableHeaderCell::new(scope).with_span(span)) + } else { + TagKind::TD(TableDataCell::new().with_span(span)) + }; + + let required_height = y + rowspan; + if self.rows.len() < required_height { + self.rows.resize_with(required_height, Vec::new); + } + + let required_width = x + colspan; + let row = &mut self.rows[y]; + if row.len() < required_width { + row.resize_with(required_width, || None); + } + + row[x] = Some((cell, tag.into(), nodes)); + } + + 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. + 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)); + } + + // TODO: generate `THead`, `TBody`, and `TFoot` + nodes.push(TagNode::Group(TagKind::TR.into(), row_nodes)); + } + + nodes + } +} + pub(crate) enum TagNode { Group(Tag, Vec), Leaf(Identifier), @@ -86,11 +144,15 @@ impl Tags { .expect("initialized placeholder node") } + pub(crate) fn is_root(&self) -> bool { + self.stack.is_empty() + } + /// Returns the current parent's list of children and the structure type ([Tag]). /// In case of the document root the structure type will be `None`. - pub(crate) fn parent(&mut self) -> (Option<&mut Tag>, &mut Vec) { + pub(crate) fn parent(&mut self) -> (Option<&mut StackEntryKind>, &mut Vec) { if let Some(entry) = self.stack.last_mut() { - (Some(&mut entry.tag), &mut entry.nodes) + (Some(&mut entry.kind), &mut entry.nodes) } else { (None, &mut self.tree) } @@ -123,7 +185,7 @@ impl Tags { } } - fn context_supports(&self, _tag: &Tag) -> bool { + fn context_supports(&self, _tag: &StackEntryKind) -> bool { // TODO: generate using: https://pdfa.org/resource/iso-ts-32005-hierarchical-inclusion-rules/ true } @@ -138,8 +200,8 @@ impl Tags { /// at the end of the last page. pub(crate) fn restart_open(gc: &mut GlobalContext, surface: &mut Surface) { // TODO: somehow avoid empty marked-content sequences - if let Some((_, kind)) = gc.tags.in_artifact { - start_artifact(gc, surface, kind); + if let Some((loc, kind)) = gc.tags.in_artifact { + start_artifact(gc, surface, loc, kind); } else if let Some(entry) = gc.tags.stack.last_mut() { let id = surface.start_tagged(ContentTag::Other); entry.nodes.push(TagNode::Leaf(id)); @@ -183,17 +245,12 @@ pub(crate) fn handle_start( let loc = elem.location().unwrap(); if let Some(artifact) = elem.to_packed::() { - if !gc.tags.stack.is_empty() { - surface.end_tagged(); - } + end_open(gc, surface); let kind = artifact.kind(StyleChain::default()); - start_artifact(gc, surface, kind); - gc.tags.in_artifact = Some((loc, kind)); + start_artifact(gc, surface, loc, kind); return; } - let mut link_id = None; - let mut wrappers = Vec::new(); let tag: Tag = if let Some(pdf_tag) = elem.to_packed::() { let kind = pdf_tag.kind(StyleChain::default()); match kind { @@ -226,11 +283,13 @@ pub(crate) fn handle_start( let id = surface.start_tagged(ContentTag::Other); let mut node = TagNode::Leaf(id); - if let Some(parent) = gc.tags.parent().0 { + if let Some(StackEntryKind::Standard(parent)) = gc.tags.parent().0 { if parent.kind == TagKind::Figure && parent.alt_text.is_none() { // HACK: set alt text of outer figure tag, if the contained image // has alt text specified parent.alt_text = alt; + } else { + node = TagNode::Group(TagKind::Figure.with_alt_text(alt), vec![node]); } } else { node = TagNode::Group(TagKind::Figure.with_alt_text(alt), vec![node]); @@ -241,29 +300,47 @@ pub(crate) fn handle_start( } else if let Some(_) = elem.to_packed::() { TagKind::Caption.into() } else if let Some(link) = elem.to_packed::() { - link_id = Some(gc.tags.next_link_id()); - if let Destination::Position(_) | Destination::Location(_) = link.dest { - wrappers.push(TagKind::Reference.into()); - } - TagKind::Link.into() + let link_id = gc.tags.next_link_id(); + push_stack(gc, surface, loc, StackEntryKind::Link(link_id, link.clone())); + return; + } else if let Some(table) = elem.to_packed::() { + let ctx = TableCtx { table: table.clone(), rows: Vec::new() }; + push_stack(gc, surface, loc, StackEntryKind::Table(ctx)); + return; + } else if let Some(cell) = elem.to_packed::() { + push_stack(gc, surface, loc, StackEntryKind::TableCell(cell.clone())); + return; + } else if let Some(_) = elem.to_packed::() { + end_open(gc, surface); + start_artifact(gc, surface, loc, ArtifactKind::Other); + return; + } else if let Some(_) = elem.to_packed::() { + end_open(gc, surface); + start_artifact(gc, surface, loc, ArtifactKind::Other); + return; } else { return; }; - if !gc.tags.context_supports(&tag) { + push_stack(gc, surface, loc, StackEntryKind::Standard(tag)); +} + +fn push_stack( + gc: &mut GlobalContext, + surface: &mut Surface, + loc: Location, + kind: StackEntryKind, +) { + if !gc.tags.context_supports(&kind) { // TODO: error or warning? } // close previous marked-content and open a nested tag. end_open(gc, surface); let id = surface.start_tagged(krilla::tagging::ContentTag::Other); - gc.tags.stack.push(StackEntry { - loc, - link_id, - wrappers, - tag, - nodes: vec![TagNode::Leaf(id)], - }); + gc.tags + .stack + .push(StackEntry { loc, kind, nodes: vec![TagNode::Leaf(id)] }); } pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Location) { @@ -285,24 +362,56 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc surface.end_tagged(); - let (parent_tag, parent_nodes) = gc.tags.parent(); - let mut node = TagNode::Group(entry.tag, entry.nodes); - for tag in entry.wrappers { - node = TagNode::Group(tag, vec![node]); - } - parent_nodes.push(node); - if parent_tag.is_some() { + let node = match entry.kind { + StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes), + StackEntryKind::Link(_, link) => { + let alt = link.alt.as_ref().map(EcoString::to_string); + let tag = TagKind::Link.with_alt_text(alt); + let mut node = TagNode::Group(tag, entry.nodes); + // Wrap link in reference tag, if it's not a url. + if let Destination::Position(_) | Destination::Location(_) = link.dest { + node = TagNode::Group(TagKind::Reference.into(), vec![node]); + } + node + } + StackEntryKind::Table(ctx) => { + let summary = ctx.table.summary(StyleChain::default()).map(EcoString::into); + let nodes = ctx.build_table(entry.nodes); + TagNode::Group(TagKind::Table(summary).into(), nodes) + } + StackEntryKind::TableCell(cell) => { + let parent = gc.tags.stack.last_mut().expect("table"); + let StackEntryKind::Table(table_ctx) = &mut parent.kind else { + unreachable!("expected table") + }; + + table_ctx.insert(cell, entry.nodes); + + // TODO: somehow avoid empty marked-content sequences + let id = surface.start_tagged(ContentTag::Other); + gc.tags.push(TagNode::Leaf(id)); + return; + } + }; + + gc.tags.push(node); + if !gc.tags.is_root() { // TODO: somehow avoid empty marked-content sequences let id = surface.start_tagged(ContentTag::Other); - parent_nodes.push(TagNode::Leaf(id)); + gc.tags.push(TagNode::Leaf(id)); } } -fn start_artifact(gc: &mut GlobalContext, surface: &mut Surface, kind: ArtifactKind) { +fn start_artifact( + gc: &mut GlobalContext, + surface: &mut Surface, + loc: Location, + kind: ArtifactKind, +) { let ty = artifact_type(kind); let id = surface.start_tagged(ContentTag::Artifact(ty)); - gc.tags.push(TagNode::Leaf(id)); + gc.tags.in_artifact = Some((loc, kind)); } fn artifact_type(kind: ArtifactKind) -> ArtifactType {