Skip to main content

typst_library/model/
table.rs

1use std::num::{NonZeroU32, NonZeroUsize};
2use std::sync::Arc;
3
4use ecow::EcoString;
5use typst_utils::NonZeroExt;
6
7use crate::diag::{HintedStrResult, HintedString, SourceResult, bail};
8use crate::engine::Engine;
9use crate::foundations::{
10    Content, Packed, Smart, StyleChain, Synthesize, cast, elem, scope,
11};
12use crate::introspection::{Locatable, Tagged};
13use crate::layout::resolve::{CellGrid, table_to_cellgrid};
14use crate::layout::{
15    Abs, Alignment, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine,
16    Length, OuterHAlignment, OuterVAlignment, Rel, Sides, TrackSizings,
17};
18use crate::model::Figurable;
19use crate::pdf::TableCellKind;
20use crate::text::LocalName;
21use crate::visualize::{Paint, Stroke};
22
23/// A table of items.
24///
25/// Tables are used to arrange content in cells. Cells can contain arbitrary
26/// content, including multiple paragraphs and are specified in row-major order.
27/// For a hands-on explanation of all the ways you can use and customize tables
28/// in Typst, check out the @guides:tables[Table Guide].
29///
30/// Because tables are just grids with different defaults for some cell
31/// properties (notably `stroke` and `inset`), refer to the
32/// @grid:track-size[grid documentation] for more information on how to size the
33/// table tracks and specify the cell appearance properties.
34///
35/// If you are unsure whether you should be using a table or a grid, consider
36/// whether the content you are arranging semantically belongs together as a set
37/// of related data points or similar or whether you are just want to enhance
38/// your presentation by arranging unrelated content in a grid. In the former
39/// case, a table is the right choice, while in the latter case, a grid is more
40/// appropriate. Furthermore, Assistive Technology (AT) like screen readers will
41/// announce content in a `table` as tabular while a grid's content will be
42/// announced no different than multiple content blocks in the document flow. AT
43/// users will be able to navigate tables two-dimensionally by cell.
44///
45/// Note that, to override a particular cell's properties or apply show rules on
46/// table cells, you can use the @table.cell element. See its documentation for
47/// more information.
48///
49/// Although the `table` and the `grid` share most properties, set and show
50/// rules on one of them do not affect the other. Locating most of your styling
51/// in set and show rules is recommended, as it keeps the table's actual usages
52/// clean and easy to read. It also allows you to easily change the appearance
53/// of all tables in one place.
54///
55/// To give a table a caption and make it @ref[referenceable], put it into a
56/// @figure[figure].
57///
58/// = Example <example>
59/// The example below demonstrates some of the most common table options.
60///
61/// ```example
62/// #table(
63///   columns: (1fr, auto, auto),
64///   inset: 10pt,
65///   align: horizon,
66///   table.header(
67///     [], [*Volume*], [*Parameters*],
68///   ),
69///   image("cylinder.svg"),
70///   $ pi h (D^2 - d^2) / 4 $,
71///   [
72///     $h$: height \
73///     $D$: outer radius \
74///     $d$: inner radius
75///   ],
76///   image("tetrahedron.svg"),
77///   $ sqrt(2) / 12 a^3 $,
78///   [$a$: edge length]
79/// )
80/// ```
81///
82/// Much like with grids, you can use @table.cell to customize the appearance
83/// and the position of each cell.
84///
85/// ```example
86/// >>> #set page(width: auto)
87/// >>> #set text(font: "IBM Plex Sans")
88/// >>> #let gray = rgb("#565565")
89/// >>>
90/// #set table(
91///   stroke: none,
92///   gutter: 0.2em,
93///   fill: (x, y) =>
94///     if x == 0 or y == 0 { gray },
95///   inset: (right: 1.5em),
96/// )
97///
98/// #show table.cell: it => {
99///   if it.x == 0 or it.y == 0 {
100///     set text(white)
101///     strong(it)
102///   } else if it.body == [] {
103///     // Replace empty cells with 'N/A'
104///     pad(..it.inset)[_N/A_]
105///   } else {
106///     it
107///   }
108/// }
109///
110/// #let a = table.cell(
111///   fill: green.lighten(60%),
112/// )[A]
113/// #let b = table.cell(
114///   fill: aqua.lighten(60%),
115/// )[B]
116///
117/// #table(
118///   columns: 4,
119///   [], [Exam 1], [Exam 2], [Exam 3],
120///
121///   [John], [], a, [],
122///   [Mary], [], a, a,
123///   [Robert], b, a, b,
124/// )
125/// ```
126///
127/// = Accessibility <accessibility>
128/// Tables are challenging to consume for users of Assistive Technology (AT). To
129/// make the life of AT users easier, we strongly recommend that you use
130/// @table.header and @table.footer to mark the header and footer sections of
131/// your table. This will allow AT to announce the column labels for each cell.
132///
133/// Because navigating a table by cell is more cumbersome than reading it
134/// visually, you should consider making the core information in your table
135/// available as text as well. You can do this by wrapping your table in a
136/// @figure[figure] and using its caption to summarize the table's content.
137#[elem(scope, Locatable, Tagged, Synthesize, LocalName, Figurable)]
138pub struct TableElem {
139    /// The column sizes. See the @grid:track-size[grid documentation] for more
140    /// information on track sizing.
141    pub columns: TrackSizings,
142
143    /// The row sizes. See the @grid:track-size[grid documentation] for more
144    /// information on track sizing.
145    pub rows: TrackSizings,
146
147    /// The gaps between rows and columns. This is a shorthand for setting
148    /// `column-gutter` and `row-gutter` to the same value. See the
149    /// @grid.gutter[grid documentation] for more information on gutters.
150    #[external]
151    pub gutter: TrackSizings,
152
153    /// The gaps between columns. Takes precedence over `gutter`. See the
154    /// @grid.gutter[grid documentation] for more information on gutters.
155    #[parse(
156        let gutter = args.named("gutter")?;
157        args.named("column-gutter")?.or_else(|| gutter.clone())
158    )]
159    pub column_gutter: TrackSizings,
160
161    /// The gaps between rows. Takes precedence over `gutter`. See the
162    /// @grid.gutter[grid documentation] for more information on gutters.
163    #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
164    pub row_gutter: TrackSizings,
165
166    /// How much to pad the cells' content.
167    ///
168    /// To specify the same inset for all cells, use a single length for all
169    /// sides, or a dictionary of lengths for individual sides. See the
170    /// @box.inset[box's documentation] for more details.
171    ///
172    /// To specify a varying inset for different cells, you can:
173    /// - use a single, uniform inset for all cells
174    /// - use an array of insets for each column
175    /// - use a function that maps a cell's X/Y position (both starting from
176    ///   zero) to its inset
177    ///
178    /// See the @grid:styling[grid documentation] for more details.
179    ///
180    /// ```example
181    /// #table(
182    ///   columns: 2,
183    ///   inset: 10pt,
184    ///   [Hello],
185    ///   [World],
186    /// )
187    ///
188    /// #table(
189    ///   columns: 2,
190    ///   inset: (x: 20pt, y: 10pt),
191    ///   [Hello],
192    ///   [World],
193    /// )
194    /// ```
195    #[fold]
196    #[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))]
197    pub inset: Celled<Sides<Option<Rel<Length>>>>,
198
199    /// How to align the cells' content.
200    ///
201    /// If set to `{auto}`, the outer alignment is used.
202    ///
203    /// You can specify the alignment in any of the following fashions:
204    /// - use a single alignment for all cells
205    /// - use an array of alignments corresponding to each column
206    /// - use a function that maps a cell's X/Y position (both starting from
207    ///   zero) to its alignment
208    ///
209    /// See the @guides:tables:alignment[Table Guide] for details.
210    ///
211    /// ```example
212    /// #table(
213    ///   columns: 3,
214    ///   align: (left, center, right),
215    ///   [Hello], [Hello], [Hello],
216    ///   [A], [B], [C],
217    /// )
218    /// ```
219    pub align: Celled<Smart<Alignment>>,
220
221    /// How to fill the cells.
222    ///
223    /// This can be:
224    /// - a single fill for all cells
225    /// - an array of fill corresponding to each column
226    /// - a function that maps a cell's position to its fill
227    ///
228    /// Most notably, arrays and functions are useful for creating striped
229    /// tables. See the @guides:tables:fills[Table Guide] for more details.
230    ///
231    /// ```example
232    /// #table(
233    ///   fill: (x, _) =>
234    ///     if calc.odd(x) { luma(240) }
235    ///     else { white },
236    ///   align: (x, y) =>
237    ///     if y == 0 { center }
238    ///     else if x == 0 { left }
239    ///     else { right },
240    ///   columns: 4,
241    ///   [], [*Q1*], [*Q2*], [*Q3*],
242    ///   [Revenue:], [1000 €], [2000 €], [3000 €],
243    ///   [Expenses:], [500 €], [1000 €], [1500 €],
244    ///   [Profit:], [500 €], [1000 €], [1500 €],
245    /// )
246    /// ```
247    pub fill: Celled<Option<Paint>>,
248
249    /// How to @stroke[stroke] the cells.
250    ///
251    /// Strokes can be disabled by setting this to `{none}`.
252    ///
253    /// If it is necessary to place lines which can cross spacing between cells
254    /// produced by the @table.gutter[`gutter`] option, or to override the
255    /// stroke between multiple specific cells, consider specifying one or more
256    /// of @table.hline and @table.vline alongside your table cells.
257    ///
258    /// To specify the same stroke for all cells, use a single @stroke[stroke]
259    /// for all sides, or a dictionary of @stroke[strokes] for individual sides.
260    /// See the @rect.stroke[rectangle's documentation] for more details.
261    ///
262    /// To specify varying strokes for different cells, you can:
263    /// - use a single stroke for all cells
264    /// - use an array of strokes corresponding to each column
265    /// - use a function that maps a cell's position to its stroke
266    ///
267    /// See the @guides:tables:strokes[Table Guide] for more details.
268    #[fold]
269    #[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))]
270    pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
271
272    /// A summary of the purpose and structure of complex tables.
273    ///
274    /// See the @crate::pdf::accessibility::table_summary function for more
275    /// information.
276    #[internal]
277    #[parse(None)]
278    pub summary: Option<EcoString>,
279
280    #[internal]
281    #[synthesized]
282    pub grid: Arc<CellGrid>,
283
284    /// The contents of the table cells, plus any extra table lines specified
285    /// with the @table.hline and @table.vline elements.
286    #[variadic]
287    pub children: Vec<TableChild>,
288}
289
290#[scope]
291impl TableElem {
292    #[elem]
293    type TableCell;
294
295    #[elem]
296    type TableHLine;
297
298    #[elem]
299    type TableVLine;
300
301    #[elem]
302    type TableHeader;
303
304    #[elem]
305    type TableFooter;
306}
307
308impl Synthesize for Packed<TableElem> {
309    fn synthesize(
310        &mut self,
311        engine: &mut Engine,
312        styles: StyleChain,
313    ) -> SourceResult<()> {
314        let grid = table_to_cellgrid(self, engine, styles)?;
315        self.grid = Some(Arc::new(grid));
316        Ok(())
317    }
318}
319
320impl LocalName for Packed<TableElem> {
321    const KEY: &'static str = "table";
322}
323
324impl Figurable for Packed<TableElem> {}
325
326cast! {
327    TableElem,
328    v: Content => v.unpack::<Self>().map_err(|_| "expected table")?,
329}
330
331/// Any child of a table element.
332#[derive(Debug, Clone, PartialEq, Hash)]
333pub enum TableChild {
334    Header(Packed<TableHeader>),
335    Footer(Packed<TableFooter>),
336    Item(TableItem),
337}
338
339cast! {
340    TableChild,
341    self => match self {
342        Self::Header(header) => header.into_value(),
343        Self::Footer(footer) => footer.into_value(),
344        Self::Item(item) => item.into_value(),
345    },
346    v: Content => {
347        v.try_into()?
348    },
349}
350
351impl TryFrom<Content> for TableChild {
352    type Error = HintedString;
353
354    fn try_from(value: Content) -> HintedStrResult<Self> {
355        if value.is::<GridHeader>() {
356            bail!(
357                "cannot use `grid.header` as a table header";
358                hint: "use `table.header` instead";
359            )
360        }
361        if value.is::<GridFooter>() {
362            bail!(
363                "cannot use `grid.footer` as a table footer";
364                hint: "use `table.footer` instead";
365            )
366        }
367
368        value
369            .into_packed::<TableHeader>()
370            .map(Self::Header)
371            .or_else(|value| value.into_packed::<TableFooter>().map(Self::Footer))
372            .or_else(|value| TableItem::try_from(value).map(Self::Item))
373    }
374}
375
376/// A table item, which is the basic unit of table specification.
377#[derive(Debug, Clone, PartialEq, Hash)]
378pub enum TableItem {
379    HLine(Packed<TableHLine>),
380    VLine(Packed<TableVLine>),
381    Cell(Packed<TableCell>),
382}
383
384cast! {
385    TableItem,
386    self => match self {
387        Self::HLine(hline) => hline.into_value(),
388        Self::VLine(vline) => vline.into_value(),
389        Self::Cell(cell) => cell.into_value(),
390    },
391    v: Content => {
392        v.try_into()?
393    },
394}
395
396impl TryFrom<Content> for TableItem {
397    type Error = HintedString;
398
399    fn try_from(value: Content) -> HintedStrResult<Self> {
400        if value.is::<GridHeader>() {
401            bail!("cannot place a grid header within another header or footer");
402        }
403        if value.is::<TableHeader>() {
404            bail!("cannot place a table header within another header or footer");
405        }
406        if value.is::<GridFooter>() {
407            bail!("cannot place a grid footer within another footer or header");
408        }
409        if value.is::<TableFooter>() {
410            bail!("cannot place a table footer within another footer or header");
411        }
412        if value.is::<GridCell>() {
413            bail!(
414                "cannot use `grid.cell` as a table cell";
415                hint: "use `table.cell` instead";
416            );
417        }
418        if value.is::<GridHLine>() {
419            bail!(
420                "cannot use `grid.hline` as a table line";
421                hint: "use `table.hline` instead";
422            );
423        }
424        if value.is::<GridVLine>() {
425            bail!(
426                "cannot use `grid.vline` as a table line";
427                hint: "use `table.vline` instead";
428            );
429        }
430
431        Ok(value
432            .into_packed::<TableHLine>()
433            .map(Self::HLine)
434            .or_else(|value| value.into_packed::<TableVLine>().map(Self::VLine))
435            .or_else(|value| value.into_packed::<TableCell>().map(Self::Cell))
436            .unwrap_or_else(|value| {
437                let span = value.span();
438                Self::Cell(Packed::new(TableCell::new(value)).spanned(span))
439            }))
440    }
441}
442
443/// A repeatable table header.
444///
445/// You should wrap your tables' heading rows in this function even if you do
446/// not plan to wrap your table across pages because Typst uses this function to
447/// attach accessibility metadata to tables and ensure
448/// @guides:accessibility:basics[Universal Access] to your document.
449///
450/// You can use the `repeat` parameter to control whether your table's header
451/// will be repeated across pages.
452///
453/// Currently, this function is unsuitable for creating a header column or
454/// single header cells. Either use regular cells, or, if you are exporting a
455/// PDF, you can also use the @pdf.header-cell function to mark a cell as a
456/// header cell. Likewise, you can use @pdf.data-cell to mark cells in this
457/// function as data cells. Note that these functions are not final and thus
458/// only available when you enable the `a11y-extras` feature (see the
459/// @pdf[PDF module documentation] for details).
460///
461/// ```example
462/// #set page(height: 11.5em)
463/// #set table(
464///   fill: (x, y) =>
465///     if x == 0 or y == 0 {
466///       gray.lighten(40%)
467///     },
468///   align: right,
469/// )
470///
471/// #show table.cell.where(x: 0): strong
472/// #show table.cell.where(y: 0): strong
473///
474/// #table(
475///   columns: 4,
476///   table.header(
477///     [], [Blue chip],
478///     [Fresh IPO], [Penny st'k],
479///   ),
480///   table.cell(
481///     rowspan: 6,
482///     align: horizon,
483///     rotate(-90deg, reflow: true)[
484///       *USD / day*
485///     ],
486///   ),
487///   [0.20], [104], [5],
488///   [3.17], [108], [4],
489///   [1.59], [84],  [1],
490///   [0.26], [98],  [15],
491///   [0.01], [195], [4],
492///   [7.34], [57],  [2],
493/// )
494/// ```
495#[elem(name = "header", title = "Table Header")]
496pub struct TableHeader {
497    /// Whether this header should be repeated across pages.
498    #[default(true)]
499    pub repeat: bool,
500
501    /// The level of the header. Must not be zero.
502    ///
503    /// This allows repeating multiple headers at once. Headers with different
504    /// levels can repeat together, as long as they have ascending levels.
505    ///
506    /// Notably, when a header with a lower level starts repeating, all higher
507    /// or equal level headers stop repeating (they are "replaced" by the new
508    /// header).
509    #[default(NonZeroU32::ONE)]
510    pub level: NonZeroU32,
511
512    /// The cells and lines within the header.
513    #[variadic]
514    pub children: Vec<TableItem>,
515}
516
517/// A repeatable table footer.
518///
519/// Just like the @table.header element, the footer can repeat itself on every
520/// page of the table. This is useful for improving legibility by adding the
521/// column labels in both the header and footer of a large table, totals, or
522/// other information that should be visible on every page.
523///
524/// No other table cells may be placed after the footer.
525#[elem(name = "footer", title = "Table Footer")]
526pub struct TableFooter {
527    /// Whether this footer should be repeated across pages.
528    #[default(true)]
529    pub repeat: bool,
530
531    /// The cells and lines within the footer.
532    #[variadic]
533    pub children: Vec<TableItem>,
534}
535
536/// A horizontal line in the table.
537///
538/// Overrides any per-cell stroke, including stroke specified through the
539/// table's `stroke` field. Can cross spacing between cells created through the
540/// table's @table.column-gutter[`column-gutter`] option.
541///
542/// Use this function instead of the table's `stroke` field if you want to
543/// manually place a horizontal line at a specific position in a single table.
544/// Consider using @table.stroke[table's `stroke`] field or
545/// @table.cell.stroke[`table.cell`'s `stroke`] field instead if the line you
546/// want to place is part of all your tables' designs.
547///
548/// ```example
549/// #set table.hline(stroke: .6pt)
550///
551/// #table(
552///   stroke: none,
553///   columns: (auto, 1fr),
554///   [09:00], [Badge pick up],
555///   [09:45], [Opening Keynote],
556///   [10:30], [Talk: Typst's Future],
557///   [11:15], [Session: Good PRs],
558///   table.hline(start: 1),
559///   [Noon], [_Lunch break_],
560///   table.hline(start: 1),
561///   [14:00], [Talk: Tracked Layout],
562///   [15:00], [Talk: Automations],
563///   [16:00], [Workshop: Tables],
564///   table.hline(),
565///   [19:00], [Day 1 Attendee Mixer],
566/// )
567/// ```
568#[elem(name = "hline", title = "Table Horizontal Line")]
569pub struct TableHLine {
570    /// The row above which the horizontal line is placed (zero-indexed).
571    /// Functions identically to the `y` field in @grid.hline.y[`grid.hline`].
572    pub y: Smart<usize>,
573
574    /// The column at which the horizontal line starts (zero-indexed,
575    /// inclusive).
576    pub start: usize,
577
578    /// The column before which the horizontal line ends (zero-indexed,
579    /// exclusive).
580    pub end: Option<NonZeroUsize>,
581
582    /// The line's stroke.
583    ///
584    /// Specifying `{none}` removes any lines previously placed across this
585    /// line's range, including hlines or per-cell stroke below it.
586    #[fold]
587    #[default(Some(Arc::new(Stroke::default())))]
588    pub stroke: Option<Arc<Stroke>>,
589
590    /// The position at which the line is placed, given its row (`y`) - either
591    /// `{top}` to draw above it or `{bottom}` to draw below it.
592    ///
593    /// This setting is only relevant when row gutter is enabled (and shouldn't
594    /// be used otherwise - prefer just increasing the `y` field by one
595    /// instead), since then the position below a row becomes different from the
596    /// position above the next row due to the spacing between both.
597    #[default(OuterVAlignment::Top)]
598    pub position: OuterVAlignment,
599}
600
601/// A vertical line in the table. See the docs for @grid.vline for more
602/// information regarding how to use this element's fields.
603///
604/// Overrides any per-cell stroke, including stroke specified through the
605/// table's `stroke` field. Can cross spacing between cells created through the
606/// table's @table.row-gutter[`row-gutter`] option.
607///
608/// Similar to @table.hline, use this function if you want to manually place a
609/// vertical line at a specific position in a single table and use the
610/// @table.stroke[table's `stroke`] field or
611/// @table.cell.stroke[`table.cell`'s `stroke`] field instead if the line you
612/// want to place is part of all your tables' designs.
613#[elem(name = "vline", title = "Table Vertical Line")]
614pub struct TableVLine {
615    /// The column before which the vertical line is placed (zero-indexed).
616    /// Functions identically to the `x` field in @grid.vline.
617    pub x: Smart<usize>,
618
619    /// The row at which the vertical line starts (zero-indexed, inclusive).
620    pub start: usize,
621
622    /// The row on top of which the vertical line ends (zero-indexed,
623    /// exclusive).
624    pub end: Option<NonZeroUsize>,
625
626    /// The line's stroke.
627    ///
628    /// Specifying `{none}` removes any lines previously placed across this
629    /// line's range, including vlines or per-cell stroke below it.
630    #[fold]
631    #[default(Some(Arc::new(Stroke::default())))]
632    pub stroke: Option<Arc<Stroke>>,
633
634    /// The position at which the line is placed, given its column (`x`) -
635    /// either `{start}` to draw before it or `{end}` to draw after it.
636    ///
637    /// The values `{left}` and `{right}` are also accepted, but discouraged as
638    /// they cause your table to be inconsistent between left-to-right and
639    /// right-to-left documents.
640    ///
641    /// This setting is only relevant when column gutter is enabled (and
642    /// shouldn't be used otherwise - prefer just increasing the `x` field by
643    /// one instead), since then the position after a column becomes different
644    /// from the position before the next column due to the spacing between
645    /// both.
646    #[default(OuterHAlignment::Start)]
647    pub position: OuterHAlignment,
648}
649
650/// A cell in the table. Use this to position a cell manually or to apply
651/// styling. To do the latter, you can either use the function to override the
652/// properties for a particular cell, or use it in show rules to apply certain
653/// styles to multiple cells at once.
654///
655/// Perhaps the most important use case of `{table.cell}` is to make a cell span
656/// multiple columns and/or rows with the `colspan` and `rowspan` fields.
657///
658/// ```example
659/// >>> #set page(width: auto)
660/// #show table.cell.where(y: 0): strong
661/// #set table(
662///   stroke: (x, y) => if y == 0 {
663///     (bottom: 0.7pt + black)
664///   },
665///   align: (x, y) => (
666///     if x > 0 { center }
667///     else { left }
668///   )
669/// )
670///
671/// #table(
672///   columns: 3,
673///   table.header(
674///     [Substance],
675///     [Subcritical °C],
676///     [Supercritical °C],
677///   ),
678///   [Hydrochloric Acid],
679///   [12.0], [92.1],
680///   [Sodium Myreth Sulfate],
681///   [16.6], [104],
682///   [Potassium Hydroxide],
683///   table.cell(colspan: 2)[24.7],
684/// )
685/// ```
686///
687/// For example, you can override the fill, alignment or inset for a single
688/// cell:
689///
690/// ```example
691/// >>> #set page(width: auto)
692/// // You can also import those.
693/// #import table: cell, header
694///
695/// #table(
696///   columns: 2,
697///   align: center,
698///   header(
699///     [*Trip progress*],
700///     [*Itinerary*],
701///   ),
702///   cell(
703///     align: right,
704///     fill: fuchsia.lighten(80%),
705///     [🚗],
706///   ),
707///   [Get in, folks!],
708///   [🚗], [Eat curbside hotdog],
709///   cell(align: left)[🌴🚗],
710///   cell(
711///     inset: 0.06em,
712///     text(1.62em)[🏝️🌅🌊],
713///   ),
714/// )
715/// ```
716///
717/// You may also apply a show rule on `table.cell` to style all cells at once.
718/// Combined with selectors, this allows you to apply styles based on a cell's
719/// position:
720///
721/// ```example
722/// #show table.cell.where(x: 0): strong
723///
724/// #table(
725///   columns: 3,
726///   gutter: 3pt,
727///   [Name], [Age], [Strength],
728///   [Hannes], [36], [Grace],
729///   [Irma], [50], [Resourcefulness],
730///   [Vikram], [49], [Perseverance],
731/// )
732/// ```
733#[elem(name = "cell", title = "Table Cell")]
734pub struct TableCell {
735    /// The cell's body.
736    #[required]
737    pub body: Content,
738
739    /// The cell's column (zero-indexed). Functions identically to the `x` field
740    /// in @grid.cell.
741    pub x: Smart<usize>,
742
743    /// The cell's row (zero-indexed). Functions identically to the `y` field in
744    /// @grid.cell.
745    pub y: Smart<usize>,
746
747    /// The amount of columns spanned by this cell.
748    #[default(NonZeroUsize::ONE)]
749    pub colspan: NonZeroUsize,
750
751    /// The amount of rows spanned by this cell.
752    #[default(NonZeroUsize::ONE)]
753    pub rowspan: NonZeroUsize,
754
755    /// The cell's @table.inset[inset] override.
756    pub inset: Smart<Sides<Option<Rel<Length>>>>,
757
758    /// The cell's @table.align[alignment] override.
759    pub align: Smart<Alignment>,
760
761    /// The cell's @table.fill[fill] override.
762    pub fill: Smart<Option<Paint>>,
763
764    /// The cell's @table.stroke[stroke] override.
765    #[fold]
766    pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
767
768    /// Whether rows spanned by this cell can be placed in different pages. When
769    /// equal to `{auto}`, a cell spanning only fixed-size rows is unbreakable,
770    /// while a cell spanning at least one `{auto}`-sized row is breakable.
771    pub breakable: Smart<bool>,
772
773    #[internal]
774    #[parse(Some(Smart::Auto))]
775    pub kind: Smart<TableCellKind>,
776
777    #[internal]
778    #[parse(Some(false))]
779    pub is_repeated: bool,
780}
781
782cast! {
783    TableCell,
784    v: Content => v.into(),
785}
786
787impl Default for Packed<TableCell> {
788    fn default() -> Self {
789        Packed::new(
790            // Explicitly set colspan and rowspan to ensure they won't be
791            // overridden by set rules (default cells are created after
792            // colspans and rowspans are processed in the resolver)
793            TableCell::new(Content::default())
794                .with_colspan(NonZeroUsize::ONE)
795                .with_rowspan(NonZeroUsize::ONE),
796        )
797    }
798}
799
800impl From<Content> for TableCell {
801    fn from(value: Content) -> Self {
802        #[allow(clippy::unwrap_or_default)]
803        value.unpack::<Self>().unwrap_or_else(Self::new)
804    }
805}