|
| 1 | +#' Get path relative to project root (Quarto-aware) |
| 2 | +#' |
| 3 | +#' @description |
| 4 | +#' `r lifecycle::badge("experimental")` |
| 5 | +#' |
| 6 | +#' This function constructs file paths relative to the project root when |
| 7 | +#' running in a Quarto context (using `QUARTO_PROJECT_ROOT` or `QUARTO_PROJECT_DIR` |
| 8 | +#' environment variables), or falls back to intelligent project root detection |
| 9 | +#' when not in a Quarto context. |
| 10 | +#' |
| 11 | +#' It is experimental and subject to change in future releases. The automatic |
| 12 | +#' project root detection may not work reliably in all contexts, especially when |
| 13 | +#' projects have complex directory structures or when running in non-standard |
| 14 | +#' environments. For a more explicit and potentially more robust approach, |
| 15 | +#' consider using [here::i_am()] to declare your project structure, |
| 16 | +#' followed by [here::here()] for path construction. See examples for comparison. |
| 17 | +#' |
| 18 | +#' @details |
| 19 | +#' The function uses the following fallback hierarchy to determine the project root: |
| 20 | +#' |
| 21 | +#' - Quarto environment variables set during Quarto commands (e.g., `quarto render`): |
| 22 | +#' - `QUARTO_PROJECT_ROOT` environment variable (set by Quarto commands) |
| 23 | +#' - `QUARTO_PROJECT_DIR` environment variable (alternative Quarto variable) |
| 24 | +#' |
| 25 | +#' - Fallback to intelligent project root detection using [xfun::proj_root()] for interactive sessions: |
| 26 | +#' - `_quarto.yml` or `_quarto.yaml` (Quarto project files) |
| 27 | +#' - `.vscode` directory (VS Code/Positron workspace) |
| 28 | +#' - `DESCRIPTION` file with `Package:` field (R package or Project) |
| 29 | +#' - `.Rproj` files with `Version:` field (RStudio projects) |
| 30 | +#' |
| 31 | +#' Last fallback is the current working directory if no project root can be determined. |
| 32 | +#' A warning is issued to alert users that behavior may differ between interactive use and Quarto rendering, |
| 33 | +#' as in this case the computed path may be wrong. |
| 34 | +#' |
| 35 | +#' @param ... Character vectors of path components to be joined |
| 36 | +#' @param root Project root directory. If `NULL` (default), automatic detection |
| 37 | +#' is used following the hierarchy described above |
| 38 | +#' @return A character vector of the normalized file path relative to the project root |
| 39 | +#' |
| 40 | +#' @examples |
| 41 | +#' \dontrun{ |
| 42 | +#' # Reference a data file from project root |
| 43 | +#' data_path <- quarto::project_path("data", "my_data.csv") |
| 44 | +#' |
| 45 | +#' # Reference a script |
| 46 | +#' script_path <- quarto::project_path("R", "analysis.R") |
| 47 | +#' |
| 48 | +#' # Reference nested directories |
| 49 | +#' output_path <- quarto::project_path("outputs", "figures", "plot.png") |
| 50 | +#' |
| 51 | +#' # Explicitly specify root (overrides automatic detection) |
| 52 | +#' custom_path <- quarto::project_path("data", "file.csv", root = "/path/to/project") |
| 53 | +#' |
| 54 | +#' # Alternative approach using here::i_am() (potentially more robust) |
| 55 | +#' # This approach requires you to declare where you are in the project: |
| 56 | +#' if (requireNamespace("here", quietly = TRUE)) { |
| 57 | +#' # Declare that this document is in the project root or subdirectory |
| 58 | +#' here::i_am("analysis.qmd") # If in project root |
| 59 | +#' # here::i_am("reports/analysis.qmd") # If in subdirectory |
| 60 | +#' |
| 61 | +#' # Now here::here() will work reliably from the project root |
| 62 | +#' data_path_alt <- here::here("data", "my_data.csv") |
| 63 | +#' script_path_alt <- here::here("R", "analysis.R") |
| 64 | +#' output_path_alt <- here::here("outputs", "figures", "plot.png") |
| 65 | +#' } |
| 66 | +#' } |
| 67 | +#' |
| 68 | +#' @seealso |
| 69 | +#' * [here::here()] for a similar function that works with R projects |
| 70 | +#' * [is_running_quarto_project()] to check if quarto is running with a project context |
| 71 | +#' * [xfun::from_root()] for the underlying path construction |
| 72 | +#' * [xfun::proj_root()] for project root detection logic |
| 73 | +#' |
| 74 | +#' @export |
| 75 | +project_path <- function(..., root = NULL) { |
| 76 | + if (is.null(root)) { |
| 77 | + # Try Quarto project environment variables first |
| 78 | + quarto_root <- Sys.getenv( |
| 79 | + "QUARTO_PROJECT_ROOT", |
| 80 | + Sys.getenv("QUARTO_PROJECT_DIR") |
| 81 | + ) |
| 82 | + |
| 83 | + if (nzchar(quarto_root)) { |
| 84 | + root <- quarto_root |
| 85 | + } else { |
| 86 | + # Try to find project root using xfun::proj_root() with extended rules |
| 87 | + tryCatch( |
| 88 | + { |
| 89 | + # Create extended rules that include Quarto and VS Code project files |
| 90 | + extended_rules <- rbind( |
| 91 | + # this should be the same as Quarto environment variables |
| 92 | + # which are only set when running Quarto commands |
| 93 | + c("_quarto.yml", ""), # Quarto project config |
| 94 | + c("_quarto.yaml", ""), # Alternative Quarto config |
| 95 | + # This is to provide some better fallback than just the working directory |
| 96 | + c(".vscode", ""), # VS Code/Positron workspace |
| 97 | + xfun::root_rules # Default rules (DESCRIPTION, .Rproj) |
| 98 | + ) |
| 99 | + |
| 100 | + proj_root <- xfun::proj_root(rules = extended_rules) |
| 101 | + root <- if (!is.null(proj_root)) { |
| 102 | + proj_root |
| 103 | + } else { |
| 104 | + cli::cli_warn( |
| 105 | + "Failed to determine project root using {.fun xfun::proj_root}. Using current working directory.", |
| 106 | + ">" = "This may lead to different behavior interactively vs running Quarto commands." |
| 107 | + ) |
| 108 | + getwd() |
| 109 | + } |
| 110 | + }, |
| 111 | + error = function(e) { |
| 112 | + # Fall back to working directory if proj_root() fails |
| 113 | + cli::cli_warn(c( |
| 114 | + "Failed to determine project root: {e$message}. Using current working directory as a fallback.", |
| 115 | + ">" = "This may lead to different behavior interactively vs running Quarto commands." |
| 116 | + )) |
| 117 | + root <- getwd() |
| 118 | + } |
| 119 | + ) |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + # Use xfun::from_root for better path handling |
| 124 | + path <- tryCatch( |
| 125 | + xfun::from_root(..., root = root, error = FALSE), |
| 126 | + error = function(e) file.path(root, ...) |
| 127 | + ) |
| 128 | + path |
| 129 | +} |
| 130 | + |
| 131 | +#' Check if running within a Quarto project context |
| 132 | +#' |
| 133 | +#' @description |
| 134 | +#' This function checks if the current R session is running within a Quarto |
| 135 | +#' project context by detecting Quarto project environment variables. |
| 136 | +#' |
| 137 | +#' @details |
| 138 | +#' Quarto sets `QUARTO_PROJECT_ROOT` and `QUARTO_PROJECT_DIR` environment |
| 139 | +#' variables when executing commands within a Quarto project context (e.g., |
| 140 | +#' `quarto render`, `quarto preview`). This function detects their presence. |
| 141 | +#' |
| 142 | +#' Note that this function will return `FALSE` when running code interactively |
| 143 | +#' in an IDE (even within a Quarto project directory), as these specific |
| 144 | +#' environment variables are only set during Quarto command execution. |
| 145 | +#' |
| 146 | +#' @return Logical indicating if Quarto project environment variables are set |
| 147 | +#' |
| 148 | +#' @seealso |
| 149 | +#' * [is_quarto_project()] for checking Quarto project structure |
| 150 | +#' * [project_path()] for constructing paths relative to the project root |
| 151 | +#' @examples |
| 152 | +#' \dontrun{ |
| 153 | +#' # This will be TRUE during `quarto render` in a project |
| 154 | +#' is_running_quarto_project() |
| 155 | +#' |
| 156 | +#' # This will be FALSE when not running during `quarto_render` (e.g. interactively) |
| 157 | +#' is_running_quarto_project() |
| 158 | +#' } |
| 159 | +#' @export |
| 160 | +is_running_quarto_project <- function() { |
| 161 | + nzchar(Sys.getenv("QUARTO_PROJECT_ROOT")) || |
| 162 | + nzchar(Sys.getenv("QUARTO_PROJECT_DIR")) |
| 163 | +} |
| 164 | + |
| 165 | +#' Check if working within a Quarto project structure |
| 166 | +#' |
| 167 | +#' @description |
| 168 | +#' This function checks if the current working directory is within a Quarto |
| 169 | +#' project by looking for Quarto project files (`_quarto.yml` or `_quarto.yaml`). |
| 170 | +#' Unlike [is_running_quarto_project()], this works both during rendering and |
| 171 | +#' interactive sessions. |
| 172 | +#' |
| 173 | +#' @param path Character. Path to check for Quarto project files. Defaults to |
| 174 | +#' current working directory. |
| 175 | +#' |
| 176 | +#' @return Logical indicating if a Quarto project structure is detected |
| 177 | +#' |
| 178 | +#' @examplesIf quarto_available() |
| 179 | +#' dir <- tempfile() |
| 180 | +#' dir.create(dir) |
| 181 | +#' is_quarto_project(dir) |
| 182 | +#' quarto_create_project(dir) |
| 183 | +#' is_quarto_project(dir) |
| 184 | +#' |
| 185 | +#' xfun::in_dir(dir, |
| 186 | +#' # Check if current directory is in a Quarto project |
| 187 | +#' is_quarto_project() |
| 188 | +#' ) |
| 189 | +#' # clean up |
| 190 | +#' unlink(dir, recursive = TRUE) |
| 191 | +#' |
| 192 | +#' |
| 193 | +#' @seealso [is_running_quarto_project()] for detecting active Quarto rendering |
| 194 | +#' @export |
| 195 | +is_quarto_project <- function(path = ".") { |
| 196 | + tryCatch( |
| 197 | + { |
| 198 | + quarto_rules <- rbind( |
| 199 | + c("_quarto.yml", ""), |
| 200 | + c("_quarto.yaml", "") |
| 201 | + ) |
| 202 | + |
| 203 | + proj_root <- xfun::proj_root(path = path, rules = quarto_rules) |
| 204 | + !is.null(proj_root) |
| 205 | + }, |
| 206 | + error = function(e) { |
| 207 | + FALSE |
| 208 | + } |
| 209 | + ) |
| 210 | +} |
0 commit comments