From 0bd0dc6d92647a30a2c84b5530a341bd47d67c83 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Thu, 17 Jul 2025 16:10:30 +0200 Subject: [PATCH] feat: generate tags for bibliographies --- crates/typst-layout/src/rules.rs | 38 ++++++++++--------- crates/typst-library/src/pdf/accessibility.rs | 31 +++++++++++---- crates/typst-pdf/src/tags/list.rs | 14 +++++++ crates/typst-pdf/src/tags/mod.rs | 16 ++++++++ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/crates/typst-layout/src/rules.rs b/crates/typst-layout/src/rules.rs index 09c247e9a..829a402d3 100644 --- a/crates/typst-layout/src/rules.rs +++ b/crates/typst-layout/src/rules.rs @@ -23,7 +23,7 @@ use typst_library::model::{ LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works, }; -use typst_library::pdf::{ArtifactElem, EmbedElem, PdfMarkerTag, PdfMarkerTagKind}; +use typst_library::pdf::{ArtifactElem, EmbedElem, PdfMarkerTag}; use typst_library::text::{ DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName, OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem, @@ -452,10 +452,7 @@ const OUTLINE_RULE: ShowFn = |elem, engine, styles| { } // Wrap the entries into a marker for pdf tagging. - seq.push( - PdfMarkerTag::new(PdfMarkerTagKind::OutlineBody, Content::sequence(entries)) - .pack(), - ); + seq.push(PdfMarkerTag::OutlineBody(Content::sequence(entries))); Ok(Content::sequence(seq)) }; @@ -543,25 +540,29 @@ const BIBLIOGRAPHY_RULE: ShowFn = |elem, engine, styles| { let mut cells = vec![]; for (prefix, reference) in references { + let prefix = PdfMarkerTag::ListItemLabel(prefix.clone().unwrap_or_default()); cells.push(GridChild::Item(GridItem::Cell( - Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) - .spanned(span), + Packed::new(GridCell::new(prefix)).spanned(span), ))); + + let reference = PdfMarkerTag::BibEntry(reference.clone()); cells.push(GridChild::Item(GridItem::Cell( - Packed::new(GridCell::new(reference.clone())).spanned(span), + Packed::new(GridCell::new(reference)).spanned(span), ))); } - seq.push( - GridElem::new(cells) - .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) - .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) - .pack() - .spanned(span), - ); + + let grid = GridElem::new(cells) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) + .pack() + .spanned(span); + // TODO(accessibility): infer list numbering from style? + seq.push(PdfMarkerTag::Bibliography(true, grid)); } else { + let mut body = vec![]; for (_, reference) in references { - let realized = reference.clone(); + let realized = PdfMarkerTag::BibEntry(reference.clone()); let block = if works.hanging_indent { let body = HElem::new((-INDENT).into()).pack() + realized; let inset = Sides::default() @@ -573,8 +574,9 @@ const BIBLIOGRAPHY_RULE: ShowFn = |elem, engine, styles| { BlockElem::new().with_body(Some(BlockBody::Content(realized))) }; - seq.push(block.pack().spanned(span)); + body.push(block.pack().spanned(span)); } + seq.push(PdfMarkerTag::Bibliography(false, Content::sequence(body))); } Ok(Content::sequence(seq)) diff --git a/crates/typst-library/src/pdf/accessibility.rs b/crates/typst-library/src/pdf/accessibility.rs index 142e7ff50..523b626a1 100644 --- a/crates/typst-library/src/pdf/accessibility.rs +++ b/crates/typst-library/src/pdf/accessibility.rs @@ -3,7 +3,10 @@ use std::num::NonZeroU32; use typst_macros::{elem, func, Cast}; use typst_utils::NonZeroExt; -use crate::foundations::{Content, NativeElement, Smart}; +use crate::diag::bail; +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{Args, Construct, Content, NativeElement, Smart}; use crate::introspection::Locatable; use crate::model::TableCell; @@ -99,21 +102,28 @@ impl TableHeaderScope { } // Used to delimit content for tagged PDF. -#[elem(Locatable)] +#[elem(Locatable, Construct)] pub struct PdfMarkerTag { + #[internal] #[required] pub kind: PdfMarkerTagKind, #[required] pub body: Content, } +impl Construct for PdfMarkerTag { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + bail!(args.span, "cannot be constructed manually"); + } +} + macro_rules! pdf_marker_tag { - ($(#[doc = $doc:expr] $variant:ident,)+) => { - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Cast)] + ($(#[doc = $doc:expr] $variant:ident$(($($name:ident: $ty:ident)+))?,)+) => { + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum PdfMarkerTagKind { $( #[doc = $doc] - $variant + $variant $(($($ty),+))? ),+ } @@ -121,9 +131,12 @@ macro_rules! pdf_marker_tag { $( #[doc = $doc] #[allow(non_snake_case)] - pub fn $variant(body: Content) -> Content { + pub fn $variant($($($name: $ty,)+)? body: Content) -> Content { let span = body.span(); - Self::new(PdfMarkerTagKind::$variant, body).pack().spanned(span) + Self { + kind: PdfMarkerTagKind::$variant $(($($name),+))?, + body, + }.pack().spanned(span) } )+ } @@ -135,6 +148,10 @@ pdf_marker_tag! { OutlineBody, /// `Figure` FigureBody, + /// `L` bibliography list + Bibliography(numbered: bool), + /// `LBody` wrapping `BibEntry` + BibEntry, /// `Lbl` (marker) of the list item ListItemLabel, /// `LBody` of the enum item diff --git a/crates/typst-pdf/src/tags/list.rs b/crates/typst-pdf/src/tags/list.rs index 4046cdcee..ce18fcd2f 100644 --- a/crates/typst-pdf/src/tags/list.rs +++ b/crates/typst-pdf/src/tags/list.rs @@ -69,6 +69,20 @@ impl ListCtx { item.body = Some(nodes); } + pub(crate) fn push_bib_entry(&mut self, nodes: Vec) { + let nodes = vec![TagNode::Group(TagKind::BibEntry.into(), nodes)]; + // Bibliography lists cannot be nested, but may be missing labels. + if let Some(item) = self.items.last_mut().filter(|item| item.body.is_none()) { + item.body = Some(nodes); + } else { + self.items.push(ListItem { + label: Vec::new(), + body: Some(nodes), + sub_list: None, + }); + } + } + pub(crate) fn build_list(self, mut nodes: Vec) -> TagNode { for item in self.items.into_iter() { nodes.push(TagNode::Group( diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index 702f4b763..18caae9ed 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -65,6 +65,16 @@ pub(crate) fn handle_start( return Ok(()); } PdfMarkerTagKind::FigureBody => TagKind::Figure.into(), + PdfMarkerTagKind::Bibliography(numbered) => { + let numbering = + if numbered { ListNumbering::Decimal } else { ListNumbering::None }; + push_stack(gc, loc, StackEntryKind::List(ListCtx::new(numbering)))?; + return Ok(()); + } + PdfMarkerTagKind::BibEntry => { + push_stack(gc, loc, StackEntryKind::BibEntry)?; + return Ok(()); + } PdfMarkerTagKind::ListItemLabel => { push_stack(gc, loc, StackEntryKind::ListItemLabel)?; return Ok(()); @@ -225,6 +235,11 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc list_ctx.push_body(entry.nodes); return; } + StackEntryKind::BibEntry => { + let list_ctx = gc.tags.stack.parent_list().expect("parent list"); + list_ctx.push_bib_entry(entry.nodes); + return; + } StackEntryKind::Link(_, link) => { let alt = link.alt.as_ref().map(EcoString::to_string); let tag = TagKind::Link.with_alt_text(alt); @@ -507,6 +522,7 @@ pub(crate) enum StackEntryKind { List(ListCtx), ListItemLabel, ListItemBody, + BibEntry, Link(LinkId, Packed), /// The footnote reference in the text. FootNoteRef,