diff --git a/crates/typst-layout/src/rules.rs b/crates/typst-layout/src/rules.rs index 0f8eb115a..8ee98b94a 100644 --- a/crates/typst-layout/src/rules.rs +++ b/crates/typst-layout/src/rules.rs @@ -384,15 +384,16 @@ const QUOTE_RULE: ShowFn = |elem, _, styles| { const FOOTNOTE_RULE: ShowFn = |elem, engine, styles| { let span = elem.span(); - let loc = elem.declaration_location(engine).at(span)?; + let decl_loc = elem.declaration_location(engine).at(span)?; let numbering = elem.numbering.get_ref(styles); let counter = Counter::of(FootnoteElem::ELEM); - let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let num = counter.display_at_loc(engine, decl_loc, styles, numbering)?; let alt = FootnoteElem::alt_text(styles, &num.plain_text()); let sup = PdfMarkerTag::Label(SuperElem::new(num).pack().spanned(span)); - let loc = loc.variant(1); + let loc = decl_loc.variant(1); // Add zero-width weak spacing to make the footnote "sticky". - Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), Some(alt))) + let link = sup.linked(Destination::Location(loc), Some(alt)); + Ok(PdfMarkerTag::FootnoteRef(decl_loc, HElem::hole().pack() + link)) }; const FOOTNOTE_ENTRY_RULE: ShowFn = |elem, engine, styles| { diff --git a/crates/typst-library/src/pdf/accessibility.rs b/crates/typst-library/src/pdf/accessibility.rs index 5705eabe8..c869c2cd1 100644 --- a/crates/typst-library/src/pdf/accessibility.rs +++ b/crates/typst-library/src/pdf/accessibility.rs @@ -8,7 +8,7 @@ use crate::diag::SourceResult; use crate::diag::bail; use crate::engine::Engine; use crate::foundations::{Args, Construct, Content, NativeElement, Smart}; -use crate::introspection::Locatable; +use crate::introspection::{Locatable, Location}; use crate::model::TableCell; /// Mark content as a PDF artifact. @@ -149,6 +149,8 @@ pdf_marker_tag! { OutlineBody, /// `Figure` FigureBody(alt: Option), + /// `Note` footnote reference + FootnoteRef(decl_loc: Location), /// `L` bibliography list Bibliography(numbered: bool), /// `LBody` wrapping `BibEntry` diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index 029a87b2c..fef10af99 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -21,9 +21,9 @@ use typst_library::introspection::Location; use typst_library::layout::{Abs, Point, Rect, RepeatElem}; use typst_library::math::EquationElem; use typst_library::model::{ - Destination, EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, - HeadingElem, ListElem, Outlinable, OutlineEntry, ParElem, QuoteElem, TableCell, - TableElem, TermsElem, + Destination, EnumElem, FigureCaption, FigureElem, FootnoteEntry, HeadingElem, + ListElem, Outlinable, OutlineEntry, ParElem, QuoteElem, TableCell, TableElem, + TermsElem, }; use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind}; use typst_library::visualize::ImageElem; @@ -74,6 +74,10 @@ pub(crate) fn handle_start( push_stack(gc, elem, StackEntryKind::Figure(FigureCtx::new(alt)))?; return Ok(()); } + PdfMarkerTagKind::FootnoteRef(decl_loc) => { + push_stack(gc, elem, StackEntryKind::FootnoteRef(*decl_loc))?; + return Ok(()); + } PdfMarkerTagKind::Bibliography(numbered) => { let numbering = if *numbered { ListNumbering::Decimal } else { ListNumbering::None }; @@ -167,12 +171,9 @@ pub(crate) fn handle_start( let link_id = gc.tags.next_link_id(); push_stack(gc, elem, StackEntryKind::Link(link_id, link.clone()))?; return Ok(()); - } else if let Some(_) = elem.to_packed::() { - push_stack(gc, elem, StackEntryKind::FootNoteRef)?; - return Ok(()); } else if let Some(entry) = elem.to_packed::() { let footnote_loc = entry.note.location().unwrap(); - push_stack(gc, elem, StackEntryKind::FootNoteEntry(footnote_loc))?; + push_stack(gc, elem, StackEntryKind::FootnoteEntry(footnote_loc))?; return Ok(()); } else if let Some(quote) = elem.to_packed::() { // TODO: should the attribution be handled somehow? @@ -244,7 +245,7 @@ pub(crate) fn handle_end( if let Some(entry) = gc.tags.stack.pop_if(|e| e.loc == loc) { // The tag nesting was properly closed. - pop_stack(gc, loc, entry); + pop_stack(gc, entry); return Ok(()); } @@ -304,12 +305,12 @@ pub(crate) fn handle_end( kind, nodes: Vec::new(), }); - pop_stack(gc, loc, entry); + pop_stack(gc, entry); } // Pop the closed entry off the stack. let closed = gc.tags.stack.pop().unwrap(); - pop_stack(gc, loc, closed); + pop_stack(gc, closed); // Push all broken and afterwards duplicated entries back on. gc.tags.stack.extend(broken_entries); @@ -317,7 +318,7 @@ pub(crate) fn handle_end( Ok(()) } -fn pop_stack(gc: &mut GlobalContext, loc: Location, entry: StackEntry) { +fn pop_stack(gc: &mut GlobalContext, entry: StackEntry) { let node = match entry.kind { StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes), StackEntryKind::Outline(ctx) => ctx.build_outline(entry.nodes), @@ -382,17 +383,25 @@ fn pop_stack(gc: &mut GlobalContext, loc: Location, entry: StackEntry) { } node } - StackEntryKind::FootNoteRef => { - // transparently inset all children. + StackEntryKind::FootnoteRef(decl_loc) => { + // transparently insert all children. gc.tags.extend(entry.nodes); - gc.tags.push(TagNode::FootnoteEntry(loc)); + + let ctx = gc.tags.footnotes.entry(decl_loc).or_insert(FootnoteCtx::new()); + + // Only insert the footnote entry once after the first reference. + if !ctx.is_referenced { + ctx.is_referenced = true; + gc.tags.push(TagNode::FootnoteEntry(decl_loc)); + } return; } - StackEntryKind::FootNoteEntry(footnote_loc) => { + 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(Tag::Note.into(), entry.nodes); - gc.tags.footnotes.insert(footnote_loc, tag); + let ctx = gc.tags.footnotes.entry(footnote_loc).or_insert(FootnoteCtx::new()); + ctx.entry = Some(tag); return; } }; @@ -473,7 +482,7 @@ pub(crate) struct Tags { /// 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) footnotes: HashMap, pub(crate) in_artifact: Option<(Location, ArtifactKind)>, /// Used to group multiple link annotations using quad points. link_id: LinkId, @@ -537,7 +546,9 @@ 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"); + let node = (self.footnotes.remove(&loc)) + .and_then(|ctx| ctx.entry) + .expect("footnote"); self.resolve_node(node) } } @@ -738,11 +749,11 @@ pub(crate) enum StackEntryKind { Figure(FigureCtx), Formula(FigureCtx), Link(LinkId, Packed), - /// The footnote reference in the text. - FootNoteRef, + /// The footnote reference in the text, contains the declaration location. + FootnoteRef(Location), /// The footnote entry at the end of the page. Contains the [`Location`] of /// the [`FootnoteElem`](typst_library::model::FootnoteElem). - FootNoteEntry(Location), + FootnoteEntry(Location), } impl StackEntryKind { @@ -836,12 +847,28 @@ impl StackEntryKind { StackEntryKind::Figure(_) => false, StackEntryKind::Formula(_) => false, StackEntryKind::Link(..) => !is_pdf_ua, - StackEntryKind::FootNoteRef => false, - StackEntryKind::FootNoteEntry(_) => false, + StackEntryKind::FootnoteRef(_) => false, + StackEntryKind::FootnoteEntry(_) => false, } } } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct FootnoteCtx { + /// Whether this footenote has been referenced inside the document. The + /// entry will be inserted inside the reading order after the first + /// reference. All other references will still have links to the footnote. + is_referenced: bool, + /// The nodes that make up the footnote entry. + entry: Option, +} + +impl FootnoteCtx { + pub const fn new() -> Self { + Self { is_referenced: false, entry: None } + } +} + /// Figure/Formula context #[derive(Debug, Clone, PartialEq)] pub(crate) struct FigureCtx {