summaryrefslogtreecommitdiff
path: root/tests/suite/layout/grid
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-04-13 10:39:45 +0200
committerGitHub <noreply@github.com>2024-04-13 08:39:45 +0000
commit020294fca9a7065d4b9cf4e677f606ebaaa29b00 (patch)
treec0027ad22046e2726c22298461327823d6b88d53 /tests/suite/layout/grid
parent72dd79210602ecc799726fc096b078afbb47f299 (diff)
Better test runner (#3922)
Diffstat (limited to 'tests/suite/layout/grid')
-rw-r--r--tests/suite/layout/grid/cell.typ132
-rw-r--r--tests/suite/layout/grid/colspan.typ142
-rw-r--r--tests/suite/layout/grid/footers.typ404
-rw-r--r--tests/suite/layout/grid/grid.typ276
-rw-r--r--tests/suite/layout/grid/headers.typ368
-rw-r--r--tests/suite/layout/grid/positioning.typ203
-rw-r--r--tests/suite/layout/grid/rowspan.typ490
-rw-r--r--tests/suite/layout/grid/rtl.typ195
-rw-r--r--tests/suite/layout/grid/stroke.typ435
-rw-r--r--tests/suite/layout/grid/styling.typ160
10 files changed, 2805 insertions, 0 deletions
diff --git a/tests/suite/layout/grid/cell.typ b/tests/suite/layout/grid/cell.typ
new file mode 100644
index 00000000..3b08c752
--- /dev/null
+++ b/tests/suite/layout/grid/cell.typ
@@ -0,0 +1,132 @@
+// Test basic styling using the grid.cell element.
+
+--- grid-cell-override ---
+// Cell override
+#grid(
+ align: left,
+ fill: red,
+ stroke: blue,
+ inset: 5pt,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ grid.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A],
+ grid.cell(align: horizon)[G2], [A\ A\ A],
+ grid.cell(inset: 0pt)[I], [F],
+ [H], grid.cell(fill: blue)[J]
+)
+
+--- grid-cell-show ---
+// Cell show rule
+#show grid.cell: it => [Zz]
+
+#grid(
+ align: left,
+ fill: red,
+ stroke: blue,
+ inset: 5pt,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ grid.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A]
+)
+
+--- grid-cell-show-and-override ---
+#show grid.cell: it => (it.align, it.fill)
+#grid(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ grid.cell(align: right)[B],
+ grid.cell(fill: aqua)[B],
+)
+
+--- grid-cell-set ---
+// Cell set rules
+#set grid.cell(align: center)
+#show grid.cell: it => (it.align, it.fill, it.inset)
+#set grid.cell(inset: 20pt)
+#grid(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ grid.cell(align: right)[B],
+ grid.cell(fill: aqua)[B],
+)
+
+--- grid-cell-folding ---
+// Test folding per-cell properties (align and inset)
+#grid(
+ columns: (1fr, 1fr),
+ rows: (2.5em, auto),
+ align: right,
+ inset: 5pt,
+ fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)),
+ [Top], grid.cell(align: bottom)[Bot],
+ grid.cell(inset: (bottom: 0pt))[Bot], grid.cell(inset: (bottom: 0pt))[Bot]
+)
+
+--- grid-cell-align-override ---
+// Test overriding outside alignment
+#set align(bottom + right)
+#grid(
+ columns: (1fr, 1fr),
+ rows: 2em,
+ align: auto,
+ fill: green,
+ [BR], [BR],
+ grid.cell(align: left, fill: aqua)[BL], grid.cell(align: top, fill: red.lighten(50%))[TR]
+)
+
+--- grid-cell-various-overrides ---
+#grid(
+ columns: 2,
+ fill: red,
+ align: left,
+ inset: 5pt,
+ [ABC], [ABC],
+ grid.cell(fill: blue)[C], [D],
+ grid.cell(align: center)[E], [F],
+ [G], grid.cell(inset: 0pt)[H]
+)
+
+--- grid-cell-show-emph ---
+#{
+ show grid.cell: emph
+ grid(
+ columns: 2,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Italics]
+ )
+}
+
+--- grid-cell-show-based-on-position ---
+// Style based on position
+#{
+ show grid.cell: it => {
+ if it.y == 0 {
+ strong(it)
+ } else if it.x == 1 {
+ emph(it)
+ } else {
+ it
+ }
+ }
+ grid(
+ columns: 3,
+ gutter: 3pt,
+ [Name], [Age], [Info],
+ [John], [52], [Nice],
+ [Mary], [50], [Cool],
+ [Jake], [49], [Epic]
+ )
+}
+
+--- table-cell-in-grid ---
+// Error: 7-19 cannot use `table.cell` as a grid cell; use `grid.cell` instead
+#grid(table.cell[])
diff --git a/tests/suite/layout/grid/colspan.typ b/tests/suite/layout/grid/colspan.typ
new file mode 100644
index 00000000..707a9456
--- /dev/null
+++ b/tests/suite/layout/grid/colspan.typ
@@ -0,0 +1,142 @@
+--- grid-colspan ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-colspan-gutter ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-colspan-thick-stroke ---
+#set page(width: 300pt)
+#table(
+ columns: (2em, 2em, auto, auto),
+ stroke: 5pt,
+ [A], [B], [C], [D],
+ table.cell(colspan: 4, lorem(20)),
+ [A], table.cell(colspan: 2)[BCBCBCBC], [D]
+)
+
+--- grid-colspan-out-of-bounds ---
+// Error: 3:8-3:32 cell's colspan would cause it to exceed the available column(s)
+// Hint: 3:8-3:32 try placing the cell in another position or reducing its colspan
+#grid(
+ columns: 3,
+ [a], grid.cell(colspan: 3)[b]
+)
+
+--- grid-colspan-overlap ---
+// Error: 4:8-4:32 cell would span a previously placed cell at column 2, row 0
+// Hint: 4:8-4:32 try specifying your cells in a different order or reducing the cell's rowspan or colspan
+#grid(
+ columns: 3,
+ grid.cell(x: 2, y: 0)[x],
+ [a], grid.cell(colspan: 2)[b]
+)
+
+--- grid-colspan-over-all-fr-columns ---
+// Colspan over all fractional columns shouldn't expand auto columns on finite pages
+#table(
+ columns: (1fr, 1fr, auto),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+#table(
+ columns: (1fr, 1fr, auto),
+ table.cell(colspan: 3, lorem(8)),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+
+--- grid-colspan-over-some-fr-columns ---
+// Colspan over only some fractional columns will not trigger the heuristic, and
+// the auto column will expand more than it should. The table looks off, as a result.
+#table(
+ columns: (1fr, 1fr, auto),
+ [], table.cell(colspan: 2, lorem(8)),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+
+--- grid-colspan-over-all-fr-columns-page-width-auto ---
+// On infinite pages, colspan over all fractional columns SHOULD expand auto columns
+#set page(width: auto)
+#table(
+ columns: (1fr, 1fr, auto),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+#table(
+ columns: (1fr, 1fr, auto),
+ table.cell(colspan: 3, lorem(8)),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+
+--- grid-colspan-multiple-regions ---
+// Test multiple regions
+#set page(height: 5em)
+#grid(
+ stroke: red,
+ fill: aqua,
+ columns: 4,
+ [a], [b], [c], [d],
+ [a], grid.cell(colspan: 2)[e, f, g, h, i], [f],
+ [e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
+ grid.cell(colspan: 4)[eeee e e e]
+)
diff --git a/tests/suite/layout/grid/footers.typ b/tests/suite/layout/grid/footers.typ
new file mode 100644
index 00000000..c73bcb39
--- /dev/null
+++ b/tests/suite/layout/grid/footers.typ
@@ -0,0 +1,404 @@
+--- grid-footer ---
+#set page(width: auto, height: 15em)
+#set text(6pt)
+#set table(inset: 2pt, stroke: 0.5pt)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 5).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten(),
+ table.footer(
+ table.hline(start: 2, end: 3, stroke: yellow),
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.cell(colspan: 5)[*Cool Zone*]
+ )
+)
+
+--- grid-footer-gutter-and-no-repeat ---
+// Gutter & no repetition
+#set page(width: auto, height: 16em)
+#set text(6pt)
+#set table(inset: 2pt, stroke: 0.5pt)
+#table(
+ columns: 5,
+ gutter: 2pt,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 5).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten(),
+ table.footer(
+ repeat: false,
+ table.hline(start: 2, end: 3, stroke: yellow),
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.cell(colspan: 5)[*Cool Zone*]
+ )
+)
+
+--- grid-cell-override-in-header-and-footer ---
+#table(
+ table.header(table.cell(stroke: red)[Hello]),
+ table.footer(table.cell(stroke: aqua)[Bye]),
+)
+
+--- grid-cell-override-in-header-and-footer-with-gutter ---
+#table(
+ gutter: 3pt,
+ table.header(table.cell(stroke: red)[Hello]),
+ table.footer(table.cell(stroke: aqua)[Bye]),
+)
+
+--- grid-footer-top-stroke ---
+// Footer's top stroke should win when repeated, but lose at the last page.
+#set page(height: 10em)
+#table(
+ stroke: green,
+ table.header(table.cell(stroke: red)[Hello]),
+ table.cell(stroke: yellow)[Hi],
+ table.cell(stroke: yellow)[Bye],
+ table.cell(stroke: yellow)[Ok],
+ table.footer[Bye],
+)
+
+--- grid-footer-relative-row-sizes ---
+// Relative lengths
+#set page(height: 10em)
+#table(
+ rows: (30%, 30%, auto),
+ [C],
+ [C],
+ table.footer[*A*][*B*],
+)
+
+--- grid-footer-cell-with-y ---
+#grid(
+ grid.footer(grid.cell(y: 2)[b]),
+ grid.cell(y: 0)[a],
+ grid.cell(y: 1)[c],
+)
+
+--- grid-footer-expand ---
+// Ensure footer properly expands
+#grid(
+ columns: 2,
+ [a], [],
+ [b], [],
+ grid.cell(x: 1, y: 3, rowspan: 4)[b],
+ grid.cell(y: 2, rowspan: 2)[a],
+ grid.footer(),
+ grid.cell(y: 4)[d],
+ grid.cell(y: 5)[e],
+ grid.cell(y: 6)[f],
+)
+
+--- grid-footer-not-at-last-row ---
+// Error: 2:3-2:19 footer must end at the last row
+#grid(
+ grid.footer([a]),
+ [b],
+)
+
+--- grid-footer-not-at-last-row-two-columns ---
+// Error: 3:3-3:19 footer must end at the last row
+#grid(
+ columns: 2,
+ grid.footer([a]),
+ [b],
+)
+
+--- grid-footer-overlap ---
+// Error: 4:3-4:19 footer would conflict with a cell placed before it at column 1 row 0
+// Hint: 4:3-4:19 try reducing that cell's rowspan or moving the footer
+#grid(
+ columns: 2,
+ grid.header(),
+ grid.footer([a]),
+ grid.cell(x: 1, y: 0, rowspan: 2)[a],
+)
+
+--- grid-footer-multiple ---
+// Error: 4:3-4:19 cannot have more than one footer
+#grid(
+ [a],
+ grid.footer([a]),
+ grid.footer([b]),
+)
+
+--- table-footer-in-grid ---
+// Error: 3:3-3:20 cannot use `table.footer` as a grid footer; use `grid.footer` instead
+#grid(
+ [a],
+ table.footer([a]),
+)
+
+--- grid-footer-in-table ---
+// Error: 3:3-3:19 cannot use `grid.footer` as a table footer; use `table.footer` instead
+#table(
+ [a],
+ grid.footer([a]),
+)
+
+--- grid-footer-in-grid-header ---
+// Error: 14-28 cannot place a grid footer within another footer or header
+#grid.header(grid.footer[a])
+
+--- table-footer-in-grid-header ---
+// Error: 14-29 cannot place a table footer within another footer or header
+#grid.header(table.footer[a])
+
+--- grid-footer-in-table-header ---
+// Error: 15-29 cannot place a grid footer within another footer or header
+#table.header(grid.footer[a])
+
+--- table-footer-in-table-header ---
+// Error: 15-30 cannot place a table footer within another footer or header
+#table.header(table.footer[a])
+
+--- grid-footer-in-grid-footer ---
+// Error: 14-28 cannot place a grid footer within another footer or header
+#grid.footer(grid.footer[a])
+
+--- table-footer-in-grid-footer ---
+// Error: 14-29 cannot place a table footer within another footer or header
+#grid.footer(table.footer[a])
+
+--- grid-footer-in-table-footer ---
+// Error: 15-29 cannot place a grid footer within another footer or header
+#table.footer(grid.footer[a])
+
+--- table-footer-in-table-footer ---
+// Error: 15-30 cannot place a table footer within another footer or header
+#table.footer(table.footer[a])
+
+--- grid-header-in-grid-footer ---
+// Error: 14-28 cannot place a grid header within another header or footer
+#grid.footer(grid.header[a])
+
+--- table-header-in-grid-footer ---
+// Error: 14-29 cannot place a table header within another header or footer
+#grid.footer(table.header[a])
+
+--- grid-header-in-table-footer ---
+// Error: 15-29 cannot place a grid header within another header or footer
+#table.footer(grid.header[a])
+
+--- table-header-in-table-footer ---
+// Error: 15-30 cannot place a table header within another header or footer
+#table.footer(table.header[a])
+
+--- grid-header-footer-block-with-fixed-height ---
+#set page(height: 17em)
+#table(
+ rows: (auto, 2.5em, auto),
+ table.header[*Hello*][*World*],
+ block(width: 2em, height: 10em, fill: red),
+ table.footer[*Bye*][*World*],
+)
+
+--- grid-header-footer-and-rowspan-non-contiguous-1 ---
+// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
+// ATM.
+#set page(height: 20em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto, 5em, 2em, 2.5em),
+ table.header[*Hello*][*World*],
+ table.cell(rowspan: 3, lorem(20)),
+ table.footer[*Ok*][*Bye*],
+)
+
+--- grid-header-footer-and-rowspan-non-contiguous-2 ---
+// This should look right
+#set page(height: 20em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto),
+ gutter: 3pt,
+ table.header[*Hello*][*World*],
+ table.cell(rowspan: 3, lorem(20)),
+ table.footer[*Ok*][*Bye*],
+)
+--- grid-header-and-footer-lack-of-space ---
+// Test lack of space for header + text.
+#set page(height: 9em + 2.5em + 1.5em)
+
+#table(
+ rows: (auto, 2.5em, auto, auto, 10em, 2.5em, auto),
+ gutter: 3pt,
+ table.header[*Hello*][*World*],
+ table.cell(rowspan: 3, lorem(30)),
+ table.footer[*Ok*][*Bye*],
+)
+
+--- grid-header-and-footer-orphan-prevention ---
+// Orphan header prevention test
+#set page(height: 13em)
+#v(8em)
+#grid(
+ columns: 3,
+ gutter: 5pt,
+ grid.header(
+ [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
+ [*Header*], [*Header* #v(0.1em)],
+ ),
+ ..([Test], [Test], [Test]) * 7,
+ grid.footer(
+ [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
+ [*Footer*], [*Footer* #v(0.1em)],
+ ),
+)
+
+--- grid-header-and-footer-empty ---
+// Empty footer should just be a repeated blank row
+#set page(height: 8em)
+#table(
+ columns: 4,
+ align: center + horizon,
+ table.header(),
+ ..range(0, 2).map(i => (
+ [John \##i],
+ table.cell(stroke: green)[123],
+ table.cell(stroke: blue)[456],
+ [789]
+ )).flatten(),
+ table.footer(),
+)
+
+--- grid-header-and-footer-containing-rowspan ---
+// When a footer has a rowspan with an empty row, it should be displayed
+// properly
+#set page(height: 14em, width: auto)
+
+#let count = counter("g")
+#table(
+ rows: (auto, 2em, auto, auto),
+ table.header(
+ [eeec],
+ table.cell(rowspan: 2, count.step() + count.display()),
+ ),
+ [d],
+ block(width: 5em, fill: yellow, lorem(7)),
+ [d],
+ table.footer(
+ [eeec],
+ table.cell(rowspan: 2, count.step() + count.display()),
+ )
+)
+#count.display()
+
+--- grid-nested-with-footers ---
+// Nested table with footer should repeat both footers
+#set page(height: 10em, width: auto)
+#table(
+ table(
+ [a\ b\ c\ d],
+ table.footer[b],
+ ),
+ table.footer[a],
+)
+
+--- grid-nested-footers ---
+#set page(height: 12em, width: auto)
+#table(
+ [a\ b\ c\ d],
+ table.footer(table(
+ [c],
+ [d],
+ table.footer[b],
+ ))
+)
+
+--- grid-footer-rowspan ---
+// General footer-only tests
+#set page(height: 9em)
+#table(
+ columns: 2,
+ [a], [],
+ [b], [],
+ [c], [],
+ [d], [],
+ [e], [],
+ table.footer(
+ [*Ok*], table.cell(rowspan: 2)[test],
+ [*Thanks*]
+ )
+)
+
+--- grid-footer-bare-1 ---
+#set page(height: 5em)
+#table(
+ table.footer[a][b][c]
+)
+
+--- grid-footer-bare-2 ---
+#table(table.footer[a][b][c])
+
+#table(
+ gutter: 3pt,
+ table.footer[a][b][c]
+)
+
+--- grid-footer-stroke-edge-cases ---
+// Test footer stroke priority edge case
+#set page(height: 10em)
+#table(
+ columns: 2,
+ stroke: black,
+ ..(table.cell(stroke: aqua)[d],) * 8,
+ table.footer(
+ table.cell(rowspan: 2, colspan: 2)[a],
+ [c], [d]
+ )
+)
+
+--- grid-footer-hline-and-vline-1 ---
+// Footer should appear at the bottom. Red line should be above the footer.
+// Green line should be on the left border.
+#set page(margin: 2pt)
+#set text(6pt)
+#table(
+ columns: 2,
+ inset: 1.5pt,
+ table.cell(y: 0)[a],
+ table.cell(x: 1, y: 1)[a],
+ table.cell(y: 2)[a],
+ table.footer(
+ table.hline(stroke: red),
+ table.vline(stroke: green),
+ [b],
+ ),
+ table.cell(x: 1, y: 3)[c]
+)
+
+--- grid-footer-hline-and-vline-2 ---
+// Table should be just one row. [c] appears at the third column.
+#set page(margin: 2pt)
+#set text(6pt)
+#table(
+ columns: 3,
+ inset: 1.5pt,
+ table.cell(y: 0)[a],
+ table.footer(
+ table.hline(stroke: red),
+ table.hline(y: 1, stroke: aqua),
+ table.cell(y: 0)[b],
+ [c]
+ )
+)
+
+--- grid-footer-below-rowspans ---
+// Footer should go below the rowspans.
+#set page(margin: 2pt)
+#set text(6pt)
+#table(
+ columns: 2,
+ inset: 1.5pt,
+ table.cell(rowspan: 2)[a], table.cell(rowspan: 2)[b],
+ table.footer()
+)
diff --git a/tests/suite/layout/grid/grid.typ b/tests/suite/layout/grid/grid.typ
new file mode 100644
index 00000000..f4f0b90a
--- /dev/null
+++ b/tests/suite/layout/grid/grid.typ
@@ -0,0 +1,276 @@
+// Test grid layouts.
+
+--- grid-columns-sizings-rect ---
+#let cell(width, color) = rect(width: width, height: 2cm, fill: color)
+#set page(width: 100pt, height: 140pt)
+#grid(
+ columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%),
+ cell(0.5cm, rgb("2a631a")),
+ cell(100%, forest),
+ cell(100%, conifer),
+ cell(100%, rgb("ff0000")),
+ cell(100%, rgb("00ff00")),
+ cell(80%, rgb("00faf0")),
+ cell(1cm, rgb("00ff00")),
+ cell(0.5cm, rgb("2a631a")),
+ cell(100%, forest),
+ cell(100%, conifer),
+ cell(100%, rgb("ff0000")),
+ cell(100%, rgb("00ff00")),
+)
+
+--- grid-gutter-fr ---
+#set rect(inset: 0pt)
+#grid(
+ columns: (auto, auto, 40%),
+ column-gutter: 1fr,
+ row-gutter: 1fr,
+ rect(fill: eastern)[dddaa aaa aaa],
+ rect(fill: conifer)[ccc],
+ rect(fill: rgb("dddddd"))[aaa],
+)
+
+--- grid-row-sizing-manual-align ---
+#set page(height: 3cm, margin: 0pt)
+#grid(
+ columns: (1fr,),
+ rows: (1fr, auto, 2fr),
+ [],
+ align(center)[A bit more to the top],
+ [],
+)
+
+--- grid-finance ---
+// Test using the `grid` function to create a finance table.
+#set page(width: 11cm, height: 2.5cm)
+#grid(
+ columns: 5,
+ column-gutter: (2fr, 1fr, 1fr),
+ row-gutter: 6pt,
+ [*Quarter*],
+ [Expenditure],
+ [External Revenue],
+ [Financial ROI],
+ [_total_],
+ [*Q1*],
+ [173,472.57 \$],
+ [472,860.91 \$],
+ [51,286.84 \$],
+ [_350,675.18 \$_],
+ [*Q2*],
+ [93,382.12 \$],
+ [439,382.85 \$],
+ [-1,134.30 \$],
+ [_344,866.43 \$_],
+ [*Q3*],
+ [96,421.49 \$],
+ [238,583.54 \$],
+ [3,497.12 \$],
+ [_145,659.17 \$_],
+)
+// Test grid cells that overflow to the next region.
+
+--- grid-cell-breaking ---
+#set page(width: 5cm, height: 3cm)
+#grid(
+ columns: 2,
+ row-gutter: 8pt,
+ [Lorem ipsum dolor sit amet.
+
+ Aenean commodo ligula eget dolor. Aenean massa. Penatibus et magnis.],
+ [Text that is rather short],
+ [Fireflies],
+ [Critical],
+ [Decorum],
+ [Rampage],
+)
+
+--- grid-consecutive-rows-breaking ---
+// Test a column that starts overflowing right after another row/column did
+// that.
+#set page(width: 5cm, height: 2cm)
+#grid(
+ columns: 4 * (1fr,),
+ row-gutter: 10pt,
+ column-gutter: (0pt, 10%),
+ align(top, image("/assets/images/rhino.png")),
+ align(top, rect(inset: 0pt, fill: eastern, align(right)[LoL])),
+ [rofl],
+ [\ A] * 3,
+ [Ha!\ ] * 3,
+)
+
+--- grid-same-row-multiple-columns-breaking ---
+// Test two columns in the same row overflowing by a different amount.
+#set page(width: 5cm, height: 2cm)
+#grid(
+ columns: 3 * (1fr,),
+ row-gutter: 8pt,
+ column-gutter: (0pt, 10%),
+ [A], [B], [C],
+ [Ha!\ ] * 6,
+ [rofl],
+ [\ A] * 3,
+ [hello],
+ [darkness],
+ [my old]
+)
+
+--- grid-nested-breaking ---
+// Test grid within a grid, overflowing.
+#set page(width: 5cm, height: 2.25cm)
+#grid(
+ columns: 4 * (1fr,),
+ row-gutter: 10pt,
+ column-gutter: (0pt, 10%),
+ [A], [B], [C], [D],
+ grid(columns: 2, [A], [B], [C\ ]*3, [D]),
+ align(top, rect(inset: 0pt, fill: eastern, align(right)[LoL])),
+ [rofl],
+ [E\ ]*4,
+)
+
+--- grid-column-sizing-auto-base ---
+// Test that auto and relative columns use the correct base.
+#grid(
+ columns: (auto, 60%),
+ rows: (auto, auto),
+ rect(width: 50%, height: 0.5cm, fill: conifer),
+ rect(width: 100%, height: 0.5cm, fill: eastern),
+ rect(width: 50%, height: 0.5cm, fill: forest),
+)
+
+--- grid-column-sizing-fr-base ---
+// Test that fr columns use the correct base.
+#grid(
+ columns: (1fr,) * 4,
+ rows: (1cm,),
+ rect(width: 50%, fill: conifer),
+ rect(width: 50%, fill: forest),
+ rect(width: 50%, fill: conifer),
+ rect(width: 50%, fill: forest),
+)
+
+--- grid-column-sizing-mixed-base ---
+// Test that all three kinds of rows use the correct bases.
+#set page(height: 4cm, margin: 0cm)
+#grid(
+ rows: (1cm, 1fr, 1fr, auto),
+ rect(height: 50%, width: 100%, fill: conifer),
+ rect(height: 50%, width: 100%, fill: forest),
+ rect(height: 50%, width: 100%, fill: conifer),
+ rect(height: 25%, width: 100%, fill: forest),
+)
+
+--- grid-trailing-linebreak-region-overflow ---
+// Test that trailing linebreak doesn't overflow the region.
+#set page(height: 2cm)
+#grid[
+ Hello \
+ Hello \
+ Hello \
+
+ World
+]
+
+--- grid-breaking-expand-vertically ---
+// Test that broken cell expands vertically.
+#set page(height: 2.25cm)
+#grid(
+ columns: 2,
+ gutter: 10pt,
+ align(bottom)[A],
+ [
+ Top
+ #align(bottom)[
+ Bottom \
+ Bottom \
+ #v(0pt)
+ Top
+ ]
+ ],
+ align(top)[B],
+)
+
+--- grid-complete-rows ---
+// Ensure grids expand enough for the given rows.
+#grid(
+ columns: (2em, 2em),
+ rows: (2em,) * 4,
+ fill: red,
+ stroke: aqua,
+ [a]
+)
+
+--- grid-auto-shrink ---
+// Test iterative auto column shrinking.
+#set page(width: 210mm - 2 * 2.5cm + 2 * 10pt)
+#set text(11pt)
+#table(
+ columns: 4,
+ [Hello!],
+ [Hello there, my friend!],
+ [Hello there, my friends! Hi!],
+ [Hello there, my friends! Hi! What is going on right now?],
+)
+
+--- issue-grid-base-auto-row ---
+// Test that grid base for auto rows makes sense.
+#set page(height: 150pt)
+#table(
+ columns: (1.5cm, auto),
+ rows: (auto, auto),
+ rect(width: 100%, fill: red),
+ rect(width: 100%, fill: blue),
+ rect(width: 100%, height: 50%, fill: green),
+)
+
+--- issue-grid-base-auto-row-list ---
+#rect(width: 100%, height: 1em)
+- #rect(width: 100%, height: 1em)
+ - #rect(width: 100%, height: 1em)
+
+--- issue-grid-skip ---
+// Grid now skips a remaining region when one of the cells
+// doesn't fit into it at all.
+#set page(height: 100pt)
+#grid(
+ columns: (2cm, auto),
+ rows: (auto, auto),
+ rect(width: 100%, fill: red),
+ rect(width: 100%, fill: blue),
+ rect(width: 100%, height: 80%, fill: green),
+ [hello \ darkness #parbreak() my \ old \ friend \ I],
+ rect(width: 100%, height: 20%, fill: blue),
+ polygon(fill: red, (0%, 0%), (100%, 0%), (100%, 20%))
+)
+
+--- issue-grid-skip-list ---
+#set page(height: 60pt)
+#lorem(5)
+- #lorem(5)
+
+--- issue-grid-double-skip ---
+// Ensure that the list does not jump to the third page.
+#set page(height: 70pt)
+#v(40pt)
+The following:
++ A
++ B
+
+--- issue-grid-gutter-skip ---
+// Ensure gutter rows at the top or bottom of a region are skipped.
+#set page(height: 10em)
+
+#table(
+ row-gutter: 1.5em,
+ inset: 0pt,
+ rows: (1fr, auto),
+ [a],
+ [],
+ [],
+ [f],
+ [e\ e],
+ [],
+ [a]
+)
diff --git a/tests/suite/layout/grid/headers.typ b/tests/suite/layout/grid/headers.typ
new file mode 100644
index 00000000..b9a90461
--- /dev/null
+++ b/tests/suite/layout/grid/headers.typ
@@ -0,0 +1,368 @@
+--- grid-headers ---
+#set page(width: auto, height: 12em)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
+
+--- grid-headers-no-repeat ---
+// Disable repetition
+#set page(width: auto, height: 12em)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow),
+ repeat: false
+ ),
+ ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
+
+--- grid-headers-gutter ---
+#set page(width: auto, height: 12em)
+#table(
+ columns: 5,
+ align: center + horizon,
+ gutter: 3pt,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow),
+ ),
+ ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
+
+--- grid-header-relative-row-sizes ---
+// Relative lengths
+#set page(height: 10em)
+#table(
+ rows: (30%, 30%, auto),
+ table.header(
+ [*A*],
+ [*B*]
+ ),
+ [C],
+ [C]
+)
+
+--- grid-header-cell-with-y ---
+#grid(
+ grid.cell(y: 1)[a],
+ grid.header(grid.cell(y: 0)[b]),
+ grid.cell(y: 2)[c]
+)
+
+--- grid-header-last-child ---
+// When the header is the last grid child, it shouldn't include the gutter row
+// after it, because there is none.
+#grid(
+ columns: 2,
+ gutter: 3pt,
+ grid.header(
+ [a], [b],
+ [c], [d]
+ )
+)
+
+--- grid-header-nested ---
+#set page(height: 14em)
+#let t(n) = table(
+ columns: 3,
+ align: center + horizon,
+ gutter: 3pt,
+ table.header(
+ table.cell(colspan: 3)[*Cool Zone #n*],
+ [*Name*], [*Num*], [*Data*]
+ ),
+ ..range(0, 5).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456])).flatten()
+)
+#grid(
+ gutter: 3pt,
+ t(0),
+ t(1)
+)
+
+--- grid-header-hline-and-vline ---
+// Test line positioning in header
+#table(
+ columns: 3,
+ stroke: none,
+ table.hline(stroke: red, end: 2),
+ table.vline(stroke: red, end: 3),
+ table.header(
+ table.hline(stroke: aqua, start: 2),
+ table.vline(stroke: aqua, start: 3), [*A*], table.hline(stroke: orange), table.vline(stroke: orange), [*B*],
+ [*C*], [*D*]
+ ),
+ [a], [b],
+ [c], [d],
+ [e], [f]
+)
+
+--- grid-header-not-at-first-row ---
+// Error: 3:3-3:19 header must start at the first row
+// Hint: 3:3-3:19 remove any rows before the header
+#grid(
+ [a],
+ grid.header([b])
+)
+
+--- grid-header-not-at-first-row-two-columns ---
+// Error: 4:3-4:19 header must start at the first row
+// Hint: 4:3-4:19 remove any rows before the header
+#grid(
+ columns: 2,
+ [a],
+ grid.header([b])
+)
+
+--- grow-header-multiple ---
+// Error: 3:3-3:19 cannot have more than one header
+#grid(
+ grid.header([a]),
+ grid.header([b]),
+ [a],
+)
+
+--- table-header-in-grid ---
+// Error: 2:3-2:20 cannot use `table.header` as a grid header; use `grid.header` instead
+#grid(
+ table.header([a]),
+ [a],
+)
+
+--- grid-header-in-table ---
+// Error: 2:3-2:19 cannot use `grid.header` as a table header; use `table.header` instead
+#table(
+ grid.header([a]),
+ [a],
+)
+
+--- grid-header-in-grid-header ---
+// Error: 14-28 cannot place a grid header within another header or footer
+#grid.header(grid.header[a])
+
+--- table-header-in-grid-header ---
+// Error: 14-29 cannot place a table header within another header or footer
+#grid.header(table.header[a])
+
+--- grid-header-in-table-header ---
+// Error: 15-29 cannot place a grid header within another header or footer
+#table.header(grid.header[a])
+
+--- table-header-in-table-header ---
+// Error: 15-30 cannot place a table header within another header or footer
+#table.header(table.header[a])
+
+--- grid-header-block-with-fixed-height ---
+#set page(height: 15em)
+#table(
+ rows: (auto, 2.5em, auto),
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ block(width: 2em, height: 20em, fill: red)
+)
+
+--- grid-header-and-rowspan-non-contiguous-1 ---
+// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
+// ATM.
+#set page(height: 15em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto, 5em),
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(40))
+)
+
+--- grid-header-and-rowspan-non-contiguous-2 ---
+// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
+// ATM.
+#set page(height: 15em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto, 5em),
+ gutter: 3pt,
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(40))
+)
+
+--- grid-header-and-rowspan-non-contiguous-3 ---
+// This should look right
+#set page(height: 15em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto),
+ gutter: 3pt,
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(40))
+)
+
+--- grid-header-lack-of-space ---
+// Test lack of space for header + text.
+#set page(height: 9em)
+
+#table(
+ rows: (auto, 2.5em, auto, auto, 10em),
+ gutter: 3pt,
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(80))
+)
+
+--- grid-header-orphan-prevention ---
+// Orphan header prevention test
+#set page(height: 12em)
+#v(8em)
+#grid(
+ columns: 3,
+ grid.header(
+ [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
+ [*Header*], [*Header* #v(0.1em)]
+ ),
+ ..([Test], [Test], [Test]) * 20
+)
+
+--- grid-header-empty ---
+// Empty header should just be a repeated blank row
+#set page(height: 12em)
+#table(
+ columns: 4,
+ align: center + horizon,
+ table.header(),
+ ..range(0, 4).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789])).flatten()
+)
+
+--- grid-header-containing-rowspan ---
+// When a header has a rowspan with an empty row, it should be displayed
+// properly
+#set page(height: 10em)
+
+#let count = counter("g")
+#table(
+ rows: (auto, 2em, auto, auto),
+ table.header(
+ [eeec],
+ table.cell(rowspan: 2, count.step() + count.display()),
+ ),
+ [d],
+ block(width: 5em, fill: yellow, lorem(15)),
+ [d]
+)
+#count.display()
+
+--- grid-header-expand ---
+// Ensure header expands to fit cell placed in it after its declaration
+#set page(height: 10em)
+#table(
+ columns: 2,
+ table.header(
+ [a], [b],
+ [c],
+ ),
+ table.cell(x: 1, y: 1, rowspan: 2, lorem(80))
+)
+
+--- grid-nested-with-headers ---
+// Nested table with header should repeat both headers
+#set page(height: 10em)
+#table(
+ table.header(
+ [a]
+ ),
+ table(
+ table.header(
+ [b]
+ ),
+ [a\ b\ c\ d]
+ )
+)
+
+--- grid-nested-headers ---
+#set page(height: 12em)
+#table(
+ table.header(
+ table(
+ table.header(
+ [b]
+ ),
+ [c],
+ [d]
+ )
+ ),
+ [a\ b]
+)
+
+--- grid-header-stroke-edge-cases ---
+// Test header stroke priority edge case (last header row removed)
+#set page(height: 8em)
+#table(
+ columns: 2,
+ stroke: black,
+ gutter: (auto, 3pt),
+ table.header(
+ [c], [d],
+ ),
+ ..(table.cell(stroke: aqua)[d],) * 8,
+)
+
+--- grid-header-hline-bottom ---
+// Yellow line should be kept here
+#set text(6pt)
+#table(
+ column-gutter: 3pt,
+ inset: 1pt,
+ table.header(
+ [a],
+ table.hline(stroke: yellow),
+ ),
+ table.cell(rowspan: 2)[b]
+)
+
+--- grid-header-hline-bottom-manually ---
+// Red line should be kept here
+#set page(height: 6em)
+#set text(6pt)
+#table(
+ column-gutter: 3pt,
+ inset: 1pt,
+ table.header(
+ table.hline(stroke: red, position: bottom),
+ [a],
+ ),
+ [a],
+ table.cell(stroke: aqua)[b]
+)
+
+--- grid-header-rowspan-base ---
+#set page(height: 7em)
+#set text(6pt)
+#let full-block = block(width: 2em, height: 100%, fill: red)
+#table(
+ columns: 3,
+ inset: 1.5pt,
+ table.header(
+ [a], full-block, table.cell(rowspan: 2, full-block),
+ [b]
+ )
+)
diff --git a/tests/suite/layout/grid/positioning.typ b/tests/suite/layout/grid/positioning.typ
new file mode 100644
index 00000000..31896d99
--- /dev/null
+++ b/tests/suite/layout/grid/positioning.typ
@@ -0,0 +1,203 @@
+// Test cell positioning in grids.
+
+--- grid-cell-show-x-y ---
+#{
+ show grid.cell: it => (it.x, it.y)
+ grid(
+ columns: 2,
+ inset: 5pt,
+ fill: aqua,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Home]
+ )
+}
+#{
+ show table.cell: it => pad(rest: it.inset)[#(it.x, it.y)]
+ table(
+ columns: 2,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Home]
+ )
+}
+
+--- grid-cell-position-out-of-order ---
+// Positioning cells in a different order than they appear
+#grid(
+ columns: 2,
+ [A], [B],
+ grid.cell(x: 1, y: 2)[C], grid.cell(x: 0, y: 2)[D],
+ grid.cell(x: 1, y: 1)[E], grid.cell(x: 0, y: 1)[F],
+)
+
+--- grid-cell-position-extra-rows ---
+// Creating more rows by positioning out of bounds
+#grid(
+ columns: 3,
+ rows: 1.5em,
+ inset: 5pt,
+ fill: (x, y) => if (x, y) == (0, 0) { blue } else if (x, y) == (2, 3) { red } else { green },
+ [A],
+ grid.cell(x: 2, y: 3)[B]
+)
+
+#table(
+ columns: (3em, 1em, 3em),
+ rows: 1.5em,
+ inset: (top: 0pt, bottom: 0pt, rest: 5pt),
+ fill: (x, y) => if (x, y) == (0, 0) { blue } else if (x, y) == (2, 3) { red } else { green },
+ align: (x, y) => (left, center, right).at(x),
+ [A],
+ table.cell(x: 2, y: 3)[B]
+)
+
+--- grid-cell-position-collide ---
+// Error: 3:3-3:42 attempted to place a second cell at column 0, row 0
+// Hint: 3:3-3:42 try specifying your cells in a different order
+#grid(
+ [A],
+ grid.cell(x: 0, y: 0)[This shall error]
+)
+
+--- table-cell-position-collide ---
+// Error: 3:3-3:43 attempted to place a second cell at column 0, row 0
+// Hint: 3:3-3:43 try specifying your cells in a different order
+#table(
+ [A],
+ table.cell(x: 0, y: 0)[This shall error]
+)
+
+--- grid-cell-position-automatic-skip-manual ---
+// Automatic position cell skips custom position cell
+#grid(
+ grid.cell(x: 0, y: 0)[This shall not error],
+ [A]
+)
+
+--- grid-cell-position-x-out-of-bounds ---
+// Error: 4:3-4:36 cell could not be placed at invalid column 2
+#grid(
+ columns: 2,
+ [A],
+ grid.cell(x: 2)[This shall error]
+)
+
+--- grid-cell-position-partial ---
+// Partial positioning
+#grid(
+ columns: 3,
+ rows: 1.5em,
+ inset: 5pt,
+ fill: aqua,
+ [A], grid.cell(y: 1, fill: green)[B], [C], grid.cell(x: auto, y: 1, fill: green)[D], [E],
+ grid.cell(y: 2, fill: green)[F], grid.cell(x: 0, fill: orange)[G], grid.cell(x: 0, y: auto, fill: orange)[H],
+ grid.cell(x: 1, fill: orange)[I]
+)
+
+#table(
+ columns: 3,
+ rows: 1.5em,
+ inset: 5pt,
+ fill: aqua,
+ [A], table.cell(y: 1, fill: green)[B], [C], table.cell(x: auto, y: 1, fill: green)[D], [E],
+ table.cell(y: 2, fill: green)[F], table.cell(x: 0, fill: orange)[G], table.cell(x: 0, y: auto, fill: orange)[H],
+ table.cell(x: 1, fill: orange)[I]
+)
+
+--- grid-cell-position-partial-collide ---
+// Error: 4:3-4:21 cell could not be placed in row 0 because it was full
+// Hint: 4:3-4:21 try specifying your cells in a different order
+#grid(
+ columns: 2,
+ [A], [B],
+ grid.cell(y: 0)[C]
+)
+
+--- table-cell-position-partial-collide ---
+// Error: 4:3-4:22 cell could not be placed in row 0 because it was full
+// Hint: 4:3-4:22 try specifying your cells in a different order
+#table(
+ columns: 2,
+ [A], [B],
+ table.cell(y: 0)[C]
+)
+
+--- grid-calendar ---
+#set page(width: auto)
+#show grid.cell: it => {
+ if it.y == 0 {
+ set text(white)
+ strong(it)
+ } else {
+ // For the second row and beyond, we will write the day number for each
+ // cell.
+
+ // In general, a cell's index is given by cell.x + columns * cell.y.
+ // Days start in the second grid row, so we subtract 1 row.
+ // But the first day is day 1, not day 0, so we add 1.
+ let day = it.x + 7 * (it.y - 1) + 1
+ if day <= 31 {
+ // Place the day's number at the top left of the cell.
+ // Only if the day is valid for this month (not 32 or higher).
+ place(top + left, dx: 2pt, dy: 2pt, text(8pt, red.darken(40%))[#day])
+ }
+ it
+ }
+}
+
+#grid(
+ fill: (x, y) => if y == 0 { gray.darken(50%) },
+ columns: (30pt,) * 7,
+ rows: (auto, 30pt),
+ // Events will be written at the bottom of each day square.
+ align: bottom,
+ inset: 5pt,
+ stroke: (thickness: 0.5pt, dash: "densely-dotted"),
+
+ [Sun], [Mon], [Tue], [Wed], [Thu], [Fri], [Sat],
+
+ // This event will occur on the first Friday (sixth column).
+ grid.cell(x: 5, fill: yellow.darken(10%))[Call],
+
+ // This event will occur every Monday (second column).
+ // We have to repeat it 5 times so it occurs every week.
+ ..(grid.cell(x: 1, fill: red.lighten(50%))[Meet],) * 5,
+
+ // This event will occur at day 19.
+ grid.cell(x: 4, y: 3, fill: orange.lighten(25%))[Talk],
+
+ // These events will occur at the second week, where available.
+ grid.cell(y: 2, fill: aqua)[Chat],
+ grid.cell(y: 2, fill: aqua)[Walk],
+)
+
+--- grid-exam ---
+#set page(width: auto)
+#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(rest: it.inset)[_N/A_]
+ } else {
+ it
+ }
+}
+
+#table(
+ fill: (x, y) => if x == 0 or y == 0 { gray.darken(50%) },
+ columns: 4,
+ [], [Exam 1], [Exam 2], [Exam 3],
+ ..([John], [Mary], [Jake], [Robert]).map(table.cell.with(x: 0)),
+
+ // Mary got grade A on Exam 3.
+ table.cell(x: 3, y: 2, fill: green)[A],
+
+ // Everyone got grade A on Exam 2.
+ ..(table.cell(x: 2, fill: green)[A],) * 4,
+
+ // Robert got grade B on other exams.
+ ..(table.cell(y: 4, fill: aqua)[B],) * 2,
+)
diff --git a/tests/suite/layout/grid/rowspan.typ b/tests/suite/layout/grid/rowspan.typ
new file mode 100644
index 00000000..f7a377b6
--- /dev/null
+++ b/tests/suite/layout/grid/rowspan.typ
@@ -0,0 +1,490 @@
+--- grid-rowspan ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ table.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+--- grid-rowspan-gutter ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ grid.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+--- grid-rowspan-fixed-size ---
+// Fixed-size rows
+#set page(height: 10em)
+#grid(
+ columns: 2,
+ rows: 1.5em,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ grid.cell(rowspan: 3)[R1], [b],
+ [c],
+ [d],
+ [e], [f],
+ grid.cell(rowspan: 5)[R2], [h],
+ [i],
+ [j],
+ [k],
+ [l],
+ [m], [n]
+)
+
+--- grid-rowspan-cell-coordinates ---
+// Cell coordinate tests
+#set page(height: 10em)
+#show table.cell: it => [(#it.x, #it.y)]
+#table(
+ columns: 3,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c],
+ table.cell(colspan: 2)[d],
+ table.cell(colspan: 3, rowspan: 10)[a],
+ table.cell(colspan: 2)[b],
+)
+#table(
+ columns: 3,
+ gutter: 3pt,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c],
+ table.cell(colspan: 2)[d],
+ table.cell(colspan: 3, rowspan: 9)[a],
+ table.cell(colspan: 2)[b],
+)
+
+--- grid-rowspan-over-auto-row ---
+// Auto row expansion
+#set page(height: 10em)
+#grid(
+ columns: (1em, 1em),
+ rows: (0.5em, 0.5em, auto),
+ fill: orange,
+ gutter: 3pt,
+ grid.cell(rowspan: 4, [x x x x] + place(bottom)[*Bot*]),
+ [a],
+ [b],
+ [c],
+ [d]
+)
+
+--- grid-rowspan-excessive ---
+// Excessive rowspan (no gutter)
+#set page(height: 10em)
+#table(
+ columns: 4,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c], [d],
+ table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f],
+ table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2],
+ [b],
+)
+
+--- grid-rowspan-excessive-gutter ---
+// Excessive rowspan (with gutter)
+#set page(height: 10em)
+#table(
+ columns: 4,
+ gutter: 3pt,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c], [d],
+ table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f],
+ table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2],
+ [b],
+)
+
+--- grid-rowspan-over-fr-row-at-end ---
+// Fractional rows
+// They cause the auto row to expand more than needed.
+#set page(height: 10em)
+#grid(
+ fill: red,
+ gutter: 3pt,
+ columns: 3,
+ rows: (1em, auto, 1fr),
+ [a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)),
+ [c], [d],
+ [e], [f]
+)
+
+--- grid-rowspan-over-fr-row-at-start ---
+// Fractional rows
+#set page(height: 10em)
+#grid(
+ fill: red,
+ gutter: 3pt,
+ columns: 3,
+ rows: (1fr, auto, 1em),
+ [a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)),
+ [c], [d],
+ [e], [f]
+)
+
+--- grid-rowspan-cell-order ---
+// Cell order
+#let count = counter("count")
+#show grid.cell: it => {
+ count.step()
+ count.display()
+}
+
+#grid(
+ columns: (2em,) * 3,
+ stroke: aqua,
+ rows: 1.2em,
+ fill: (x, y) => if calc.odd(x + y) { red } else { orange },
+ [a], grid.cell(rowspan: 2)[b], grid.cell(rowspan: 2)[c],
+ [d],
+ grid.cell(rowspan: 2)[f], [g], [h],
+ [i], [j],
+ [k], [l], [m],
+ grid.cell(rowspan: 2)[n], [o], [p],
+ [q], [r],
+ [s], [t], [u]
+)
+
+--- grid-rowspan-unbreakable-1 ---
+#table(
+ columns: 3,
+ rows: (auto, auto, auto, 2em),
+ gutter: 3pt,
+ table.cell(rowspan: 4)[a \ b\ c\ d\ e], [c], [d],
+ [e], table.cell(breakable: false, rowspan: 2)[f],
+ [g]
+)
+
+--- grid-rowspan-unbreakable-2 ---
+// Test cell breakability
+#show grid.cell: it => {
+ assert.eq(it.breakable, (it.x, it.y) != (0, 6) and (it.y in (2, 5, 6) or (it.x, it.y) in ((0, 1), (2, 3), (1, 7))))
+ it.breakable
+}
+#grid(
+ columns: 3,
+ rows: (6pt, 1fr, auto, 1%, 1em, auto, auto, 0.2in),
+ row-gutter: (0pt, 0pt, 0pt, auto),
+ [a], [b], [c],
+ grid.cell(rowspan: 3)[d], [e], [f],
+ [g], [h],
+ [i], grid.cell(rowspan: 2)[j],
+ [k],
+ grid.cell(y: 5)[l],
+ grid.cell(y: 6, breakable: false)[m], grid.cell(y: 6, breakable: true)[n],
+ grid.cell(y: 7, breakable: false)[o], grid.cell(y: 7, breakable: true)[p], grid.cell(y: 7, breakable: auto)[q]
+)
+
+--- grid-rowspan-in-all-columns-stroke ---
+#table(
+ columns: 2,
+ table.cell(stroke: (bottom: red))[a], [b],
+ table.hline(stroke: green),
+ table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e],
+ [f],
+ [g]
+)
+
+--- grid-rowspan-in-all-columns-stroke-gutter ---
+#table(
+ columns: 2,
+ gutter: 3pt,
+ table.cell(stroke: (bottom: red))[a], [b],
+ table.hline(stroke: green),
+ table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e],
+ [f],
+ [g]
+)
+
+--- grid-rowspan-block-full-height ---
+// Block below shouldn't expand to the end of the page, but stay within its
+// rows' boundaries.
+#set page(height: 9em)
+#table(
+ rows: (1em, 1em, 1fr, 1fr, auto),
+ table.cell(rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+ table.cell(rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+ [a]
+)
+
+--- grid-rowspan-block-overflow ---
+#set page(height: 7em)
+#table(
+ columns: 3,
+ [], [], table.cell(breakable: true, rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+ table.cell(breakable: false, block(width: 2em, height: 100%, fill: red)),
+ table.cell(breakable: false, rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+)
+
+// Rowspan split tests
+
+--- grid-rowspan-split-1 ---
+#set page(height: 10em)
+#table(
+ columns: 2,
+ rows: (auto, auto, 3em),
+ fill: red,
+ [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
+ [e],
+ [f]
+)
+
+--- grid-rowspan-split-2 ---
+#set page(height: 10em)
+#table(
+ columns: 2,
+ rows: (auto, auto, 3em),
+ row-gutter: 1em,
+ fill: red,
+ [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
+ [e],
+ [f]
+)
+
+--- grid-rowspan-split-3 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-4 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ gutter: 2pt,
+ table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-5 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-6 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ gutter: 2pt,
+ table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-7 ---
+#set page(height: 5em)
+#grid(
+ columns: 2,
+ stroke: red,
+ inset: 5pt,
+ grid.cell(rowspan: 5)[a\ b\ c\ d\ e]
+)
+
+--- grid-rowspan-split-8 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ gutter: 3pt,
+ stroke: red,
+ inset: 5pt,
+ table.cell(rowspan: 5)[a\ b\ c\ d\ e]
+)
+
+// Rowspan split without ending at the auto row
+
+--- grid-rowspan-split-9 ---
+#set page(height: 6em)
+#table(
+ rows: (4em,) * 7 + (auto,) + (4em,) * 7,
+ columns: 2,
+ column-gutter: 1em,
+ row-gutter: (1em, 2em) * 4,
+ fill: (x, y) => if calc.odd(x + y) { orange.lighten(20%) } else { red },
+ table.cell(rowspan: 15, [a \ ] * 15),
+ [] * 15
+)
+
+--- grid-rowspan-split-10 ---
+#set page(height: 6em)
+#table(
+ rows: (4em,) * 7 + (auto,) + (4em,) * 7,
+ columns: 2,
+ column-gutter: 1em,
+ row-gutter: (1em, 2em) * 4,
+ fill: (x, y) => if calc.odd(x + y) { green } else { green.darken(40%) },
+ table.cell(rowspan: 15, block(fill: blue, width: 2em, height: 4em * 14 + 3em)),
+ [] * 15
+)
+
+--- grid-rowspan-split-11 ---
+#set page(height: 6em)
+#table(
+ rows: (3em,) * 15,
+ columns: 2,
+ column-gutter: 1em,
+ row-gutter: (1em, 2em) * 4,
+ fill: (x, y) => if calc.odd(x + y) { aqua } else { blue },
+ table.cell(breakable: true, rowspan: 15, [a \ ] * 15),
+ [] * 15
+)
+
+// Some splitting corner cases
+
+--- grid-rowspan-split-12 ---
+// Inside the larger rowspan's range, there's an unbreakable rowspan and a
+// breakable rowspan. This should work normally.
+// The auto row will also expand ignoring the last fractional row.
+#set page(height: 10em)
+#table(
+ gutter: 0.5em,
+ columns: 2,
+ rows: (2em,) * 10 + (auto, auto, 2em, 1fr),
+ fill: (_, y) => if calc.even(y) { aqua } else { blue },
+ table.cell(rowspan: 14, block(width: 2em, height: 2em * 10 + 2em + 5em, fill: red)[]),
+ ..([a],) * 5,
+ table.cell(rowspan: 3)[a\ b],
+ table.cell(rowspan: 5, [a\ b\ c\ d\ e\ f\ g\ h]),
+ [z]
+)
+
+--- grid-rowspan-split-13 ---
+// Inset moving to next region bug
+#set page(width: 10cm, height: 2.5cm, margin: 0.5cm)
+#set text(size: 11pt)
+#table(
+ columns: (1fr, 1fr, 1fr),
+ [A],
+ [B],
+ [C],
+ [D],
+ table.cell(rowspan: 2, lorem(4)),
+ [E],
+ [F],
+ [G],
+)
+
+--- grid-rowspan-split-14 ---
+// Second lorem must be sent to the next page, too big
+#set page(width: 10cm, height: 9cm, margin: 1cm)
+#set text(size: 11pt)
+#table(
+ columns: (1fr, 1fr, 1fr),
+ align: center,
+ rows: (4cm, auto),
+ [A], [B], [C],
+ table.cell(rowspan: 4, breakable: false, lorem(10)),
+ [D],
+ table.cell(rowspan: 2, breakable: false, lorem(20)),
+ [E],
+)
+
+--- grid-rowspan-split-15 ---
+// Auto row must expand properly in both cases
+#set text(10pt)
+#show table.cell: it => if it.x == 0 { it } else { layout(size => size.height) }
+#table(
+ columns: 2,
+ rows: (1em, auto, 2em, 3em, 4em),
+ gutter: 3pt,
+ table.cell(rowspan: 5, block(fill: orange, height: 15em)[a]),
+ [b],
+ [c],
+ [d],
+ [e],
+ [f]
+)
+
+#table(
+ columns: 2,
+ rows: (1em, auto, 2em, 3em, 4em),
+ gutter: 3pt,
+ table.cell(rowspan: 5, breakable: false, block(fill: orange, height: 15em)[a]),
+ [b],
+ [c],
+ [d],
+ [e],
+ [f]
+)
+
+--- grid-rowspan-split-16 ---
+// Expanding on unbreakable auto row
+#set page(height: 7em, margin: (bottom: 2em))
+#grid(
+ columns: 2,
+ rows: (1em, 1em, auto, 1em, 1em, 1em),
+ fill: (x, y) => if x == 0 { aqua } else { blue },
+ stroke: black,
+ gutter: 2pt,
+ grid.cell(rowspan: 5, block(height: 10em)[a]),
+ [a],
+ [b],
+ grid.cell(breakable: false, v(3em) + [c]),
+ [d],
+ [e],
+ [f], [g]
+)
+
+--- grid-rowspan-split-17 ---
+#show table.cell.where(x: 0): strong
+#show table.cell.where(y: 0): strong
+#set page(height: 13em)
+#let lets-repeat(thing, n) = ((thing + colbreak(),) * (calc.max(0, n - 1)) + (thing,)).join()
+#table(
+ columns: 4,
+ fill: (x, y) => if x == 0 or y == 0 { gray },
+ [], [Test 1], [Test 2], [Test 3],
+ table.cell(rowspan: 15, align: horizon, lets-repeat((rotate(-90deg, reflow: true)[*All Tests*]), 3)),
+ ..([123], [456], [789]) * 15
+)
diff --git a/tests/suite/layout/grid/rtl.typ b/tests/suite/layout/grid/rtl.typ
new file mode 100644
index 00000000..7c0e999a
--- /dev/null
+++ b/tests/suite/layout/grid/rtl.typ
@@ -0,0 +1,195 @@
+// Test RTL grid.
+
+--- list-rtl ---
+#set text(dir: rtl)
+- מימין לשמאל
+
+--- grid-rtl ---
+#set text(dir: rtl)
+#table(columns: 2)[A][B][C][D]
+
+--- grid-rtl-colspan ---
+// Test interaction between RTL and colspans
+#set text(dir: rtl)
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-rtl-colspan-stroke ---
+#set text(dir: rtl)
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-rtl-multiple-regions ---
+// Test multiple regions
+#set page(height: 5em)
+#set text(dir: rtl)
+#grid(
+ stroke: red,
+ fill: aqua,
+ columns: 4,
+ [a], [b], [c], [d],
+ [a], grid.cell(colspan: 2)[e, f, g, h, i], [f],
+ [e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
+ grid.cell(colspan: 4)[eeee e e e]
+)
+
+--- grid-rtl-vline-position ---
+// Test left and right for vlines in RTL
+#set text(dir: rtl)
+#grid(
+ columns: 3,
+ inset: 5pt,
+ grid.vline(stroke: red, position: left), grid.vline(stroke: green, position: right), [a],
+ grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [b],
+ grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [c],
+ grid.vline(stroke: aqua, position: right)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
+ grid.vline(stroke: 2pt, position: right)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
+ grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [b],
+ grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [c],
+ grid.vline(stroke: 2pt, position: start)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
+ grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [b],
+ grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [c],
+ grid.vline(stroke: 2pt, position: start)
+)
+
+--- grid-rtl-vline-out-of-bounds ---
+// Error: 3:8-3:34 cannot place vertical line at the 'end' position of the end border (x = 1)
+// Hint: 3:8-3:34 set the line's position to 'start' or place it at a smaller 'x' index
+#set text(dir: rtl)
+#grid(
+ [a], grid.vline(position: left)
+)
+
+--- grid-rtl-complex ---
+#set text(dir: rtl)
+
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+--- grid-rtl-rowspan ---
+#set page(height: 10em)
+#set text(dir: rtl)
+#table(
+ columns: 2,
+ rows: (auto, auto, 3em),
+ row-gutter: 1em,
+ fill: red,
+ [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
+ [e],
+ [f]
+)
+
+--- grid-rtl-header ---
+// Headers
+#set page(height: 15em)
+#set text(dir: rtl)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*N1*], table.cell(stroke: aqua)[*N2*], [*D1*], [*D2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 10).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
diff --git a/tests/suite/layout/grid/stroke.typ b/tests/suite/layout/grid/stroke.typ
new file mode 100644
index 00000000..9c1c3482
--- /dev/null
+++ b/tests/suite/layout/grid/stroke.typ
@@ -0,0 +1,435 @@
+--- grid-stroke-pattern ---
+#let double-line = pattern(size: (1.5pt, 1.5pt), {
+ place(line(stroke: .6pt, start: (0%, 50%), end: (100%, 50%)))
+})
+
+#table(
+ stroke: (_, y) => if y != 1 { (bottom: black) },
+ columns: 3,
+ table.cell(colspan: 3, align: center)[*Epic Table*],
+ align(center)[*Name*], align(center)[*Age*], align(center)[*Data*],
+ table.hline(stroke: (paint: double-line, thickness: 2pt)),
+ [John], [30], [None],
+ [Martha], [20], [A],
+ [Joseph], [35], [D]
+)
+
+--- grid-stroke-folding ---
+// Test folding
+#set grid(stroke: red)
+#set grid(stroke: 5pt)
+
+#grid(
+ inset: 10pt,
+ columns: 2,
+ stroke: stroke(dash: "loosely-dotted"),
+ grid.vline(start: 2, end: 3, stroke: (paint: green, dash: none)),
+ [a], [b],
+ grid.hline(end: 1, stroke: blue),
+ [c], [d],
+ [e], grid.cell(stroke: aqua)[f]
+)
+
+--- grid-stroke-set-on-cell-and-line ---
+// Test set rules on cells and folding
+#set table.cell(stroke: 4pt)
+#set table.cell(stroke: blue)
+#set table.hline(stroke: red)
+#set table.hline(stroke: 0.75pt)
+#set table.vline(stroke: 0.75pt)
+#set table.vline(stroke: aqua)
+
+#table(
+ columns: 3,
+ gutter: 3pt,
+ inset: 5pt,
+ [a], [b], table.vline(position: end), [c],
+ [d], [e], [f],
+ table.hline(position: bottom),
+ [g], [h], [i],
+)
+
+--- grid-stroke-field-in-show ---
+// Test stroke field on cell show rules
+#set grid.cell(stroke: (x: 4pt))
+#set grid.cell(stroke: (x: blue))
+#show grid.cell: it => {
+ test(it.stroke, (left: stroke(paint: blue, thickness: 4pt, dash: "loosely-dotted"), right: blue + 4pt, top: stroke(thickness: 1pt), bottom: none))
+ it
+}
+#grid(
+ stroke: (left: (dash: "loosely-dotted")),
+ inset: 5pt,
+ grid.hline(stroke: red),
+ grid.cell(stroke: (top: 1pt))[a], grid.vline(stroke: yellow),
+)
+
+--- grid-stroke-complex ---
+#table(
+ columns: 3,
+ [a], table.cell(colspan: 2)[b c],
+ table.cell(stroke: blue)[d], [e], [f],
+ [g], [h], table.cell(stroke: (left: yellow, top: green, right: aqua, bottom: red))[i],
+ [j], [k], [l],
+ table.cell(stroke: 3pt)[m], [n], table.cell(stroke: (dash: "loosely-dotted"))[o],
+)
+
+--- grid-stroke-array ---
+// Test per-column stroke array
+#let t = table(
+ columns: 3,
+ stroke: (red, blue, green),
+ [a], [b], [c],
+ [d], [e], [f],
+ [h], [i], [j],
+)
+#t
+#set text(dir: rtl)
+#t
+
+--- grid-stroke-func ---
+#grid(
+ columns: 3,
+ inset: 3pt,
+ stroke: (x, _) => (right: (5pt, (dash: "dotted")).at(calc.rem(x, 2)), bottom: (dash: "densely-dotted")),
+ grid.vline(x: 0, stroke: red),
+ grid.vline(x: 1, stroke: red),
+ grid.vline(x: 2, stroke: red),
+ grid.vline(x: 3, stroke: red),
+ grid.hline(y: 0, end: 1, stroke: blue),
+ grid.hline(y: 1, end: 1, stroke: blue),
+ grid.cell[a],
+ [b], [c]
+)
+
+--- grid-stroke-manually-positioned-lines ---
+#set page(height: 5em)
+#table(
+ columns: 3,
+ inset: 3pt,
+ table.hline(y: 0, end: none, stroke: 3pt + blue),
+ table.vline(x: 0, end: none, stroke: 3pt + green),
+ table.hline(y: 5, end: none, stroke: 3pt + red),
+ table.vline(x: 3, end: none, stroke: 3pt + yellow),
+ [a], [b], [c],
+ [a], [b], [c],
+ [a], [b], [c],
+ [a], [b], [c],
+ [a], [b], [c],
+)
+
+--- grid-stroke-automatically-positioned-lines ---
+// Automatically positioned lines
+// Plus stroke thickness ordering
+#table(
+ columns: 3,
+ table.hline(stroke: red + 5pt),
+ table.vline(stroke: blue + 5pt),
+ table.vline(stroke: 2pt),
+ [a],
+ table.vline(x: 1, stroke: aqua + 5pt),
+ [b],
+ table.vline(stroke: aqua + 5pt),
+ [c],
+ table.vline(stroke: yellow + 5.2pt),
+ table.hline(stroke: green + 5pt),
+ [a], [b], [c],
+ [a], table.hline(stroke: green + 2pt), table.vline(stroke: 2pt), [b], [c],
+)
+
+--- grid-stroke-priority-line ---
+// Line specification order priority
+// The last line should be blue, not red.
+// The middle aqua line should be gone due to the 'none' override.
+#grid(
+ columns: 2,
+ inset: 2pt,
+ grid.hline(y: 2, stroke: red + 5pt),
+ grid.vline(),
+ [a], [b],
+ grid.hline(stroke: red),
+ grid.hline(stroke: none),
+ [c], grid.cell(stroke: (top: aqua))[d],
+ grid.hline(stroke: blue),
+)
+
+--- grid-stroke-hline-position-bottom-gutter ---
+// Position: bottom and position: end with gutter should have a visible effect
+// of moving the lines after the next track.
+#table(
+ columns: 3,
+ gutter: 3pt,
+ stroke: blue,
+ table.hline(end: 2, stroke: red),
+ table.hline(end: 2, stroke: aqua, position: bottom),
+ table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), table.vline(end: 2, position: end, stroke: orange), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
+ [d], [e], [f],
+ table.hline(end: 2, stroke: red),
+ [g], [h], [ie],
+ table.hline(end: 2, stroke: green),
+)
+
+--- grid-stroke-hline-position-bottom ---
+// Using position: bottom and position: end without gutter should be the same
+// as placing a line after the next track.
+#table(
+ columns: 3,
+ stroke: blue,
+ table.hline(end: 2, stroke: red),
+ table.hline(end: 2, stroke: aqua, position: bottom),
+ table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
+ table.hline(end: 2, stroke: 5pt),
+ [d], [e], [f],
+ table.hline(end: 2, stroke: red),
+ [g], [h], [i],
+ table.hline(end: 2, stroke: red),
+)
+
+--- grid-stroke-vline-position-left-and-right ---
+// Test left and right for grid vlines.
+#grid(
+ columns: 3,
+ inset: 5pt,
+ grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
+ grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [b],
+ grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [c],
+ grid.vline(stroke: 2pt, position: left)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
+ grid.vline(stroke: 2pt, position: left)
+)
+
+--- table-stroke-vline-position-left-and-right ---
+// Test left and right for table vlines.
+#table(
+ columns: 3,
+ inset: 5pt,
+ table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
+ table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [b],
+ table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [c],
+ table.vline(stroke: 2pt, position: left)
+)
+
+#table(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
+ table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [b],
+ table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [c],
+ table.vline(stroke: 2pt, position: left)
+)
+
+--- grid-stroke-priority-line-cell ---
+// Hlines and vlines should always appear on top of cell strokes.
+#table(
+ columns: 3,
+ stroke: aqua,
+ table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
+ table.cell(stroke: blue)[d], [e], [f],
+ table.hline(stroke: red),
+ [g], table.cell(stroke: blue)[h], [i],
+)
+
+#table(
+ columns: 3,
+ gutter: 3pt,
+ stroke: aqua,
+ table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
+ table.cell(stroke: blue)[d], [e], [f],
+ table.hline(stroke: red),
+ [g], table.cell(stroke: blue)[h], [i],
+)
+
+--- grid-stroke-priority-cell ---
+// Ensure cell stroke overrides always appear on top.
+#table(
+ columns: 2,
+ stroke: black,
+ table.cell(stroke: red)[a], [b],
+ [c], [d],
+)
+
+#table(
+ columns: 2,
+ table.cell(stroke: red)[a], [b],
+ [c], [d],
+)
+
+--- grid-stroke-hline-position-bad ---
+// Error: 7:3-7:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
+// Hint: 7:3-7:32 set the line's position to 'top' or place it at a smaller 'y' index
+#table(
+ columns: 2,
+ [a], [b],
+ [c], [d],
+ table.hline(stroke: aqua),
+ table.hline(position: top),
+ table.hline(position: bottom)
+)
+
+--- grid-stroke-border-partial ---
+// Test partial border line overrides
+#set page(width: auto, height: 7em, margin: (bottom: 1em))
+#table(
+ columns: 4,
+ stroke: (x, y) => if y == 0 or y == 4 { orange } else { aqua },
+ table.hline(stroke: blue, start: 1, end: 2), table.cell(stroke: red, v(3em)), table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M],
+ [a], [b], [c], [M],
+ [d], [e], [f], [M],
+ [g], [h], [i], [M],
+ table.cell(stroke: red)[a], table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M],
+ table.hline(stroke: blue, start: 1, end: 2),
+)
+
+--- grid-stroke-vline-colspan ---
+// - Vline should be placed after the colspan.
+// - Hline should be placed under the full-width rowspan.
+#table(
+ columns: 3,
+ rows: 1.25em,
+ inset: 1pt,
+ stroke: none,
+ table.cell(colspan: 2)[a], table.vline(stroke: red), table.hline(stroke: blue), [b],
+ [c], [d], [e],
+ table.cell(colspan: 3, rowspan: 2)[a], table.vline(stroke: blue), table.hline(stroke: red)
+)
+
+--- grid-stroke-hline-rowspan ---
+// Red line should be above [c] (hline skips the shortest rowspan).
+#set text(6pt)
+#table(
+ rows: 1em,
+ columns: 2,
+ inset: 1.5pt,
+ table.cell(rowspan: 3)[a], table.cell(rowspan: 2)[b],
+ table.hline(stroke: red),
+ [c]
+)
+
+--- grid-stroke-hline-position-bottom-out-of-bounds ---
+// Error: 8:3-8:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
+// Hint: 8:3-8:32 set the line's position to 'top' or place it at a smaller 'y' index
+#table(
+ columns: 2,
+ gutter: 3pt,
+ [a], [b],
+ [c], [d], table.vline(stroke: red),
+ table.hline(stroke: aqua),
+ table.hline(position: top),
+ table.hline(position: bottom)
+)
+
+--- grid-stroke-vline-position-bottom-out-of-bounds ---
+// Error: 6:3-6:28 cannot place vertical line at the 'end' position of the end border (x = 2)
+// Hint: 6:3-6:28 set the line's position to 'start' or place it at a smaller 'x' index
+#grid(
+ columns: 2,
+ [a], [b],
+ grid.vline(stroke: aqua),
+ grid.vline(position: start),
+ grid.vline(position: end)
+)
+
+--- grid-stroke-vline-position-bottom-out-of-bounds-gutter ---
+// Error: 7:3-7:28 cannot place vertical line at the 'end' position of the end border (x = 2)
+// Hint: 7:3-7:28 set the line's position to 'start' or place it at a smaller 'x' index
+#grid(
+ columns: 2,
+ gutter: 3pt,
+ [a], [b],
+ grid.vline(stroke: aqua),
+ grid.vline(position: start),
+ grid.vline(position: end)
+)
+
+--- grid-stroke-hline-out-of-bounds ---
+// Error: 4:3-4:19 cannot place horizontal line at invalid row 3
+#grid(
+ [a],
+ [b],
+ grid.hline(y: 3)
+)
+
+--- grid-stroke-hline-out-of-bounds-gutter ---
+// Error: 5:3-5:19 cannot place horizontal line at invalid row 3
+#grid(
+ gutter: 3pt,
+ [a],
+ [b],
+ grid.hline(y: 3)
+)
+
+--- grid-stroke-vline-out-of-bounds ---
+// Error: 4:3-4:20 cannot place vertical line at invalid column 3
+#table(
+ columns: 2,
+ [a], [b],
+ table.vline(x: 3)
+)
+
+--- grid-stroke-vline-out-of-bounds-gutter ---
+// Error: 5:3-5:20 cannot place vertical line at invalid column 3
+#table(
+ columns: 2,
+ gutter: 3pt,
+ [a], [b],
+ table.vline(x: 3)
+)
+
+--- table-hline-in-grid ---
+// Error: 7-20 cannot use `table.hline` as a grid line; use `grid.hline` instead
+#grid(table.hline())
+
+--- table-vline-in-grid ---
+// Error: 7-20 cannot use `table.vline` as a grid line; use `grid.vline` instead
+#grid(table.vline())
+
+--- grid-hline-in-table ---
+// Error: 8-20 cannot use `grid.hline` as a table line; use `table.hline` instead
+#table(grid.hline())
+
+--- grid-vline-in-table ---
+// Error: 8-20 cannot use `grid.vline` as a table line; use `table.vline` instead
+#table(grid.vline())
+
+--- grid-hline-end-before-start-1 ---
+// Error: 3:3-3:31 line cannot end before it starts
+#grid(
+ columns: 3,
+ grid.hline(start: 2, end: 1),
+ [a], [b], [c],
+)
+
+--- grid-hline-end-before-start-2 ---
+// Error: 3:3-3:32 line cannot end before it starts
+#table(
+ columns: 3,
+ table.vline(start: 2, end: 1),
+ [a], [b], [c],
+ [d], [e], [f],
+ [g], [h], [i],
+)
+
+--- grid-hline-position-horizon ---
+// Error: 24-31 expected `top` or `bottom`, found horizon
+#table.hline(position: horizon)
+
+--- grid-vline-position-center ---
+// Error: 24-30 expected `start`, `left`, `right`, or `end`, found center
+#table.vline(position: center)
+
+--- grid-hline-position-right ---
+// Error: 24-29 expected `top` or `bottom`, found right
+#table.hline(position: right)
+
+--- grid-vline-position-top ---
+// Error: 24-27 expected `start`, `left`, `right`, or `end`, found top
+#table.vline(position: top)
diff --git a/tests/suite/layout/grid/styling.typ b/tests/suite/layout/grid/styling.typ
new file mode 100644
index 00000000..f7cfb97d
--- /dev/null
+++ b/tests/suite/layout/grid/styling.typ
@@ -0,0 +1,160 @@
+// Test grid styling options.
+
+--- grid-fill-func ---
+#set page(height: 70pt)
+#set grid(fill: (x, y) => if calc.even(x + y) { rgb("aaa") })
+
+#grid(
+ columns: (1fr,) * 3,
+ stroke: 2pt + rgb("333"),
+ [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
+)
+
+--- grid-stroke-none ---
+#grid(columns: 3, stroke: none, fill: green, [A], [B], [C])
+
+--- grid-align ---
+// Test general alignment.
+#grid(
+ columns: 3,
+ align: left,
+ [Hello], [Hello], [Hello],
+ [A], [B], [C],
+)
+
+// Test alignment with a function.
+#grid(
+ columns: 3,
+ align: (x, y) => (left, center, right).at(x),
+ [Hello], [Hello], [Hello],
+ [A], [B], [C],
+)
+
+// Test alignment with array.
+#grid(
+ columns: (1fr, 1fr, 1fr),
+ align: (left, center, right),
+ [A], [B], [C]
+)
+
+// Test empty array.
+#set align(center)
+#grid(
+ columns: (1fr, 1fr, 1fr),
+ align: (),
+ [A], [B], [C]
+)
+
+a
+
+--- grid-inset ---
+// Test inset.
+#grid(
+ columns: (1fr,) * 3,
+ stroke: 2pt + rgb("333"),
+ inset: 5pt,
+ [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
+)
+
+#grid(
+ columns: 3,
+ inset: 10pt,
+ fill: blue,
+ [A], [B], [C]
+)
+
+#grid(
+ columns: 3,
+ inset: (y: 10pt),
+ [A], [B], [C]
+)
+
+#grid(
+ columns: 3,
+ inset: (left: 20pt, rest: 10pt),
+ stroke: 3pt + red,
+ [A], [B], [C]
+)
+
+#grid(
+ columns: 2,
+ inset: (
+ left: 20pt,
+ right: 5pt,
+ top: 10pt,
+ bottom: 3pt,
+ ),
+ [A],
+ [B],
+)
+
+#grid(
+ columns: 3,
+ fill: (x, y) => (if y == 0 { aqua } else { orange }).darken(x * 15%),
+ inset: (x, y) => (left: if x == 0 { 0pt } else { 5pt }, right: if x == 0 { 5pt } else { 0pt }, y: if y == 0 { 0pt } else { 5pt }),
+ [A], [B], [C],
+ [A], [B], [C],
+)
+
+#grid(
+ columns: 3,
+ inset: (0pt, 5pt, 10pt),
+ fill: (x, _) => aqua.darken(x * 15%),
+ [A], [B], [C],
+)
+
+--- grid-inset-folding ---
+// Test inset folding
+#set grid(inset: 10pt)
+#set grid(inset: (left: 0pt))
+
+#grid(
+ fill: red,
+ inset: (right: 0pt),
+ grid.cell(inset: (top: 0pt))[a]
+)
+
+--- grid-funcs-gutter ---
+// Test interaction with gutters.
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ row-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ column-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)