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}