Skip to content

handle knit_asis output correctly without wrapping cell divs #12024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
57 changes: 36 additions & 21 deletions src/resources/rmd/execute.R
Original file line number Diff line number Diff line change
Expand Up @@ -142,36 +142,51 @@ execute <- function(input, format, tempDir, libDir, dependencies, cwd, params, r
df_print = df_print
)

# we need ojs only if markdown has ojs code cells
# inspect code cells for spaces after line breaks

needs_ojs <- grepl("(\n|^)[[:space:]]*```+\\{ojs[^}]*\\}", markdown)
# FIXME this test isn't failing in shiny mode, but it doesn't look to be
# breaking quarto-shiny-ojs. We should make sure this is right.
if (
!is_shiny_prerendered(knitr::opts_knit$get("rmarkdown.runtime")) &&
needs_ojs
) {
local({
# create a hidden environment to store specific objects
.quarto_tools_env <- attach(NULL, name = "tools:quarto")
# source ojs_define() function and save it in the tools environment
source(file.path(resourceDir, "rmd", "ojs_static.R"), local = TRUE)
assign("ojs_define", ojs_define, envir = .quarto_tools_env)
})
# create a hidden environment to store specific objects
# Beware to use non conflicted name as this will be in second position right after globalenv.
.quarto_tools_env <- attach(NULL, name = "tools:quarto")
.quarto_tools_env$.assignToQuartoToolsEnv <- function(name, value) {
assign(name, value, envir = .quarto_tools_env)
}
.quarto_tools_env$.getFromQuartoToolsEnv <- function(name) {
get0(name, envir = .quarto_tools_env)
}
.quarto_tools_env$.rmFromQuartoToolsEnv <- function(name) {
if (exists(name, envir = .quarto_tools_env)) {
rm(list = c(name), envir = .quarto_tools_env)
}
}

env <- globalenv()
env$.QuartoInlineRender <- function(v) {
# special internal function for rendering inline code using Quarto syntax
.assignToQuartoToolsEnv(".QuartoInlineRender", function(v) { # nolint: object_usage_linter, line_length_linter.
if (is.null(v)) {
"NULL"
} else if (inherits(v, "AsIs")) {
v
} else if (is.character(v)) {
gsub(pattern="(\\[|\\]|[`*_{}()>#+-.!])", x=v, replacement="\\\\\\1")
gsub(
pattern = "(\\[|\\]|[`*_{}()>#+-.!])",
x = v, replacement = "\\\\\\1"
)
} else {
v
}
})

# we need ojs only if markdown has ojs code cells
# inspect code cells for spaces after line breaks
needs_ojs <- grepl("(\n|^)[[:space:]]*```+\\{ojs[^}]*\\}", markdown)
# FIXME this test isn't failing in shiny mode, but it doesn't look to be
# breaking quarto-shiny-ojs. We should make sure this is right.
if (
!is_shiny_prerendered(knitr::opts_knit$get("rmarkdown.runtime")) &&
needs_ojs
) {
# source ojs_define() function into the tools environment
source(
file = file.path(resourceDir, "rmd", "ojs_static.R"),
local = .quarto_tools_env
)
}

render_output <- rmarkdown::render(
Expand All @@ -180,7 +195,7 @@ execute <- function(input, format, tempDir, libDir, dependencies, cwd, params, r
knit_root_dir = knit_root_dir,
params = params,
run_pandoc = FALSE,
envir = env
envir = globalenv()
)
knit_meta <- attr(render_output, "knit_meta")
files_dir <- attr(render_output, "files_dir")
Expand Down
25 changes: 23 additions & 2 deletions src/resources/rmd/hooks.R
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) {
}
delegating_output_hook = function(type, classes) {
delegating_hook(type, function(x, options) {
### START Knitr hack:
# since knitr 1.49, we can detect if output: asis
# was set by an R function itself (not cell option)
# We save the information for our other processing
# after output hook (i.e after sew method is called)
if (identical(options[["results"]], "asis")) {
.assignToQuartoToolsEnv("cell_options", list(asis_output = TRUE)) # nolint: object_usage_linter, line_length_linter.
}
### END

if (identical(options[["results"]], "asis") ||
isTRUE(options[["collapse"]])) {
x
Expand All @@ -182,6 +192,16 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) {
# entire chunk
knit_hooks$chunk <- delegating_hook("chunk", function(x, options) {

## START knitr hack:
## catch knit_asis output from save output hook state
asis_output <- .getFromQuartoToolsEnv("cell_options")$asis_output # nolint: object_usage_linter, line_length_linter.
if (isTRUE(asis_output)) {
options[["results"]] <- "asis"
}
# chunk hook is called last and we can clean the cell storage
on.exit(.rmFromQuartoToolsEnv("cell_options"), add = TRUE) # nolint: object_usage_linter, line_length_linter.
## END

# Do nothing more for some specific chunk content -----

# Quarto language handler
Expand Down Expand Up @@ -400,7 +420,7 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) {
# if there is a label, additional classes, a forwardAttr, or a cell.cap
# then the user is deemed to have implicitly overridden results = "asis"
# (as those features don't work w/o an enclosing div)
needCell <- isTRUE(nzchar(label)) ||
needCell <- isTRUE(nzchar(label)) ||
length(classes) > 1 ||
isTRUE(nzchar(forwardAttr)) ||
isTRUE(nzchar(cell.cap))
Expand All @@ -409,7 +429,8 @@ knitr_hooks <- function(format, resourceDir, handledLanguages) {
} else {
paste0(
options[["indent"]], "::: {",
labelId(label), paste(classes, collapse = " ") ,forwardAttr, "}\n", x, "\n", cell.cap ,
labelId(label), paste(classes, collapse = " "),
forwardAttr, "}\n", x, "\n", cell.cap,
options[["indent"]], ":::"
)
}
Expand Down
23 changes: 19 additions & 4 deletions src/resources/rmd/patch.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ wrap_asis_output <- function(options, x) {
if (identical(options[["html-table-processing"]], "none")) {
attrs <- paste(attrs, "html-table-processing=none")
}

# if this is an html table then wrap it further in ```{=html}
# (necessary b/c we no longer do this by overriding kable_html,
# which is in turn necessary to allow kableExtra to parse
Expand All @@ -112,9 +112,17 @@ wrap_asis_output <- function(options, x) {
!grepl('^<div class="kable-table">', x)) {
x <- paste0("`````{=html}\n", x, "\n`````")
}

# If asis output, don't include the output div
if (identical(options[["results"]], "asis")) return(x)

# If asis output, don't include the output div,
# unless a feature requiring it is used
# if there additional classes, some added attr,
# then the user is deemed to have implicitly overridden results = "asis"
# (as those features don't work w/o an enclosing div)
needCell <- length(classes) > 1 || isTRUE(nzchar(attrs)) # nolint: object_name_linter, line_length_linter.

if (identical(options[["results"]], "asis") && !needCell) {
return(x)
}

output_div(x, output_label_placeholder(options), classes, attrs)
}
Expand Down Expand Up @@ -152,6 +160,13 @@ if (utils::packageVersion("knitr") >= "1.32.8") {
} else {
knitr:::sew.knit_asis(x, options, ...)
}
## START knitr hack:
## catch knit_asis output from save output hook state
asis_output <- .getFromQuartoToolsEnv("cell_options")$asis_output # nolint: object_usage_linter, line_length_linter
if (isTRUE(asis_output)) {
options[["results"]] <- "asis"
}
## END

# if it's an html widget then it was already wrapped
# by add_html_caption
Expand Down
38 changes: 38 additions & 0 deletions tests/docs/smoke-all/inline-execution/inline-jupyter.md.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
_quarto: {}
title: inline jupyter expressions
toc-title: Table of contents
---

::: {.cell execution_count="1"}
``` {.python .cell-code}
from IPython.display import Markdown
x = 1
y = "foo"
z = '"foo"'
a = '"foo'
b = '*foo*'
c = "*foo*"
d = "'foo"
e = "1"
f = False
g = True
h = None
i = Markdown("*foo*")
```
:::

Here's inline output:

- 1
- foo
- "foo"
- "foo
- \*foo\*
- \*foo\*
- 'foo
- 1
- False
- True
- None
- *foo*
5 changes: 5 additions & 0 deletions tests/docs/smoke-all/inline-execution/inline-jupyter.qmd
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
---
title: "inline jupyter expressions"
format: markdown
engine: jupyter
_quarto:
tests:
markdown:
ensureSnapshotMatches: true
---

```{python}
Expand Down
38 changes: 38 additions & 0 deletions tests/docs/smoke-all/inline-execution/inline-knitr.md.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
_quarto: {}
title: inline knitr expressions
toc-title: Table of contents
---

::: cell
``` {.r .cell-code}
x = 1
y = "foo"
z = '"foo"'
a = '"foo'
b = '*foo*'
c = "*foo*"
d = "'foo"
e = "1"
f = FALSE
g = TRUE
h = NULL
i = "*foo*"
class(i) <- c("character", "asis")
```
:::

Here's inline output:

- 1
- foo
- "foo"
- "foo
- \*foo\*
- \*foo\*
- 'foo
- 1
- FALSE
- TRUE
- NULL
- \*foo\*
36 changes: 19 additions & 17 deletions tests/docs/smoke-all/inline-execution/inline-knitr.qmd
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
---
title: "inline knitr expressions"
engine: knitr
_quarto: {}
title: inline knitr expressions
toc-title: Table of contents
---

```{r}
::: cell
``` {.r .cell-code}
x = 1
y = "foo"
z = '"foo"'
Expand All @@ -18,19 +20,19 @@ h = NULL
i = "*foo*"
class(i) <- c("character", "asis")
```
:::

Here's inline output:

- `{r} x`
- `{r} y`
- `{r} z`
- `{r} a`
- `{r} b`
- `{r} c`
- `{r} d`
- `{r} e`
- `{r} f`
- `{r} g`
- `{r} h`
- `{r} i`
Here's inline output:

- 1
- foo
- "foo"
- "foo
- \*foo\*
- \*foo\*
- 'foo
- 1
- FALSE
- TRUE
- NULL
- \*foo\*
Loading