feat: generate tags for bibliographies

This commit is contained in:
Tobias Schmitz 2025-07-17 16:10:30 +02:00
parent 8d2c8712d5
commit 0bd0dc6d92
No known key found for this signature in database
4 changed files with 74 additions and 25 deletions

View File

@ -23,7 +23,7 @@ use typst_library::model::{
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem, LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works, 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::{ use typst_library::text::{
DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName, DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName,
OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem, OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem,
@ -452,10 +452,7 @@ const OUTLINE_RULE: ShowFn<OutlineElem> = |elem, engine, styles| {
} }
// Wrap the entries into a marker for pdf tagging. // Wrap the entries into a marker for pdf tagging.
seq.push( seq.push(PdfMarkerTag::OutlineBody(Content::sequence(entries)));
PdfMarkerTag::new(PdfMarkerTagKind::OutlineBody, Content::sequence(entries))
.pack(),
);
Ok(Content::sequence(seq)) Ok(Content::sequence(seq))
}; };
@ -543,25 +540,29 @@ const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
let mut cells = vec![]; let mut cells = vec![];
for (prefix, reference) in references { for (prefix, reference) in references {
let prefix = PdfMarkerTag::ListItemLabel(prefix.clone().unwrap_or_default());
cells.push(GridChild::Item(GridItem::Cell( cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) Packed::new(GridCell::new(prefix)).spanned(span),
.spanned(span),
))); )));
let reference = PdfMarkerTag::BibEntry(reference.clone());
cells.push(GridChild::Item(GridItem::Cell( 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) let grid = GridElem::new(cells)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) .with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) .with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
.pack() .pack()
.spanned(span), .spanned(span);
); // TODO(accessibility): infer list numbering from style?
seq.push(PdfMarkerTag::Bibliography(true, grid));
} else { } else {
let mut body = vec![];
for (_, reference) in references { for (_, reference) in references {
let realized = reference.clone(); let realized = PdfMarkerTag::BibEntry(reference.clone());
let block = if works.hanging_indent { let block = if works.hanging_indent {
let body = HElem::new((-INDENT).into()).pack() + realized; let body = HElem::new((-INDENT).into()).pack() + realized;
let inset = Sides::default() let inset = Sides::default()
@ -573,8 +574,9 @@ const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
BlockElem::new().with_body(Some(BlockBody::Content(realized))) 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)) Ok(Content::sequence(seq))

View File

@ -3,7 +3,10 @@ use std::num::NonZeroU32;
use typst_macros::{elem, func, Cast}; use typst_macros::{elem, func, Cast};
use typst_utils::NonZeroExt; 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::introspection::Locatable;
use crate::model::TableCell; use crate::model::TableCell;
@ -99,21 +102,28 @@ impl TableHeaderScope {
} }
// Used to delimit content for tagged PDF. // Used to delimit content for tagged PDF.
#[elem(Locatable)] #[elem(Locatable, Construct)]
pub struct PdfMarkerTag { pub struct PdfMarkerTag {
#[internal]
#[required] #[required]
pub kind: PdfMarkerTagKind, pub kind: PdfMarkerTagKind,
#[required] #[required]
pub body: Content, pub body: Content,
} }
impl Construct for PdfMarkerTag {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
macro_rules! pdf_marker_tag { macro_rules! pdf_marker_tag {
($(#[doc = $doc:expr] $variant:ident,)+) => { ($(#[doc = $doc:expr] $variant:ident$(($($name:ident: $ty:ident)+))?,)+) => {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Cast)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum PdfMarkerTagKind { pub enum PdfMarkerTagKind {
$( $(
#[doc = $doc] #[doc = $doc]
$variant $variant $(($($ty),+))?
),+ ),+
} }
@ -121,9 +131,12 @@ macro_rules! pdf_marker_tag {
$( $(
#[doc = $doc] #[doc = $doc]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn $variant(body: Content) -> Content { pub fn $variant($($($name: $ty,)+)? body: Content) -> Content {
let span = body.span(); 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, OutlineBody,
/// `Figure` /// `Figure`
FigureBody, FigureBody,
/// `L` bibliography list
Bibliography(numbered: bool),
/// `LBody` wrapping `BibEntry`
BibEntry,
/// `Lbl` (marker) of the list item /// `Lbl` (marker) of the list item
ListItemLabel, ListItemLabel,
/// `LBody` of the enum item /// `LBody` of the enum item

View File

@ -69,6 +69,20 @@ impl ListCtx {
item.body = Some(nodes); item.body = Some(nodes);
} }
pub(crate) fn push_bib_entry(&mut self, nodes: Vec<TagNode>) {
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>) -> TagNode { pub(crate) fn build_list(self, mut nodes: Vec<TagNode>) -> TagNode {
for item in self.items.into_iter() { for item in self.items.into_iter() {
nodes.push(TagNode::Group( nodes.push(TagNode::Group(

View File

@ -65,6 +65,16 @@ pub(crate) fn handle_start(
return Ok(()); return Ok(());
} }
PdfMarkerTagKind::FigureBody => TagKind::Figure.into(), 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 => { PdfMarkerTagKind::ListItemLabel => {
push_stack(gc, loc, StackEntryKind::ListItemLabel)?; push_stack(gc, loc, StackEntryKind::ListItemLabel)?;
return Ok(()); 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); list_ctx.push_body(entry.nodes);
return; 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) => { StackEntryKind::Link(_, link) => {
let alt = link.alt.as_ref().map(EcoString::to_string); let alt = link.alt.as_ref().map(EcoString::to_string);
let tag = TagKind::Link.with_alt_text(alt); let tag = TagKind::Link.with_alt_text(alt);
@ -507,6 +522,7 @@ pub(crate) enum StackEntryKind {
List(ListCtx), List(ListCtx),
ListItemLabel, ListItemLabel,
ListItemBody, ListItemBody,
BibEntry,
Link(LinkId, Packed<LinkMarker>), Link(LinkId, Packed<LinkMarker>),
/// The footnote reference in the text. /// The footnote reference in the text.
FootNoteRef, FootNoteRef,