Skip to main content

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