Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: knitr
Type: Package
Title: A General-Purpose Package for Dynamic Report Generation in R
Version: 1.51.14
Version: 1.51.15
Authors@R: c(
person("Yihui", "Xie", role = c("aut", "cre"), email = "xie@yihui.name", comment = c(ORCID = "0000-0003-0645-5666", URL = "https://yihui.org")),
person("Abhraneel", "Sarma", role = "ctb"),
Expand Down
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

## NEW FEATURES

- `hook_plot_tex()` now respects the chunk option `animation.hook` (or the package option `animation.fun`) when it is set to a function of your own, and calls it to generate the LaTeX code for a chunk with `fig.show = 'animate'` instead of the default `\animategraphics{}`. This makes the LaTeX plot hook extensible in the same way as `hook_plot_html()`, which has always supported these options, and allows for LaTeX packages other than **animate** to be used for animations, e.g. **xmpmulti** for beamer overlays. The built-in hooks such as `hook_ffmpeg_html()` generate HTML and continue to be ignored for LaTeX output, so this does not change the output of existing documents (thanks, @jolars #2452).
- Added a new chunk option `fig.note` to add a note (e.g., a source or explanatory note) below a figure, separate from its caption (thanks, @turbanisch, #2022). It works for LaTeX/PDF, HTML, and Typst output. For LaTeX, the note is emitted via a `\figurenote{}` command inside the figure environment; a default definition is provided (footnotesize italic) that you may override in the preamble, e.g., `\newcommand{\figurenote}[1]{\floatfoot{#1}}` (with the **floatrow** package). For HTML, the note is placed in `<p class="figure-note">` inside the figure `<div>`, which you can style with CSS.

- Added support for a new input format `.Rtyp` for the [Typst](https://typst.app) typesetting system (thanks, @blset #2401, @aksigkvgithub #2283). You can use code chunks and inline R expressions in `.Rtyp` files, knit them via `knitr::knit()` to `.typ` output, or compile to PDF directly via `knit2pdf('input.Rtyp')`. See https://github.com/yihui/knitr-examples/blob/master/128-minimal.Rtyp for a minimal example.

- Added a `knitr::rtyp` vignette engine to build `.Rtyp` vignettes to PDF via Typst, so packages no longer need to register their own engine for this purpose (thanks, @ggrothendieck, #2447).

- `hook_plot_tex()` now respects the chunk option `animation.hook` (or the package option `animation.fun`) when it is set to a function of your own, and calls it to generate the LaTeX code for a chunk with `fig.show = 'animate'` instead of the default `\animategraphics{}`. This makes the LaTeX plot hook extensible in the same way as `hook_plot_html()`, which has always supported these options, and allows for LaTeX packages other than **animate** to be used for animations, e.g. **xmpmulti** for beamer overlays. The built-in hooks such as `hook_ffmpeg_html()` generate HTML and continue to be ignored for LaTeX output, so this does not change the output of existing documents (thanks, @jolars #2452).

- Added support for `ragg::agg_webp()` as a graphics device for WebP output (thanks, @heavywatal, #2434). To use this device, set `dev = 'agg_webp'` in the chunk options. This device is available in **ragg** >= 1.5.0.

- The chunk option `dev.args` can now customize the `dev = 'gridSVG'` device. Arguments are passed to both `grDevices::svg()` (e.g., `pointsize`) and `gridSVG::grid.export()` (e.g., `strict`), with each function receiving only the arguments it recognizes (thanks, @deepayan, #2450, #2451).
Expand Down
5 changes: 3 additions & 2 deletions R/defaults.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ opts_chunk = new_defaults(list(

fig.keep = 'high', fig.show = 'asis', fig.align = 'default', fig.path = 'figure/',
dev = NULL, dev.args = NULL, dpi = 72, fig.ext = NULL, fig.width = 7, fig.height = 7,
fig.env = 'figure', fig.cap = NULL, fig.scap = NULL, fig.lp = 'fig:', fig.subcap = NULL,
fig.env = 'figure', fig.cap = NULL, fig.scap = NULL, fig.note = NULL,
fig.lp = 'fig:', fig.subcap = NULL,
fig.pos = '', out.width = NULL, out.height = NULL, out.extra = NULL, fig.retina = 1,
external = TRUE, sanitize = FALSE, interval = 1, aniopts = 'controls,loop',

Expand Down Expand Up @@ -198,7 +199,7 @@ set_alias = function(...) {
#' }
#' @include hooks-html.R
opts_knit = new_defaults(list(
progress = TRUE, verbose = FALSE, eval.after = c('fig.cap', 'fig.scap', 'fig.alt'),
progress = TRUE, verbose = FALSE, eval.after = c('fig.cap', 'fig.scap', 'fig.alt', 'fig.note'),
base.dir = NULL, base.url = NULL, root.dir = NULL, child.path = '',
upload.fun = identity, global.device = FALSE, global.par = FALSE,
concordance = FALSE, documentation = 1L, self.contained = TRUE,
Expand Down
16 changes: 15 additions & 1 deletion R/hooks-latex.R
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ hook_plot_tex = function(x, options) {
'\\caption%s{%s}%s\n', escape_percent(scap), escape_percent(cap),
create_label(lab, if (mcap) c('-', fig.cur), latex = TRUE)
)
fig2 = sprintf('%s\\end{%s}\n', cap, options$fig.env)
note = options$fig.note
note = if (is.null(note) || is.na(note) || note == '') '' else
sprintf('%s\\figurenote{%s}\n', define_figurenote(), escape_percent(note))
fig2 = sprintf('%s%s\\end{%s}\n', cap, note, options$fig.env)
}
} else if (pandoc_to(c('latex', 'beamer'))) {
# use alignment environments for R Markdown latex output (\centering won't work)
Expand Down Expand Up @@ -216,6 +219,17 @@ animation_hook_tex = function(options) {
fun
}

# provide a default \figurenote command (for the chunk option fig.note) the
# first time it is needed in a document; \providecommand is a no-op if the user
# has defined \figurenote in the preamble (e.g. via \newcommand), so this both
# works out of the box and stays customizable; emitting it only once avoids
# repeating the long definition before every figure note
define_figurenote = function() {
if (isTRUE(.knitEnv$fig.note.defined)) return('')
.knitEnv$fig.note.defined = TRUE
'\\providecommand{\\figurenote}[1]{\\vspace{2pt}\\par\\raggedright\\footnotesize\\emph{#1}}\n'
}

# % -> \%, but do not touch \%
escape_percent = function(x) gsub('(?<!\\\\)%', '\\\\%', x, perl = TRUE)

Expand Down
15 changes: 9 additions & 6 deletions R/hooks-md.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ need_special_plot_hook = function(options) {
opts = opts_chunk$get(default = TRUE)
for (i in c(
'out.width', 'out.height', 'out.extra', 'fig.align', 'fig.subcap',
'fig.env', 'fig.scap', 'fig.alt'
'fig.env', 'fig.scap', 'fig.alt', 'fig.note'
)) if (!identical(options[[i]], opts[[i]])) return(TRUE)
FALSE
}
Expand All @@ -61,13 +61,15 @@ hook_plot_md_base = function(x, options) {
# self-contained mode?
sc = any(c('--embed-resources', '--self-contained') %in% opts_knit$get('rmarkdown.pandoc.args'))
lnk = options$fig.link
pandoc_html = cap != '' && is_html_output()
note = options$fig.note
has_note = !is.null(note) && !is.na(note) && note != ''
pandoc_html = (cap != '' || has_note) && is_html_output()
in_bookdown = isTRUE(opts_knit$get('bookdown.internal.label'))
plot1 = ai || options$fig.cur <= 1L
plot2 = ai || options$fig.cur == options$fig.num
to = pandoc_to(); from = pandoc_from()
if (is.null(w) && is.null(h) && is.null(s) && is.null(options$fig.alt) &&
a == 'default' && !(pandoc_html && in_bookdown) && !is_svg) {
a == 'default' && !(pandoc_html && in_bookdown) && !is_svg && !has_note) {
# append <!-- --> to ![]() to prevent the figure environment in these cases
nocap = cap == '' && !is.null(to) && !grepl('^markdown', to) &&
(options$fig.num == 1 || ai) && !grepl('-implicit_figures', from)
Expand All @@ -88,13 +90,14 @@ hook_plot_md_base = function(x, options) {
# use HTML syntax <img src=...>
if (pandoc_html && !isTRUE(grepl('-implicit_figures', from))) {
d1 = if (plot1) sprintf('<div class="figure"%s>\n', css_text_align(a))
d2 = sprintf('<p class="caption">%s</p>', cap)
d2 = if (cap != '') sprintf('<p class="caption">%s</p>', cap)
d3 = if (has_note) sprintf('<p class="figure-note">%s</p>', note)
img = img_code()
# whether to place figure caption at the top or bottom of a figure
if (isTRUE(options$fig.topcaption)) {
paste0(d1, if (ai || options$fig.cur <= 1) d2, img, if (plot2) '</div>')
paste0(d1, if (ai || options$fig.cur <= 1) d2, img, if (plot2) paste0(d3, '</div>'))
} else {
paste0(d1, img, if (plot2) paste0('\n', d2, '\n</div>'))
paste0(d1, img, if (plot2) paste0('\n', d2, d3, '\n</div>'))
}
} else {
img_code(sprintf('style="%s"', css_align(a)))
Expand Down
8 changes: 7 additions & 1 deletion R/hooks-typst.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ hook_plot_typst = function(x, options) {
img_path = paste0(opts_knit$get('base.url'), .upload.url(x))
img_args = paste(c(sprintf('"%s"', img_path), args), collapse = ', ')

note = options$fig.note
note = if (is.null(note) || is.na(note) || note == '') '' else
sprintf('\n#block(inset: (top: 4pt))[#text(size: 0.85em)[#emph[%s]]]\n', note)

if (nzchar(cap)) {
sprintf('\n#figure(\n image(%s),\n caption: [%s],\n)\n', img_args, cap)
sprintf('\n#figure(\n image(%s),\n caption: [%s],\n)%s\n', img_args, cap, note)
} else if (nzchar(note)) {
sprintf('\n#image(%s)%s\n', img_args, note)
} else {
sprintf('\n#image(%s)\n', img_args)
}
Expand Down
3 changes: 3 additions & 0 deletions R/output.R
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ knit = function(
# we need some special treatment for chunks in Quarto document
.knitEnv$is_quarto = !is.null(opts_knit$get('quarto.version')) || ext == 'qmd'

# reset the flag for defining \figurenote (fig.note) once per document
if (!child_mode()) .knitEnv$fig.note.defined = FALSE

text = if (is.null(text)) xfun::read_utf8(input) else split_lines(text)
if (!length(text)) {
if (is.character(output)) file.create(output)
Expand Down
2 changes: 1 addition & 1 deletion R/plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ is_low_change.default = function(p1, p2) {
# recycle some plot options such as fig.cap, out.width/height, etc when there
# are multiple plots per chunk
.recyle.opts = c('fig.cap', 'fig.scap', 'fig.alt', 'fig.env', 'fig.pos', 'fig.subcap',
'out.width', 'out.height', 'out.extra', 'fig.link')
'fig.note', 'out.width', 'out.height', 'out.extra', 'fig.link')

# when passing options to plot hooks, reduce the recycled options to scalars
reduce_plot_opts = function(options) {
Expand Down
4 changes: 4 additions & 0 deletions inst/misc/vignette.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ table > caption span, div.figure p.caption span {
font-style: normal;
font-weight: bold;
}
div.figure p.figure-note {
font-size: 90%;
font-style: italic;
}

img:not([class]) {
background-color: #FFFFFF;
Expand Down
28 changes: 28 additions & 0 deletions tests/testit/test-hooks-latex.R
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,31 @@ assert("a user-provided animation hook generates the LaTeX code for animations",
'foo-3.pdf', opts(fig.cur = 3, fig.num = 3)
), fixed = TRUE))
})

assert("fig.note produces \\figurenote{} inside the figure environment", {
note_opts = function(note) opts_chunk$merge(list(
label = 'l', fig.cap = 'Cap', fig.note = note, fig.show = 'asis'
))

# pretend we are at the start of a fresh document
.knitEnv$fig.note.defined = FALSE

res = hook_plot_tex('foo.pdf', note_opts('A note.'))
# \figurenote{} appears after \caption and before \end{figure}
(grepl('\\figurenote{A note.}', res, fixed = TRUE))
(grepl('\\providecommand{\\figurenote}', res, fixed = TRUE))
# \figurenote{} comes after \caption and before \end{figure}
(regexpr('\\caption', res, fixed = TRUE) <
regexpr('\\figurenote{A note.}', res, fixed = TRUE))
(regexpr('\\figurenote{A note.}', res, fixed = TRUE) <
regexpr('\\end{figure}', res, fixed = TRUE))

# the definition is emitted only once per document: a second figure note
# calls \figurenote{} but does not repeat \providecommand
res2 = hook_plot_tex('foo.pdf', note_opts('Another note.'))
(grepl('\\figurenote{Another note.}', res2, fixed = TRUE))
(!grepl('\\providecommand', res2, fixed = TRUE))

# an empty/NA note adds nothing
(!grepl('figurenote', hook_plot_tex('foo.pdf', note_opts(NA)), fixed = TRUE))
})
14 changes: 14 additions & 0 deletions tests/testit/test-hooks-md.R
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ assert('empty alt text is preserved and NA alt is discarded', {
(hook_plot_md(x, opts_chunk$merge(list(fig.alt = NA, out.width = '100'))) %==% '<img src="1.png" width="100" />')
})

assert("fig.note is placed in a figure-note paragraph for HTML output", {
old = opts_knit$get('rmarkdown.pandoc.to')
opts_knit$set(rmarkdown.pandoc.to = 'html')
# note together with a caption
(hook_plot_md(x, opt(cap = cap, fig.note = 'A note.')) %==%
paste0('<div class="figure">\n<img src="1.png" alt="foo" />\n',
'<p class="caption">foo</p><p class="figure-note">A note.</p>\n</div>'))
# note without a caption (no empty caption paragraph)
(hook_plot_md(x, opt(fig.note = 'A note.')) %==%
paste0('<div class="figure">\n<img src="1.png" alt="" />\n',
'<p class="figure-note">A note.</p>\n</div>'))
opts_knit$set('rmarkdown.pandoc.to' = old)
})

assert("fig.alt does not break office document", {
old = opts_knit$get('rmarkdown.pandoc.to')
opts_knit$set(rmarkdown.pandoc.to = "docx")
Expand Down