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