diff --git a/NAMESPACE b/NAMESPACE index 18c32e5..09ac028 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,7 @@ S3method(print,quarto_block) export(map_qto) +export(pmap_qto) export(qto_attributes) export(qto_block) export(qto_callout) diff --git a/R/import-standalone-purrr.R b/R/import-standalone-purrr.R new file mode 100644 index 0000000..a4627b0 --- /dev/null +++ b/R/import-standalone-purrr.R @@ -0,0 +1,53 @@ +# nocov start + +#' all functions here are copied from the +#' `standalone-purrr.R` script in rlang: +#' https://github.com/r-lib/rlang/blob/main/R/standalone-purrr.R +#' @noRd +map <- function(.x, .f, ...) { + .f <- as_function(.f, env = global_env()) + lapply(.x, .f, ...) +} + +#' @noRd +map_chr <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, character(1L), ...) +} + +#' @noRd +map_int <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, integer(1), ...) +} + +#' @noRd +.rlang_purrr_map_mold <- function(.x, .f, .mold, ...) { + .f <- as_function(.f, env = global_env()) + out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) + names(out) <- names(.x) + out +} + +#' @noRd +pmap <- function(.l, .f, ...) { + .f <- as.function(.f) + args <- .rlang_purrr_args_recycle(.l) + do.call("mapply", c( + FUN = list(quote(.f)), + args, MoreArgs = quote(list(...)), + SIMPLIFY = FALSE, USE.NAMES = FALSE + )) +} + +#' @noRd +.rlang_purrr_args_recycle <- function(args) { + lengths <- map_int(args, length) + n <- max(lengths) + + stopifnot(all(lengths == 1L | lengths == n)) + to_recycle <- lengths == 1L + args[to_recycle] <- map(args[to_recycle], function(x) rep.int(x, n)) + + args +} + +# nocov end \ No newline at end of file diff --git a/R/map.R b/R/map.R index 0c23a76..c959e40 100644 --- a/R/map.R +++ b/R/map.R @@ -1,23 +1,28 @@ -#' map, map_chr, and .rlang_purrr_map_mold are copied from the -#' `standalone-purrr.R` script in rlang: -#' https://github.com/r-lib/rlang/blob/main/R/standalone-purrr.R -#' @noRd -map <- function(.x, .f, ...) { - .f <- as_function(.f, env = global_env()) - lapply(.x, .f, ...) +partial_qto_func <- function(f, collapse, sep) { + if (identical(f, qto_callout)) { + function(...) f(...) + } else if ((identical(f, qto_div))) { + function(...) f(..., collapse = collapse) + } else { + function(...) f(..., collapse = collapse, sep = sep) + } } -#' @noRd -map_chr <- function(.x, .f, ...) { - .rlang_purrr_map_mold(.x, .f, character(1L), ...) -} - -#' @noRd -.rlang_purrr_map_mold <- function(.x, .f, .mold, ...) { - .f <- as_function(.f, env = global_env()) - out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) - names(out) <- names(.x) - out +resolve_mapping_function <- function(f = NULL, + type = NULL, + collapse = NULL, + sep = NULL, + call = NULL) { + f <- f %||% switch(type, + block = partial_qto_func(qto_block, collapse, sep), + div = partial_qto_func(qto_div, collapse, sep), + callout = partial_qto_func(qto_callout, collapse, sep), + heading = partial_qto_func(qto_heading, collapse, sep), + ) + if (!is_function(f)) { + f <- as_function(f, call = call) + } + f } #' Apply a function to each element of a vector and return Quarto block vector @@ -35,7 +40,8 @@ map_chr <- function(.x, .f, ...) { #' "heading". #' @param .sep,.collapse Additional parameters passed to [qto_block()] if .f #' does not return a quarto block class object. Ignored if .f does return a -#' quarto block class object. +#' quarto block class object. Also passed to the relevant .type function if it supports +#' the collapse and/or sep parameters. #' @inheritParams rlang::args_error_context #' @examples #' qto_list <- map_qto( @@ -45,6 +51,7 @@ map_chr <- function(.x, .f, ...) { #' #' qto_block(qto_list) #' +#' @seealso [quartools::pmap_qto()], [purrr::map()] #' @export map_qto <- function(.x, .f = NULL, @@ -52,30 +59,81 @@ map_qto <- function(.x, .type = c("block", "div", "callout", "heading"), .sep = "", .collapse = "", - .call = caller_env()) { + call = caller_env()) { .type <- arg_match(.type, error_call = call) - + .f <- resolve_mapping_function( + f = .f, + type = .type, + collapse = .collapse, + sep = .sep, + call = call + ) map( .x, - \(x) { - .f <- .f %||% switch(.type, - block = qto_block, - div = qto_div, - callout = qto_callout, - heading = qto_heading - ) - - if (!rlang::is_function(.f)) { - .f <- rlang::as_function(.f, call = call) - } - + function(x) { x <- .f(x, ...) - - if (inherits(x, "quarto_block")) { - return(x) - } - - qto_block(x, sep = .sep, collapse = .collapse, call = .call) + if (inherits(x, "quarto_block")) return(x) + qto_block(x, sep = .sep, collapse = .collapse, call = call) } ) } + +#' Map over multiple inputs simultaenously and return Quarto block vector +#' +#' [pmap_qto()] loops a list of vectors over a package function defined by .type or a custom +#' function that returns a quarto block output. This function always returns a +#' list of quarto block objects. +#' +#' @param .l An input vector. +#' @inheritParams rlang::args_error_context +#' @inheritParams map_qto +#' @examples +#' qto_list <- pmap_qto( +#' list( +#' list("Answer:", "Answer:", "Answer:"), +#' list("Yes", "No", "Yes") +#' ) +#' ) +#' qto_block(qto_list) +#' +#' qto_list <- pmap_qto( +#' mtcars[seq(3L), seq(3L)], +#' function(mpg, cyl, disp) { +#' qto_li( +#' .list = list( +#' sprintf("mpg is: %s", mpg), +#' sprintf("cyl is: %s", cyl), +#' sprintf("disp is: %s", disp) +#' ) +#' ) +#' } +#' ) +#' qto_block(qto_list) +#' +#' @seealso [quartools::map_qto()], [purrr::pmap()] +#' @export +pmap_qto <- function(.l, + .f = NULL, + ..., + .type = c("block", "div", "callout", "heading"), + .sep = "", + .collapse = "", + call = caller_env()) { + .type <- arg_match(.type, error_call = call) + .f <- resolve_mapping_function( + f = .f, + type = .type, + collapse = .collapse, + sep = .sep, + call = call + ) + pmap( + .l, + function(...) { + x <- .f(...) + if (inherits(x, "quarto_block")) return(x) + qto_block(x, sep = .sep, collapse = .collapse, call = call) + }, + ... + ) +} diff --git a/man/map_qto.Rd b/man/map_qto.Rd index b6f5e28..d73bd5a 100644 --- a/man/map_qto.Rd +++ b/man/map_qto.Rd @@ -11,7 +11,7 @@ map_qto( .type = c("block", "div", "callout", "heading"), .sep = "", .collapse = "", - .call = caller_env() + call = caller_env() ) } \arguments{ @@ -28,9 +28,10 @@ each element of the vector. Options include "block", "div", "callout", or \item{.sep, .collapse}{Additional parameters passed to \code{\link[=qto_block]{qto_block()}} if .f does not return a quarto block class object. Ignored if .f does return a -quarto block class object.} +quarto block class object. Also passed to the relevant .type function if it supports +the collapse and/or sep parameters.} -\item{.call}{The execution environment of a currently +\item{call}{The execution environment of a currently running function, e.g. \code{caller_env()}. The function will be mentioned in error messages as the source of the error. See the \code{call} argument of \code{\link[rlang:abort]{abort()}} for more information.} @@ -49,3 +50,6 @@ qto_list <- map_qto( qto_block(qto_list) } +\seealso{ +\code{\link[=pmap_qto]{pmap_qto()}}, \code{\link[purrr:map]{purrr::map()}} +} diff --git a/man/pmap_qto.Rd b/man/pmap_qto.Rd new file mode 100644 index 0000000..237b516 --- /dev/null +++ b/man/pmap_qto.Rd @@ -0,0 +1,70 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/map.R +\name{pmap_qto} +\alias{pmap_qto} +\title{Map over multiple inputs simultaenously and return Quarto block vector} +\usage{ +pmap_qto( + .l, + .f = NULL, + ..., + .type = c("block", "div", "callout", "heading"), + .sep = "", + .collapse = "", + call = caller_env() +) +} +\arguments{ +\item{.l}{An input vector.} + +\item{.f}{Optional function to apply to each element. If function does not +return a "quarto_block" class object, the output is passed to \code{\link[=qto_block]{qto_block()}}} + +\item{...}{Additional parameters passed to function defined by \code{.f}.} + +\item{.type}{If .f is \code{NULL}, type is used to define the function applied to +each element of the vector. Options include "block", "div", "callout", or +"heading".} + +\item{.sep, .collapse}{Additional parameters passed to \code{\link[=qto_block]{qto_block()}} if .f +does not return a quarto block class object. Ignored if .f does return a +quarto block class object. Also passed to the relevant .type function if it supports +the collapse and/or sep parameters.} + +\item{call}{The execution environment of a currently +running function, e.g. \code{caller_env()}. The function will be +mentioned in error messages as the source of the error. See the +\code{call} argument of \code{\link[rlang:abort]{abort()}} for more information.} +} +\description{ +\code{\link[=pmap_qto]{pmap_qto()}} loops a list of vectors over a package function defined by .type or a custom +function that returns a quarto block output. This function always returns a +list of quarto block objects. +} +\examples{ +qto_list <- pmap_qto( + list( + list("Answer:", "Answer:", "Answer:"), + list("Yes", "No", "Yes") + ) +) +qto_block(qto_list) + +qto_list <- pmap_qto( + mtcars[seq(3L), seq(3L)], + function(mpg, cyl, disp) { + qto_li( + .list = list( + sprintf("mpg is: \%s", mpg), + sprintf("cyl is: \%s", cyl), + sprintf("disp is: \%s", disp) + ) + ) + } +) +qto_block(qto_list) + +} +\seealso{ +\code{\link[=map_qto]{map_qto()}}, \code{\link[purrr:pmap]{purrr::pmap()}} +} diff --git a/tests/testthat/_snaps/map.md b/tests/testthat/_snaps/map.md new file mode 100644 index 0000000..8d7f967 --- /dev/null +++ b/tests/testthat/_snaps/map.md @@ -0,0 +1,81 @@ +# map_qto works + + Code + qto_list + Output + [[1]] + + ::: {.callout-note} + This is a note. + ::: + + + [[2]] + + ::: {.callout-note} + And this is a note. + ::: + + + [[3]] + + ::: {.callout-note} + And this is a note + ::: + + + +--- + + Code + qto_list + Output + [[1]] + foo + + [[2]] + bar + + [[3]] + baz + + +# pmap_qto works + + Code + qto_list + Output + [[1]] + Answer: Yes + + [[2]] + Answer: No + + [[3]] + Answer: Yes + + +--- + + Code + qto_list + Output + [[1]] + + * mpg is: 21 + * cyl is: 6 + * disp is: 160 + + [[2]] + + * mpg is: 21 + * cyl is: 6 + * disp is: 160 + + [[3]] + + * mpg is: 22.8 + * cyl is: 4 + * disp is: 108 + + diff --git a/tests/testthat/test_map.R b/tests/testthat/test_map.R new file mode 100644 index 0000000..bc7dd33 --- /dev/null +++ b/tests/testthat/test_map.R @@ -0,0 +1,100 @@ +check_types <- function(lst) { + res <- vapply(lst, function(x) { + "quarto_block" %in% class(x) + }, logical(1L)) + all(res) +} + +test_that("resolve_mapping_function works", { + expect_type(resolve_mapping_function(f = ~ .x + 1L), "closure") + + block_fn <- resolve_mapping_function( + type = "block", + sep = " ", + collapse = " " + ) + expect_identical( + block_fn("Hello", c("world", "!")), + qto_block("Hello", c("world", "!"), sep = " ", collapse = " ") + ) + + div_fn <- resolve_mapping_function( + type = "div", + sep = " ", + collapse = "bar" + ) + expect_identical( + div_fn("foo", "baz"), + qto_div("foo", "bar", "baz") + ) + + callout_fn <- resolve_mapping_function( + type = "callout", + sep = " ", + collapse = "bar" + ) + expect_identical( + callout_fn("foo", "baz"), + qto_callout("foo", "baz") + ) + + heading_fn <- resolve_mapping_function( + type = "heading", + sep = " ", + collapse = " " + ) + expect_identical( + heading_fn("foo", "baz"), + qto_heading("foo", "baz", collapse = " ", sep = " ") + ) + +}) + + +test_that("map_qto works", { + qto_list <- map_qto( + list("This is a note.", "And this is a note.", "And this is a note"), + .type = "callout" + ) + expect_length(qto_list, 3L) + expect_true(check_types(qto_list)) + expect_snapshot(qto_list) + + + qto_list <- map_qto( + list("foo", "bar", "baz"), + .f = function(x) x + ) + expect_length(qto_list, 3L) + expect_true(check_types(qto_list)) + expect_snapshot(qto_list) +}) + +test_that("pmap_qto works", { + qto_list <- pmap_qto( + list( + list("Answer: ", "Answer: ", "Answer: "), + list("Yes", "No", "Yes") + ) + ) + expect_length(qto_list, 3L) + expect_true(check_types(qto_list)) + expect_snapshot(qto_list) + + + qto_list <- pmap_qto( + mtcars[seq(3L), seq(3L)], + function(mpg, cyl, disp) { + qto_li( + .list = list( + sprintf("mpg is: %s", mpg), + sprintf("cyl is: %s", cyl), + sprintf("disp is: %s", disp) + ) + ) + } + ) + expect_length(qto_list, 3L) + expect_true(check_types(qto_list)) + expect_snapshot(qto_list) +})