From d979556342a4062139031b3877b45ba9931aad7a Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:51:34 +0000 Subject: [PATCH 01/12] feat(knitr-extras): adding mutable header and extra logging features --- NAMESPACE | 3 + R/reporter.R | 231 +++++++++++++++------------ R/utils_knitr.R | 139 ++++++++++++++++ inst/report/package/pkg_template.qmd | 9 +- man/knit_print.knitr_log.Rd | 18 +++ man/knitr_logger.Rd | 19 +++ man/knitr_mutable_header.Rd | 19 +++ 7 files changed, 333 insertions(+), 105 deletions(-) create mode 100644 R/utils_knitr.R create mode 100644 man/knit_print.knitr_log.Rd create mode 100644 man/knitr_logger.Rd create mode 100644 man/knitr_mutable_header.Rd diff --git a/NAMESPACE b/NAMESPACE index 0e24e4a..bfa4b27 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,6 +4,9 @@ export(assessment) export(check_reporter) export(environ_report) export(is_risk_error) +export(knit_print.knitr_log) +export(knitr_logger) +export(knitr_mutable_header) export(options_report) export(package_report) export(summary_table) diff --git a/R/reporter.R b/R/reporter.R index 42eccab..a0fe7c9 100644 --- a/R/reporter.R +++ b/R/reporter.R @@ -25,116 +25,139 @@ #' pr #' file.remove(pr) package_report <- function( - package_name, - package_version, - package = NULL, - template_path = system.file("report/package", package = "val.report"), - output_format = "all", - params = list(), - ... + package_name, + package_version, + package = NULL, + template_path = system.file("report/package", package = "val.report"), + output_format = "all", + params = list(), + ... ) { - empty_pkg_info <- is.empty(package_name) && is.empty(package_version) - if (empty_pkg_info && !is.empty(package)) { - package_name <- basename(package) - desc <- read.dcf(file.path(package, "DESCRIPTION")) - - stopifnot("Mismatch between path and DESCRIPTION name" = package_name == desc[, "Package"]) - package_version <- desc[, "Version"] - params$package <- package - Sys.setenv("INPUT_REPORT_PKG_DIR" = package) - } else if (empty_pkg_info && is.empty(package)) { - stop("Package information missing for the report") - } else { - params$package <- package_name - } - - full_name <- paste0(package_name, "_v", package_version) - output_file <- paste0("validation_report_", full_name, ".qmd") - - params$package_name <- package_name - params$package_version <- package_version - params$image <- get_image_name(params) - - if (is.null(template_path) || !nzchar(template_path)) { - template_path <- system.file("report/package", - package = "val.report") - } else if (!dir.exists(template_path)) { - stop("Template directory is not available") - } - - params$package <- normalizePath(params$package, mustWork = FALSE, winslash = "/") - if (length(params$assessment_path) == 1L && !nzchar(params$assessment_path)) { - params$assessment_path <- normalizePath(params$assessment_path, mustWork = TRUE, winslash = "/") - } - - # Bug on https://github.com/quarto-dev/quarto-cli/issues/5765 - v <- quarto::quarto_version() - if (v < package_version("1.7.13")) { - warning("Please install the latest (devel) version of Quarto") - } - - if (is.null(params$source)) warning("Please provide the source of the package assessment") - - # https://github.com/quarto-dev/quarto-r/issues/81#issuecomment-1375691267 - # quarto rendering happens in the same place as the file/project - # To avoid issues copy to a different place and render there. - render_dir <- output_dir() + empty_pkg_info <- is.empty(package_name) && is.empty(package_version) + if (empty_pkg_info && !is.empty(package)) { + package_name <- basename(package) + desc <- read.dcf(file.path(package, "DESCRIPTION")) + + stopifnot( + "Mismatch between path and DESCRIPTION name" = package_name == + desc[, "Package"] + ) + package_version <- desc[, "Version"] + params$package <- package + Sys.setenv("INPUT_REPORT_PKG_DIR" = package) + } else if (empty_pkg_info && is.empty(package)) { + stop("Package information missing for the report") + } else { + params$package <- package_name + } + + full_name <- paste0(package_name, "_v", package_version) + output_file <- paste0("validation_report_", full_name, ".qmd") + + params$package_name <- package_name + params$package_version <- package_version + params$image <- get_image_name(params) + + if (is.null(template_path) || !nzchar(template_path)) { + template_path <- system.file("report/package", package = "val.report") + } else if (!dir.exists(template_path)) { + stop("Template directory is not available") + } + + params$package <- normalizePath( + params$package, + mustWork = FALSE, + winslash = "/" + ) + if (length(params$assessment_path) == 1L && !nzchar(params$assessment_path)) { + params$assessment_path <- normalizePath( + params$assessment_path, + mustWork = TRUE, + winslash = "/" + ) + } + + # Bug on https://github.com/quarto-dev/quarto-cli/issues/5765 + v <- quarto::quarto_version() + if (v < package_version("1.7.13")) { + warning("Please install the latest (devel) version of Quarto") + } + + if (is.null(params$source)) { + warning("Please provide the source of the package assessment") + } + + # https://github.com/quarto-dev/quarto-r/issues/81#issuecomment-1375691267 + # quarto rendering happens in the same place as the file/project + # To avoid issues copy to a different place and render there. + render_dir <- output_dir() + if (!dir.exists(render_dir)) { + render_dir <- paste0(render_dir, "/") if (!dir.exists(render_dir)) { - render_dir <- paste0(render_dir, "/") - if (!dir.exists(render_dir)) { - stop("Render directory doesn't exists. Please check the 'getOptions(\"valreport_output_dir\")' and sys.getEnv(\"VALREPORT_OUTPUT_DIR\")" ) - } - } - files_to_copy <- list.files(template_path, full.names = TRUE) - fc <- file.copy(from = files_to_copy, - to = render_dir, - overwrite = TRUE, - copy.date = TRUE) - - if (any(!fc)) { - stop("Copying to the rendering directory failed.") - } - - template_all_files <- list.files(render_dir, full.names = TRUE) - template <- template_all_files[endsWith(template_all_files, "qmd")] - - if (length(template) > 1) { - stop("There are more than one template!\n", - "Please have only one quarto file on the directory.") - } - - file_template <- file.path(render_dir, - paste0("validation_report_", full_name, ".qmd")) - file.rename(template, file_template) - - # replace the title of the place header by the package name and header - top_page_file <- readLines(file.path(render_dir, "top_page.html")) - title_line <- grep("
- as.character() - writeLines(top_page_file, file.path(render_dir, "top_page.html")) - - pre_rendering <- list.files(render_dir, full.names = TRUE) - - suppressMessages({suppressWarnings({ - out <- quarto::quarto_render( - input = file_template, - output_format = output_format, - execute_params = params, - ... + stop( + "Render directory doesn't exists. Please check the 'getOptions(\"valreport_output_dir\")' and sys.getEnv(\"VALREPORT_OUTPUT_DIR\")" ) - })}) + } + } + files_to_copy <- list.files(template_path, full.names = TRUE) + fc <- file.copy( + from = files_to_copy, + to = render_dir, + overwrite = TRUE, + copy.date = TRUE + ) + + if (any(!fc)) { + stop("Copying to the rendering directory failed.") + } + + template_all_files <- list.files(render_dir, full.names = TRUE) + template <- template_all_files[endsWith(template_all_files, "qmd")] + + if (length(template) > 1) { + stop( + "There are more than one template!\n", + "Please have only one quarto file on the directory." + ) + } + + file_template <- file.path( + render_dir, + paste0("validation_report_", full_name, ".qmd") + ) + file.rename(template, file_template) + + # replace the title of the place header by the package name and header + top_page_file <- readLines(file.path(render_dir, "top_page.html")) + title_line <- grep("
+ as.character() + writeLines(top_page_file, file.path(render_dir, "top_page.html")) + + pre_rendering <- list.files(render_dir, full.names = TRUE) + + out <- quarto::quarto_render( + input = file_template, + output_format = output_format, + execute_params = params, + ... + ) - post_rendering <- list.files(render_dir, full.names = TRUE) + post_rendering <- list.files(render_dir, full.names = TRUE) - files_to_remove <- intersect(pre_rendering, post_rendering) - fr <- file.remove(files_to_remove) - if (any(!fr)) { - warning("Failed to remove the quarto template used from the directory.") - } + files_to_remove <- intersect(pre_rendering, post_rendering) + fr <- file.remove(files_to_remove) + if (any(!fr)) { + warning("Failed to remove the quarto template used from the directory.") + } - output_files <- setdiff(post_rendering, pre_rendering) - invisible(output_files) + output_files <- setdiff(post_rendering, pre_rendering) + invisible(output_files) } is.empty <- function(x) { diff --git a/R/utils_knitr.R b/R/utils_knitr.R new file mode 100644 index 0000000..2395e41 --- /dev/null +++ b/R/utils_knitr.R @@ -0,0 +1,139 @@ +#' Create mutable header object +#' +#' Inject a custom document rendering hook into the knitr engine and return +#' a mutable environment that we can modify, which will be used to update the +#' knitr document header upon render completion. +#' +#' @param params Optionally, provide default parameters to initialize with +#' @param envir Only used when `params` is not provided, as the source for where +#' to try to discover the default knitr frontmatter parameters. +#' +#' @export +knitr_mutable_header <- function(params = NULL, envir = parent.frame()) { + header <- new.env(parent = emptyenv()) + + # if not provided, try to get params from envir + if (is.null(params)) { + params <- get0("params", envir = envir, ifnotfound = list()) + } + + # initialize with param values + for (name in names(params)) { + header[[name]] <- params[[name]] + } + + # add a hook that will replace the hard-coded front-matter with dynamic + # front-matter just before rendering. + knitr::knit_hooks$set( + document = local({ + default_document_hook <- knitr::knit_hooks$get("document") + function(x, output) { + # extract and split our document front-matter and body + fm <- knitr:::yaml_front_matter(strsplit(x, "\n")[[1L]]) + body <- sub("^\\s*---.*---", "", x) + + # pragmatically update front-matter at build-time + fm <- yaml::yaml.load(fm) + for (name in names(header)) { + fm[[name]] <- header[[name]] + } + + # rebuild our document + x <- paste0(c("---", yaml::as.yaml(fm), "---", body), collapse = "\n") + default_document_hook(x) + } + }) + ) + + header +} + +#' Special handler for emitting knitr logs +#' +#' @inheritParams knitr::knit_print +#' +#' @export +# nolint start +knit_print.knitr_log <- local({ + # nolint end + + prefix <- " \u205A " # vertical two dot punctuation + last_log_trailing_newline <- FALSE + + function(x, ...) { + # prefix newline only for the first message in each chunk + knitr_log_env <- environment(knitr_logger) + first_chunk_log <- knitr_log_env$first_chunk_log + knitr_log_env$first_chunk_log <- FALSE + + # split content on non-character (or AsIs) objects + is_char <- vapply(x, is.character, logical(1L)) + is_asis <- vapply(x, inherits, logical(1L), "AsIs") + is_char <- is_char & !is_asis + chunks <- cumsum( + !is_char | c(FALSE, tail(is_char, -1) & !head(is_char, -1)) + ) + is_char_chunk <- vapply(split(is_char, chunks), any, logical(1L)) + + # if output is a string, join them for pretty printing; otherwise capture + # console output for logging + x <- Map( + function(chunk, is_char) { + if (is_char) { + paste(chunk, collapse = "") + } else { + paste(capture.output(chunk[[1L]]), collapse = "\n") + } + }, + chunk = split(x, chunks), + is_char = is_char_chunk + ) + + # determine where to inject newline prefixes + x <- paste0(x, collapse = "") + x <- strsplit(x, "(?<=\n)", perl = TRUE)[[1L]] + prefixed <- if (first_chunk_log || last_log_trailing_newline) TRUE else -1L + x[prefixed] <- paste0(prefix, x[prefixed]) + x <- paste0(x, collapse = "") + last_log_trailing_newline <<- endsWith(x[[length(x)]], "\n") + + # emit to stderr so that we see it immediately + if (first_chunk_log) { + cat("\n", file = stderr(), sep = "") + } + cat(x, file = stderr(), sep = "") + } +}) + +#' Create a knitr log function +#' +#' Sets necessary knitr hooks and returns a logging function that will emit +#' messages to the console during knitting. +#' +#' @return A `function` accepting `...` arguments, which will be used for +#' printing out to the console while rendering the knitr document. Character +#' values are logged directrly, while any other object is printed as though +#' being printed to the console. To render character values as they would +#' be printed, mark them as "AsIs" using [I()]. +#' +#' @export +knitr_logger <- local({ + first_chunk_log <- TRUE + + function() { + # reset our chunk start flag on chunk output + knitr::knit_hooks$set( + chunk = local({ + default_chunk_hook <- knitr::knit_hooks$get("chunk") + function(x, options) { + first_chunk_log <<- TRUE + default_chunk_hook(x, options) + } + }) + ) + + function(...) { + knitr::knit_print(structure(list(...), class = c("knitr_log", "list"))) + } + } +}) diff --git a/inst/report/package/pkg_template.qmd b/inst/report/package/pkg_template.qmd index 690b107..f4ced33 100644 --- a/inst/report/package/pkg_template.qmd +++ b/inst/report/package/pkg_template.qmd @@ -1,5 +1,5 @@ --- -title: "This report is fully automated and builds on [`r params$image`](`r paste('https://hub.docker.com/r', params$image, sep = '/')`) image." +title: "Validation Report" date-format: "YYYY-MM-DD hh:mm:ss" date: today published-title: "" @@ -49,6 +49,9 @@ knitr::opts_chunk$set( library("tools") library("knitr") library("val.report") + +header <- val.report::knitr_mutable_header() +log <- val.report::knitr_logger() ``` # Context @@ -58,6 +61,10 @@ library("val.report") pkg <- params$package pkg_name <- params$package_name pkg_version <- params$package_version + +log("Loaded package: ", pkg_name, " @ v", pkg_version, "\n") +header$title <- paste0(pkg_name, " (v", pkg_version, ")") + risk_path <- params$assessment_path image <- params$image ``` diff --git a/man/knit_print.knitr_log.Rd b/man/knit_print.knitr_log.Rd new file mode 100644 index 0000000..8478047 --- /dev/null +++ b/man/knit_print.knitr_log.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knit_print.knitr_log} +\alias{knit_print.knitr_log} +\title{Special handler for emitting knitr logs} +\usage{ +knit_print.knitr_log(x, ...) +} +\arguments{ +\item{x}{An R object to be printed} + +\item{...}{Additional arguments passed to the S3 method. Currently ignored, +except two optional arguments \code{options} and \code{inline}; see +the references below.} +} +\description{ +Special handler for emitting knitr logs +} diff --git a/man/knitr_logger.Rd b/man/knitr_logger.Rd new file mode 100644 index 0000000..74e83d1 --- /dev/null +++ b/man/knitr_logger.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knitr_logger} +\alias{knitr_logger} +\title{Create a knitr log function} +\usage{ +knitr_logger() +} +\value{ +A \code{function} accepting \code{...} arguments, which will be used for +printing out to the console while rendering the knitr document. Character +values are logged directrly, while any other object is printed as though +being printed to the console. To render character values as they would +be printed, mark them as "AsIs" using \code{\link[=I]{I()}}. +} +\description{ +Sets necessary knitr hooks and returns a logging function that will emit +messages to the console during knitting. +} diff --git a/man/knitr_mutable_header.Rd b/man/knitr_mutable_header.Rd new file mode 100644 index 0000000..dd3dcb9 --- /dev/null +++ b/man/knitr_mutable_header.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knitr_mutable_header} +\alias{knitr_mutable_header} +\title{Create mutable header object} +\usage{ +knitr_mutable_header(params = NULL, envir = parent.frame()) +} +\arguments{ +\item{params}{Optionally, provide default parameters to initialize with} + +\item{envir}{Only used when \code{params} is not provided, as the source for where +to try to discover the default knitr frontmatter parameters.} +} +\description{ +Inject a custom document rendering hook into the knitr engine and return +a mutable environment that we can modify, which will be used to update the +knitr document header upon render completion. +} From f6fa6cc77598769e8974efe787b84357dc9572f9 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:06:15 +0000 Subject: [PATCH 02/12] chore: update NAMESPACE --- NAMESPACE | 3 +++ R/utils_knitr.R | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index bfa4b27..455513f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,4 +13,7 @@ export(summary_table) importFrom(htmltools,div) importFrom(methods,is) importFrom(tools,check_packages_in_dir_details) +importFrom(utils,capture.output) importFrom(utils,getFromNamespace) +importFrom(utils,head) +importFrom(utils,tail) diff --git a/R/utils_knitr.R b/R/utils_knitr.R index 2395e41..4120dc2 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -29,8 +29,8 @@ knitr_mutable_header <- function(params = NULL, envir = parent.frame()) { default_document_hook <- knitr::knit_hooks$get("document") function(x, output) { # extract and split our document front-matter and body - fm <- knitr:::yaml_front_matter(strsplit(x, "\n")[[1L]]) - body <- sub("^\\s*---.*---", "", x) + body <- sub("^\\s*(---|\\.\\.\\.).*\\1", "", x) + fm <- substring(x, 0L, nchar(x) - nchar(body)) # pragmatically update front-matter at build-time fm <- yaml::yaml.load(fm) @@ -52,6 +52,7 @@ knitr_mutable_header <- function(params = NULL, envir = parent.frame()) { #' #' @inheritParams knitr::knit_print #' +#' @importFrom utils head tail capture.output #' @export # nolint start knit_print.knitr_log <- local({ From 81ff316198b20a7c916193c7a5d681b17fcf1242 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:35:52 -0500 Subject: [PATCH 03/12] feat(knitr-extras): Adding another that was missed in the first pass --- NAMESPACE | 1 + R/utils_knitr.R | 32 ++++++++++++++++++++++++++++++++ man/knitr_update_options.Rd | 22 ++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 man/knitr_update_options.Rd diff --git a/NAMESPACE b/NAMESPACE index 455513f..36a10ac 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,7 @@ export(is_risk_error) export(knit_print.knitr_log) export(knitr_logger) export(knitr_mutable_header) +export(knitr_update_options) export(options_report) export(package_report) export(summary_table) diff --git a/R/utils_knitr.R b/R/utils_knitr.R index 4120dc2..e80071c 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -1,3 +1,35 @@ +#' Handler for complex option passing through from a quarto parameter +#' +#' Importantly, handles S7 objects which cannot be passed through as a quarto +#' parameter because they can not be deparsed. Allows passing arbitrary +#' expressions using the `!expr` prefix used by `yaml`. +#' +#' @param opts A `list` of options. For any complex values that cannot be passed +#' through the `yaml` frontmatter, you may pass them as an expression string +#' such as `"!expr mtcars"`. +#' +#' @return The `opts` list after parsing any complex expressions. This function +#' is used primarily for modifying the global state by calling [`options()`] +#' +#' @export +knitr_update_options <- function(opts) { + opts <- as.list(opts) + opt_is_char <- vapply(opts, is.character, logical(1L)) + opt_is_expr <- opt_is_char + opt_is_expr[opt_is_char] <- startsWith( + as.character(opts[opt_is_char]), + "!expr" + ) + opts[opt_is_expr] <- lapply( + opts[opt_is_expr], + function(expr) eval(parse(text = sub("^!expr", "", expr))) + ) + + do.call(options, opts) + opts +} + + #' Create mutable header object #' #' Inject a custom document rendering hook into the knitr engine and return diff --git a/man/knitr_update_options.Rd b/man/knitr_update_options.Rd new file mode 100644 index 0000000..57d9e70 --- /dev/null +++ b/man/knitr_update_options.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\name{knitr_update_options} +\alias{knitr_update_options} +\title{Handler for complex option passing through from a quarto parameter} +\usage{ +knitr_update_options(opts) +} +\arguments{ +\item{opts}{A \code{list} of options. For any complex values that cannot be passed +through the \code{yaml} frontmatter, you may pass them as an expression string +such as \code{"!expr mtcars"}.} +} +\value{ +The \code{opts} list after parsing any complex expressions. This function +is used primarily for modifying the global state by calling \code{\link[=options]{options()}} +} +\description{ +Importantly, handles S7 objects which cannot be passed through as a quarto +parameter because they can not be deparsed. Allows passing arbitrary +expressions using the \code{!expr} prefix used by \code{yaml}. +} From cd25d4685eb4ef9ff0585a9614528cceffef8f11 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:51:02 -0500 Subject: [PATCH 04/12] chore: add tests; apply review feedback --- DESCRIPTION | 4 +- NAMESPACE | 1 + R/utils_knitr.R | 38 +++++++--- man/knitr_update_options.Rd | 35 +++++++-- tests/testthat/test-utils-knitr.R | 114 ++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 tests/testthat/test-utils-knitr.R diff --git a/DESCRIPTION b/DESCRIPTION index f97fe60..a2c29a4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,11 +17,13 @@ Imports: methods, htmltools, quarto (>= 1.4.4), - reactable + reactable, + yaml Suggests: knitr, riskmetric (>= 0.2.4), rmarkdown, + S7, spelling, testthat (>= 3.0.0), withr (>= 3.0.2) diff --git a/NAMESPACE b/NAMESPACE index 36a10ac..912a718 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -18,3 +18,4 @@ importFrom(utils,capture.output) importFrom(utils,getFromNamespace) importFrom(utils,head) importFrom(utils,tail) +importFrom(yaml,yaml.load) diff --git a/R/utils_knitr.R b/R/utils_knitr.R index e80071c..0cd8733 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -1,18 +1,38 @@ #' Handler for complex option passing through from a quarto parameter #' -#' Importantly, handles S7 objects which cannot be passed through as a quarto -#' parameter because they can not be deparsed. Allows passing arbitrary -#' expressions using the `!expr` prefix used by `yaml`. +#' Importantly, handles [`S7::new_class()`] objects which cannot be passed +#' through as a `quarto` command-line parameter because they can not be +#' deparsed. This helper allows passing arbitrary expressions using the +#' `!expr` prefix, which is standardized by `yaml`. #' -#' @param opts A `list` of options. For any complex values that cannot be passed -#' through the `yaml` frontmatter, you may pass them as an expression string -#' such as `"!expr mtcars"`. +#' @param opts A `list` of options. For any non-serializable values that would +#' fail using your preferred interface to `knitr`, you may pass them as an +#' expression string such as `"!expr mtcars"`. Notably, +#' [`quarto::quarto_render`], which attempts to serialize objects for passing +#' to the command-line `quarto`, will fail for such objects and requires the +#' `"!expr"` prefix. #' #' @return The `opts` list after parsing any complex expressions. This function #' is used primarily for modifying the global state by calling [`options()`] #' +#' @examples +#' # in a knitr document, we'll imagine we have some `params` object that we +#' # want to use to set options: +#' params <- list(options = list( +#' # most objects can be passed just fine +#' example_1 = list(a = 1L, b = "two"), +#' +#' # but objects that cannot be deserialized from their serialized (using, +#' # for example [`dput()`], [`dump()`], and [`deparse()`]) expression would +#' # fail as they are attempted to be passed to the `quarto` command-line +#' # utility. +#' complex_option = '!expr S7::new_class("example")()' +#' )) +#' +#' knitr_update_options(params$options) +#' #' @export -knitr_update_options <- function(opts) { +knitr_update_options <- function(opts, envir = parent.frame()) { opts <- as.list(opts) opt_is_char <- vapply(opts, is.character, logical(1L)) opt_is_expr <- opt_is_char @@ -20,9 +40,10 @@ knitr_update_options <- function(opts) { as.character(opts[opt_is_char]), "!expr" ) + opts[opt_is_expr] <- lapply( opts[opt_is_expr], - function(expr) eval(parse(text = sub("^!expr", "", expr))) + function(expr) eval(parse(text = sub("^!expr", "", expr)), envir = envir) ) do.call(options, opts) @@ -40,6 +61,7 @@ knitr_update_options <- function(opts) { #' @param envir Only used when `params` is not provided, as the source for where #' to try to discover the default knitr frontmatter parameters. #' +#' @importFrom yaml yaml.load #' @export knitr_mutable_header <- function(params = NULL, envir = parent.frame()) { header <- new.env(parent = emptyenv()) diff --git a/man/knitr_update_options.Rd b/man/knitr_update_options.Rd index 57d9e70..2b619bc 100644 --- a/man/knitr_update_options.Rd +++ b/man/knitr_update_options.Rd @@ -4,19 +4,40 @@ \alias{knitr_update_options} \title{Handler for complex option passing through from a quarto parameter} \usage{ -knitr_update_options(opts) +knitr_update_options(opts, envir = parent.frame()) } \arguments{ -\item{opts}{A \code{list} of options. For any complex values that cannot be passed -through the \code{yaml} frontmatter, you may pass them as an expression string -such as \code{"!expr mtcars"}.} +\item{opts}{A \code{list} of options. For any non-serializable values that would +fail using your preferred interface to \code{knitr}, you may pass them as an +expression string such as \code{"!expr mtcars"}. Notably, +\code{\link[quarto:quarto_render]{quarto::quarto_render}}, which attempts to serialize objects for passing +to the command-line \code{quarto}, will fail for such objects and requires the +\code{"!expr"} prefix.} } \value{ The \code{opts} list after parsing any complex expressions. This function is used primarily for modifying the global state by calling \code{\link[=options]{options()}} } \description{ -Importantly, handles S7 objects which cannot be passed through as a quarto -parameter because they can not be deparsed. Allows passing arbitrary -expressions using the \code{!expr} prefix used by \code{yaml}. +Importantly, handles \code{\link[S7:new_class]{S7::new_class()}} objects which cannot be passed +through as a \code{quarto} command-line parameter because they can not be +deparsed. This helper allows passing arbitrary expressions using the +\code{!expr} prefix, which is standardized by \code{yaml}. +} +\examples{ +# in a knitr document, we'll imagine we have some `params` object that we +# want to use to set options: +params <- list(options = list( + # most objects can be passed just fine + example_1 = list(a = 1L, b = "two"), + + # but objects that cannot be deserialized from their serialized (using, + # for example [`dput()`], [`dump()`], and [`deparse()`]) expression would + # fail as they are attempted to be passed to the `quarto` command-line + # utility. + complex_option = '!expr S7::new_class("example")()' +)) + +knitr_update_options(params$options) + } diff --git a/tests/testthat/test-utils-knitr.R b/tests/testthat/test-utils-knitr.R new file mode 100644 index 0000000..e650323 --- /dev/null +++ b/tests/testthat/test-utils-knitr.R @@ -0,0 +1,114 @@ +example_1 <- list(a = 8L, b = "bee") +example_2 <- S7::new_class("test", package = "example")() +example_2_yaml_expr <- '!expr S7::new_class("test", package = "example")()' + +params_r <- list( + options = list( + example_1 = example_1, + example_2 = example_2 + ) +) + +params_expr <- list( + options = list( + example_1 = example_1, + example_2 = example_2_yaml_expr + ) +) + +example_qmd <- tempfile(fileext = ".qmd") +example_rds <- tempfile(fileext = ".Rds") +example_parsed_options <- function() readRDS(example_rds) + +writeLines( + con = example_qmd, + sprintf( + ' +--- +title: Example +params: { options: NULL } +--- +```{r} +library(val.report) +opts <- knitr_update_options(params$options) +saveRDS(opts, "%s") +```', + example_rds + ) +) + +test_that("knitr_update_options can accept complex R objects", { + opts <- options() + on.exit(options(opts)) + + expect_silent(knitr_update_options(params_r$options)) + expect_identical(getOption("example_1"), example_1) + expect_identical(getOption("example_2"), example_2) +}) + +test_that( + paste( + "knitr_update_options accepts complex R objects through", + "rmarkdown::render" + ), + { + skip_if_not_installed("rmarkdown") + + opts <- options() + on.exit(options(opts)) + + # this would work even without our function by just setting + # `options(params$options)`. However, we want to make sure it still works + # when it gets passed through our helper + rmarkdown::render(example_qmd, params = params_r) + parsed_options <- example_parsed_options() + + expect_identical(parsed_options$example_2, example_2) + } +) + +test_that("knitr_update_options is still required for quarto::quarto_render", { + skip_if_not_installed("quarto") + skip_if(is.null(quarto::quarto_path())) + + # ignore this test on cran because it is testing behavior of another package + skip_on_cran() + + opts <- options() + on.exit(options(opts)) + + # if this test starts to fail, it probably means that S7 has found a solution + # to serializing their objects, removing the need for `knitr_update_options` + # + # See: + # - https://github.com/RConsortium/S7/issues/225 + # - https://github.com/RConsortium/S7/issues/524 + # + quarto::quarto_render(example_qmd, execute_params = params_r) |> + expect_error("not implemented.*'object'") |> + expect_warning("'object'") +}) + +test_that( + paste( + "knitr_update_options accepts complex R objects through", + "quarto::quarto_render" + ), + { + skip_if_not_installed("quarto") + skip_if(is.null(quarto::quarto_path())) + + opts <- options() + on.exit(options(opts)) + + quarto::quarto_render( + example_qmd, + execute_params = params_expr, + quiet = TRUE + ) |> + expect_silent() + + parsed_options <- example_parsed_options() + expect_identical(parsed_options$example_2, example_2) + } +) From 7fac550c34ab6db6cbaefcac4a21873ee435d567 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:10:44 -0500 Subject: [PATCH 05/12] chore: adding tests, adding note about exports --- R/utils_knitr.R | 42 +++++++++++++------- man/knit_print.knitr_log.Rd | 2 +- man/knitr_logger.Rd | 3 ++ man/knitr_mutable_header.Rd | 5 ++- man/knitr_update_options.Rd | 3 ++ tests/testthat/test-utils-knitr.R | 65 ++++++++++++++++++++++++------- 6 files changed, 92 insertions(+), 28 deletions(-) diff --git a/R/utils_knitr.R b/R/utils_knitr.R index 0cd8733..92496a0 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -1,3 +1,15 @@ +roxygen2_knitr_note <- function() { + sprintf( + paste( + "`%s` `knitr` utilities are exported for use in", + "reports maintained by the R Validation Hub.", + "If you choose to use these functions for other purposes, be aware", + "that these are not considered stable for broader use." + ), + packageName() + ) +} + #' Handler for complex option passing through from a quarto parameter #' #' Importantly, handles [`S7::new_class()`] objects which cannot be passed @@ -5,6 +17,8 @@ #' deparsed. This helper allows passing arbitrary expressions using the #' `!expr` prefix, which is standardized by `yaml`. #' +#' @note `r roxygen2_knitr_note()` +#' #' @param opts A `list` of options. For any non-serializable values that would #' fail using your preferred interface to `knitr`, you may pass them as an #' expression string such as `"!expr mtcars"`. Notably, @@ -57,32 +71,25 @@ knitr_update_options <- function(opts, envir = parent.frame()) { #' a mutable environment that we can modify, which will be used to update the #' knitr document header upon render completion. #' +#' @note `r roxygen2_knitr_note()` +#' #' @param params Optionally, provide default parameters to initialize with #' @param envir Only used when `params` is not provided, as the source for where #' to try to discover the default knitr frontmatter parameters. #' #' @importFrom yaml yaml.load #' @export -knitr_mutable_header <- function(params = NULL, envir = parent.frame()) { +knitr_mutable_header <- function() { header <- new.env(parent = emptyenv()) - # if not provided, try to get params from envir - if (is.null(params)) { - params <- get0("params", envir = envir, ifnotfound = list()) - } - - # initialize with param values - for (name in names(params)) { - header[[name]] <- params[[name]] - } - # add a hook that will replace the hard-coded front-matter with dynamic # front-matter just before rendering. knitr::knit_hooks$set( document = local({ default_document_hook <- knitr::knit_hooks$get("document") - function(x, output) { + function(x, options) { # extract and split our document front-matter and body + x <- paste(x, collapse = "\n") body <- sub("^\\s*(---|\\.\\.\\.).*\\1", "", x) fm <- substring(x, 0L, nchar(x) - nchar(body)) @@ -115,7 +122,13 @@ knit_print.knitr_log <- local({ prefix <- " \u205A " # vertical two dot punctuation last_log_trailing_newline <- FALSE - function(x, ...) { + # "progress" is used by knitr::knit function internally to store whether + # progress should be written to console, equivalent to !quiet + function(x, ..., quiet = !knitr::opts_knit$get("progress")) { + if (quiet) { + return() + } + # prefix newline only for the first message in each chunk knitr_log_env <- environment(knitr_logger) first_chunk_log <- knitr_log_env$first_chunk_log @@ -156,6 +169,7 @@ knit_print.knitr_log <- local({ if (first_chunk_log) { cat("\n", file = stderr(), sep = "") } + cat(x, file = stderr(), sep = "") } }) @@ -165,6 +179,8 @@ knit_print.knitr_log <- local({ #' Sets necessary knitr hooks and returns a logging function that will emit #' messages to the console during knitting. #' +#' @note `r roxygen2_knitr_note()` +#' #' @return A `function` accepting `...` arguments, which will be used for #' printing out to the console while rendering the knitr document. Character #' values are logged directrly, while any other object is printed as though diff --git a/man/knit_print.knitr_log.Rd b/man/knit_print.knitr_log.Rd index 8478047..efab128 100644 --- a/man/knit_print.knitr_log.Rd +++ b/man/knit_print.knitr_log.Rd @@ -4,7 +4,7 @@ \alias{knit_print.knitr_log} \title{Special handler for emitting knitr logs} \usage{ -knit_print.knitr_log(x, ...) +knit_print.knitr_log(x, ..., quiet = !knitr::opts_knit$get("progress")) } \arguments{ \item{x}{An R object to be printed} diff --git a/man/knitr_logger.Rd b/man/knitr_logger.Rd index 74e83d1..b66b463 100644 --- a/man/knitr_logger.Rd +++ b/man/knitr_logger.Rd @@ -17,3 +17,6 @@ be printed, mark them as "AsIs" using \code{\link[=I]{I()}}. Sets necessary knitr hooks and returns a logging function that will emit messages to the console during knitting. } +\note{ +\code{val.report} \code{knitr} utilities are exported for use in reports maintained by the R Validation Hub. If you choose to use these functions for other purposes, be aware that these are not considered stable for broader use. +} diff --git a/man/knitr_mutable_header.Rd b/man/knitr_mutable_header.Rd index dd3dcb9..1ea28fd 100644 --- a/man/knitr_mutable_header.Rd +++ b/man/knitr_mutable_header.Rd @@ -4,7 +4,7 @@ \alias{knitr_mutable_header} \title{Create mutable header object} \usage{ -knitr_mutable_header(params = NULL, envir = parent.frame()) +knitr_mutable_header() } \arguments{ \item{params}{Optionally, provide default parameters to initialize with} @@ -17,3 +17,6 @@ Inject a custom document rendering hook into the knitr engine and return a mutable environment that we can modify, which will be used to update the knitr document header upon render completion. } +\note{ +\code{val.report} \code{knitr} utilities are exported for use in reports maintained by the R Validation Hub. If you choose to use these functions for other purposes, be aware that these are not considered stable for broader use. +} diff --git a/man/knitr_update_options.Rd b/man/knitr_update_options.Rd index 2b619bc..f4d3182 100644 --- a/man/knitr_update_options.Rd +++ b/man/knitr_update_options.Rd @@ -24,6 +24,9 @@ through as a \code{quarto} command-line parameter because they can not be deparsed. This helper allows passing arbitrary expressions using the \code{!expr} prefix, which is standardized by \code{yaml}. } +\note{ +\code{val.report} \code{knitr} utilities are exported for use in reports maintained by the R Validation Hub. If you choose to use these functions for other purposes, be aware that these are not considered stable for broader use. +} \examples{ # in a knitr document, we'll imagine we have some `params` object that we # want to use to set options: diff --git a/tests/testthat/test-utils-knitr.R b/tests/testthat/test-utils-knitr.R index e650323..2296120 100644 --- a/tests/testthat/test-utils-knitr.R +++ b/tests/testthat/test-utils-knitr.R @@ -28,8 +28,19 @@ writeLines( title: Example params: { options: NULL } --- -```{r} + +```{r, echo = FALSE} library(val.report) + +# logging +log <- knitr_logger() +log("test 1 2 3\n") + +# update header +header <- knitr_mutable_header() +header$title <- "Mutable Title" + +# update and save out options opts <- knitr_update_options(params$options) saveRDS(opts, "%s") ```', @@ -54,13 +65,10 @@ test_that( { skip_if_not_installed("rmarkdown") - opts <- options() - on.exit(options(opts)) - # this would work even without our function by just setting # `options(params$options)`. However, we want to make sure it still works # when it gets passed through our helper - rmarkdown::render(example_qmd, params = params_r) + rmarkdown::render(example_qmd, params = params_r, quiet = TRUE) parsed_options <- example_parsed_options() expect_identical(parsed_options$example_2, example_2) @@ -69,14 +77,14 @@ test_that( test_that("knitr_update_options is still required for quarto::quarto_render", { skip_if_not_installed("quarto") - skip_if(is.null(quarto::quarto_path())) + skip_if( + is.null(quarto::quarto_path()), + "system `quarto` executable unavailable" + ) # ignore this test on cran because it is testing behavior of another package skip_on_cran() - opts <- options() - on.exit(options(opts)) - # if this test starts to fail, it probably means that S7 has found a solution # to serializing their objects, removing the need for `knitr_update_options` # @@ -96,10 +104,10 @@ test_that( ), { skip_if_not_installed("quarto") - skip_if(is.null(quarto::quarto_path())) - - opts <- options() - on.exit(options(opts)) + skip_if( + is.null(quarto::quarto_path()), + "system `quarto` executable unavailable" + ) quarto::quarto_render( example_qmd, @@ -112,3 +120,34 @@ test_that( expect_identical(parsed_options$example_2, example_2) } ) + +test_that("knitr_mutable_header can modify yaml frontmatter during runtime", { + skip_if_not_installed("rmarkdown") + + rmarkdown::render( + example_qmd, + # pass content that will be used to modify the yaml frontmatter at runtime + output_file = example_md <- tempfile(fileext = ".md"), + output_format = rmarkdown::github_document(), + quiet = TRUE + ) + + content <- readLines(example_md) + expect_match(paste(content, collapse = "\n"), "^Mutable Title\n===", ) +}) + +test_that("knitr_logger writes to console output", { + skip_if_not_installed("rmarkdown") + + output <- capture.output({ + messages <- capture.output(type = "message", { + rmarkdown::render( + example_qmd, + output_file = example_md <- tempfile(fileext = ".md"), + output_format = rmarkdown::github_document() + ) + }) + }) + + expect_true(any(grepl("test 1 2 3", messages))) +}) From 512e8dd49902bc6641fa9bb06646a2d14e12e996 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:35:11 -0500 Subject: [PATCH 06/12] chore: add requiresNamespace check for suggests deps, fix yaml header parsing --- NAMESPACE | 1 + R/reporter.R | 19 +++++++++++++------ R/utils_knitr.R | 21 +++++++++++++++------ man/knit_print.knitr_log.Rd | 2 +- man/knitr_mutable_header.Rd | 6 ------ man/knitr_update_options.Rd | 2 ++ man/package_report.Rd | 15 ++++++++++----- 7 files changed, 42 insertions(+), 24 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 912a718..f067a1d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,5 +17,6 @@ importFrom(tools,check_packages_in_dir_details) importFrom(utils,capture.output) importFrom(utils,getFromNamespace) importFrom(utils,head) +importFrom(utils,packageName) importFrom(utils,tail) importFrom(yaml,yaml.load) diff --git a/R/reporter.R b/R/reporter.R index a0fe7c9..c3efc85 100644 --- a/R/reporter.R +++ b/R/reporter.R @@ -2,28 +2,35 @@ #' #' @param package_name Package name. #' @param package_version Package version number. -#' @param package Path where to find a package source to retrieve name and version number. -#' @param template_path Path to a directory with one quarto template file (and the files required for rendering it). +#' @param package Path where to find a package source to retrieve name and +#' version number. +#' @param template_path Path to a directory with one quarto template file (and +#' the files required for rendering it). #' @param output_format Output format for the report. Default is "all". #' @param params A list of execute parameters passed to the template #' @param ... Additional arguments passed to `quarto::quarto_render()` #' #' @return A path to the reports generated, called by its side effects. -#' @details Please include source as part of `params` content. Source is returned after -#' calling function `riskmetric::pkg_ref` before the risk assessment is executed -#' @export +#' +#' @details Please include source as part of `params` content. Source is +#' returned after calling function `riskmetric::pkg_ref` before the risk +#' assessment is executed +#' #' @examples #' options("valreport_output_dir" = tempdir()) #' pr <- package_report( #' package_name = "dplyr", #' package_version = "1.1.4", #' params = list( -#' assessment_path = system.file("assessments/dplyr.rds", package = "val.report"), +#' assessment_path = +#' system.file("assessments/dplyr.rds", package = "val.report"), #' image = "rhub/ref-image"), #' quiet = FALSE #' ) #' pr #' file.remove(pr) +#' +#' @export package_report <- function( package_name, package_version, diff --git a/R/utils_knitr.R b/R/utils_knitr.R index 92496a0..b77c2f2 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -1,3 +1,4 @@ +#' @importFrom utils packageName roxygen2_knitr_note <- function() { sprintf( paste( @@ -25,6 +26,7 @@ roxygen2_knitr_note <- function() { #' [`quarto::quarto_render`], which attempts to serialize objects for passing #' to the command-line `quarto`, will fail for such objects and requires the #' `"!expr"` prefix. +#' @param envir `environment` in which expressions should be evaluated. #' #' @return The `opts` list after parsing any complex expressions. This function #' is used primarily for modifying the global state by calling [`options()`] @@ -73,13 +75,14 @@ knitr_update_options <- function(opts, envir = parent.frame()) { #' #' @note `r roxygen2_knitr_note()` #' -#' @param params Optionally, provide default parameters to initialize with -#' @param envir Only used when `params` is not provided, as the source for where -#' to try to discover the default knitr frontmatter parameters. -#' #' @importFrom yaml yaml.load #' @export knitr_mutable_header <- function() { + if (!requireNamespace("knitr", quietly = TRUE)) { + warning("`knitr` is not available for use with knitr helpers") + return() + } + header <- new.env(parent = emptyenv()) # add a hook that will replace the hard-coded front-matter with dynamic @@ -90,7 +93,7 @@ knitr_mutable_header <- function() { function(x, options) { # extract and split our document front-matter and body x <- paste(x, collapse = "\n") - body <- sub("^\\s*(---|\\.\\.\\.).*\\1", "", x) + body <- sub("^\\s*(---|\\.\\.\\.).*?\\1", "", x, perl = TRUE) fm <- substring(x, 0L, nchar(x) - nchar(body)) # pragmatically update front-matter at build-time @@ -124,7 +127,8 @@ knit_print.knitr_log <- local({ # "progress" is used by knitr::knit function internally to store whether # progress should be written to console, equivalent to !quiet - function(x, ..., quiet = !knitr::opts_knit$get("progress")) { + function(x, ...) { + quiet <- !knitr::opts_knit$get("progress") if (quiet) { return() } @@ -192,6 +196,11 @@ knitr_logger <- local({ first_chunk_log <- TRUE function() { + if (!requireNamespace("knitr", quietly = TRUE)) { + warning("`knitr` is not available for use with knitr helpers") + return() + } + # reset our chunk start flag on chunk output knitr::knit_hooks$set( chunk = local({ diff --git a/man/knit_print.knitr_log.Rd b/man/knit_print.knitr_log.Rd index efab128..8478047 100644 --- a/man/knit_print.knitr_log.Rd +++ b/man/knit_print.knitr_log.Rd @@ -4,7 +4,7 @@ \alias{knit_print.knitr_log} \title{Special handler for emitting knitr logs} \usage{ -knit_print.knitr_log(x, ..., quiet = !knitr::opts_knit$get("progress")) +knit_print.knitr_log(x, ...) } \arguments{ \item{x}{An R object to be printed} diff --git a/man/knitr_mutable_header.Rd b/man/knitr_mutable_header.Rd index 1ea28fd..6e0bda8 100644 --- a/man/knitr_mutable_header.Rd +++ b/man/knitr_mutable_header.Rd @@ -6,12 +6,6 @@ \usage{ knitr_mutable_header() } -\arguments{ -\item{params}{Optionally, provide default parameters to initialize with} - -\item{envir}{Only used when \code{params} is not provided, as the source for where -to try to discover the default knitr frontmatter parameters.} -} \description{ Inject a custom document rendering hook into the knitr engine and return a mutable environment that we can modify, which will be used to update the diff --git a/man/knitr_update_options.Rd b/man/knitr_update_options.Rd index f4d3182..e0ca853 100644 --- a/man/knitr_update_options.Rd +++ b/man/knitr_update_options.Rd @@ -13,6 +13,8 @@ expression string such as \code{"!expr mtcars"}. Notably, \code{\link[quarto:quarto_render]{quarto::quarto_render}}, which attempts to serialize objects for passing to the command-line \code{quarto}, will fail for such objects and requires the \code{"!expr"} prefix.} + +\item{envir}{\code{environment} in which expressions should be evaluated.} } \value{ The \code{opts} list after parsing any complex expressions. This function diff --git a/man/package_report.Rd b/man/package_report.Rd index 9b69335..debaf52 100644 --- a/man/package_report.Rd +++ b/man/package_report.Rd @@ -19,9 +19,11 @@ package_report( \item{package_version}{Package version number.} -\item{package}{Path where to find a package source to retrieve name and version number.} +\item{package}{Path where to find a package source to retrieve name and +version number.} -\item{template_path}{Path to a directory with one quarto template file (and the files required for rendering it).} +\item{template_path}{Path to a directory with one quarto template file (and +the files required for rendering it).} \item{output_format}{Output format for the report. Default is "all".} @@ -36,8 +38,9 @@ A path to the reports generated, called by its side effects. Reports about a package } \details{ -Please include source as part of \code{params} content. Source is returned after -calling function \code{riskmetric::pkg_ref} before the risk assessment is executed +Please include source as part of \code{params} content. Source is +returned after calling function \code{riskmetric::pkg_ref} before the risk +assessment is executed } \examples{ options("valreport_output_dir" = tempdir()) @@ -45,10 +48,12 @@ pr <- package_report( package_name = "dplyr", package_version = "1.1.4", params = list( - assessment_path = system.file("assessments/dplyr.rds", package = "val.report"), + assessment_path = + system.file("assessments/dplyr.rds", package = "val.report"), image = "rhub/ref-image"), quiet = FALSE ) pr file.remove(pr) + } From 28ec1cf65d58e8d7f153e03ee309b476fc74a1cf Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:07:54 -0500 Subject: [PATCH 07/12] chore: make style of logging output dependent on whether quarto or knitr/rmarkdown is used --- R/utils_knitr.R | 42 +++++++++++++++++++++++++++++++------ man/knit_print.knitr_log.Rd | 2 +- man/knitr_logger_styles.Rd | 16 ++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 man/knitr_logger_styles.Rd diff --git a/R/utils_knitr.R b/R/utils_knitr.R index b77c2f2..b54c8b4 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -112,6 +112,20 @@ knitr_mutable_header <- function() { header } +#' Various elements used for tailoring output style to command-line utility +knitr_logger_styles <- list( + knitr = list( + chunk_prefix = "\n", + line_prefix = " \u205A ", # vertical two dot punctuation + chunk_suffix = "" + ), + quarto = list( + chunk_prefix = "\n", + line_prefix = " ", + chunk_suffix = "\n" + ) +) + #' Special handler for emitting knitr logs #' #' @inheritParams knitr::knit_print @@ -121,18 +135,19 @@ knitr_mutable_header <- function() { # nolint start knit_print.knitr_log <- local({ # nolint end - - prefix <- " \u205A " # vertical two dot punctuation last_log_trailing_newline <- FALSE + style <- knitr_logger_styles # "progress" is used by knitr::knit function internally to store whether # progress should be written to console, equivalent to !quiet - function(x, ...) { + function(x, ..., output_style = c("knitr", "quarto")) { quiet <- !knitr::opts_knit$get("progress") if (quiet) { return() } + output_style <- match.arg(output_style) + # prefix newline only for the first message in each chunk knitr_log_env <- environment(knitr_logger) first_chunk_log <- knitr_log_env$first_chunk_log @@ -165,13 +180,13 @@ knit_print.knitr_log <- local({ x <- paste0(x, collapse = "") x <- strsplit(x, "(?<=\n)", perl = TRUE)[[1L]] prefixed <- if (first_chunk_log || last_log_trailing_newline) TRUE else -1L - x[prefixed] <- paste0(prefix, x[prefixed]) + x[prefixed] <- paste0(style[[output_style]]$line_prefix, x[prefixed]) x <- paste0(x, collapse = "") last_log_trailing_newline <<- endsWith(x[[length(x)]], "\n") # emit to stderr so that we see it immediately if (first_chunk_log) { - cat("\n", file = stderr(), sep = "") + cat(style[[output_style]]$chunk_prefix, file = stderr(), sep = "") } cat(x, file = stderr(), sep = "") @@ -194,6 +209,7 @@ knit_print.knitr_log <- local({ #' @export knitr_logger <- local({ first_chunk_log <- TRUE + style <- knitr_logger_styles function() { if (!requireNamespace("knitr", quietly = TRUE)) { @@ -201,11 +217,22 @@ knitr_logger <- local({ return() } + # infer what what style to adopt for our console output + output_style <- "knitr" + if (nzchar(Sys.getenv("QUARTO_DOCUMENT_PATH"))) { + output_style <- "quarto" + } + # reset our chunk start flag on chunk output knitr::knit_hooks$set( chunk = local({ default_chunk_hook <- knitr::knit_hooks$get("chunk") function(x, options) { + # if we've emitted any logs in this chunk, print the prefix + if (!first_chunk_log) { + cat(style[[output_style]]$chunk_suffix, file = stderr(), sep = "") + } + first_chunk_log <<- TRUE default_chunk_hook(x, options) } @@ -213,7 +240,10 @@ knitr_logger <- local({ ) function(...) { - knitr::knit_print(structure(list(...), class = c("knitr_log", "list"))) + knitr::knit_print( + structure(list(...), class = c("knitr_log", "list")), + output_style = output_style + ) } } }) diff --git a/man/knit_print.knitr_log.Rd b/man/knit_print.knitr_log.Rd index 8478047..78018f1 100644 --- a/man/knit_print.knitr_log.Rd +++ b/man/knit_print.knitr_log.Rd @@ -4,7 +4,7 @@ \alias{knit_print.knitr_log} \title{Special handler for emitting knitr logs} \usage{ -knit_print.knitr_log(x, ...) +knit_print.knitr_log(x, ..., output_style = c("knitr", "quarto")) } \arguments{ \item{x}{An R object to be printed} diff --git a/man/knitr_logger_styles.Rd b/man/knitr_logger_styles.Rd new file mode 100644 index 0000000..9859a8d --- /dev/null +++ b/man/knitr_logger_styles.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_knitr.R +\docType{data} +\name{knitr_logger_styles} +\alias{knitr_logger_styles} +\title{Various elements used for tailoring output style to command-line utility} +\format{ +An object of class \code{list} of length 2. +} +\usage{ +knitr_logger_styles +} +\description{ +Various elements used for tailoring output style to command-line utility +} +\keyword{datasets} From cb6435fb36cd4cfa532d361c6b5a1703c86ce143 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:09:38 -0500 Subject: [PATCH 08/12] chore: document new style parameter --- R/utils_knitr.R | 3 +++ man/knit_print.knitr_log.Rd | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/R/utils_knitr.R b/R/utils_knitr.R index b54c8b4..106a67f 100644 --- a/R/utils_knitr.R +++ b/R/utils_knitr.R @@ -129,6 +129,9 @@ knitr_logger_styles <- list( #' Special handler for emitting knitr logs #' #' @inheritParams knitr::knit_print +#' @param output_style Choose which command-line output format to use when +#' logging. This will typically be set when a logger is created and should +#' not need to be manually provided. #' #' @importFrom utils head tail capture.output #' @export diff --git a/man/knit_print.knitr_log.Rd b/man/knit_print.knitr_log.Rd index 78018f1..56ba133 100644 --- a/man/knit_print.knitr_log.Rd +++ b/man/knit_print.knitr_log.Rd @@ -12,6 +12,10 @@ knit_print.knitr_log(x, ..., output_style = c("knitr", "quarto")) \item{...}{Additional arguments passed to the S3 method. Currently ignored, except two optional arguments \code{options} and \code{inline}; see the references below.} + +\item{output_style}{Choose which command-line output format to use when +logging. This will typically be set when a logger is created and should +not need to be manually provided.} } \description{ Special handler for emitting knitr logs From 9bc6fd9495f1ff0c69aa77b54eb65fb33eb46e94 Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:27:57 -0500 Subject: [PATCH 09/12] feat: add session() function for quarto report --- NAMESPACE | 1 + R/session.R | 21 +++++++++++++++++++++ man/session.Rd | 11 +++++++++++ 3 files changed, 33 insertions(+) create mode 100644 R/session.R create mode 100644 man/session.Rd diff --git a/NAMESPACE b/NAMESPACE index f067a1d..20e4a83 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ export(knitr_mutable_header) export(knitr_update_options) export(options_report) export(package_report) +export(session) export(summary_table) importFrom(htmltools,div) importFrom(methods,is) diff --git a/R/session.R b/R/session.R new file mode 100644 index 0000000..a125dc9 --- /dev/null +++ b/R/session.R @@ -0,0 +1,21 @@ +#' Collect session information +#' +#' Catalogues characteristics of the R session and execution environment. +#' +#' @export +session <- function() { + # NOTE: data that is provided as named character vectors are coerced to + # lists because the names otherwise get lost when passed as a quarto parameter + + list( + time = Sys.time(), + r_version = unclass(R.version), + session_info = sessionInfo(), + platform = .Platform, + machine = .Machine, + rng_kind = RNGkind(), + capabilities = as.list(capabilities()), + external_software = as.list(extSoftVersion()), + graphics_software = as.list(grSoftVersion()) + ) +} diff --git a/man/session.Rd b/man/session.Rd new file mode 100644 index 0000000..f0c5b08 --- /dev/null +++ b/man/session.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/session.R +\name{session} +\alias{session} +\title{Collect session information} +\usage{ +session() +} +\description{ +Catalogues characteristics of the R session and execution environment. +} From b29282b9b5d4faf0bcb868440ca42b52d1e2360b Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:45:22 -0500 Subject: [PATCH 10/12] chore: adding new deps --- DESCRIPTION | 2 ++ NAMESPACE | 2 ++ R/session.R | 2 ++ R/utils.R | 3 ++- 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index a2c29a4..cac5fbc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,10 +14,12 @@ BugReports: https://github.com/pharmaR/val.report/issues Depends: R (>= 4.3.0) Imports: + grDevices, methods, htmltools, quarto (>= 1.4.4), reactable, + utils, yaml Suggests: knitr, diff --git a/NAMESPACE b/NAMESPACE index 20e4a83..4384d48 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,7 @@ export(options_report) export(package_report) export(session) export(summary_table) +importFrom(grDevices,grSoftVersion) importFrom(htmltools,div) importFrom(methods,is) importFrom(tools,check_packages_in_dir_details) @@ -19,5 +20,6 @@ importFrom(utils,capture.output) importFrom(utils,getFromNamespace) importFrom(utils,head) importFrom(utils,packageName) +importFrom(utils,sessionInfo) importFrom(utils,tail) importFrom(yaml,yaml.load) diff --git a/R/session.R b/R/session.R index a125dc9..9894e76 100644 --- a/R/session.R +++ b/R/session.R @@ -2,6 +2,8 @@ #' #' Catalogues characteristics of the R session and execution environment. #' +#' @importFrom grDevices grSoftVersion +#' @importFrom utils sessionInfo #' @export session <- function() { # NOTE: data that is provided as named character vectors are coerced to diff --git a/R/utils.R b/R/utils.R index 2918d7a..445bb64 100644 --- a/R/utils.R +++ b/R/utils.R @@ -54,7 +54,8 @@ is.empty <- function(x) { #' @details Please check `riskmetric::pkg_ref_class_hierarchy` and `riskmetric::pkg_ref` code #' to see the allowed values and structure get_pkg_origin <- function(source) { - source_values <- unlist(riskmetric:::pkg_ref_class_hierarchy) + pkg_ref_classes <- getNamespace("riskmetric")[["pkg_ref_class_hierarchy"]] + source_values <- unlist(pkg_ref_classes) report_source_values <- c("Missing Origin", "Source Package", "Internal Package", "CRAN", "Bioconductor", "Github") names(report_source_values) <- source_values report_source_values[source] From 5730622c11aff13659101230bba97bd32924503e Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:51:04 -0500 Subject: [PATCH 11/12] chore: add import from tools --- NAMESPACE | 1 + R/render_utils.R | 1 + 2 files changed, 2 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 4384d48..30c3c46 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -15,6 +15,7 @@ export(summary_table) importFrom(grDevices,grSoftVersion) importFrom(htmltools,div) importFrom(methods,is) +importFrom(tools,analyze_license) importFrom(tools,check_packages_in_dir_details) importFrom(utils,capture.output) importFrom(utils,getFromNamespace) diff --git a/R/render_utils.R b/R/render_utils.R index 2eaf721..5625dab 100644 --- a/R/render_utils.R +++ b/R/render_utils.R @@ -5,6 +5,7 @@ #' @details The function analyzes if the license is standard. If it is standard, then it will see if there is available #' information for the footer. The footer information is only the multiple version number for the moment. #' @keywords internal +#' @importFrom tools analyze_license #' @importFrom utils getFromNamespace extract_license <- function(assessment) { license_output <- list(main = as.character(assessment$license)) From f71c69742c4cda0c06efdb539df93a7fb0aaa99b Mon Sep 17 00:00:00 2001 From: dgkf <18220321+dgkf@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:01:16 -0500 Subject: [PATCH 12/12] chore: cleanup tools imports --- NAMESPACE | 1 - R/render_utils.R | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 30c3c46..4384d48 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -15,7 +15,6 @@ export(summary_table) importFrom(grDevices,grSoftVersion) importFrom(htmltools,div) importFrom(methods,is) -importFrom(tools,analyze_license) importFrom(tools,check_packages_in_dir_details) importFrom(utils,capture.output) importFrom(utils,getFromNamespace) diff --git a/R/render_utils.R b/R/render_utils.R index 5625dab..639927f 100644 --- a/R/render_utils.R +++ b/R/render_utils.R @@ -5,7 +5,6 @@ #' @details The function analyzes if the license is standard. If it is standard, then it will see if there is available #' information for the footer. The footer information is only the multiple version number for the moment. #' @keywords internal -#' @importFrom tools analyze_license #' @importFrom utils getFromNamespace extract_license <- function(assessment) { license_output <- list(main = as.character(assessment$license)) @@ -13,7 +12,7 @@ extract_license <- function(assessment) { if (getRversion() < "4.4.0") { analyze_license <- utils::getFromNamespace("tools", "analyze_license") # nolint } else { - analyze_license <- tools::analyze_license + analyze_license <- getExportedValue("tools", "analyze_license") } license_analysis <- analyze_license(license_output$main)