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,
TargetElem,
};
use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use crate::introspection::Locator;
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
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};
#[elem(scope, Show, LocalName, Figurable)]
pub struct TableElem {
#[borrowed]
pub columns: TrackSizings,
#[borrowed]
pub rows: TrackSizings,
#[external]
pub gutter: TrackSizings,
#[borrowed]
#[parse(
let gutter = args.named("gutter")?;
args.named("column-gutter")?.or_else(|| gutter.clone())
)]
pub column_gutter: TrackSizings,
#[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
#[borrowed]
pub row_gutter: TrackSizings,
#[borrowed]
pub fill: Celled<Option<Paint>>,
#[borrowed]
pub align: Celled<Smart<Alignment>>,
#[resolve]
#[fold]
#[default(Celled::Value(Sides::splat(Some(Some(Arc::new(Stroke::default()))))))]
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
#[fold]
#[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))]
pub inset: Celled<Sides<Option<Rel<Length>>>>,
#[variadic]
pub children: Vec<TableChild>,
}
#[scope]
impl TableElem {
#[elem]
type TableCell;
#[elem]
type TableHLine;
#[elem]
type TableVLine;
#[elem]
type TableHeader;
#[elem]
type TableFooter;
}
fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
let cell = cell.body.clone();
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
let mut attrs = HtmlAttrs::default();
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
if let Some(colspan) = span(cell.colspan(styles)) {
attrs.push(attr::colspan, colspan);
}
if let Some(rowspan) = span(cell.rowspan(styles)) {
attrs.push(attr::rowspan, rowspan);
}
HtmlElem::new(tag)
.with_body(Some(cell.body.clone()))
.with_attrs(attrs)
.pack()
.spanned(cell.span())
}
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect();
let tr = |tag, row: &[Entry]| {
let row = row
.iter()
.flat_map(|entry| entry.as_cell())
.map(|cell| show_cell_html(tag, cell, styles));
elem(tag::tr, Content::sequence(row))
};
let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.unwrap().start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
});
let header = grid.header.map(|hd| {
let rows = rows.drain(..hd.unwrap().end);
elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
});
let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row)));
if header.is_some() || footer.is_some() {
body = elem(tag::tbody, body);
}
let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
elem(tag::table, Content::sequence(content))
}
impl Show for Packed<TableElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(if TargetElem::target_in(styles).is_html() {
let locator = Locator::root();
show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
} else {
BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
}
.spanned(self.span()))
}
}
impl LocalName for Packed<TableElem> {
const KEY: &'static str = "table";
}
impl Figurable for Packed<TableElem> {}
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum TableChild {
Header(Packed<TableHeader>),
Footer(Packed<TableFooter>),
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<Content> for TableChild {
type Error = HintedString;
fn try_from(value: Content) -> HintedStrResult<Self> {
if value.is::<GridHeader>() {
bail!(
"cannot use `grid.header` as a table header";
hint: "use `table.header` instead"
)
}
if value.is::<GridFooter>() {
bail!(
"cannot use `grid.footer` as a table footer";
hint: "use `table.footer` instead"
)
}
value
.into_packed::<TableHeader>()
.map(Self::Header)
.or_else(|value| value.into_packed::<TableFooter>().map(Self::Footer))
.or_else(|value| TableItem::try_from(value).map(Self::Item))
}
}
#[derive(Debug, PartialEq, Clone, Hash)]
pub enum TableItem {
HLine(Packed<TableHLine>),
VLine(Packed<TableVLine>),
Cell(Packed<TableCell>),
}
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<Content> for TableItem {
type Error = HintedString;
fn try_from(value: Content) -> HintedStrResult<Self> {
if value.is::<GridHeader>() {
bail!("cannot place a grid header within another header or footer");
}
if value.is::<TableHeader>() {
bail!("cannot place a table header within another header or footer");
}
if value.is::<GridFooter>() {
bail!("cannot place a grid footer within another footer or header");
}
if value.is::<TableFooter>() {
bail!("cannot place a table footer within another footer or header");
}
if value.is::<GridCell>() {
bail!(
"cannot use `grid.cell` as a table cell";
hint: "use `table.cell` instead"
);
}
if value.is::<GridHLine>() {
bail!(
"cannot use `grid.hline` as a table line";
hint: "use `table.hline` instead"
);
}
if value.is::<GridVLine>() {
bail!(
"cannot use `grid.vline` as a table line";
hint: "use `table.vline` instead"
);
}
Ok(value
.into_packed::<TableHLine>()
.map(Self::HLine)
.or_else(|value| value.into_packed::<TableVLine>().map(Self::VLine))
.or_else(|value| value.into_packed::<TableCell>().map(Self::Cell))
.unwrap_or_else(|value| {
let span = value.span();
Self::Cell(Packed::new(TableCell::new(value)).spanned(span))
}))
}
}
#[elem(name = "header", title = "Table Header")]
pub struct TableHeader {
#[default(true)]
pub repeat: bool,
#[variadic]
pub children: Vec<TableItem>,
}
#[elem(name = "footer", title = "Table Footer")]
pub struct TableFooter {
#[default(true)]
pub repeat: bool,
#[variadic]
pub children: Vec<TableItem>,
}
#[elem(name = "hline", title = "Table Horizontal Line")]
pub struct TableHLine {
pub y: Smart<usize>,
pub start: usize,
pub end: Option<NonZeroUsize>,
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
#[default(OuterVAlignment::Top)]
pub position: OuterVAlignment,
}
#[elem(name = "vline", title = "Table Vertical Line")]
pub struct TableVLine {
pub x: Smart<usize>,
pub start: usize,
pub end: Option<NonZeroUsize>,
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
#[default(OuterHAlignment::Start)]
pub position: OuterHAlignment,
}
#[elem(name = "cell", title = "Table Cell", Show)]
pub struct TableCell {
#[required]
pub body: Content,
pub x: Smart<usize>,
pub y: Smart<usize>,
#[default(NonZeroUsize::ONE)]
pub colspan: NonZeroUsize,
#[default(NonZeroUsize::ONE)]
pub rowspan: NonZeroUsize,
pub fill: Smart<Option<Paint>>,
pub align: Smart<Alignment>,
pub inset: Smart<Sides<Option<Rel<Length>>>>,
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
pub breakable: Smart<bool>,
}
cast! {
TableCell,
v: Content => v.into(),
}
impl Show for Packed<TableCell> {
fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles))
}
}
impl Default for Packed<TableCell> {
fn default() -> Self {
Packed::new(TableCell::new(Content::default()))
}
}
impl From<Content> for TableCell {
fn from(value: Content) -> Self {
#[allow(clippy::unwrap_or_default)]
value.unpack::<Self>().unwrap_or_else(Self::new)
}
}