Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- `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).
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
4 changes: 2 additions & 2 deletions R/plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ 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')
.recyle.opts = c('fig.cap', 'fig.scap', 'fig.alt', 'fig.note', 'fig.env', 'fig.pos',
'fig.subcap', 'out.width', 'out.height', 'out.extra', 'fig.link')
Comment thread
yihui marked this conversation as resolved.
Outdated

# 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
29 changes: 29 additions & 0 deletions tests/testit/test-hooks-latex.R
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,32 @@ 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
knitr_env = getFromNamespace('.knitEnv', 'knitr')
Comment thread
yihui marked this conversation as resolved.
Outdated
knitr_env$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
Loading