use std::num::NonZeroUsize; use std::sync::Arc; use typst_utils::NonZeroExt; use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, }; use crate::layout::{ show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, TrackSizings, }; use crate::model::Figurable; use crate::text::LocalName; use crate::visualize::{Paint, Stroke}; /// A table of items. /// /// Tables are used to arrange content in cells. Cells can contain arbitrary /// content, including multiple paragraphs and are specified in row-major order. /// For a hands-on explanation of all the ways you can use and customize tables /// in Typst, check out the [table guide]($guides/table-guide). /// /// Because tables are just grids with different defaults for some cell /// properties (notably `stroke` and `inset`), refer to the [grid /// documentation]($grid) for more information on how to size the table tracks /// and specify the cell appearance properties. /// /// If you are unsure whether you should be using a table or a grid, consider /// whether the content you are arranging semantically belongs together as a set /// of related data points or similar or whether you are just want to enhance /// your presentation by arranging unrelated content in a grid. In the former /// case, a table is the right choice, while in the latter case, a grid is more /// appropriate. Furthermore, Typst will annotate its output in the future such /// that screenreaders will announce content in `table` as tabular while a /// grid's content will be announced no different than multiple content blocks /// in the document flow. /// /// Note that, to override a particular cell's properties or apply show rules on /// table cells, you can use the [`table.cell`]($table.cell) element. See its /// documentation for more information. /// /// Although the `table` and the `grid` share most properties, set and show /// rules on one of them do not affect the other. /// /// To give a table a caption and make it [referenceable]($ref), put it into a /// [figure]. /// /// # Example /// /// The example below demonstrates some of the most common table options. /// ```example /// #table( /// columns: (1fr, auto, auto), /// inset: 10pt, /// align: horizon, /// table.header( /// [], [*Volume*], [*Parameters*], /// ), /// image("cylinder.svg"), /// $ pi h (D^2 - d^2) / 4 $, /// [ /// $h$: height \ /// $D$: outer radius \ /// $d$: inner radius /// ], /// image("tetrahedron.svg"), /// $ sqrt(2) / 12 a^3 $, /// [$a$: edge length] /// ) /// ``` /// /// Much like with grids, you can use [`table.cell`]($table.cell) to customize /// the appearance and the position of each cell. /// /// ```example /// >>> #set page(width: auto) /// >>> #set text(font: "IBM Plex Sans") /// >>> #let gray = rgb("#565565") /// >>> /// #set table( /// stroke: none, /// gutter: 0.2em, /// fill: (x, y) => /// if x == 0 or y == 0 { gray }, /// inset: (right: 1.5em), /// ) /// /// #show table.cell: it => { /// if it.x == 0 or it.y == 0 { /// set text(white) /// strong(it) /// } else if it.body == [] { /// // Replace empty cells with 'N/A' /// pad(..it.inset)[_N/A_] /// } else { /// it /// } /// } /// /// #let a = table.cell( /// fill: green.lighten(60%), /// )[A] /// #let b = table.cell( /// fill: aqua.lighten(60%), /// )[B] /// /// #table( /// columns: 4, /// [], [Exam 1], [Exam 2], [Exam 3], /// /// [John], [], a, [], /// [Mary], [], a, a, /// [Robert], b, a, b, /// ) /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct TableElem { /// The column sizes. See the [grid documentation]($grid) for more /// information on track sizing. #[borrowed] pub columns: TrackSizings, /// The row sizes. See the [grid documentation]($grid) for more information /// on track sizing. #[borrowed] pub rows: TrackSizings, /// The gaps between rows and columns. This is a shorthand for setting /// `column-gutter` and `row-gutter` to the same value. See the [grid /// documentation]($grid) for more information on gutters. #[external] pub gutter: TrackSizings, /// The gaps between columns. Takes precedence over `gutter`. See the /// [grid documentation]($grid) for more information on gutters. #[borrowed] #[parse( let gutter = args.named("gutter")?; args.named("column-gutter")?.or_else(|| gutter.clone()) )] pub column_gutter: TrackSizings, /// The gaps between rows. Takes precedence over `gutter`. See the /// [grid documentation]($grid) for more information on gutters. #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] #[borrowed] pub row_gutter: TrackSizings, /// How to fill the cells. /// /// This can be a color or a function that returns a color. The function /// receives the cells' column and row indices, starting from zero. This can /// be used to implement striped tables. /// /// ```example /// #table( /// fill: (x, _) => /// if calc.odd(x) { luma(240) } /// else { white }, /// align: (x, y) => /// if y == 0 { center } /// else if x == 0 { left } /// else { right }, /// columns: 4, /// [], [*Q1*], [*Q2*], [*Q3*], /// [Revenue:], [1000 €], [2000 €], [3000 €], /// [Expenses:], [500 €], [1000 €], [1500 €], /// [Profit:], [500 €], [1000 €], [1500 €], /// ) /// ``` #[borrowed] pub fill: Celled>, /// How to align the cells' content. /// /// This can either be a single alignment, an array of alignments /// (corresponding to each column) or a function that returns an alignment. /// The function receives the cells' column and row indices, starting from /// zero. If set to `{auto}`, the outer alignment is used. /// /// ```example /// #table( /// columns: 3, /// align: (left, center, right), /// [Hello], [Hello], [Hello], /// [A], [B], [C], /// ) /// ``` #[borrowed] pub align: Celled>, /// How to [stroke] the cells. /// /// Strokes can be disabled by setting this to `{none}`. /// /// If it is necessary to place lines which can cross spacing between cells /// produced by the `gutter` option, or to override the stroke between /// multiple specific cells, consider specifying one or more of /// [`table.hline`]($table.hline) and [`table.vline`]($table.vline) /// alongside your table cells. /// /// See the [grid documentation]($grid.stroke) for more information on /// strokes. #[resolve] #[fold] #[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))] pub stroke: Celled>>>>, /// How much to pad the cells' content. /// /// ```example /// #table( /// inset: 10pt, /// [Hello], /// [World], /// ) /// /// #table( /// columns: 2, /// inset: ( /// x: 20pt, /// y: 10pt, /// ), /// [Hello], /// [World], /// ) /// ``` #[fold] #[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))] pub inset: Celled>>>, /// The contents of the table cells, plus any extra table lines specified /// with the [`table.hline`]($table.hline) and /// [`table.vline`]($table.vline) elements. #[variadic] pub children: Vec, } #[scope] impl TableElem { #[elem] type TableCell; #[elem] type TableHLine; #[elem] type TableVLine; #[elem] type TableHeader; #[elem] type TableFooter; } impl Show for Packed { fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table) .pack() .spanned(self.span())) } } impl LocalName for Packed { const KEY: &'static str = "table"; } impl Figurable for Packed {} /// Any child of a table element. #[derive(Debug, PartialEq, Clone, Hash)] pub enum TableChild { Header(Packed), Footer(Packed), Item(TableItem), } cast! { TableChild, self => match self { Self::Header(header) => header.into_value(), Self::Footer(footer) => footer.into_value(), Self::Item(item) => item.into_value(), }, v: Content => { v.try_into()? }, } impl TryFrom for TableChild { type Error = HintedString; fn try_from(value: Content) -> HintedStrResult { if value.is::() { bail!( "cannot use `grid.header` as a table header"; hint: "use `table.header` instead" ) } if value.is::() { bail!( "cannot use `grid.footer` as a table footer"; hint: "use `table.footer` instead" ) } value .into_packed::() .map(Self::Header) .or_else(|value| value.into_packed::().map(Self::Footer)) .or_else(|value| TableItem::try_from(value).map(Self::Item)) } } /// A table item, which is the basic unit of table specification. #[derive(Debug, PartialEq, Clone, Hash)] pub enum TableItem { HLine(Packed), VLine(Packed), Cell(Packed), } cast! { TableItem, self => match self { Self::HLine(hline) => hline.into_value(), Self::VLine(vline) => vline.into_value(), Self::Cell(cell) => cell.into_value(), }, v: Content => { v.try_into()? }, } impl TryFrom for TableItem { type Error = HintedString; fn try_from(value: Content) -> HintedStrResult { if value.is::() { bail!("cannot place a grid header within another header or footer"); } if value.is::() { bail!("cannot place a table header within another header or footer"); } if value.is::() { bail!("cannot place a grid footer within another footer or header"); } if value.is::() { bail!("cannot place a table footer within another footer or header"); } if value.is::() { bail!( "cannot use `grid.cell` as a table cell"; hint: "use `table.cell` instead" ); } if value.is::() { bail!( "cannot use `grid.hline` as a table line"; hint: "use `table.hline` instead" ); } if value.is::() { bail!( "cannot use `grid.vline` as a table line"; hint: "use `table.vline` instead" ); } Ok(value .into_packed::() .map(Self::HLine) .or_else(|value| value.into_packed::().map(Self::VLine)) .or_else(|value| value.into_packed::().map(Self::Cell)) .unwrap_or_else(|value| { let span = value.span(); Self::Cell(Packed::new(TableCell::new(value)).spanned(span)) })) } } /// A repeatable table header. /// /// You should wrap your tables' heading rows in this function even if you do not /// plan to wrap your table across pages because Typst will use this function to /// attach accessibility metadata to tables in the future and ensure universal /// access to your document. /// /// You can use the `repeat` parameter to control whether your table's header /// will be repeated across pages. /// /// ```example /// #set page(height: 11.5em) /// #set table( /// fill: (x, y) => /// if x == 0 or y == 0 { /// gray.lighten(40%) /// }, /// align: right, /// ) /// /// #show table.cell.where(x: 0): strong /// #show table.cell.where(y: 0): strong /// /// #table( /// columns: 4, /// table.header( /// [], [Blue chip], /// [Fresh IPO], [Penny st'k], /// ), /// table.cell( /// rowspan: 6, /// align: horizon, /// rotate(-90deg, reflow: true)[ /// *USD / day* /// ], /// ), /// [0.20], [104], [5], /// [3.17], [108], [4], /// [1.59], [84], [1], /// [0.26], [98], [15], /// [0.01], [195], [4], /// [7.34], [57], [2], /// ) /// ``` #[elem(name = "header", title = "Table Header")] pub struct TableHeader { /// Whether this header should be repeated across pages. #[default(true)] pub repeat: bool, /// The cells and lines within the header. #[variadic] pub children: Vec, } /// A repeatable table footer. /// /// Just like the [`table.header`]($table.header) element, the footer can repeat /// itself on every page of the table. This is useful for improving legibility /// by adding the column labels in both the header and footer of a large table, /// totals, or other information that should be visible on every page. /// /// No other table cells may be placed after the footer. #[elem(name = "footer", title = "Table Footer")] pub struct TableFooter { /// Whether this footer should be repeated across pages. #[default(true)] pub repeat: bool, /// The cells and lines within the footer. #[variadic] pub children: Vec, } /// A horizontal line in the table. /// /// Overrides any per-cell stroke, including stroke specified through the /// table's `stroke` field. Can cross spacing between cells created through the /// table's [`column-gutter`]($table.column-gutter) option. /// /// Use this function instead of the table's `stroke` field if you want to /// manually place a horizontal line at a specific position in a single table. /// Consider using [table's `stroke`]($table.stroke) field or [`table.cell`'s /// `stroke`]($table.cell.stroke) field instead if the line you want to place is /// part of all your tables' designs. /// /// ```example /// #set table.hline(stroke: .6pt) /// /// #table( /// stroke: none, /// columns: (auto, 1fr), /// [09:00], [Badge pick up], /// [09:45], [Opening Keynote], /// [10:30], [Talk: Typst's Future], /// [11:15], [Session: Good PRs], /// table.hline(start: 1), /// [Noon], [_Lunch break_], /// table.hline(start: 1), /// [14:00], [Talk: Tracked Layout], /// [15:00], [Talk: Automations], /// [16:00], [Workshop: Tables], /// table.hline(), /// [19:00], [Day 1 Attendee Mixer], /// ) /// ``` #[elem(name = "hline", title = "Table Horizontal Line")] pub struct TableHLine { /// The row above which the horizontal line is placed (zero-indexed). /// Functions identically to the `y` field in [`grid.hline`]($grid.hline.y). pub y: Smart, /// The column at which the horizontal line starts (zero-indexed, inclusive). pub start: usize, /// The column before which the horizontal line ends (zero-indexed, /// exclusive). pub end: Option, /// The line's stroke. /// /// Specifying `{none}` removes any lines previously placed across this /// line's range, including hlines or per-cell stroke below it. #[resolve] #[fold] #[default(Some(Arc::new(Stroke::default())))] pub stroke: Option>, /// The position at which the line is placed, given its row (`y`) - either /// `{top}` to draw above it or `{bottom}` to draw below it. /// /// This setting is only relevant when row gutter is enabled (and /// shouldn't be used otherwise - prefer just increasing the `y` field by /// one instead), since then the position below a row becomes different /// from the position above the next row due to the spacing between both. #[default(OuterVAlignment::Top)] pub position: OuterVAlignment, } /// A vertical line in the table. See the docs for [`grid.vline`]($grid.vline) /// for more information regarding how to use this element's fields. /// /// Overrides any per-cell stroke, including stroke specified through the /// table's `stroke` field. Can cross spacing between cells created through the /// table's [`row-gutter`]($table.row-gutter) option. /// /// Similar to [`table.hline`]($table.hline), use this function if you want to /// manually place a vertical line at a specific position in a single table and /// use the [table's `stroke`]($table.stroke) field or [`table.cell`'s /// `stroke`]($table.cell.stroke) field instead if the line you want to place is /// part of all your tables' designs. #[elem(name = "vline", title = "Table Vertical Line")] pub struct TableVLine { /// The column before which the horizontal line is placed (zero-indexed). /// Functions identically to the `x` field in [`grid.vline`]($grid.vline). pub x: Smart, /// The row at which the vertical line starts (zero-indexed, inclusive). pub start: usize, /// The row on top of which the vertical line ends (zero-indexed, /// exclusive). pub end: Option, /// The line's stroke. /// /// Specifying `{none}` removes any lines previously placed across this /// line's range, including vlines or per-cell stroke below it. #[resolve] #[fold] #[default(Some(Arc::new(Stroke::default())))] pub stroke: Option>, /// The position at which the line is placed, given its column (`x`) - /// either `{start}` to draw before it or `{end}` to draw after it. /// /// The values `{left}` and `{right}` are also accepted, but discouraged as /// they cause your table to be inconsistent between left-to-right and /// right-to-left documents. /// /// This setting is only relevant when column gutter is enabled (and /// shouldn't be used otherwise - prefer just increasing the `x` field by /// one instead), since then the position after a column becomes different /// from the position before the next column due to the spacing between /// both. #[default(OuterHAlignment::Start)] pub position: OuterHAlignment, } /// A cell in the table. Use this to position a cell manually or to apply /// styling. To do the latter, you can either use the function to override the /// properties for a particular cell, or use it in show rules to apply certain /// styles to multiple cells at once. /// /// Perhaps the most important use case of `{table.cell}` is to make a cell span /// multiple columns and/or rows with the `colspan` and `rowspan` fields. /// /// ```example /// >>> #set page(width: auto) /// #show table.cell.where(y: 0): strong /// #set table( /// stroke: (x, y) => if y == 0 { /// (bottom: 0.7pt + black) /// }, /// align: (x, y) => ( /// if x > 0 { center } /// else { left } /// ) /// ) /// /// #table( /// columns: 3, /// table.header( /// [Substance], /// [Subcritical °C], /// [Supercritical °C], /// ), /// [Hydrochloric Acid], /// [12.0], [92.1], /// [Sodium Myreth Sulfate], /// [16.6], [104], /// [Potassium Hydroxide], /// table.cell(colspan: 2)[24.7], /// ) /// ``` /// /// For example, you can override the fill, alignment or inset for a single /// cell: /// /// ```example /// >>> #set page(width: auto) /// // You can also import those. /// #import table: cell, header /// /// #table( /// columns: 2, /// align: center, /// header( /// [*Trip progress*], /// [*Itinerary*], /// ), /// cell( /// align: right, /// fill: fuchsia.lighten(80%), /// [🚗], /// ), /// [Get in, folks!], /// [🚗], [Eat curbside hotdog], /// cell(align: left)[🌴🚗], /// cell( /// inset: 0.06em, /// text(1.62em)[🛖🌅🌊], /// ), /// ) /// ``` /// /// You may also apply a show rule on `table.cell` to style all cells at once. /// Combined with selectors, this allows you to apply styles based on a cell's /// position: /// /// ```example /// #show table.cell.where(x: 0): strong /// /// #table( /// columns: 3, /// gutter: 3pt, /// [Name], [Age], [Strength], /// [Hannes], [36], [Grace], /// [Irma], [50], [Resourcefulness], /// [Vikram], [49], [Perseverance], /// ) /// ``` #[elem(name = "cell", title = "Table Cell", Show)] pub struct TableCell { /// The cell's body. #[required] pub body: Content, /// The cell's column (zero-indexed). /// Functions identically to the `x` field in [`grid.cell`]($grid.cell). pub x: Smart, /// The cell's row (zero-indexed). /// Functions identically to the `y` field in [`grid.cell`]($grid.cell). pub y: Smart, /// The amount of columns spanned by this cell. #[default(NonZeroUsize::ONE)] pub colspan: NonZeroUsize, /// The amount of rows spanned by this cell. #[default(NonZeroUsize::ONE)] pub rowspan: NonZeroUsize, /// The cell's [fill]($table.fill) override. pub fill: Smart>, /// The cell's [alignment]($table.align) override. pub align: Smart, /// The cell's [inset]($table.inset) override. pub inset: Smart>>>, /// The cell's [stroke]($table.stroke) override. #[resolve] #[fold] pub stroke: Sides>>>, /// Whether rows spanned by this cell can be placed in different pages. /// When equal to `{auto}`, a cell spanning only fixed-size rows is /// unbreakable, while a cell spanning at least one `{auto}`-sized row is /// breakable. pub breakable: Smart, } cast! { TableCell, v: Content => v.into(), } impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } impl Default for Packed { fn default() -> Self { Packed::new(TableCell::new(Content::default())) } } impl From for TableCell { fn from(value: Content) -> Self { #[allow(clippy::unwrap_or_default)] value.unpack::().unwrap_or_else(Self::new) } }