typst_library/pdf/
accessibility.rs

1use std::num::NonZeroU32;
2
3use ecow::EcoString;
4use typst_macros::{Cast, elem, func};
5use typst_utils::NonZeroExt;
6
7use crate::diag::SourceResult;
8use crate::diag::bail;
9use crate::engine::Engine;
10use crate::foundations::{Args, Construct, Content, NativeElement, Smart};
11use crate::introspection::Tagged;
12use crate::model::{TableCell, TableElem};
13
14/// Marks content as a PDF artifact.
15///
16/// Artifacts are parts of the document that are not meant to be read by
17/// Assistive Technology (AT), such as screen readers. Typical examples include
18/// purely decorative images that do not contribute to the meaning of the
19/// document, watermarks, or repeated content such as page numbers.
20///
21/// Typst will automatically mark certain content, such as page headers,
22/// footers, backgrounds, and foregrounds, as artifacts. Likewise, paths and
23/// shapes are automatically marked as artifacts, but their content is not.
24/// Repetitions of table headers and footers are also marked as artifacts.
25///
26/// Once something is marked as an artifact, you cannot make any of its
27/// contents accessible again. If you need to mark only part of something as an
28/// artifact, you may need to use this function multiple times.
29///
30/// If you are unsure what constitutes an artifact, check the [Accessibility
31/// Guide]($guides/accessibility/#artifacts).
32///
33/// In the future, this function may be moved out of the `pdf` module, making it
34/// possible to hide content in HTML export from AT.
35// TODO: maybe generalize this and use it to mark html elements with `aria-hidden="true"`?
36#[elem(Tagged)]
37pub struct ArtifactElem {
38    /// The artifact kind.
39    ///
40    /// This will govern how the PDF reader treats the artifact during reflow
41    /// and content extraction (e.g. copy and paste).
42    #[default(ArtifactKind::Other)]
43    pub kind: ArtifactKind,
44
45    /// The content that is an artifact.
46    #[required]
47    pub body: Content,
48}
49
50/// The type of artifact.
51#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
52pub enum ArtifactKind {
53    /// Repeats on the top of each page.
54    Header,
55    /// Repeats at the bottom of each page.
56    Footer,
57    /// Not part of the document, but rather the page it is printed on. An
58    /// example would be cut marks or color bars.
59    Page,
60    /// Other artifacts, including purely cosmetic content, backgrounds,
61    /// watermarks, and repeated content.
62    #[default]
63    Other,
64}
65
66/// A summary of the purpose and structure of a complex table.
67///
68/// This will be available for Assistive Technology (AT), such as screen
69/// readers, when exporting to PDF, but not for sighted readers of your file.
70///
71/// This field is intended for instructions that help the user navigate the
72/// table using AT. It is not an alternative description, so do not duplicate
73/// the contents of the table within. Likewise, do not use this for the core
74/// takeaway of the table. Instead, include that in the text around the table
75/// or, even better, in a [figure caption]($figure.caption).
76///
77/// If in doubt whether your table is complex enough to warrant a summary, err
78/// on the side of not including one. If you are certain that your table is
79/// complex enough, consider whether a sighted user might find it challenging.
80/// They might benefit from the instructions you put here, so consider printing
81/// them visibly in the document instead.
82///
83/// The API of this feature is temporary. Hence, calling this function requires
84/// enabling the `a11y-extras` feature flag at the moment. Even if this
85/// functionality should be available without a feature flag in the future, the
86/// summary will remain exclusive to PDF export.
87///
88/// ```example
89/// #figure(
90///   pdf.table-summary(
91///     // The summary just provides orientation and structural
92///     // information for AT users.
93///     summary: "The first two columns list the names of each participant. The last column contains cells spanning multiple rows for their assigned group.",
94///     table(
95///       columns: 3,
96///       table.header[First Name][Given Name][Group],
97///       [Mike], [Davis], table.cell(rowspan: 3)[Sales],
98///       [Anna], [Smith],
99///       [John], [Johnson],
100///       [Sara], [Wilkins], table.cell(rowspan: 2)[Operations],
101///       [Tom], [Brown],
102///     ),
103///   ),
104///   // This is the key takeaway of the table, so we put it in the caption.
105///   caption: [The Sales org now has a new member],
106/// )
107/// ```
108#[func]
109pub fn table_summary(
110    #[named] summary: Option<EcoString>,
111    /// The table.
112    table: TableElem,
113) -> Content {
114    table.with_summary(summary).pack()
115}
116
117/// Explicitly defines a cell as a header cell.
118///
119/// Header cells help users of Assistive Technology (AT) understand and navigate
120/// complex tables. When your table is correctly marked up with header cells, AT
121/// can announce the relevant header information on-demand when entering a cell.
122///
123/// By default, Typst will automatically mark all cells within [`table.header`]
124/// as header cells. They will apply to the columns below them. You can use that
125/// function's [`level`]($table.header.level) parameter to make header cells
126/// labelled by other header cells.
127///
128/// The `pdf.header-cell` function allows you to indicate that a cell is a
129/// header cell in the following additional situations:
130///
131/// - You have a **header column** in which each cell applies to its row. In
132///   that case, you pass `{"row"}` as an argument to the [`scope`
133///   parameter]($pdf.header-cell.scope) to indicate that the header cell
134///   applies to the row.
135/// - You have a cell in [`table.header`], for example at the very start, that
136///   labels both its row and column. In that case, you pass `{"both"}` as an
137///   argument to the [`scope`]($pdf.header-cell.scope) parameter.
138/// - You have a header cell in a row not containing other header cells. In that
139///   case, you can use this function to mark it as a header cell.
140///
141/// The API of this feature is temporary. Hence, calling this function requires
142/// enabling the `a11y-extras` feature flag at the moment. In a future Typst
143/// release, this functionality may move out of the `pdf` module so that tables
144/// in other export targets can contain the same information.
145///
146/// ```example
147/// >>> #set text(font: "IBM Plex Sans")
148/// #show table.cell.where(x: 0): set text(weight: "medium")
149/// #show table.cell.where(y: 0): set text(weight: "bold")
150///
151/// #table(
152///   columns: 3,
153///   align: (start, end, end),
154///
155///   table.header(
156///     // Top-left cell: Labels both the nutrient rows
157///     // and the serving size columns.
158///     pdf.header-cell(scope: "both")[Nutrient],
159///     [Per 100g],
160///     [Per Serving],
161///   ),
162///
163///   // First column cells are row headers
164///   pdf.header-cell(scope: "row")[Calories],
165///   [250 kcal], [375 kcal],
166///   pdf.header-cell(scope: "row")[Protein],
167///   [8g], [12g],
168///   pdf.header-cell(scope: "row")[Fat],
169///   [12g], [18g],
170///   pdf.header-cell(scope: "row")[Carbs],
171///   [30g], [45g],
172/// )
173/// ```
174#[func]
175pub fn header_cell(
176    /// The nesting level of this header cell.
177    #[named]
178    #[default(NonZeroU32::ONE)]
179    level: NonZeroU32,
180    /// What track of the table this header cell applies to.
181    #[named]
182    #[default]
183    scope: TableHeaderScope,
184    /// The table cell.
185    ///
186    /// This can be content or a call to [`table.cell`].
187    cell: TableCell,
188) -> Content {
189    cell.with_kind(Smart::Custom(TableCellKind::Header(level, scope)))
190        .pack()
191}
192
193/// Explicitly defines this cell as a data cell.
194///
195/// Each cell in a table is either a header cell or a data cell. By default, all
196/// cells in [`table.header`] are header cells, and all other cells data cells.
197///
198/// If your header contains a cell that is not a header cell, you can use this
199/// function to mark it as a data cell.
200///
201/// The API of this feature is temporary. Hence, calling this function requires
202/// enabling the `a11y-extras` feature flag at the moment. In a future Typst
203/// release, this functionality may move out of the `pdf` module so that tables
204/// in other export targets can contain the same information.
205///
206/// ```example
207/// #show table.cell.where(x: 0): set text(weight: "bold")
208/// #show table.cell.where(x: 1): set text(style: "italic")
209/// #show table.cell.where(x: 1, y: 0): set text(style: "normal")
210///
211/// #table(
212///   columns: 3,
213///   align: (left, left, center),
214///
215///   table.header[Objective][Key Result][Status],
216///
217///   table.header(
218///     level: 2,
219///     table.cell(colspan: 2)[Improve Customer Satisfaction],
220///     // Status is data for this objective, not a header
221///     pdf.data-cell[✓ On Track],
222///   ),
223///   [], [Increase NPS to 50+], [45],
224///   [], [Reduce churn to \<5%], [4.2%],
225///
226///   table.header(
227///     level: 2,
228///     table.cell(colspan: 2)[Grow Revenue],
229///     pdf.data-cell[⚠ At Risk],
230///   ),
231///   [], [Achieve \$2M ARR], [\$1.8M],
232///   [], [Close 50 enterprise deals], [38],
233/// )
234/// ```
235#[func]
236pub fn data_cell(
237    /// The table cell.
238    ///
239    /// This can be content or a call to [`table.cell`].
240    cell: TableCell,
241) -> Content {
242    cell.with_kind(Smart::Custom(TableCellKind::Data)).pack()
243}
244
245#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
246pub enum TableCellKind {
247    Header(NonZeroU32, TableHeaderScope),
248    Footer,
249    #[default]
250    Data,
251}
252
253/// Which table track a header cell labels.
254#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
255pub enum TableHeaderScope {
256    /// The header cell refers to both the row and the column.
257    Both,
258    /// The header cell refers to the column.
259    #[default]
260    Column,
261    /// The header cell refers to the row.
262    Row,
263}
264
265impl TableHeaderScope {
266    pub fn refers_to_column(&self) -> bool {
267        match self {
268            TableHeaderScope::Both => true,
269            TableHeaderScope::Column => true,
270            TableHeaderScope::Row => false,
271        }
272    }
273
274    pub fn refers_to_row(&self) -> bool {
275        match self {
276            TableHeaderScope::Both => true,
277            TableHeaderScope::Column => false,
278            TableHeaderScope::Row => true,
279        }
280    }
281}
282
283/// Used to delimit content for tagged PDF.
284#[elem(Construct, Tagged)]
285pub struct PdfMarkerTag {
286    #[internal]
287    #[required]
288    pub kind: PdfMarkerTagKind,
289    #[required]
290    pub body: Content,
291}
292
293impl Construct for PdfMarkerTag {
294    fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
295        bail!(args.span, "cannot be constructed manually");
296    }
297}
298
299macro_rules! pdf_marker_tag {
300    ($(#[doc = $doc:expr] $variant:ident$(($($name:ident: $ty:ty)+))?,)+) => {
301        #[derive(Debug, Clone, Eq, PartialEq, Hash)]
302        pub enum PdfMarkerTagKind {
303            $(
304                #[doc = $doc]
305                $variant $(($($ty),+))?
306            ),+
307        }
308
309        impl PdfMarkerTag {
310            $(
311                #[doc = $doc]
312                #[allow(non_snake_case)]
313                pub fn $variant($($($name: $ty,)+)? body: Content) -> Content {
314                    let span = body.span();
315                    Self {
316                        kind: PdfMarkerTagKind::$variant $(($($name),+))?,
317                        body,
318                    }.pack().spanned(span)
319                }
320            )+
321        }
322    }
323}
324
325pdf_marker_tag! {
326    /// `TOC`.
327    OutlineBody,
328    /// `L` bibliography list.
329    Bibliography(numbered: bool),
330    /// `LBody` wrapping `BibEntry`.
331    BibEntry,
332    /// `Lbl` (marker) of the list item.
333    ListItemLabel,
334    /// `LBody` of the list item.
335    ListItemBody,
336    /// `Lbl` of the term item.
337    TermsItemLabel,
338    /// `LBody` the term item including the label.
339    TermsItemBody,
340    /// A generic `Lbl`.
341    Label,
342}