From 58a3944d0fd97aaead87d372efbbebc822e70b8d Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 30 Jun 2026 14:00:38 +0200 Subject: [PATCH 1/2] fix(pdf): place bottom caption in longtable foot for computational tables (#14575) With tbl-cap-location: bottom, a cross-referenceable table whose content is wrapped in .cell-output-display (knitr/Jupyter engine output) routes through floatreftarget.lua's manual longtable surgery instead of Pandoc's native path. That branch emitted the caption after the data rows but inside the longtable body, so it rendered inside the table rather than below it. Inject the caption (plus \tabularnewline) immediately before \endlastfoot so it lands in the longtable foot, matching the native path. Top captions and footless tables (e.g. kable(longtable=TRUE)) fall through to the prior behavior; the latter is tracked separately as a follow-up. --- news/changelog-1.10.md | 1 + .../filters/customnodes/floatreftarget.lua | 21 ++++- .../docs/smoke-all/2026/06/30/14575-knitr.qmd | 44 +++++++++ tests/docs/smoke-all/2026/06/30/14575.qmd | 89 +++++++++++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 tests/docs/smoke-all/2026/06/30/14575-knitr.qmd create mode 100644 tests/docs/smoke-all/2026/06/30/14575.qmd diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index cd0c745708b..57580b39ca7 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -29,6 +29,7 @@ All changes included in 1.10: - ([#13588](https://github.com/quarto-dev/quarto-cli/issues/13588)): Fix Lua error when rendering PDF with `reference-location: margin` and a footnote alongside a figure with `fig-cap`. (author: @mcanouil) - ([#14553](https://github.com/quarto-dev/quarto-cli/issues/14553)): Fix font fallbacks (`mainfontfallback`, `sansfontfallback`, `monofontfallback`) crashing LuaLaTeX on TeX Live 2026 (luaotfload v3.29+) instead of filling in missing glyphs. Bare fallback names (e.g. `"DejaVu Sans"`) are now automatically colon-terminated as luaotfload requires; names that already carry a terminator or feature options (e.g. `"FreeSans:"`, `"Noto:mode=harf"`) are left untouched. A clear, actionable error is also reported if a fallback crash is still detected. - ([#14553](https://github.com/quarto-dev/quarto-cli/issues/14553), [#14558](https://github.com/quarto-dev/quarto-cli/issues/14558)): Fix PDF render failing instead of auto-installing a missing font referenced by `monofontfallback` (and other `mainfont`/`sansfont`/`monofont` fallbacks). +- ([#14575](https://github.com/quarto-dev/quarto-cli/issues/14575)): Fix caption being placed inside the table body instead of below it for cross-referenceable computational (knitr/Jupyter) tables with `tbl-cap-location: bottom`. ### `typst` diff --git a/src/resources/filters/customnodes/floatreftarget.lua b/src/resources/filters/customnodes/floatreftarget.lua index 7b4f457a6cc..8be049de802 100644 --- a/src/resources/filters/customnodes/floatreftarget.lua +++ b/src/resources/filters/customnodes/floatreftarget.lua @@ -505,10 +505,29 @@ end, function(float) end return result else + -- For a bottom caption, place the caption inside the longtable + -- foot (immediately before \endlastfoot) so it renders below the + -- table, matching Pandoc's native longtable output. Without this, + -- the caption lands after the data rows but inside the body. See #14575. + -- Tables without a foot (e.g. kable(longtable=TRUE)) fall through + -- to the behavior below. + local foot_pos = cap_loc ~= "top" and content:find("\\endlastfoot", 1, true) + if foot_pos then + return pandoc.Blocks({ + pandoc.RawBlock("latex", longtable_preamble), + pandoc.RawBlock("latex", start), + pandoc.RawBlock("latex", content:sub(1, foot_pos - 1)), + latex_caption, + pandoc.RawInline("latex", "\\tabularnewline"), + pandoc.RawBlock("latex", content:sub(foot_pos)), + pandoc.RawBlock("latex", "\\end{longtable}"), + pandoc.RawBlock("latex", longtable_postamble), + }) + end local result = pandoc.Blocks({latex_caption, pandoc.RawInline("latex", "\\tabularnewline")}) -- if cap_loc is top, insert content on bottom if cap_loc == "top" then - result:insert(pandoc.RawBlock("latex", content)) + result:insert(pandoc.RawBlock("latex", content)) else result:insert(1, pandoc.RawBlock("latex", content)) end diff --git a/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd b/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd new file mode 100644 index 00000000000..4f6503a485b --- /dev/null +++ b/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd @@ -0,0 +1,44 @@ +--- +# Issue #14575 (knitr engine cases). With tbl-cap-location: bottom, a +# cross-referenced knitr/kable table (wrapped by Quarto in +# .cell-output-display) places its caption inside the longtable body +# instead of the foot. +# +# Correct behaviour: \caption{...}\tabularnewline immediately before +# \endlastfoot. +# +# Case 7 is the reported bug (currently fails; should pass after fix). +# Case 8 already renders correctly (regression guard). +# +# The kable(longtable = TRUE, caption = ...) variant (no \endlastfoot +# in kable's own \hline longtable) is tracked separately. +format: pdf +tbl-cap-location: bottom +keep-tex: true +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\end\{document\}' + # Case 7: knitr kable with label + tbl-cap (the reported bug) + - '\\caption\{[\s\S]{0,60}CAP7 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + # Case 8: knitr kable with tbl-cap, no label (guard) + - '\\caption\{[\s\S]{0,60}CAP8 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' +--- + +# Case 7: knitr kable with label (reported bug) + +```{r} +#| label: tbl-c7 +#| tbl-cap: "CAP7 caption" +knitr::kable(head(cars, 3)) +``` + +# Case 8: knitr kable with tbl-cap, no label (guard) + +```{r} +#| tbl-cap: "CAP8 caption" +knitr::kable(head(cars, 3)) +``` diff --git a/tests/docs/smoke-all/2026/06/30/14575.qmd b/tests/docs/smoke-all/2026/06/30/14575.qmd new file mode 100644 index 00000000000..4f4be88471b --- /dev/null +++ b/tests/docs/smoke-all/2026/06/30/14575.qmd @@ -0,0 +1,89 @@ +--- +# Issue #14575: with tbl-cap-location: bottom, the caption of a +# cross-referenceable table whose content is wrapped in +# .cell-output-display (knitr/jupyter engine output) is placed inside +# the longtable body instead of in the foot (\endlastfoot). +# +# This file covers the markdown-only cases (CI-safe, no engine needed). +# Correct behaviour: the bottom caption sits in the longtable foot, +# i.e. \caption{...}\tabularnewline immediately before \endlastfoot. +# +# Cases 1-4 already render correctly (regression guards). +# Cases 5-6 are the bug (currently fail; should pass after the fix). +format: pdf +tbl-cap-location: bottom +keep-tex: true +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\end\{document\}' + # Case 1: markdown table + caption syntax, no id (guard) + - '\\caption\{[\s\S]{0,60}CAP1 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + # Case 2: markdown table + caption syntax + id (guard) + - '\\caption\{[\s\S]{0,60}CAP2 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + # Case 3: div float + bare table + trailing caption paragraph (guard) + - '\\caption\{[\s\S]{0,60}CAP3 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + # Case 4: div float + tbl-cap attribute + bare table (guard) + - '\\caption\{[\s\S]{0,60}CAP4 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + # Case 5: div float + tbl-cap + .cell-output-display (the bug) + - '\\caption\{[\s\S]{0,60}CAP5 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + # Case 6: .cell + .cell-output-display, mcanouil reprex (the bug) + - '\\caption\{[\s\S]{0,60}CAP6 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' +--- + +# Case 1: markdown table, caption syntax, no id + +| speed| dist| +|-----:|----:| +| 4| 2| + +: CAP1 caption + +# Case 2: markdown table, caption syntax, with id + +| speed| dist| +|-----:|----:| +| 4| 2| + +: CAP2 caption {#tbl-c2} + +# Case 3: div float, bare table, trailing caption paragraph + +::: {#tbl-c3} +| speed| dist| +|-----:|----:| +| 4| 2| + +CAP3 caption +::: + +# Case 4: div float, tbl-cap attribute, bare table + +::: {#tbl-c4 tbl-cap='CAP4 caption'} +| speed| dist| +|-----:|----:| +| 4| 2| +::: + +# Case 5: div float, tbl-cap attribute, .cell-output-display wrapper + +::: {#tbl-c5 tbl-cap='CAP5 caption'} +::: {.cell-output-display} +| speed| dist| +|-----:|----:| +| 4| 2| +::: +::: + +# Case 6: .cell + .cell-output-display (mcanouil reprex) + +::: {#tbl-c6 .cell tbl-cap='CAP6 caption'} +::: {.cell-output-display} +| speed| dist| +|-----:|----:| +| 4| 2| +::: +::: From c5090eac86d36d84a288878f4843b745e3455019 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 30 Jun 2026 14:23:56 +0200 Subject: [PATCH 2/2] test: move issue-14575 case docs from YAML into test body --- .../docs/smoke-all/2026/06/30/14575-knitr.qmd | 29 +++++++++---------- tests/docs/smoke-all/2026/06/30/14575.qmd | 29 ++++++++----------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd b/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd index 4f6503a485b..aa3f8160d0e 100644 --- a/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd +++ b/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd @@ -1,17 +1,5 @@ --- -# Issue #14575 (knitr engine cases). With tbl-cap-location: bottom, a -# cross-referenced knitr/kable table (wrapped by Quarto in -# .cell-output-display) places its caption inside the longtable body -# instead of the foot. -# -# Correct behaviour: \caption{...}\tabularnewline immediately before -# \endlastfoot. -# -# Case 7 is the reported bug (currently fails; should pass after fix). -# Case 8 already renders correctly (regression guard). -# -# The kable(longtable = TRUE, caption = ...) variant (no \endlastfoot -# in kable's own \hline longtable) is tracked separately. +# Issue #14575 — knitr engine cases. See the body below for details. format: pdf tbl-cap-location: bottom keep-tex: true @@ -22,12 +10,23 @@ _quarto: ensureLatexFileRegexMatches: - - '\\end\{document\}' - # Case 7: knitr kable with label + tbl-cap (the reported bug) - '\\caption\{[\s\S]{0,60}CAP7 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' - # Case 8: knitr kable with tbl-cap, no label (guard) - '\\caption\{[\s\S]{0,60}CAP8 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' --- +With `tbl-cap-location: bottom`, a knitr `kable()` table that Quarto wraps in +`.cell-output-display` must place its bottom caption in the longtable foot — +`\caption{...}\tabularnewline` immediately before `\endlastfoot` — not inside +the table body. Both assertions above check that placement. + +Case 7 is the reported bug (cross-referenced kable with a label); it passes +only after the fix. Case 8 (kable with a caption but no label) already +rendered correctly and acts as a regression guard. + +The `kable(longtable = TRUE, caption = ...)` variant is a different shape — +its longtable has no `\endlastfoot`, so the caption is orphaned — and is +tracked separately, out of scope here. + # Case 7: knitr kable with label (reported bug) ```{r} diff --git a/tests/docs/smoke-all/2026/06/30/14575.qmd b/tests/docs/smoke-all/2026/06/30/14575.qmd index 4f4be88471b..5457fb27ef8 100644 --- a/tests/docs/smoke-all/2026/06/30/14575.qmd +++ b/tests/docs/smoke-all/2026/06/30/14575.qmd @@ -1,15 +1,6 @@ --- -# Issue #14575: with tbl-cap-location: bottom, the caption of a -# cross-referenceable table whose content is wrapped in -# .cell-output-display (knitr/jupyter engine output) is placed inside -# the longtable body instead of in the foot (\endlastfoot). -# -# This file covers the markdown-only cases (CI-safe, no engine needed). -# Correct behaviour: the bottom caption sits in the longtable foot, -# i.e. \caption{...}\tabularnewline immediately before \endlastfoot. -# -# Cases 1-4 already render correctly (regression guards). -# Cases 5-6 are the bug (currently fail; should pass after the fix). +# Issue #14575 — markdown-only cases (CI-safe, no engine needed). +# See the body below for what each case exercises. format: pdf tbl-cap-location: bottom keep-tex: true @@ -20,20 +11,24 @@ _quarto: ensureLatexFileRegexMatches: - - '\\end\{document\}' - # Case 1: markdown table + caption syntax, no id (guard) - '\\caption\{[\s\S]{0,60}CAP1 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' - # Case 2: markdown table + caption syntax + id (guard) - '\\caption\{[\s\S]{0,60}CAP2 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' - # Case 3: div float + bare table + trailing caption paragraph (guard) - '\\caption\{[\s\S]{0,60}CAP3 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' - # Case 4: div float + tbl-cap attribute + bare table (guard) - '\\caption\{[\s\S]{0,60}CAP4 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' - # Case 5: div float + tbl-cap + .cell-output-display (the bug) - '\\caption\{[\s\S]{0,60}CAP5 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' - # Case 6: .cell + .cell-output-display, mcanouil reprex (the bug) - '\\caption\{[\s\S]{0,60}CAP6 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' --- +With `tbl-cap-location: bottom`, a cross-referenceable table's bottom caption +must land in the longtable foot — `\caption{...}\tabularnewline` immediately +before `\endlastfoot` — not after the data rows inside the table body. Each +`CAPn caption` assertion above checks that placement for one input shape. + +Cases 1–4 already rendered correctly before the fix and act as regression +guards. Cases 5–6 wrap the table in a `.cell-output-display` Div (the shape +Quarto produces for knitr/Jupyter output); these triggered the bug and pass +only after the fix. + # Case 1: markdown table, caption syntax, no id | speed| dist|