fix: footnotes referencing other footnotes

This commit is contained in:
Tobias Schmitz 2025-07-29 13:18:31 +02:00
parent be04cdb029
commit e4825f7957
No known key found for this signature in database
3 changed files with 58 additions and 28 deletions

View File

@ -384,15 +384,16 @@ const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
const FOOTNOTE_RULE: ShowFn<FootnoteElem> = |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<FootnoteEntry> = |elem, engine, styles| {

View File

@ -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<EcoString>),
/// `Note` footnote reference
FootnoteRef(decl_loc: Location),
/// `L` bibliography list
Bibliography(numbered: bool),
/// `LBody` wrapping `BibEntry`

View File

@ -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::<FootnoteElem>() {
push_stack(gc, elem, StackEntryKind::FootNoteRef)?;
return Ok(());
} else if let Some(entry) = elem.to_packed::<FootnoteEntry>() {
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::<QuoteElem>() {
// 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<Location, TagNode>,
pub(crate) footnotes: HashMap<Location, FootnoteCtx>,
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<LinkMarker>),
/// 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<TagNode>,
}
impl FootnoteCtx {
pub const fn new() -> Self {
Self { is_referenced: false, entry: None }
}
}
/// Figure/Formula context
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct FigureCtx {