diff --git a/NEWS.md b/NEWS.md index ff4bf06312..008063632d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,8 @@ ## BUG FIXES +- Figure captions are no longer dropped when a single chunk produces multiple captioned figures for Markdown-based output (e.g., HTML or PDF via Pandoc). Previously, consecutive images were emitted in one paragraph (`![cap1](a) ![cap2](b)`), which Pandoc treats as inline images and renders without captions. knitr now inserts a blank line after each captioned figure except the last, so Pandoc emits a separate figure (with caption) for each image (thanks, @atusy, #2032, #1524, #1760). + - knitr now emits a warning when a code chunk opens a new graphics device (e.g., via `dev.new()`), because plots drawn on such devices cannot be captured by knitr and will silently fail to render in the output (thanks, @Higgs32584, #2355). - `include_graphics()` now converts absolute paths to paths relative to the output directory of the rendered document (communicated by **rmarkdown** >= 2.32 via `opts_knit$get('rmarkdown.output_dir')`) instead of knitr's working directory (the input directory), which fixes broken image paths when the input and output directories differ (e.g., rendering to a different `output_dir`). The existence check (when `error = TRUE`) also uses the original path so that it no longer reports false negatives when `root.dir` differs from the output directory (thanks, @naikymen @jameelalsalam @cderv, #2171, r-lib/pkgdown#2334). diff --git a/R/hooks-md.R b/R/hooks-md.R index 5a7b843540..5e0df48253 100644 --- a/R/hooks-md.R +++ b/R/hooks-md.R @@ -75,7 +75,7 @@ hook_plot_md_base = function(x, options) { res = sprintf('![%s](%s)', cap, x2) if (!is.null(lnk) && !is.na(lnk)) res = sprintf('[%s](%s)', res, lnk) res = paste0(res, if (nocap) '' else '', if (is_latex_output()) ' ' else '') - return(res) + return(sep_captioned_fig(res, cap, options)) } add_link = function(x) { if (is.null(lnk) || is.na(lnk)) return(x) @@ -131,7 +131,20 @@ hook_plot_md_pandoc = function(x, options) { ) if (at != '') at = paste0('{', at, '}') - sprintf('![%s](%s%s)%s', cap, base, .upload.url(x), at) + sep_captioned_fig(sprintf('![%s](%s%s)%s', cap, base, .upload.url(x), at), cap, options) +} + +# When a chunk generates multiple figures that carry captions, the plot hook is +# called once per figure and the bare `![cap](path)` images would otherwise be +# concatenated into a single paragraph, e.g. `![cap1](a) ![cap2](b)`. Pandoc +# treats several images in one paragraph as inline images and drops the captions +# (no figure environment /
). Append a blank line after each +# captioned figure except the last so that Pandoc emits a figure per image. +# See https://github.com/yihui/knitr/issues/2032 and #1524. +sep_captioned_fig = function(res, cap, options) { + if (cap != '' && (options$fig.cur %n% 1L) < (options$fig.num %n% 1L)) + res = paste0(res, '\n\n') + res } css_align = function(align) { diff --git a/tests/testit/test-hooks-md.R b/tests/testit/test-hooks-md.R index 7ff7e1058e..9f3cf52291 100644 --- a/tests/testit/test-hooks-md.R +++ b/tests/testit/test-hooks-md.R @@ -101,6 +101,17 @@ assert("Include a plot by pandoc md", { sprintf("![%s](1.png){width=%s %s}", cap, w, ex)) }) +assert("captioned figures in a multi-figure chunk are separated by a blank line (#2032)", { + fig = function(cur, num, cap = NULL) opt(cap = cap, fig.cur = cur, fig.num = num) + # a captioned figure that is not the last one gets a trailing blank line + (hook_plot_md_pandoc(x, fig(1, 2, cap)) %==% sprintf("![%s](1.png)\n\n", cap)) + # the last figure and single-figure chunks do not + (hook_plot_md_pandoc(x, fig(2, 2, cap)) %==% sprintf("![%s](1.png)", cap)) + (hook_plot_md_pandoc(x, fig(1, 1, cap)) %==% sprintf("![%s](1.png)", cap)) + # figures without a caption are left inline (Pandoc keeps them as inline images) + (hook_plot_md_pandoc(x, fig(1, 2)) %==% "![](1.png)") +}) + assert('empty alt text is preserved and NA alt is discarded', { (hook_plot_md(x, opts_chunk$merge(list(fig.alt = ''))) %==% '') (hook_plot_md(x, opts_chunk$merge(list(fig.alt = NA, out.width = '100'))) %==% '')