From bc09df0c8b8eb939e01fa37ea3dae0bc15ab0790 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Wed, 16 Jul 2025 14:49:38 +0200 Subject: [PATCH] feat: insert footnotes after the reference in the reading order --- crates/typst-pdf/src/tags/mod.rs | 54 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index 2ae06c22f..664c19477 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -1,4 +1,5 @@ use std::cell::OnceCell; +use std::collections::HashMap; use std::num::NonZeroU32; use std::ops::{Deref, DerefMut}; @@ -18,8 +19,8 @@ use typst_library::foundations::{ use typst_library::introspection::Location; use typst_library::layout::RepeatElem; use typst_library::model::{ - Destination, EnumElem, FigureCaption, FigureElem, FootnoteEntry, HeadingElem, - ListElem, Outlinable, OutlineEntry, TableCell, TableElem, TermsElem, + Destination, EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, + HeadingElem, ListElem, Outlinable, OutlineEntry, TableCell, TableElem, TermsElem, }; use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind}; use typst_library::visualize::ImageElem; @@ -142,8 +143,13 @@ pub(crate) fn handle_start( let link_id = gc.tags.next_link_id(); push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()))?; return Ok(()); - } else if let Some(_) = elem.to_packed::() { - TagKind::Note.into() + } else if let Some(_) = elem.to_packed::() { + push_stack(gc, loc, StackEntryKind::FootNoteRef)?; + return Ok(()); + } else if let Some(entry) = elem.to_packed::() { + let footnote_loc = entry.note.location().unwrap(); + push_stack(gc, loc, StackEntryKind::FootNoteEntry(footnote_loc))?; + return Ok(()); } else { return Ok(()); }; @@ -217,6 +223,19 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc } node } + StackEntryKind::FootNoteRef => { + // transparently inset all children. + gc.tags.extend(entry.nodes); + gc.tags.push(TagNode::FootnoteEntry(loc)); + return; + } + StackEntryKind::FootNoteEntry(footnote_loc) => { + // Store footnotes separately so they can be inserted directly after + // the footnote reference in the reading order. + let tag = TagNode::Group(TagKind::Note.into(), entry.nodes); + gc.tags.footnotes.insert(footnote_loc, tag); + return; + } }; gc.tags.push(node); @@ -294,6 +313,11 @@ pub(crate) struct Tags { pub(crate) stack: TagStack, /// A list of placeholders corresponding to a [`TagNode::Placeholder`]. pub(crate) placeholders: Placeholders, + /// Footnotes are inserted directly after the footenote reference in the + /// reading order. Because of some layouting bugs, the entry might appear + /// before the reference in the text, so we only resolve them once tags + /// for the whole document are generated. + pub(crate) footnotes: HashMap, pub(crate) in_artifact: Option<(Location, ArtifactKind)>, /// Used to group multiple link annotations using quad points. pub(crate) link_id: LinkId, @@ -310,11 +334,13 @@ impl Tags { Self { stack: TagStack(Vec::new()), placeholders: Placeholders(Vec::new()), + footnotes: HashMap::new(), in_artifact: None, - tree: Vec::new(), link_id: LinkId(0), table_id: TableId(0), + + tree: Vec::new(), } } @@ -326,6 +352,14 @@ impl Tags { } } + pub(crate) fn extend(&mut self, nodes: impl IntoIterator) { + if let Some(entry) = self.stack.last_mut() { + entry.nodes.extend(nodes); + } else { + self.tree.extend(nodes); + } + } + pub(crate) fn build_tree(&mut self) -> TagTree { let children = std::mem::take(&mut self.tree) .into_iter() @@ -346,6 +380,10 @@ impl Tags { } TagNode::Leaf(identifier) => Node::Leaf(identifier), TagNode::Placeholder(placeholder) => self.placeholders.take(placeholder), + TagNode::FootnoteEntry(loc) => { + let node = self.footnotes.remove(&loc).expect("footnote"); + self.resolve_node(node) + } } } @@ -458,6 +496,11 @@ pub(crate) enum StackEntryKind { ListItemLabel, ListItemBody, Link(LinkId, Packed), + /// The footnote reference in the text. + FootNoteRef, + /// The footnote entry at the end of the page. Contains the [`Location`] of + /// the [`FootnoteElem`](typst_library::model::FootnoteElem). + FootNoteEntry(Location), } impl StackEntryKind { @@ -509,6 +552,7 @@ pub(crate) enum TagNode { /// Allows inserting a placeholder into the tag tree. /// Currently used for [`krilla::page::Page::add_tagged_annotation`]. Placeholder(Placeholder), + FootnoteEntry(Location), } #[derive(Clone, Copy, Debug, Eq, PartialEq)]