Skip to content
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

Bring support for rendering from R file #7696

Merged
merged 2 commits into from
Nov 27, 2023
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
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);
},
),
]);