diff --git a/DESCRIPTION b/DESCRIPTION index 3fd70fb4..0b602a24 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: quarto Title: R Interface to 'Quarto' Markdown Publishing System -Version: 1.4.4.9024 +Version: 1.4.4.9025 Authors@R: c( person("JJ", "Allaire", , "jj@posit.co", role = "aut", comment = c(ORCID = "0000-0003-0174-9868")), @@ -23,6 +23,7 @@ Imports: htmltools, jsonlite, later, + lifecycle, processx, rlang, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index c26973a7..d96ee908 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,8 +2,11 @@ export(add_spin_preamble) export(check_newer_version) +export(find_project_root) +export(get_running_project_root) export(is_using_quarto) export(new_blog_post) +export(project_path) export(qmd_to_r_script) export(quarto_add_extension) export(quarto_available) @@ -47,6 +50,7 @@ importFrom(htmltools,div) importFrom(htmltools,span) importFrom(jsonlite,fromJSON) importFrom(later,later) +importFrom(lifecycle,deprecated) importFrom(processx,process) importFrom(processx,run) importFrom(rlang,caller_env) @@ -58,5 +62,6 @@ importFrom(tools,vignetteEngine) importFrom(utils,browseURL) importFrom(xfun,base64_encode) importFrom(xfun,env_option) +importFrom(xfun,normalize_path) importFrom(yaml,as.yaml) importFrom(yaml,write_yaml) diff --git a/NEWS.md b/NEWS.md index 75a535a3..ce5110ab 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # quarto (development version) +- Added `project_path()`, `get_running_project_root()`, and `find_project_root()` functions for Quarto-aware project path construction. These functions provide a consistent way to reference files relative to the project root, working both during Quarto rendering (using `QUARTO_PROJECT_ROOT` environment variables) and in interactive sessions (using intelligent project detection). The `project_path()` function is particularly useful in Quarto document cells where you need to reference data files or scripts from the project root regardless of the document's location in subdirectories (#180). + - `quarto_preview()` now explicitly returns the preview server URL (invisibly) and documents this behavior. This enables programmatic workflows such as taking screenshots with **webshot2** or passing the URL to other automation tools (thanks, @cwickham, #233). - Added NA value detection in YAML processing to prevent silent failures when passing R's `NA` values to Quarto CLI. Functions `as_yaml()` and `write_yaml()` now validate for NA values and provide clear error messages with actionable suggestions. This addresses issues where R's `NA` values get converted to YAML strings (like `.na.real`) that Quarto doesn't recognize as missing values, because they are not supported in YAML 1.2 spec. This is to help users handle missing data appropriately before passing to Quarto (#168). diff --git a/R/quarto-package.R b/R/quarto-package.R index 660855c6..bcf487c8 100644 --- a/R/quarto-package.R +++ b/R/quarto-package.R @@ -6,9 +6,11 @@ #' @importFrom cli cli_inform #' @importFrom htmltools div #' @importFrom htmltools span +#' @importFrom lifecycle deprecated #' @importFrom rlang caller_env #' @importFrom tools vignetteEngine #' @importFrom xfun base64_encode #' @importFrom xfun env_option +#' @importFrom xfun normalize_path ## usethis namespace: end NULL diff --git a/R/quarto.R b/R/quarto.R index a4ee8816..c0e7b8f5 100644 --- a/R/quarto.R +++ b/R/quarto.R @@ -21,7 +21,7 @@ quarto_path <- function(normalize = TRUE) { if (!normalize) { return(quarto_path) } - normalizePath(quarto_path, winslash = "/", mustWork = FALSE) + xfun::normalize_path(quarto_path) } get_quarto_path_env <- function() { @@ -257,7 +257,7 @@ quarto_binary_sitrep <- function(verbose = TRUE, debug = FALSE) { return(FALSE) } - quarto_found <- normalizePath(quarto_found, mustWork = FALSE) + quarto_found <- xfun::normalize_path(quarto_found) same_config <- TRUE if (debug) { @@ -271,8 +271,8 @@ quarto_binary_sitrep <- function(verbose = TRUE, debug = FALSE) { )) } - quarto_r_env <- normalizePath(get_quarto_path_env(), mustWork = FALSE) - quarto_system <- normalizePath(unname(Sys.which("quarto")), mustWork = FALSE) + quarto_r_env <- xfun::normalize_path(get_quarto_path_env()) + quarto_system <- xfun::normalize_path(unname(Sys.which("quarto"))) # quarto R package will use QUARTO_PATH env var with higher priority than latest version on path $PATH # and RStudio IDE does not use this environment variable if (!is.na(quarto_r_env) && identical(quarto_r_env, quarto_found)) { @@ -296,7 +296,7 @@ quarto_binary_sitrep <- function(verbose = TRUE, debug = FALSE) { # RStudio IDE > Render button will use RSTUDIO_QUARTO env var with higher priority than latest version on path $PATH rstudio_env <- Sys.getenv("RSTUDIO_QUARTO", unset = NA) if (!is.na(rstudio_env)) { - rstudio_env <- normalizePath(rstudio_env, mustWork = FALSE) + rstudio_env <- xfun::normalize_path(rstudio_env) if (!identical(rstudio_env, quarto_found)) { same_config <- FALSE if (verbose) { diff --git a/R/use.R b/R/use.R index 6cb98637..fc4f882d 100644 --- a/R/use.R +++ b/R/use.R @@ -36,6 +36,10 @@ quarto_use_template <- function( ) { rlang::check_required(template) + if (!fs::dir_exists(dir)) { + fs::dir_create(dir) + } + if (!is_empty_dir(dir) && quarto_available("1.5.15")) { cli::cli_abort(c( "{.arg dir} must be an empty directory.", diff --git a/R/utils-projects.R b/R/utils-projects.R new file mode 100644 index 00000000..6449599a --- /dev/null +++ b/R/utils-projects.R @@ -0,0 +1,245 @@ +#' Get path relative to project root (Quarto-aware) +#' +#' @description +#' `r lifecycle::badge("experimental")` +#' +#' This function constructs file paths relative to the project root when +#' running in a Quarto context (using `QUARTO_PROJECT_ROOT` or `QUARTO_PROJECT_DIR` +#' environment variables), or falls back to intelligent project root detection +#' when not in a Quarto context. +#' +#' It is experimental and subject to change in future releases. The automatic +#' project root detection may not work reliably in all contexts, especially when +#' projects have complex directory structures or when running in non-standard +#' environments. For a more explicit and potentially more robust approach, +#' consider using [here::i_am()] to declare your project structure, +#' followed by [here::here()] for path construction. See examples for comparison. +#' +#' @details +#' The function uses the following fallback hierarchy to determine the project root: +#' +#' - Quarto environment variables set during Quarto commands (e.g., `quarto render`): +#' - `QUARTO_PROJECT_ROOT` environment variable (set by Quarto commands) +#' - `QUARTO_PROJECT_DIR` environment variable (alternative Quarto variable) +#' +#' - Fallback to intelligent project root detection using [xfun::proj_root()] for interactive sessions: +#' - `_quarto.yml` or `_quarto.yaml` (Quarto project files) +#' - `DESCRIPTION` file with `Package:` field (R package or Project) +#' - `.Rproj` files with `Version:` field (RStudio projects) +#' +#' Last fallback is the current working directory if no project root can be determined. +#' A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, +#' as in this case the computed path may be wrong. +#' +#' @section Use in Quarto document cells: +#' +#' This function is particularly useful in Quarto document cells where you want to +#' use a path relative to the project root dynamically during rendering. +#' +#' ````markdown +#' ```{r}`r ''` +#' # Get a csv path from data directory in the Quarto project root +#' data <- project_path("data", "my_data.csv") +#' ``` +#' ```` +#' +#' @param ... Character vectors of path components to be joined +#' @param root Project root directory. If `NULL` (default), automatic detection +#' is used following the hierarchy described above +#' @return A character vector of the normalized file path relative to the project root. +#' +#' @examples +#' \dontrun{ +#' # Create a dummy Quarto project structure for example +#' tmpdir <- tempfile("quarto_project") +#' dir.create(tmpdir) +#' quarto::quarto_create_project( +#' 'test project', type = 'blog', +#' dir = tmpdir, no_prompt = TRUE, quiet = TRUE +#' ) +#' project_dir <- file.path(tmpdir, "test project") +#' +#' # Simulate working within a blog post +#' xfun::in_dir( +#' dir = file.path(project_dir, "posts", "welcome"), { +#' +#' # Reference a data file from project root +#' # ../../data/my_data.csv +#' quarto::project_path("data", "my_data.csv") +#' +#' # Reference a script from project root +#' # ../../R/analysis.R +#' quarto::project_path("R", "analysis.R") +#' +#' # Explicitly specify root (overrides automatic detection) +#' # ../../data/file.csv +#' quarto::project_path("data", "file.csv", root = "../..") +#' +#' # Alternative approach using here::i_am() (potentially more robust) +#' # This approach requires you to declare where you are in the project: +#' if (requireNamespace("here", quietly = TRUE)) { +#' # Declare that this document is in the project root or subdirectory +#' here::i_am("posts/welcome/index.qmd") +#' +#' # Now here::here() will work reliably from the project root +#' here::here("data", "my_data.csv") +#' here::here("R", "analysis.R") +#' } +#' }) +#' +#' } +#' +#' @seealso +#' * [here::here()] and [here::i_am()] for a similar function that works with R projects +#' * [find_project_root()] to search for Quarto Project configuration in parents directories +#' * [get_running_project_root()] for detecting the project root in Quarto commands +#' * [xfun::from_root()] for the underlying path construction +#' * [xfun::proj_root()] for project root detection logic +#' +#' @export +project_path <- function(..., root = NULL) { + if (is.null(root)) { + # Try Quarto project environment variables first + quarto_root <- get_running_project_root() + + root <- if (!is.null(quarto_root) && nzchar(quarto_root)) { + quarto_root + } else { + # Try to find project root using xfun::proj_root() with extended rules + tryCatch( + { + # Create extended rules that include Quarto and VS Code project files + extended_rules <- rbind( + # this should be the same as Quarto environment variables + # which are only set when running Quarto commands + c("_quarto.yml", ""), # Quarto project config + c("_quarto.yaml", ""), # Alternative Quarto config + xfun::root_rules # Default rules (DESCRIPTION, .Rproj) + ) + + proj_root <- xfun::proj_root(rules = extended_rules) + if (!is.null(proj_root)) { + proj_root + } else { + cli::cli_warn(c( + "Failed to determine project root using {.fun xfun::proj_root}. Using current working directory.", + ">" = "This may lead to different behavior interactively vs running Quarto commands." + )) + getwd() + } + }, + error = function(e) { + # Fall back to working directory if proj_root() fails + cli::cli_warn(c( + "Failed to determine project root: {e$message}. Using current working directory as a fallback.", + ">" = "This may lead to different behavior interactively vs running Quarto commands." + )) + getwd() # Return the working directory + } + ) + } + } + + # Normalize the root path + root <- xfun::normalize_path(root) + # Use xfun::from_root for better path handling + path <- rlang::try_fetch( + xfun::from_root(..., root = root, error = TRUE), + error = function(e) { + rlang::abort( + c( + "Failed to construct project path", + ">" = "Ensure you are using valid path components." + ), + parent = e, + call = rlang::caller_env() + ) + } + ) + path +} + +#' Get the root of the currently running Quarto project +#' +#' @description +#' This function is to be used inside cells and will return the project root +#' when doing [quarto_render()] by detecting Quarto project environment variables. +#' +#' @details +#' Quarto sets `QUARTO_PROJECT_ROOT` and `QUARTO_PROJECT_DIR` environment +#' variables when executing commands within a Quarto project context (e.g., +#' `quarto render`, `quarto preview`). This function detects their presence. +#' +#' Note that this function will return `NULL` when running code interactively +#' in an IDE (even within a Quarto project directory), as these specific +#' environment variables are only set during Quarto command execution. +#' +#' @section Use in Quarto document cells: +#' +#' This function is particularly useful in Quarto document cells where you want to +#' get the project root path dynamically during rendering. Cell example: +#' +#' ````markdown +#' ```{r}`r ''` +#' # Get the project root path +#' project_root <- get_running_project_root() +#' ``` +#' ```` +#' +#' @return Character Quarto project root path from set environment variables. +#' +#' @seealso +#' * [find_project_root()] for finding the Quarto project root directory +#' * [project_path()] for constructing paths relative to the project root +#' @examples +#' \dontrun{ +#' get_running_project_root() +#' } +#' @export +get_running_project_root <- function() { + root <- Sys.getenv("QUARTO_PROJECT_ROOT", Sys.getenv("QUARTO_PROJECT_DIR")) + if (!nzchar(root)) { + return() + } + root +} + +#' Find the root of a Quarto project +#' +#' @description +#' This function checks if the current working directory is within a Quarto +#' project by looking for Quarto project files (`_quarto.yml` or `_quarto.yaml`). +#' Unlike [get_running_project_root()], this works both during rendering and +#' interactive sessions. +#' +#' @param path Character. Path to check for Quarto project files. Defaults to +#' current working directory. +#' +#' @return Character Path of the project root directory if found, or `NULL` +#' +#' @examplesIf quarto_available() +#' tmpdir <- tempfile() +#' dir.create(tmpdir) +#' find_project_root(tmpdir) +#' quarto_create_project("test-proj", type = "blog", dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +#' blog_post_dir <- file.path(tmpdir, "test-proj", "posts", "welcome") +#' find_project_root(blog_post_dir) +#' +#' xfun::in_dir(blog_post_dir, { +#' # Check if current directory is a Quarto project or in one +#' !is.null(find_project_root()) +#' }) +#' +#' # clean up +#' unlink(tmpdir, recursive = TRUE) +#' +#' +#' @seealso [get_running_project_root()] for detecting active Quarto rendering +#' @export +find_project_root <- function(path = ".") { + quarto_rules <- rbind( + c("_quarto.yml", ""), + c("_quarto.yaml", "") + ) + xfun::proj_root(path = path, rules = quarto_rules) +} diff --git a/R/utils.R b/R/utils.R index 66643837..5f8abe85 100644 --- a/R/utils.R +++ b/R/utils.R @@ -106,7 +106,10 @@ has_internet <- function(host = "https://www.google.com") { is_empty_dir <- function(dir) { if (!dir.exists(dir)) { - return(FALSE) + rlang::warn( + "Directory {.path {dir}} does not exist. Assuming it is empty." + ) + return(TRUE) } files <- list.files(dir, all.files = TRUE, no.. = TRUE) length(files) == 0 diff --git a/_pkgdown.yml b/_pkgdown.yml index 3de383ea..5d26cb18 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -35,6 +35,9 @@ reference: contents: - quarto_create_project - new_blog_post + - project_path + - find_project_root + - get_running_project_root - title: "Configuration" desc: > diff --git a/man/figures/lifecycle-deprecated.svg b/man/figures/lifecycle-deprecated.svg new file mode 100644 index 00000000..b61c57c3 --- /dev/null +++ b/man/figures/lifecycle-deprecated.svg @@ -0,0 +1,21 @@ + diff --git a/man/figures/lifecycle-experimental.svg b/man/figures/lifecycle-experimental.svg new file mode 100644 index 00000000..5d88fc2c --- /dev/null +++ b/man/figures/lifecycle-experimental.svg @@ -0,0 +1,21 @@ + diff --git a/man/figures/lifecycle-stable.svg b/man/figures/lifecycle-stable.svg new file mode 100644 index 00000000..9bf21e76 --- /dev/null +++ b/man/figures/lifecycle-stable.svg @@ -0,0 +1,29 @@ + diff --git a/man/figures/lifecycle-superseded.svg b/man/figures/lifecycle-superseded.svg new file mode 100644 index 00000000..db8d757f --- /dev/null +++ b/man/figures/lifecycle-superseded.svg @@ -0,0 +1,21 @@ + diff --git a/man/find_project_root.Rd b/man/find_project_root.Rd new file mode 100644 index 00000000..d5df6e1c --- /dev/null +++ b/man/find_project_root.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-projects.R +\name{find_project_root} +\alias{find_project_root} +\title{Find the root of a Quarto project} +\usage{ +find_project_root(path = ".") +} +\arguments{ +\item{path}{Character. Path to check for Quarto project files. Defaults to +current working directory.} +} +\value{ +Character Path of the project root directory if found, or \code{NULL} +} +\description{ +This function checks if the current working directory is within a Quarto +project by looking for Quarto project files (\verb{_quarto.yml} or \verb{_quarto.yaml}). +Unlike \code{\link[=get_running_project_root]{get_running_project_root()}}, this works both during rendering and +interactive sessions. +} +\examples{ +\dontshow{if (quarto_available()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +tmpdir <- tempfile() +dir.create(tmpdir) +find_project_root(tmpdir) +quarto_create_project("test-proj", type = "blog", dir = tmpdir, no_prompt = TRUE, quiet = TRUE) +blog_post_dir <- file.path(tmpdir, "test-proj", "posts", "welcome") +find_project_root(blog_post_dir) + +xfun::in_dir(blog_post_dir, { + # Check if current directory is a Quarto project or in one + !is.null(find_project_root()) +}) + +# clean up +unlink(tmpdir, recursive = TRUE) + +\dontshow{\}) # examplesIf} +} +\seealso{ +\code{\link[=get_running_project_root]{get_running_project_root()}} for detecting active Quarto rendering +} diff --git a/man/get_running_project_root.Rd b/man/get_running_project_root.Rd new file mode 100644 index 00000000..486191e6 --- /dev/null +++ b/man/get_running_project_root.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-projects.R +\name{get_running_project_root} +\alias{get_running_project_root} +\title{Get the root of the currently running Quarto project} +\usage{ +get_running_project_root() +} +\value{ +Character Quarto project root path from set environment variables. +} +\description{ +This function is to be used inside cells and will return the project root +when doing \code{\link[=quarto_render]{quarto_render()}} by detecting Quarto project environment variables. +} +\details{ +Quarto sets \code{QUARTO_PROJECT_ROOT} and \code{QUARTO_PROJECT_DIR} environment +variables when executing commands within a Quarto project context (e.g., +\verb{quarto render}, \verb{quarto preview}). This function detects their presence. + +Note that this function will return \code{NULL} when running code interactively +in an IDE (even within a Quarto project directory), as these specific +environment variables are only set during Quarto command execution. +} +\section{Use in Quarto document cells}{ + + +This function is particularly useful in Quarto document cells where you want to +get the project root path dynamically during rendering. Cell example: + +\if{html}{\out{