fix: handle some edge cases instead of panicking

This commit is contained in:
Tobias Schmitz 2025-07-03 13:56:10 +02:00
parent 377dc87325
commit 0bc39338a1
No known key found for this signature in database
4 changed files with 94 additions and 44 deletions

View File

@ -293,7 +293,7 @@ pub(crate) fn handle_frame(
handle_image(gc, fc, image, *size, surface, *span)? handle_image(gc, fc, image, *size, surface, *span)?
} }
FrameItem::Link(dest, size) => handle_link(fc, gc, dest, *size), FrameItem::Link(dest, size) => handle_link(fc, gc, dest, *size),
FrameItem::Tag(Tag::Start(elem)) => tags::handle_start(gc, elem), FrameItem::Tag(Tag::Start(elem)) => tags::handle_start(gc, elem)?,
FrameItem::Tag(Tag::End(loc, _)) => tags::handle_end(gc, *loc), FrameItem::Tag(Tag::End(loc, _)) => tags::handle_end(gc, *loc),
} }

View File

@ -8,7 +8,7 @@ use typst_library::layout::{Abs, Point, Position, Size};
use typst_library::model::Destination; use typst_library::model::Destination;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::tags::{self, Placeholder, StackEntryKind, TagNode}; use crate::tags::{self, Placeholder, TagNode};
use crate::util::{AbsExt, PointExt}; use crate::util::{AbsExt, PointExt};
pub(crate) struct LinkAnnotation { pub(crate) struct LinkAnnotation {
@ -49,8 +49,7 @@ pub(crate) fn handle_link(
} }
}; };
let entry = gc.tags.stack.last_mut().expect("a link parent"); let Some((link_id, link)) = gc.tags.find_parent_link() else {
let StackEntryKind::Link(link_id, ref link) = entry.kind else {
unreachable!("expected a link parent") unreachable!("expected a link parent")
}; };
let alt = link.alt.as_ref().map(EcoString::to_string); let alt = link.alt.as_ref().map(EcoString::to_string);

View File

@ -2,12 +2,14 @@ use std::cell::OnceCell;
use std::num::NonZeroU32; use std::num::NonZeroU32;
use ecow::EcoString; use ecow::EcoString;
use krilla::configure::Validator;
use krilla::page::Page; use krilla::page::Page;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::{ use krilla::tagging::{
ArtifactType, ContentTag, Identifier, Node, SpanTag, Tag, TagBuilder, TagGroup, ArtifactType, ContentTag, Identifier, Node, SpanTag, TableDataCell, Tag, TagBuilder,
TagKind, TagTree, TagGroup, TagKind, TagTree,
}; };
use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain}; use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain};
use typst_library::introspection::Location; use typst_library::introspection::Location;
use typst_library::layout::RepeatElem; use typst_library::layout::RepeatElem;
@ -26,21 +28,21 @@ use crate::tags::table::TableCtx;
mod outline; mod outline;
mod table; mod table;
pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) { pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) -> SourceResult<()> {
if gc.tags.in_artifact.is_some() { if gc.tags.in_artifact.is_some() {
// Don't nest artifacts // Don't nest artifacts
return; return Ok(());
} }
let loc = elem.location().unwrap(); let loc = elem.location().expect("elem to be locatable");
if let Some(artifact) = elem.to_packed::<ArtifactElem>() { if let Some(artifact) = elem.to_packed::<ArtifactElem>() {
let kind = artifact.kind(StyleChain::default()); let kind = artifact.kind(StyleChain::default());
start_artifact(gc, loc, kind); start_artifact(gc, loc, kind);
return; return Ok(());
} else if let Some(_) = elem.to_packed::<RepeatElem>() { } else if let Some(_) = elem.to_packed::<RepeatElem>() {
start_artifact(gc, loc, ArtifactKind::Other); start_artifact(gc, loc, ArtifactKind::Other);
return; return Ok(());
} }
let tag: Tag = if let Some(pdf_tag) = elem.to_packed::<PdfTagElem>() { let tag: Tag = if let Some(pdf_tag) = elem.to_packed::<PdfTagElem>() {
@ -54,11 +56,11 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
let name = heading.body.plain_text().to_string(); let name = heading.body.plain_text().to_string();
TagKind::Hn(level, Some(name)).into() TagKind::Hn(level, Some(name)).into()
} else if let Some(_) = elem.to_packed::<OutlineBody>() { } else if let Some(_) = elem.to_packed::<OutlineBody>() {
push_stack(gc, loc, StackEntryKind::Outline(OutlineCtx::new())); push_stack(gc, loc, StackEntryKind::Outline(OutlineCtx::new()))?;
return; return Ok(());
} else if let Some(entry) = elem.to_packed::<OutlineEntry>() { } else if let Some(entry) = elem.to_packed::<OutlineEntry>() {
push_stack(gc, loc, StackEntryKind::OutlineEntry(entry.clone())); push_stack(gc, loc, StackEntryKind::OutlineEntry(entry.clone()))?;
return; return Ok(());
} else if let Some(_) = elem.to_packed::<FigureElem>() { } else if let Some(_) = elem.to_packed::<FigureElem>() {
let alt = None; // TODO let alt = None; // TODO
TagKind::Figure.with_alt_text(alt) TagKind::Figure.with_alt_text(alt)
@ -73,7 +75,7 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
if figure_tag.alt_text.is_none() { if figure_tag.alt_text.is_none() {
figure_tag.alt_text = alt; figure_tag.alt_text = alt;
} }
return; return Ok(());
} else { } else {
TagKind::Figure.with_alt_text(alt) TagKind::Figure.with_alt_text(alt)
} }
@ -82,19 +84,16 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
} else if let Some(table) = elem.to_packed::<TableElem>() { } else if let Some(table) = elem.to_packed::<TableElem>() {
let table_id = gc.tags.next_table_id(); let table_id = gc.tags.next_table_id();
let ctx = TableCtx::new(table_id, table.clone()); let ctx = TableCtx::new(table_id, table.clone());
push_stack(gc, loc, StackEntryKind::Table(ctx)); push_stack(gc, loc, StackEntryKind::Table(ctx))?;
return; return Ok(());
} else if let Some(cell) = elem.to_packed::<TableCell>() { } else if let Some(cell) = elem.to_packed::<TableCell>() {
let parent = gc.tags.stack.last_mut().expect("table"); let table_ctx = gc.tags.parent_table();
let StackEntryKind::Table(table_ctx) = &mut parent.kind else {
unreachable!("expected table")
};
// Only repeated table headers and footer cells are layed out multiple // Only repeated table headers and footer cells are layed out multiple
// times. Mark duplicate headers as artifacts, since they have no // times. Mark duplicate headers as artifacts, since they have no
// semantic meaning in the tag tree, which doesn't use page breaks for // semantic meaning in the tag tree, which doesn't use page breaks for
// it's semantic structure. // it's semantic structure.
if table_ctx.contains(cell) { if table_ctx.is_some_and(|ctx| ctx.contains(cell)) {
// TODO: currently the first layouted cell is picked to be part of // TODO: currently the first layouted cell is picked to be part of
// the tag tree, for repeating footers this will be the cell on the // the tag tree, for repeating footers this will be the cell on the
// first page. Maybe it should be the cell on the last page, but that // first page. Maybe it should be the cell on the last page, but that
@ -102,26 +101,38 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
// on the frames to figure out if there are other footers following. // on the frames to figure out if there are other footers following.
start_artifact(gc, loc, ArtifactKind::Other); start_artifact(gc, loc, ArtifactKind::Other);
} else { } else {
push_stack(gc, loc, StackEntryKind::TableCell(cell.clone())); push_stack(gc, loc, StackEntryKind::TableCell(cell.clone()))?;
} }
return; return Ok(());
} else if let Some(link) = elem.to_packed::<LinkMarker>() { } else if let Some(link) = elem.to_packed::<LinkMarker>() {
let link_id = gc.tags.next_link_id(); let link_id = gc.tags.next_link_id();
push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone())); push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()))?;
return; return Ok(());
} else { } else {
return; return Ok(());
}; };
push_stack(gc, loc, StackEntryKind::Standard(tag)); push_stack(gc, loc, StackEntryKind::Standard(tag))?;
Ok(())
} }
fn push_stack(gc: &mut GlobalContext, loc: Location, kind: StackEntryKind) { fn push_stack(
gc: &mut GlobalContext,
loc: Location,
kind: StackEntryKind,
) -> SourceResult<()> {
if !gc.tags.context_supports(&kind) { if !gc.tags.context_supports(&kind) {
// TODO: error or warning? if gc.options.standards.config.validator() == Validator::UA1 {
// TODO: error
} else {
// TODO: warning
}
} }
gc.tags.stack.push(StackEntry { loc, kind, nodes: Vec::new() }); gc.tags.stack.push(StackEntry { loc, kind, nodes: Vec::new() });
Ok(())
} }
pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) { pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
@ -143,13 +154,20 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
TagNode::Group(TagKind::TOC.into(), nodes) TagNode::Group(TagKind::TOC.into(), nodes)
} }
StackEntryKind::OutlineEntry(outline_entry) => { StackEntryKind::OutlineEntry(outline_entry) => {
let parent = gc.tags.stack.last_mut().expect("outline"); let parent = gc.tags.stack.last_mut().and_then(|parent| {
let StackEntryKind::Outline(outline_ctx) = &mut parent.kind else { let ctx = parent.kind.as_outline_mut()?;
unreachable!("expected outline") Some((&mut parent.nodes, ctx))
});
let Some((parent_nodes, outline_ctx)) = parent else {
// PDF/UA compliance of the structure hierarchy is checked
// elsewhere. While this doesn't make a lot of sense, just
// avoid crashing here.
let tag = TagKind::TOCI.into();
gc.tags.push(TagNode::Group(tag, entry.nodes));
return;
}; };
outline_ctx.insert(&mut parent.nodes, outline_entry, entry.nodes); outline_ctx.insert(parent_nodes, outline_entry, entry.nodes);
return; return;
} }
StackEntryKind::Table(ctx) => { StackEntryKind::Table(ctx) => {
@ -158,13 +176,16 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
TagNode::Group(TagKind::Table(summary).into(), nodes) TagNode::Group(TagKind::Table(summary).into(), nodes)
} }
StackEntryKind::TableCell(cell) => { StackEntryKind::TableCell(cell) => {
let parent = gc.tags.stack.last_mut().expect("table"); let Some(table_ctx) = gc.tags.parent_table() else {
let StackEntryKind::Table(table_ctx) = &mut parent.kind else { // PDF/UA compliance of the structure hierarchy is checked
unreachable!("expected table") // elsewhere. While this doesn't make a lot of sense, just
// avoid crashing here.
let tag = TagKind::TD(TableDataCell::new()).into();
gc.tags.push(TagNode::Group(tag, entry.nodes));
return;
}; };
table_ctx.insert(cell, entry.nodes); table_ctx.insert(cell, entry.nodes);
return; return;
} }
StackEntryKind::Link(_, link) => { StackEntryKind::Link(_, link) => {
@ -248,12 +269,18 @@ impl Tags {
.expect("initialized placeholder node") .expect("initialized placeholder node")
} }
/// 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 StackEntryKind> { pub(crate) fn parent(&mut self) -> Option<&mut StackEntryKind> {
self.stack.last_mut().map(|e| &mut e.kind) self.stack.last_mut().map(|e| &mut e.kind)
} }
pub(crate) fn parent_table(&mut self) -> Option<&mut TableCtx> {
self.parent()?.as_table_mut()
}
pub(crate) fn find_parent_link(&self) -> Option<(LinkId, &Packed<LinkMarker>)> {
self.stack.iter().rev().find_map(|entry| entry.kind.as_link())
}
pub(crate) fn push(&mut self, node: TagNode) { pub(crate) fn push(&mut self, node: TagNode) {
if let Some(entry) = self.stack.last_mut() { if let Some(entry) = self.stack.last_mut() {
entry.nodes.push(node); entry.nodes.push(node);
@ -330,6 +357,30 @@ impl StackEntryKind {
None None
} }
} }
pub(crate) fn as_outline_mut(&mut self) -> Option<&mut OutlineCtx> {
if let Self::Outline(v) = self {
Some(v)
} else {
None
}
}
pub(crate) fn as_table_mut(&mut self) -> Option<&mut TableCtx> {
if let Self::Table(v) = self {
Some(v)
} else {
None
}
}
pub(crate) fn as_link(&self) -> Option<(LinkId, &Packed<LinkMarker>)> {
if let Self::Link(id, link) = self {
Some((*id, link))
} else {
None
}
}
} }
#[derive(Clone)] #[derive(Clone)]

View File

@ -159,8 +159,8 @@ impl TableCtx {
.filter_map(|cell| { .filter_map(|cell| {
let cell = cell.into_cell()?; let cell = cell.into_cell()?;
let span = TableCellSpan { let span = TableCellSpan {
rows: cell.rowspan.try_into().unwrap(), rows: cell.rowspan.try_into().unwrap_or(NonZeroU32::MAX),
cols: cell.colspan.try_into().unwrap(), cols: cell.colspan.try_into().unwrap_or(NonZeroU32::MAX),
}; };
let tag = match cell.unwrap_kind() { let tag = match cell.unwrap_kind() {
TableCellKind::Header(_, scope) => { TableCellKind::Header(_, scope) => {