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