diff --git a/DESCRIPTION b/DESCRIPTION index 20db12b..657d0bd 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,10 @@ Imports: roxygen2 Suggests: rstudioapi, - testthat (>= 3.0.0) + testthat (>= 3.0.0), + jsonlite, + base64enc, + desc Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 47fb5c7..c7cf901 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,8 @@ # Generated by roxygen2: do not edit by hand S3method(roxygen2::roxy_tag_parse,roxy_tag_examplesTempdir) +S3method(roxygen2::roxy_tag_parse,roxy_tag_examplesWebR) S3method(roxygen2::roxy_tag_rd,roxy_tag_examplesTempdir) +S3method(roxygen2::roxy_tag_rd,roxy_tag_examplesWebR) export(insert_examplesTempdir_template) +export(insert_examplesWebR_template) diff --git a/R/examplesWebR.R b/R/examplesWebR.R new file mode 100644 index 0000000..e0dfc64 --- /dev/null +++ b/R/examplesWebR.R @@ -0,0 +1,791 @@ +#' Custom `@examplesWebR` tag. +#' +#' When roxygen2 processes your documentation, the `@examplesWebR` tag generates +#' both an example section and webR integration. Specifically, the +#' example code is split into a regular `@examples` section and a WebR section. +#' The webR section contains a clickable link or embedding an interactive iframe +#' that interacts with the webR REPL by automatically encoding the code +#' for sharing. +#' +#' @section Implementation: +#' +#' When the `@examplesWebR` tag is processed, it creates: +#' +#' 1. Regular `@examples` section with the code +#' 2. A `"WebR"` section containing format-appropriate webR integration link +#' +#' The webR URL format is: +#' +#' ``` +#' https://webr.r-wasm.org/{version}/?mode={mode}&channel={channel}#code={encoded_code}&ju{a} +#' ``` +#' +#' where the code is JSON-encoded and uncompressed. +#' +#' @section Generated Structure: +#' +#' The generated RD file will have the structure: +#' +#' ``` +#' \section{WebR}{ +#' \ifelse{html}{ +#' \out{ +#' \U0001F310 View in webR REPL +#' } +#' }{ +#' \ifelse{latex}{ +#' \url{webR_URL} +#' }{ +#' Interactive webR content not available for this output format. +#' } +#' } +#' } +#' \examples{ +#' # Your R code here +#' plot(1:5, 1:5) +#' } +#' ``` +#' +#' where `webR_URL` is the link to the webR REPL with the encoded code. +#' +#' @section Output Format Support: +#' +#' The webR integration adapts to different documentation formats: +#' +#' - **HTML**: Interactive buttons/iframes with full webR functionality +#' - **LaTeX/PDF**: Plain URL link to webR session +#' - **Other formats**: Informational message about limited support +#' +#' @section Parameters: +#' +#' The tag supports optional parameters (these override global `DESCRIPTION` config): +#' +#' - `@examplesWebR`: Default behavior (link in WebR section) +#' - `@examplesWebR embed`: Embed iframe instead of link +#' - `@examplesWebR embed=false` - Explicitly disable embedding (override global config) +#' - `@examplesWebR version=v0.5.4`: Specify webR version greater than or equal to 0.5.4 (default: `latest`) +#' - `@examplesWebR height=500`: Set iframe height in pixels (default: `400`) +#' - `@examplesWebR autorun` - Enable autorun on webR session open +#' - `@examplesWebR autorun=false` - Explicitly disable autorun (override global config) +#' - `@examplesWebR mode=editor-plot` - Configure embedded webR interface (editor, plot, terminal, files) (default: `"editor-plot-terminal"`) +#' - `@examplesWebR channel=Automatic` - Set webR communication channel (default: `"Automatic`) +#' +#' **Version Requirements**: The version parameter must be either `"latest"` or a +#' version string `v0.5.4` or higher. Earlier versions are not supported +#' as the embedding feature is new. +#' +#' **Mode Options**: Valid mode components are `editor`, `plot`, `terminal`, and `files`. +#' Combine multiple components with hyphens (e.g., `editor-plot-terminal`). +#' +#' @section Global Configuration: +#' +#' You can set global defaults for tags in your package's `DESCRIPTION` file: +#' +#' ``` +#' Config/rocleteer/webr-embed: false +#' Config/rocleteer/webr-height: 500 +#' Config/rocleteer/webr-version: v0.5.4 +#' Config/rocleteer/webr-autorun: true +#' Config/rocleteer/webr-mode: editor-plot +#' Config/rocleteer/webr-channel: Automatic +#' ``` +#' +#' @section Package Configuration: +#' +#' To use the `@examplesWebR` tag in your package, add the required dependencies: +#' +#' In your `DESCRIPTION` file: +#' +#' ``` +#' Suggests: +#' rocleteer +#' Remotes: coatless-rpkg/rocleteer +#' +#' Roxygen: list(..., packages = c("rocleteer")) +#' ``` +#' +#' You will also need to specify a location where we can obtain the webR +#' compiled package in `DESCRIPTION`. This will usually be a GitHub Pages URL +#' or an R-universe URL. The recommended way is to use the +#' `Config/rocleteer/webr-repo` field in your `DESCRIPTION` file: +#' +#' ``` +#' Config/rocleteer/webr-repo: https://user.github.io/pkgname/ +#' ``` +#' +#' Alternatively, you can use the `URL` field in your `DESCRIPTION` file to +#' specify the repository URL. The tag will attempt to auto-detect the +#' repository URL from the `URL` field if the `Config/rocleteer/webr-repo` field +#' is not set. The tag will look for: +#' +#' ``` +#' URL: https://user.github.io/pkgname/, https://github.com/user/pkgname +#' ``` +#' +#' or +#' +#' ``` +#' URL: https://username.r-universe.dev +#' ``` +#' +#' If we cannot find a suitable repository URL, the tag will throw an error +#' during processing. +#' +#' The generated webR examples will use this URL to install the package +#' and load it in the webR REPL. The installation code will look like this: +#' +#' ```r +#' # Install and load webR Package +#' install.packages("pkgname", repos = "https://user.github.io/pkgname/") +#' library("pkgname") +#' +#' # Example code: +#' your_function() +#' ``` +#' +#' @name tag-examplesWebR +#' +#' @usage +#' #' @examplesWebR +#' #' # Your example code that will be available in webR +#' #' +#' #' @examplesWebR embed +#' #' # Your example code with embedded webR iframe +#' #' +#' #' @examplesWebR embed version=v0.5.4 height=400 +#' #' # Your example code with embedded webR iframe with height 400 set to version 0.5.4 +#' #' +#' #' @examplesWebR embed=false autorun mode=editor-plot +#' #' # Your example code with custom configuration overriding global settings +#' #' +#' @examples +#' # A function with webR integration +#' # +#' #' @title Plot Simple Data +#' #' @description +#' #' This function creates a simple plot +#' #' +#' #' @param x Numeric vector for x-axis +#' #' @param y Numeric vector for y-axis +#' #' +#' #' @return +#' #' A plot object +#' #' +#' #' @examplesWebR +#' #' # Create some data +#' #' x <- 1:10 +#' #' y <- x^2 +#' #' +#' #' # Create a plot +#' #' plot(x, y, type = "b", main = "Simple Quadratic") +#' #' +#' #' # Add a line +#' #' lines(x, x^1.5, col = "red") +NULL + +# Internal functions to handle the `@examplesWebR` tag ---- + +#' Read webR configuration from DESCRIPTION file +#' +#' Extract webR-related configuration from the package DESCRIPTION file +#' +#' @param base_path Base path to find DESCRIPTION file +#' +#' @return +#' List of configuration options +#' +#' @noRd +read_webr_config <- function(base_path) { + # Check for required packages + if (!requireNamespace("desc", quietly = TRUE)) { + stop("Package 'desc' is required for @examplesWebR tag DESCRIPTION integration") + } + + # Find DESCRIPTION file + desc_path <- file.path(base_path, "DESCRIPTION") + if (!file.exists(desc_path)) { + warning("DESCRIPTION file not found, using default webR configuration") + return(list()) + } + + # Read DESCRIPTION + d <- desc::desc(desc_path) + + # Extract webR configuration + config <- list() + + # Global webR options + config$embed <- d$get_field("Config/rocleteer/webr-embed", default = "false") + config$embed <- tolower(config$embed) %in% c("true", "yes", "1") + + config$height <- as.numeric(d$get_field("Config/rocleteer/webr-height", default = "300")) + config$version <- d$get_field("Config/rocleteer/webr-version", default = "latest") + config$autorun <- d$get_field("Config/rocleteer/webr-autorun", default = "false") + config$autorun <- tolower(config$autorun) %in% c("true", "yes", "1") + + # Package info for installation + config$package_name <- d$get_field("Package", default = "") + + # Repository detection + config$webr_repo <- detect_webr_repo(d) + + return(config) +} + +#' Detect webR repository from DESCRIPTION +#' +#' Find the appropriate repository URL for webR package installation +#' +#' @param desc_obj A desc object from the desc package +#' +#' @return +#' Repository URL string, or stops with error if not found +#' +#' @noRd +detect_webr_repo <- function(desc_obj) { + # Check for dedicated webR repo field first + webr_repo <- desc_obj$get_field("Config/rocleteer/webr-repo", default = NA) + if (!is.na(webr_repo) && nzchar(webr_repo)) { + return(webr_repo) + } + + # Try to detect from URL field + url <- desc_obj$get_field("URL", default = NA) + if (!is.na(url) && nzchar(url)) { + # Handle multiple URLs (comma or whitespace separated) + urls <- strsplit(url, "[,\\s]+")[[1]] + urls <- trimws(urls) + + for (u in urls) { + # Check for GitHub Pages pattern: https://user.github.io/pkgname/ + if (grepl("https://[^/]+\\.github\\.io/[^/]+/?$", u)) { + return(u) + } + # Check for R-universe pattern: https://username.r-universe.dev + if (grepl("https://[^/]+\\.r-universe\\.dev/?$", u)) { + return(u) + } + } + } + + # If no suitable repo found, throw an error + stop( + "No suitable webR repository found. Please add one of:\n", + "1. Config/Needs/WebRRepo: https://user.github.io/pkgname/\n", + "2. URL field with GitHub Pages (https://user.github.io/pkgname/) or R-universe (https://user.r-universe.dev) pattern" + ) +} + + +#' Generate package installation code for webR +#' +#' Create R code to install and load the package in webR +#' +#' @param package_name Name of the package +#' @param repo_url Repository URL +#' +#' @return +#' R code string for package installation +#' +#' @noRd +generate_package_install_code <- function(package_name, repo_url) { + if (!nzchar(package_name) || is.na(repo_url)) { + return("") + } + + install_code <- paste0( + "# Install and load webR Package\n", + 'webr::install(\n "', package_name, '",\n repos = "', repo_url, '",\n mount = FALSE)\n', + 'library("', package_name, '")\n', + '\n', + "# Example code:\n" + ) + + return(install_code) +} + +#' Encode R code for webR REPL sharing +#' +#' This function encodes R code in the format expected by webR REPL +#' for URL sharing. It matches the exact encoding used by webR: +#' JSON format + no compression + base64 + proper flags +#' +#' @param code Character string containing R code +#' @param filename Optional filename (default: "example.R") +#' @param autorun Whether to enable autorun (adds 'a' flag) +#' +#' @return +#' Base64 encoded string suitable for webR URLs with proper flags +#' +#' @noRd +encode_webr_code <- function(code, filename = "example.R", autorun = FALSE) { + # Check for required packages + if (!requireNamespace("jsonlite", quietly = TRUE)) { + stop("Package 'jsonlite' is required for @examplesWebR tag") + } + if (!requireNamespace("base64enc", quietly = TRUE)) { + stop("Package 'base64enc' is required for @examplesWebR tag") + } + + # Create the share item structure exactly as webR expects + share_item <- list( + list( + name = filename, + path = paste0("/", filename), + text = code + ) + ) + + # Convert to JSON to avoid a dependency on RcppMsgPack + # Note: webR supports JSON with the 'j' flag + json_str <- jsonlite::toJSON(share_item, auto_unbox = TRUE, pretty = FALSE) + + # webR expects UTF-8 bytes + json_bytes <- charToRaw(json_str) + + # TODO: Use uncompressed for better compatibility with the webR URL + + # Base64 encode (matching webR's base64 encoding) + base64_str <- base64enc::base64encode(json_bytes) + + # URL encode for safe inclusion in URLs + encoded <- utils::URLencode(base64_str, reserved = TRUE) + + # Escape percent signs... + escape_comment <- function(x) { + gsub("%", "\\%", x, fixed = TRUE) + } + # ...to avoid issues with webR REPL parsing + encoded <- escape_comment(encoded) + + # Add flags to encoding: 'jua' = JSON format + uncompressed + autorun + flags <- if (autorun) "jua" else "ju" + encoded_with_flags <- paste0(encoded, "&", flags) + + return(encoded_with_flags) +} + +#' Validate webR version +#' +#' Check if the provided webR version is valid (latest or v0.5.4+) +#' +#' @param version_str Version string to validate +#' +#' @return +#' TRUE if valid, FALSE otherwise +#' +#' @section Interal Examples: +#' +#' ```r +#' # Valid versions: +#' validate_webr_version("latest") # TRUE +#' validate_webr_version("v0.5.4") # TRUE +#' validate_webr_version("v0.6.0") # TRUE +#' validate_webr_version("v1.0.0") # TRUE +#' +#' # Invalid versions: +#' validate_webr_version("v0.5.3") # FALSE (too old) +#' validate_webr_version("v0.3.0") # FALSE (too old) +#' validate_webr_version("0.6.0") # FALSE (missing 'v') +#' validate_webr_version("invalid") # FALSE (not a version) +#' ``` +#' +#' @noRd +validate_webr_version <- function(version_str) { + # "latest" is always valid + if (version_str == "latest") { + return(TRUE) + } + + # Check if version starts with 'v' and has a valid format + if (!grepl("^v\\d+\\.\\d+\\.\\d+", version_str)) { + return(FALSE) + } + + # Extract numeric version (remove 'v' prefix) + numeric_part <- sub("^v", "", version_str) + + # Try to parse as version and compare with minimum + tryCatch({ + provided_version <- numeric_version(numeric_part) + min_version <- numeric_version("0.5.4") + + return(provided_version >= min_version) + }, error = function(e) { + # If version parsing fails, it's invalid + return(FALSE) + }) +} + + +#' Validate webR mode +#' +#' Check if the provided webR mode contains only valid options +#' +#' @param mode_str Mode string to validate (e.g., "editor-plot-terminal") +#' +#' @return +#' TRUE if valid, FALSE otherwise +#' +#' @noRd +validate_webr_mode <- function(mode_str) { + if (!nzchar(mode_str)) { + return(TRUE) # Empty mode is valid (use webR default) + } + + valid_modes <- c("editor", "plot", "terminal", "files") + provided_modes <- strsplit(mode_str, "-")[[1]] + + # Check if all provided modes are valid + all(provided_modes %in% valid_modes) +} + +#' Parse webR tag parameters +#' +#' Extract parameters from the webR tag line and merge with global config +#' +#' @param tag_line First line of the tag +#' @param global_config Global configuration from DESCRIPTION file +#' +#' @return +#' List of parameters +#' +#' @noRd +parse_webr_params <- function(tag_line, global_config = list()) { + # Start with global config defaults + params <- list( + embed = global_config$embed %||% TRUE, + version = global_config$version %||% "latest", + height = global_config$height %||% 300, + autorun = global_config$autorun %||% FALSE, + mode = global_config$mode %||% "editor-plot-terminal", + channel = global_config$channel %||% "" + ) + + # Parse local parameters from tag line (these override global config) + + # Handle embed (including explicit false) + if (grepl("embed=false", tag_line, ignore.case = TRUE)) { + params$embed <- FALSE + } else if (grepl("embed", tag_line, ignore.case = TRUE)) { + params$embed <- TRUE + } + + # Handle autorun (including explicit false) + if (grepl("autorun=false", tag_line, ignore.case = TRUE)) { + params$autorun <- FALSE + } else if (grepl("autorun", tag_line, ignore.case = TRUE)) { + params$autorun <- TRUE + } + + # Extract version if specified + version_match <- regmatches(tag_line, regexpr("version=\\S+", tag_line)) + if (length(version_match) > 0) { + version_str <- sub("version=", "", version_match) + + # Validate version + if (!validate_webr_version(version_str)) { + stop("Invalid webR version '", version_str, "'. Must be 'latest' or v0.5.4 or higher (e.g., v0.5.4, v0.6.0)") + } + + params$version <- version_str + } + + # Extract height if specified + height_match <- regmatches(tag_line, regexpr("height=\\d+", tag_line)) + if (length(height_match) > 0) { + params$height <- as.numeric(sub("height=", "", height_match)) + } + + # Extract mode if specified + mode_match <- regmatches(tag_line, regexpr("mode=[\\w\\-]+", tag_line)) + if (length(mode_match) > 0) { + mode_str <- sub("mode=", "", mode_match) + + # Validate mode + if (!validate_webr_mode(mode_str)) { + stop("Invalid webR mode '", mode_str, "'. Valid options are: editor, plot, terminal, files (separated by hyphens)") + } + + params$mode <- mode_str + } + + # Extract channel if specified + channel_match <- regmatches(tag_line, regexpr("channel=\\S+", tag_line)) + if (length(channel_match) > 0) { + params$channel <- sub("channel=", "", channel_match) + } + + return(params) +} + +#' Generate webR warning HTML +#' +#' Create standardized warning message for webR examples +#' +#' @return +#' HTML string with warning message and contact information +#' +#' @noRd +webr_experimental_warning <- function() { + paste0( + '
', + '\U0001F9EA Experimental: Interactive webR examples are a new feature. ', + 'Loading may take a moment, and the package version might differ from this documentation.', + '
' + ) +} + +#' Generate webR REPL link +#' +#' Create a webR URL with version, mode, channel, and encoded code +#' +#' @param encoded_code Base64 encoded code with flags +#' @param version webR version (e.g. "latest", "v0.5.4"). Default: `"latest"` +#' @param mode webR interface mode (e.g., "editor-plot-terminal-files"). Default: `""` +#' @param channel webR channel (e.g., "Automatic"). Default: `""` +#' +#' @return +#' A URL string for the webR REPL +#' +#' @keywords internal +webr_repl_href <- function(encoded_code, version = "latest", mode = "", channel = "") { + base_url <- paste0("https://webr.r-wasm.org/", version,"/") + + # Build query parameters + query_params <- c() + + if (nzchar(mode)) { + query_params <- c(query_params, paste0("mode=", mode)) + } + + if (nzchar(channel)) { + query_params <- c(query_params, paste0("channel=", channel)) + } + + # Construct URL + if (length(query_params) > 0) { + url <- paste0(base_url, "?", paste(query_params, collapse = "&"), "#code=", encoded_code) + } else { + url <- paste0(base_url, "#code=", encoded_code) + } + + return(url) + +} + +#' Generate webR link HTML +#' +#' Create HTML for webR REPL link +#' +#' @param encoded_code Base64 encoded code +#' @param version webR version +#' @param mode webR interface mode +#' @param channel webR channel +#' +#' @return +#' HTML string for link +#' +#' @noRd +webr_repl_link <- function(encoded_code, version = "latest", mode = "", channel = "") { + url <- webr_repl_href(encoded_code, version, mode, channel) + html <- paste0( + '
', + + # Warning message + webr_experimental_warning(), + + # Link button + '

', + '\U0001F310 View in webR REPL

', + + '
' + ) + return(html) +} + +#' Generate webR iframe HTML +#' +#' Create HTML for embedded webR iframe +#' +#' @param encoded_code Base64 encoded code +#' @param version webR version +#' @param height iframe height in pixels +#' @param mode webR interface mode +#' @param channel webR channel +#' +#' @return +#' HTML string for iframe +#' +#' @noRd +webr_repl_iframe <- function(encoded_code, version = "latest", height = 300, mode = "", channel = "") { + url <- webr_repl_href(encoded_code, version, mode, channel) + + # Create an ID for this iframe (based on the encoded code) + hashed_id <- abs(sum(utf8ToInt(substr(encoded_code, 1, 10)))) + iframe_id <- paste0("webr_iframe_", hashed_id) + + html <- paste0( + '
', + + # Warning message + webr_experimental_warning(), + + # Initial buttons (before iframe loads) + '
', + '

Interactive Example Available

', + '', + '', + '
', + + # Iframe container (hidden initially) + '', + + '
', + + # JavaScript for iframe management + '' + ) + + return(html) +} + +#' Parse the `@examplesWebR` tag +#' +#' This function handles the new `@examplesWebR` tag, which automatically +#' creates both regular examples and webR REPL integration. +#' +#' @param x A roxygen2 tag +#' +#' @return +#' A parsed roxygen2 tag that contains examples plus webR content +#' +#' @noRd +#' @exportS3Method roxygen2::roxy_tag_parse roxy_tag_examplesWebR +roxy_tag_parse.roxy_tag_examplesWebR <- function(x) { + # Split the raw text into lines + lines <- strsplit(x$raw, "\r?\n")[[1]] + + # Read global config from DESCRIPTION (we'll need base_path in roxy_tag_rd) + # For now, store the tag parameters for later processing + x$tag_line <- lines[1] + + # Extract the actual code (skip first line if it contains only the tag) + code_lines <- if (grepl("^\\s*$", lines[2])) lines[-c(1, 2)] else lines[-1] + code <- paste(code_lines, collapse = "\n") + + # Process as examples tag first + x$raw <- paste(code_lines, collapse = "\n") + x$tag <- "examples" + results <- roxygen2::tag_examples(x) + + # Store the original code and tag line for later processing + results$original_code <- code + results$tag_line <- x$tag_line + + return(results) +} + +#' @noRd +#' @exportS3Method roxygen2::roxy_tag_rd roxy_tag_examplesWebR +roxy_tag_rd.roxy_tag_examplesWebR <- function(x, base_path, env) { + + # If no original code, just return examples + if (is.null(x$original_code) || !nzchar(trimws(x$original_code))) { + return(roxygen2::rd_section("examples", x$val)) + } + + global_config <- list() + + # Read global configuration from DESCRIPTION + tryCatch({ + global_config <- read_webr_config(base_path) + }, error = function(e) { + warning("Failed to read webR config from DESCRIPTION:\n", e$message) + }) + + # Parse parameters, merging tag-level with global config + params <- parse_webr_params(x$tag_line %||% "", global_config) + + # Generate package installation code if we have repo info + install_code <- "" + if (!is.null(global_config$webr_repo) && !is.null(global_config$package_name)) { + tryCatch({ + install_code <- generate_package_install_code(global_config$package_name, global_config$webr_repo) + }, error = function(e) { + warning("Failed to generate package installation code: ", e$message) + }) + } + + # Combine installation code with original example code + full_code <- paste0(install_code, x$original_code) + + # Encode the code for webR + webr_data <- NULL + tryCatch({ + encoded_code <- encode_webr_code(full_code, autorun = params$autorun) + + # Generate the webR URL for all formats + webr_url <- paste0("https://webr.r-wasm.org/", params$version, "/#code=", encoded_code) + + # Generate webR integration HTML for HTML format + webr_html <- if (params$embed) { + webr_repl_iframe(encoded_code, params$version, params$height, params$mode, params$channel) + } else { + webr_repl_link(encoded_code, params$version, params$mode, params$channel) + } + + # Create content that adapts to different output formats + webr_content <- paste0( + "\\ifelse{html}{\\out{\n", + webr_html, + "\n}}{\\ifelse{latex}{\\url{", webr_url, "}}{Interactive webR content not available for this output format.}}" + ) + + # Create custom WebR section + webr_section <- roxygen2::rd_section("section", list(title = "WebR", content = webr_content)) + + # Return both examples and WebR sections + return(list( + roxygen2::rd_section("examples", x$val), + webr_section + )) + + }, error = function(e) { + warning("Failed to create webR integration: ", e$message) + return(roxygen2::rd_section("examples", x$val)) + }) +} diff --git a/R/rstudio-addins.R b/R/rstudio-addins.R index e971dd3..e613b87 100644 --- a/R/rstudio-addins.R +++ b/R/rstudio-addins.R @@ -17,3 +17,24 @@ insert_examplesTempdir_template <- function() { rstudioapi::insertText(template) } + +#' Insert a webR examples template +#' +#' This function inserts a template for the `@examplesWebR` tag at the cursor position. +#' It's designed to be used as an RStudio addin. +#' +#' @export +insert_examplesWebR_template <- function() { + if (!requireNamespace("rstudioapi", quietly = TRUE) || !rstudioapi::isAvailable()) { + stop("This function must be used within RStudio") + } + + template <- paste( + "#' @examplesWebR", + "#' # Your interactive R code here", + "#' plot(1:10, 1:10)", + sep = "\n" + ) + + rstudioapi::insertText(template) +} diff --git a/README.Rmd b/README.Rmd index 30a98c6..0b56c21 100644 --- a/README.Rmd +++ b/README.Rmd @@ -118,6 +118,166 @@ example_function <- function() { > > This roclet is inspired by [an old post of mine](https://blog.thecoatlessprofessor.com/programming/r/hiding-tempdir-and-tempfile-statements-in-r-documentation/) that I initially shared in 2018 covering this pattern. +### `@examplesWebR` + +The `@examplesWebR` tag creates interactive examples that can be run directly in +the browser using [webR](https://docs.r-wasm.org/webr/latest/). This makes your +package documentation more engaging and allows users to experiment with examples +without installing R locally. + +For this to work with developmental versions of your package, you will need to +either have an account with [r-universe.dev](https://r-universe.dev/) or +use the following `pkgdown` + `rwasm` build action: + + + +For a fast setup, please use: + +```r +usethis::use_github_action(repos = "https://github.com/r-wasm/actions/blob/main/examples/rwasm-binary-and-pkgdown-site.yml") +``` + +> [!IMPORTANT] +> +> Please make sure to delete your old `pkgdown.yml` file. + +## Requirements + +For `@examplesWebR` functionality, your package's `DESCRIPTION` file must +have: + +``` +Suggests: + rocleteer +Remotes: coatless-rpkg/rocleteer + +Roxygen: list(..., packages = c("rocleteer")) + +Config/rocleteer/webr-repo: https://user.github.io/pkgname/ +``` + +For the package to be usable in webR examples, you must +specify a webR-compatible repository in your `DESCRIPTION` file. This +repository can be generated by [`r-universe`](https://r-universe.dev/) or +by using the above GitHub Action to build and serve a webR R binary alongside +your pkgdown site. + +By default, the `@examplesWebR` tag will look for the following: + +- `Config/rocleteer/webr-repo: https://user.r-universe.dev/` (recommended) +- `Config/rocleteer/webr-repo: https://user.github.io/pkgname/` +- `URL` field containing GitHub Pages or R-universe patterns (shown above) + +If none of these are found, the tag will produce a warning during processing +and will not generate the webR section in the Rd file. + +#### WebR Version Support + +Only webR versions v0.5.4 and higher are supported. +The tag will validate the version parameter and produce an error if an +unsupported version is specified. + +#### Generated Structure + +When you use `@examplesWebR`, it generates: + +1. **Examples Section**: Contains your R code as normal examples +2. **WebR Section**: Contains the webR integration (link or iframe) + +That is, from: + +```r +#' @examplesWebR +#' # Create some data +#' x <- 1:10 +#' y <- x^2 +#' +#' # Create a plot +#' plot(x, y, type = "b", main = "Interactive Example") +``` + +it generates: + +```rd +\section{WebR}{ + \ifelse{html}{ + \out{ + + + } + }{ + \ifelse{latex}{ + \url{webR_URL} + }{ + Interactive webR content not available for this output format. + } + } +} +\examples{ +# Create some data +x <- 1:10 +y <- x^2 + +# Create a plot +plot(x, y, type = "b", main = "Interactive Example") +} +``` + +This creates: + +- Regular examples with your R code +- A "WebR" section with a "Try it in your browser" and "Open in Tab" buttons + in HTML documentation or a URL in LaTeX documentation. +- The button opens either an embedded webR session or + a new tab with the code in an interactive webR REPL. + +> [!NOTE] +> +> The `@examplesWebR` tag uses a simplified encoding scheme compatible with webR. + +#### Standalone Mode + +To avoid embedding the webR REPL, you can use the `@examplesWebR embed=false` tag. +This will generate a link to the webR REPL without embedding it directly in the documentation. + +```r +#' @examplesWebR embed +#' # This code will be available in a new web browser tab with the webR REPL +#' library(ggplot2) +#' ggplot(mtcars, aes(mpg, wt)) + +#' geom_point() + +#' theme_minimal() +``` + +#### Additional Options + +You can customize the `@examplesWebR` tag with additional options: + +- `autorun`: Embed an iframe instead of showing a link (default: `"false"`) +- `embed`: Embed an iframe instead of showing a link (default: `"true"`) +- `height=N`: Set iframe height in pixels (default: `400`) +- `version=X`: Specify webR version (default: `"latest"`) +- `mode=X-Y`: Configure embedded webR interface (editor, plot, terminal, files) (default: `"editor-plot-terminal"`) +- `channel=X`: Set webR communication channel (default: `"Automatic`) + +For example, to embed with a specific height and version: + +```r +#' @examplesWebR autorun height=500 version=v0.5.4 +#' # Custom height iframe with specific webR version that autoruns code +#' summary(cars) +#' plot(cars) +``` + +> [!NOTE] +> +> I've been on a quest to make R package documentation more interactive and +> engaging, and this tag is a step towards that goal. It first started as +> a way to [build and serve a webR R binary alongside pkgdown sites](https://github.com/r-wasm/actions/issues/15) and, then, +> moved to [`altdocs` with the `quarto-webr` Quarto Extension](https://github.com/coatless-r-n-d/quarto-webr-in-altdoc)... +> And now, we finally have a way to do this with roxygen2 and pkgdown! + + ## License AGPL (>=3) diff --git a/README.md b/README.md index ef4817e..7c689f9 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,173 @@ example_function <- function() { > mine](https://blog.thecoatlessprofessor.com/programming/r/hiding-tempdir-and-tempfile-statements-in-r-documentation/) > that I initially shared in 2018 covering this pattern. +### `@examplesWebR` + +The `@examplesWebR` tag creates interactive examples that can be run +directly in the browser using +[webR](https://docs.r-wasm.org/webr/latest/). This makes your package +documentation more engaging and allows users to experiment with examples +without installing R locally. + +For this to work with developmental versions of your package, you will +need to either have an account with +[r-universe.dev](https://r-universe.dev/) or use the following +`pkgdown` + `rwasm` build action: + + + +For a fast setup, please use: + +``` r +usethis::use_github_action(repos = "https://github.com/r-wasm/actions/blob/main/examples/rwasm-binary-and-pkgdown-site.yml") +``` + +> \[!IMPORTANT\] +> +> Please make sure to delete your old `pkgdown.yml` file. + +## Requirements + +For `@examplesWebR` functionality, your package’s `DESCRIPTION` file +must have: + + Suggests: + rocleteer + Remotes: coatless-rpkg/rocleteer + + Roxygen: list(..., packages = c("rocleteer")) + + Config/rocleteer/webr-repo: https://user.github.io/pkgname/ + +For the package to be usable in webR examples, you must specify a +webR-compatible repository in your `DESCRIPTION` file. This repository +can be generated by [`r-universe`](https://r-universe.dev/) or by using +the above GitHub Action to build and serve a webR R binary alongside +your pkgdown site. + +By default, the `@examplesWebR` tag will look for the following: + +- `Config/rocleteer/webr-repo: https://user.r-universe.dev/` + (recommended) +- `Config/rocleteer/webr-repo: https://user.github.io/pkgname/` +- `URL` field containing GitHub Pages or R-universe patterns (shown + above) + +If none of these are found, the tag will produce a warning during +processing and will not generate the webR section in the Rd file. + +#### WebR Version Support + +Only webR versions v0.5.4 and higher are supported. The tag will +validate the version parameter and produce an error if an unsupported +version is specified. + +#### Generated Structure + +When you use `@examplesWebR`, it generates: + +1. **Examples Section**: Contains your R code as normal examples +2. **WebR Section**: Contains the webR integration (link or iframe) + +That is, from: + +``` r +#' @examplesWebR +#' # Create some data +#' x <- 1:10 +#' y <- x^2 +#' +#' # Create a plot +#' plot(x, y, type = "b", main = "Interactive Example") +``` + +it generates: + +``` rd +\section{WebR}{ + \ifelse{html}{ + \out{ + + + } + }{ + \ifelse{latex}{ + \url{webR_URL} + }{ + Interactive webR content not available for this output format. + } + } +} +\examples{ +# Create some data +x <- 1:10 +y <- x^2 + +# Create a plot +plot(x, y, type = "b", main = "Interactive Example") +} +``` + +This creates: + +- Regular examples with your R code +- A “WebR” section with a “Try it in your browser” and “Open in Tab” + buttons in HTML documentation or a URL in LaTeX documentation. +- The button opens either an embedded webR session or a new tab with the + code in an interactive webR REPL. + +> \[!NOTE\] +> +> The `@examplesWebR` tag uses a simplified encoding scheme compatible +> with webR. + +#### Standalone Mode + +To avoid embedding the webR REPL, you can use the +`@examplesWebR embed=false` tag. This will generate a link to the webR +REPL without embedding it directly in the documentation. + +``` r +#' @examplesWebR embed +#' # This code will be available in a new web browser tab with the webR REPL +#' library(ggplot2) +#' ggplot(mtcars, aes(mpg, wt)) + +#' geom_point() + +#' theme_minimal() +``` + +#### Additional Options + +You can customize the `@examplesWebR` tag with additional options: + +- `autorun`: Embed an iframe instead of showing a link (default: + `"false"`) +- `embed`: Embed an iframe instead of showing a link (default: `"true"`) +- `height=N`: Set iframe height in pixels (default: `400`) +- `version=X`: Specify webR version (default: `"latest"`) +- `mode=X-Y`: Configure embedded webR interface (editor, plot, terminal, + files) (default: `"editor-plot-terminal"`) +- `channel=X`: Set webR communication channel (default: `"Automatic`) + +For example, to embed with a specific height and version: + +``` r +#' @examplesWebR autorun height=500 version=v0.5.4 +#' # Custom height iframe with specific webR version that autoruns code +#' summary(cars) +#' plot(cars) +``` + +> \[!NOTE\] +> +> I’ve been on a quest to make R package documentation more interactive +> and engaging, and this tag is a step towards that goal. It first +> started as a way to [build and serve a webR R binary alongside pkgdown +> sites](https://github.com/r-wasm/actions/issues/15) and, then, moved +> to [`altdocs` with the `quarto-webr` Quarto +> Extension](https://github.com/coatless-r-n-d/quarto-webr-in-altdoc)… +> And now, we finally have a way to do this with roxygen2 and pkgdown! + ## License AGPL (\>=3) diff --git a/inst/rstudio/addins.dcf b/inst/rstudio/addins.dcf index d4d63f7..4600c05 100644 --- a/inst/rstudio/addins.dcf +++ b/inst/rstudio/addins.dcf @@ -2,3 +2,8 @@ Name: Insert examplesTempdir Description: Insert a template for examples that run in a temporary working directory Binding: insert_examplesTempdir_template Interactive: false + +Name: Insert examplesWebR +Description: Insert a template for examples that can be run in webR REPL +Binding: insert_examplesWebR_template +Interactive: false \ No newline at end of file diff --git a/man/insert_examplesWebR_template.Rd b/man/insert_examplesWebR_template.Rd new file mode 100644 index 0000000..86e69f8 --- /dev/null +++ b/man/insert_examplesWebR_template.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rstudio-addins.R +\name{insert_examplesWebR_template} +\alias{insert_examplesWebR_template} +\title{Insert a webR examples template} +\usage{ +insert_examplesWebR_template() +} +\description{ +This function inserts a template for the \verb{@examplesWebR} tag at the cursor position. +It's designed to be used as an RStudio addin. +} diff --git a/man/tag-examplesWebR.Rd b/man/tag-examplesWebR.Rd new file mode 100644 index 0000000..90bf5ce --- /dev/null +++ b/man/tag-examplesWebR.Rd @@ -0,0 +1,198 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/examplesWebR.R +\name{tag-examplesWebR} +\alias{tag-examplesWebR} +\title{Custom \verb{@examplesWebR} tag.} +\usage{ +#' @examplesWebR +#' # Your example code that will be available in webR +#' +#' @examplesWebR embed +#' # Your example code with embedded webR iframe +#' +#' @examplesWebR embed version=v0.5.4 height=400 +#' # Your example code with embedded webR iframe with height 400 set to version 0.5.4 +#' +#' @examplesWebR embed=false autorun mode=editor-plot +#' # Your example code with custom configuration overriding global settings +#' +} +\description{ +When roxygen2 processes your documentation, the \verb{@examplesWebR} tag generates +both an example section and webR integration. Specifically, the +example code is split into a regular \verb{@examples} section and a WebR section. +The webR section contains a clickable link or embedding an interactive iframe +that interacts with the webR REPL by automatically encoding the code +for sharing. +} +\section{Implementation}{ + + +When the \verb{@examplesWebR} tag is processed, it creates: +\enumerate{ +\item Regular \verb{@examples} section with the code +\item A \code{"WebR"} section containing format-appropriate webR integration link +} + +The webR URL format is: + +\if{html}{\out{
}}\preformatted{https://webr.r-wasm.org/\{version\}/?mode=\{mode\}&channel=\{channel\}#code=\{encoded_code\}&ju\{a\} +}\if{html}{\out{
}} + +where the code is JSON-encoded and uncompressed. +} + +\section{Generated Structure}{ + + +The generated RD file will have the structure: + +\if{html}{\out{
}}\preformatted{\\section\{WebR\}\{ + \ifelse{html}{ + \out{ + \U0001F310 View in webR REPL + } + }{ + \ifelse{latex}{ + \url{webR_URL} + }{ + Interactive webR content not available for this output format. + } + } +\} +\\examples\{ +# Your R code here +plot(1:5, 1:5) +\} +}\if{html}{\out{
}} + +where \code{webR_URL} is the link to the webR REPL with the encoded code. +} + +\section{Output Format Support}{ + + +The webR integration adapts to different documentation formats: +\itemize{ +\item \strong{HTML}: Interactive buttons/iframes with full webR functionality +\item \strong{LaTeX/PDF}: Plain URL link to webR session +\item \strong{Other formats}: Informational message about limited support +} +} + +\section{Parameters}{ + + +The tag supports optional parameters (these override global \code{DESCRIPTION} config): +\itemize{ +\item \verb{@examplesWebR}: Default behavior (link in WebR section) +\item \verb{@examplesWebR embed}: Embed iframe instead of link +\itemize{ +\item \verb{@examplesWebR embed=false} - Explicitly disable embedding (override global config) +} +\item \verb{@examplesWebR version=v0.5.4}: Specify webR version greater than or equal to 0.5.4 (default: \code{latest}) +\item \verb{@examplesWebR height=500}: Set iframe height in pixels (default: \code{400}) +\item \verb{@examplesWebR autorun} - Enable autorun on webR session open +\itemize{ +\item \verb{@examplesWebR autorun=false} - Explicitly disable autorun (override global config) +} +\item \verb{@examplesWebR mode=editor-plot} - Configure embedded webR interface (editor, plot, terminal, files) (default: \code{"editor-plot-terminal"}) +\item \verb{@examplesWebR channel=Automatic} - Set webR communication channel (default: \verb{"Automatic}) +} + +\strong{Version Requirements}: The version parameter must be either \code{"latest"} or a +version string \code{v0.5.4} or higher. Earlier versions are not supported +as the embedding feature is new. + +\strong{Mode Options}: Valid mode components are \code{editor}, \code{plot}, \code{terminal}, and \code{files}. +Combine multiple components with hyphens (e.g., \code{editor-plot-terminal}). +} + +\section{Global Configuration}{ + + +You can set global defaults for tags in your package's \code{DESCRIPTION} file: + +\if{html}{\out{
}}\preformatted{Config/rocleteer/webr-embed: false +Config/rocleteer/webr-height: 500 +Config/rocleteer/webr-version: v0.5.4 +Config/rocleteer/webr-autorun: true +Config/rocleteer/webr-mode: editor-plot +Config/rocleteer/webr-channel: Automatic +}\if{html}{\out{
}} +} + +\section{Package Configuration}{ + + +To use the \verb{@examplesWebR} tag in your package, add the required dependencies: + +In your \code{DESCRIPTION} file: + +\if{html}{\out{
}}\preformatted{Suggests: + rocleteer +Remotes: coatless-rpkg/rocleteer + +Roxygen: list(..., packages = c("rocleteer")) +}\if{html}{\out{
}} + +You will also need to specify a location where we can obtain the webR +compiled package in \code{DESCRIPTION}. This will usually be a GitHub Pages URL +or an R-universe URL. The recommended way is to use the +\code{Config/rocleteer/webr-repo} field in your \code{DESCRIPTION} file: + +\if{html}{\out{
}}\preformatted{Config/rocleteer/webr-repo: https://user.github.io/pkgname/ +}\if{html}{\out{
}} + +Alternatively, you can use the \code{URL} field in your \code{DESCRIPTION} file to +specify the repository URL. The tag will attempt to auto-detect the +repository URL from the \code{URL} field if the \code{Config/rocleteer/webr-repo} field +is not set. The tag will look for: + +\if{html}{\out{
}}\preformatted{URL: https://user.github.io/pkgname/, https://github.com/user/pkgname +}\if{html}{\out{
}} + +or + +\if{html}{\out{
}}\preformatted{URL: https://username.r-universe.dev +}\if{html}{\out{
}} + +If we cannot find a suitable repository URL, the tag will throw an error +during processing. + +The generated webR examples will use this URL to install the package +and load it in the webR REPL. The installation code will look like this: + +\if{html}{\out{
}}\preformatted{# Install and load webR Package +install.packages("pkgname", repos = "https://user.github.io/pkgname/") +library("pkgname") + +# Example code: +your_function() +}\if{html}{\out{
}} +} + +\examples{ +# A function with webR integration +# +#' @title Plot Simple Data +#' @description +#' This function creates a simple plot +#' +#' @param x Numeric vector for x-axis +#' @param y Numeric vector for y-axis +#' +#' @return +#' A plot object +#' +#' @examplesWebR +#' # Create some data +#' x <- 1:10 +#' y <- x^2 +#' +#' # Create a plot +#' plot(x, y, type = "b", main = "Simple Quadratic") +#' +#' # Add a line +#' lines(x, x^1.5, col = "red") +} diff --git a/man/webr_repl_href.Rd b/man/webr_repl_href.Rd new file mode 100644 index 0000000..4f805d3 --- /dev/null +++ b/man/webr_repl_href.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/examplesWebR.R +\name{webr_repl_href} +\alias{webr_repl_href} +\title{Generate webR REPL link} +\usage{ +webr_repl_href(encoded_code, version = "latest", mode = "", channel = "") +} +\arguments{ +\item{encoded_code}{Base64 encoded code with flags} + +\item{version}{webR version (e.g. "latest", "v0.5.4"). Default: \code{"latest"}} + +\item{mode}{webR interface mode (e.g., "editor-plot-terminal-files"). Default: \code{""}} + +\item{channel}{webR channel (e.g., "Automatic"). Default: \code{""}} +} +\value{ +A URL string for the webR REPL +} +\description{ +Create a webR URL with version, mode, channel, and encoded code +} +\keyword{internal}