Skip to content

Commit

Permalink
Merge pull request #7696 from quarto-dev/feature/spin
Browse files Browse the repository at this point in the history
Bring support for rendering from R file
  • Loading branch information
jjallaire authored Nov 27, 2023
2 parents 9d65435 + 6e4f158 commit 150ba45
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 9 deletions.
64 changes: 57 additions & 7 deletions src/execute/rmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ import {
} from "./types.ts";
import { postProcessRestorePreservedHtml } from "./engine-shared.ts";
import { mappedStringFromFile } from "../core/mapped-text.ts";
import { mappedIndexToLineCol, MappedString } from "../core/lib/mapped-text.ts";
import {
asMappedString,
mappedIndexToLineCol,
MappedString,
} from "../core/lib/mapped-text.ts";
import { lineColToIndex } from "../core/lib/text.ts";
import { executeInlineCodeHandler } from "../core/execute-inline.ts";
import { globalTempContext } from "../core/temp.ts";

const kRmdExtensions = [".rmd", ".rmarkdown"];

Expand All @@ -58,17 +63,26 @@ export const knitrEngine: ExecutionEngine = {

validExtensions: () => kRmdExtensions.concat(kRmdExtensions),

claimsFile: (_file: string, ext: string) => {
return kRmdExtensions.includes(ext.toLowerCase());
claimsFile: (file: string, ext: string) => {
return kRmdExtensions.includes(ext.toLowerCase()) ||
isKnitrSpinScript(file);
},

claimsLanguage: (language: string) => {
return language.toLowerCase() === "r";
},

target: (file: string, _quiet?: boolean, markdown?: MappedString) => {
target: async (
file: string,
_quiet?: boolean,
markdown?: MappedString,
): Promise<ExecutionTarget | undefined> => {
if (markdown === undefined) {
markdown = mappedStringFromFile(file);
if (isKnitrSpinScript(file)) {
markdown = asMappedString(await markdownFromKnitrSpinScript(file));
} else {
markdown = mappedStringFromFile(file);
}
}
let metadata;
try {
Expand All @@ -86,8 +100,12 @@ export const knitrEngine: ExecutionEngine = {
return Promise.resolve(target);
},

partitionedMarkdown: (file: string) => {
return Promise.resolve(partitionMarkdown(Deno.readTextFileSync(file)));
partitionedMarkdown: async (file: string) => {
if (isKnitrSpinScript(file)) {
return partitionMarkdown(await markdownFromKnitrSpinScript(file));
} else {
return partitionMarkdown(Deno.readTextFileSync(file));
}
},

execute: async (options: ExecuteOptions): Promise<ExecuteResult> => {
Expand Down Expand Up @@ -353,3 +371,35 @@ function resolveInlineExecute(code: string) {
(expr) => `${"`"}r .QuartoInlineRender(${expr})${"`"}`,
)(code);
}

export function isKnitrSpinScript(file: string) {
const ext = extname(file).toLowerCase();
if (ext == ".r") {
const text = Deno.readTextFileSync(file);
// Consider a .R script that can be spinned if it contains a YAML header inside a special `#'` comment
return /^\s*#'\s*---[\s\S]+?\s*#'\s*---/.test(text);
} else {
return false;
}
}

export async function markdownFromKnitrSpinScript(file: string) {
// run spin to get .qmd and get markdown from .qmd

// TODO: implement a caching system because spin is slow and it seems we call this twice for each run
// 1. First as part of the target() call
// 2. Second as part of renderProject() call to get `partitioned` information to get `resourcesFrom` with `resourceFilesFromRenderedFile()`

// we need a temp dir for `CallR` to work but we don't have access to usual options.tempDir.
const tempDir = globalTempContext().createDir();

const result = await callR<string>(
"spin",
{ input: file },
tempDir,
undefined,
true,
);

return result;
}
8 changes: 7 additions & 1 deletion src/resources/rmd/rmd.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
# execute knitr::spin
spin <- function(input) {

if (utils::packageVersion("knitr") < "1.44") {
stop(
"knitr >= 1.44 is required for rendering with Quarto from `.R` files. ",
"Please update knitr.", call. = FALSE)
}

# read file
text <- xfun::read_utf8(input)

# spin and return
knitr::spin(text = text, knit = FALSE)
knitr::spin(text = text, knit = FALSE, format = "qmd")

}

Expand Down
30 changes: 30 additions & 0 deletions tests/docs/knitr-spin.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#' ---
#' title: "A report generated from a pure R script"
#' format: html
#' ---
#'
#' This is a report generated by `knitr::spin()`.
#'
#' ## Code block {#block}
#'
#' Let's try some **knitr** options:

#| echo: false
#| fig-with: 7
# This is a normal R comment.
plot(cars)

#' ## Inline value {#inline}
#'
#' Now write an inline value. We know the value of $\pi$ is
{{ pi }}
#' .
#'
#' Finally please note that all roxygen comments are
#' optional. You do not need chunk options, either,
#' unless you want more control over the output
#' elements such as the size of plots.

# /* Write comments between /* and */ like C comments:
Sys.sleep(60)
# */
17 changes: 16 additions & 1 deletion tests/smoke/render/render-r.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { docs, fileLoader, inTempDirectory } from "../../utils.ts";
import { join } from "path/mod.ts";
import { ensureHtmlSelectorSatisfies, fileExists } from "../../verify.ts";
import { ensureHtmlElements, ensureHtmlSelectorSatisfies, fileExists } from "../../verify.ts";
import { testRender } from "./render.ts";

inTempDirectory((dir) => {
Expand Down Expand Up @@ -72,3 +72,18 @@ testRender(knitrOptions.input, "html", false, [

const sqlEngine = fileLoader()("test-knitr-sql.qmd", "html");
testRender(sqlEngine.input, "html", false);


const toSpin = fileLoader()("knitr-spin.R", "html");
testRender(toSpin.input, "html", false, [
ensureHtmlElements(
toSpin.output.outputPath, ["#block img"]
),
ensureHtmlSelectorSatisfies(
toSpin.output.outputPath,
"#inline code",
(nodeList) => {
return /^3\.14+/.test(nodeList[0].textContent);
},
),
]);

0 comments on commit 150ba45

Please sign in to comment.