diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index cd0c745708..57580b39ca 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 7b4f457a6c..8be049de80 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 0000000000..aa3f8160d0 --- /dev/null +++ b/tests/docs/smoke-all/2026/06/30/14575-knitr.qmd @@ -0,0 +1,43 @@ +--- +# Issue #14575 — knitr engine cases. See the body below for details. +format: pdf +tbl-cap-location: bottom +keep-tex: true +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\end\{document\}' + - '\\caption\{[\s\S]{0,60}CAP7 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + - '\\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} +#| 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 0000000000..5457fb27ef --- /dev/null +++ b/tests/docs/smoke-all/2026/06/30/14575.qmd @@ -0,0 +1,84 @@ +--- +# 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 +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\end\{document\}' + - '\\caption\{[\s\S]{0,60}CAP1 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + - '\\caption\{[\s\S]{0,60}CAP2 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + - '\\caption\{[\s\S]{0,60}CAP3 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + - '\\caption\{[\s\S]{0,60}CAP4 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + - '\\caption\{[\s\S]{0,60}CAP5 caption[\s\S]{0,60}\\tabularnewline\s*\\endlastfoot' + - '\\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| +|-----:|----:| +| 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| +::: +:::