From 3d4d5489349f0d13707903462d0f245e19b54298 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Thu, 3 Jul 2025 18:38:54 +0200 Subject: [PATCH] feat: [WIP] generate alt text for ref elements --- .../typst-library/src/model/bibliography.rs | 4 +-- crates/typst-library/src/model/footnote.rs | 4 +-- crates/typst-library/src/model/outline.rs | 36 +++++++++++++------ crates/typst-library/src/model/reference.rs | 9 +++-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 3b14c8cbf..ec9d66774 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -877,7 +877,7 @@ impl<'a> Generator<'a> { renderer.display_elem_child(elem, &mut None, false)?; if let Some(location) = first_occurrences.get(item.key.as_str()) { let dest = Destination::Location(*location); - // TODO: accept user supplied alt text + // TODO(accessibility): generate alt text content = content.linked(dest, None); } StrResult::Ok(content) @@ -1013,7 +1013,7 @@ impl ElemRenderer<'_> { if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta { if let Some(location) = (self.link)(i) { let dest = Destination::Location(location); - // TODO: accept user supplied alt text + // TODO(accessibility): generate alt text content = content.linked(dest, None); } } diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index 872827d90..f1eeb0c3e 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -147,7 +147,7 @@ impl Show for Packed { let sup = SuperElem::new(num).pack().spanned(span); let loc = loc.variant(1); // Add zero-width weak spacing to make the footnote "sticky". - // TODO: accept user supplied alt text + // TODO(accessibility): generate alt text Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), None)) } } @@ -297,7 +297,7 @@ impl Show for Packed { let sup = SuperElem::new(num) .pack() .spanned(span) - // TODO: accept user supplied alt text + // TODO(accessibility): generate alt text .linked(Destination::Location(loc), None) .located(loc.variant(1)); diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index adca57830..8e2f18da6 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -2,7 +2,7 @@ use std::num::NonZeroUsize; use std::str::FromStr; use comemo::{Track, Tracked}; -use ecow::eco_format; +use ecow::{eco_format, EcoString}; use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{Get, NonZeroExt}; @@ -23,7 +23,7 @@ use crate::layout::{ }; use crate::math::EquationElem; use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; -use crate::text::{LocalName, SpaceElem, TextElem}; +use crate::text::{LocalName, SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// A table of contents, figures, or other elements. /// @@ -435,18 +435,11 @@ impl Show for Packed { let context = Context::new(None, Some(styles)); let context = context.track(); - // TODO: prefix should be wrapped in a `Lbl` structure element + // TODO(accessibility): prefix should be wrapped in a `Lbl` structure element let prefix = self.prefix(engine, context, span)?; let body = self.body().at(span)?; let page = self.page(engine, context, span)?; - let alt = { - // TODO: accept user supplied alt text - let prefix = prefix.as_ref().map(|p| p.plain_text()).unwrap_or_default(); - let body = body.plain_text(); - let page_str = PageElem::local_name_in(styles); - let page_nr = page.plain_text(); - eco_format!("{prefix} \"{body}\", {page_str} {page_nr}") - }; + let alt = alt_text(styles, &prefix, &body, &page); let inner = self.inner(context, span, body, page)?; let block = if self.element.is::() { let body = prefix.unwrap_or_default() + inner; @@ -704,6 +697,27 @@ cast! { v: Content => v.unpack::().map_err(|_| "expected outline entry")? } +fn alt_text( + styles: StyleChain, + prefix: &Option, + body: &Content, + page: &Content, +) -> EcoString { + let prefix = prefix.as_ref().map(|p| p.plain_text()).unwrap_or_default(); + let body = body.plain_text(); + let page_str = PageElem::local_name_in(styles); + let page_nr = page.plain_text(); + let quotes = SmartQuotes::get( + SmartQuoteElem::quotes_in(styles), + TextElem::lang_in(styles), + TextElem::region_in(styles), + SmartQuoteElem::alternative_in(styles), + ); + let open = quotes.double_open; + let close = quotes.double_close; + eco_format!("{prefix} {open}{body}{close} {page_str} {page_nr}",) +} + /// Measures the width of a prefix. fn measure_prefix( engine: &mut Engine, diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index b04c57c4a..2b35a826e 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -338,13 +338,18 @@ fn show_reference( Smart::Custom(Some(supplement)) => supplement.resolve(engine, styles, [elem])?, }; + let alt = { + let supplement = supplement.plain_text(); + let numbering = numbers.plain_text(); + eco_format!("{supplement} {numbering}",) + }; + let mut content = numbers; if !supplement.is_empty() { content = supplement + TextElem::packed("\u{a0}") + content; } - // TODO: accept user supplied alt text - Ok(content.linked(Destination::Location(loc), None)) + Ok(content.linked(Destination::Location(loc), Some(alt))) } /// Turn a reference into a citation.