diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs
index 62146f867..71422a0fc 100644
--- a/crates/typst-html/src/encode.rs
+++ b/crates/typst-html/src/encode.rs
@@ -120,7 +120,10 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
/// Whether the element should be pretty-printed.
fn is_pretty(element: &HtmlElement) -> bool {
- tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
+ matches!(
+ element.tag,
+ tag::meta | tag::table | tag::thead | tag::tbody | tag::tfoot | tag::tr
+ ) || tag::is_block_by_default(element.tag)
}
/// Escape a character.
diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs
index 504159e83..f6df57a37 100644
--- a/crates/typst-library/src/layout/grid/resolve.rs
+++ b/crates/typst-library/src/layout/grid/resolve.rs
@@ -602,7 +602,7 @@ pub enum Entry<'a> {
impl<'a> Entry<'a> {
/// Obtains the cell inside this entry, if this is not a merged cell.
- fn as_cell(&self) -> Option<&Cell<'a>> {
+ pub fn as_cell(&self) -> Option<&Cell<'a>> {
match self {
Self::Cell(cell) => Some(cell),
Self::Merged { .. } => None,
diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs
index fa44cb58a..ba7924422 100644
--- a/crates/typst-library/src/model/table.rs
+++ b/crates/typst-library/src/model/table.rs
@@ -7,7 +7,11 @@ 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::{tag, HtmlAttr, 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,
@@ -258,11 +262,65 @@ impl TableElem {
type TableFooter;
}
+fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
+ let cell = cell.body.clone();
+ let Some(cell) = cell.to_packed::() 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(HtmlAttr::constant("colspan"), colspan);
+ }
+ if let Some(rowspan) = span(cell.rowspan(styles)) {
+ attrs.push(HtmlAttr::constant("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 {
- fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult {
- Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table)
- .pack()
- .spanned(self.span()))
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult {
+ Ok(if TargetElem::target_in(styles).is_html() {
+ // TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
+ // How can we find out whether locator is actually used?
+ 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()))
}
}
diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html
new file mode 100644
index 000000000..6ba1864ef
--- /dev/null
+++ b/tests/ref/html/basic-table.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+ The | first | and |
+
+
+ the | second | row |
+
+
+
+
+ Foo | Baz | Bar |
+
+
+ 1 | 2 |
+
+
+ 3 | 4 |
+
+
+
+
+ The | last | row |
+
+
+
+
+
diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ
new file mode 100644
index 000000000..2a7dfc2ce
--- /dev/null
+++ b/tests/suite/layout/grid/html.typ
@@ -0,0 +1,32 @@
+--- basic-table html ---
+#table(
+ columns: 3,
+ rows: 3,
+
+ table.header(
+ [The],
+ [first],
+ [and],
+ [the],
+ [second],
+ [row],
+ table.hline(stroke: red)
+ ),
+
+ table.cell(x: 1, rowspan: 2)[Baz],
+ [Foo],
+ [Bar],
+
+ [1],
+ // Baz spans into the next cell
+ [2],
+
+ table.cell(colspan: 2)[3],
+ [4],
+
+ table.footer(
+ [The],
+ [last],
+ [row],
+ ),
+)