Skip to content

Let captioned tables break across pages in Typst - #15278

Open
faysou wants to merge 1 commit into
quarto-dev:mainfrom
faysou:fix/typst-breakable-table-figures
Open

faysou wants to merge 1 commit into
quarto-dev:mainfrom
faysou:fix/typst-breakable-table-figures

Conversation

@faysou

@faysou faysou commented Nov 3, 2026 •

Copy link
Copy Markdown

Description

Typst figures do not break across pages, so a captioned table taller than the page is not split. The whole figure moves to the next page and its last rows run past the page bottom. Several test documents work around this with a hand-written #show figure: set block(breakable: true), for example tests/docs/smoke-all/typst/juice/gt-table-images.qmd.

---
format: typst
---

| A | B |
|---|---|
| 1 | row 1 |
...
| 120 | row 120 |

: Long table {#tbl-long}

Before this change the PDF has one page with 32 of the 120 rows. After it, the table spans four pages with every row.

definitions.typ now has this show rule, next to the non-breaking rule for definition items (#13978):

#show figure.where(kind: "quarto-float-tbl"): set block(breakable: true)

It applies only to Quarto table floats, so image figures and listings keep their current behavior. The rule comes before include-in-header in template.typ, so a document that wants whole tables can still turn it off with one line in its header:

#show figure.where(kind: "quarto-float-tbl"): set block(breakable: false)

Tests

  • tests/docs/smoke-all/typst/table-figure-breakable.qmd renders a 70-row captioned table and checks that its first row is on page 1. Without the rule, the unbreakable figure moves whole to page 2 and the check fails.
  • tests/docs/smoke-all/typst/table-figure-unbreakable-override.qmd sets breakable: false in include-in-header and checks that the table moves whole to page 2. It fails if the rule is placed after the header, as an earlier version of this PR did by injecting it from the float filter.

The other Typst smoke-all documents still pass, apart from those with R or Python cells, which I could not run locally.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR

@posit-snyk-bot

posit-snyk-bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@mcanouil

mcanouil commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR.

I lean towards not adding this to Quarto, but I will leave the decision to the maintainers.

Users can already get this with one line, in include-in-header, in a partial, or in a raw typst block:

#show figure.where(kind: "quarto-float-tbl"): set block(breakable: true)

That is the workaround a maintainer gave in #14385, where the same rule was also mentioned as something that might go in the template (rather than injected from Lua as done here).
Adding it by default changes every captioned table in every Typst document.
A short table near the bottom of a page can now split, and users who prefer whole tables would have to opt out.

The way the rule is added also matters.
quarto.doc.include_text("before-body", ...) puts it after everything the user and the format write.
I checked this on a Typst render with keep-typ: true, using a Lua filter that makes the same include_text call.
The output has the user's include-in-header text, then the user's include-before-body text, then the text from the filter, then the body.
The template partials come before include-before in template.typ, so they also come before the injected rule.
Typst applies the last matching set rule, which I confirmed with a 90-row table: breakable: false in include-in-header followed by the injected breakable: true gives a table that breaks.
So a user or a custom format could no longer set breakable: false for these tables from a header, a partial or a template.
Only a raw typst block in the body would still work.

If the maintainers do want it as a default, I would put it in definitions.typ instead of injecting it from Lua.
That is where #13978 put the non-breaking rule for definition items.
Anything that comes after it, such as include-in-header, can then override it with one line.

@faysou
faysou force-pushed the fix/typst-breakable-table-figures branch from dee2eca to 231fb33 Compare November 7, 2026 13:46
@faysou

faysou commented Nov 7, 2026

Copy link
Copy Markdown
Author

Thanks for checking the include order, you're right: injecting the rule from the float filter put it after include-in-header, so a document could no longer turn it off from its header.

I've moved the rule to definitions.typ as you suggested and dropped the Lua injection. A document can now keep its tables whole with:

#show figure.where(kind: "quarto-float-tbl"): set block(breakable: false)

I also added table-figure-unbreakable-override.qmd, which sets that line in include-in-header and checks that the table stays whole. It fails with the earlier Lua version.

Whether tables should be breakable by default is up to the maintainers. If they'd rather keep the current behaviour, the PR can be closed.

Typst figures do not break across pages, so a captioned table taller
than the page ran past the page bottom and its last rows were lost.

definitions.typ now makes table figures breakable. Because the rule
comes before include-in-header in the template, a document can still
keep its tables whole with one line in its header:

    #show figure.where(kind: "quarto-float-tbl"): set block(breakable: false)
@faysou
faysou force-pushed the fix/typst-breakable-table-figures branch from 231fb33 to 45cddd5 Compare November 7, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants