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}